diff options
850 files changed, 18758 insertions, 9461 deletions
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java index 68717623d05d..762e2af09cd3 100644 --- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java +++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java @@ -366,7 +366,7 @@ public class UserLifecycleTests { mRunner.pauseTiming(); Log.i(TAG, "Stopping timer"); - stopUser(userId, /* force */true); + stopUser(userId); mRunner.resumeTimingForNextIteration(); } @@ -429,7 +429,7 @@ public class UserLifecycleTests { mRunner.pauseTiming(); Log.i(TAG, "Stopping timer"); - stopUser(userId, /* force */true); + stopUser(userId); mRunner.resumeTimingForNextIteration(); } @@ -545,7 +545,7 @@ public class UserLifecycleTests { mRunner.pauseTiming(); Log.d(TAG, "Stopping timer"); switchUserNoCheck(currentUserId); - stopUserAfterWaitingForBroadcastIdle(userId, /* force */true); + stopUserAfterWaitingForBroadcastIdle(userId); attestFalse("Failed to stop user " + userId, mAm.isUserRunning(userId)); mRunner.resumeTimingForNextIteration(); } @@ -571,7 +571,7 @@ public class UserLifecycleTests { mRunner.pauseTiming(); Log.d(TAG, "Stopping timer"); switchUserNoCheck(startUser); - stopUserAfterWaitingForBroadcastIdle(testUser, true); + stopUserAfterWaitingForBroadcastIdle(testUser); attestFalse("Failed to stop user " + testUser, mAm.isUserRunning(testUser)); mRunner.resumeTimingForNextIteration(); } @@ -660,7 +660,7 @@ public class UserLifecycleTests { mRunner.resumeTiming(); Log.i(TAG, "Starting timer"); - stopUser(userId, false); + stopUser(userId); mRunner.pauseTiming(); Log.i(TAG, "Stopping timer"); @@ -685,7 +685,7 @@ public class UserLifecycleTests { Log.d(TAG, "Starting timer"); mRunner.resumeTiming(); - stopUser(userId, false); + stopUser(userId); mRunner.pauseTiming(); Log.d(TAG, "Stopping timer"); @@ -883,7 +883,7 @@ public class UserLifecycleTests { final int userId = createManagedProfile(); // Start the profile initially, then stop it. Similar to setQuietModeEnabled. startUserInBackgroundAndWaitForUnlock(userId); - stopUserAfterWaitingForBroadcastIdle(userId, true); + stopUserAfterWaitingForBroadcastIdle(userId); mRunner.resumeTiming(); Log.i(TAG, "Starting timer"); @@ -905,7 +905,7 @@ public class UserLifecycleTests { startUserInBackgroundAndWaitForUnlock(userId); while (mRunner.keepRunning()) { mRunner.pauseTiming(); - stopUserAfterWaitingForBroadcastIdle(userId, true); + stopUserAfterWaitingForBroadcastIdle(userId); mRunner.resumeTiming(); Log.d(TAG, "Starting timer"); @@ -987,7 +987,7 @@ public class UserLifecycleTests { installPreexistingApp(userId, DUMMY_PACKAGE_NAME); startUserInBackgroundAndWaitForUnlock(userId); startApp(userId, DUMMY_PACKAGE_NAME); - stopUserAfterWaitingForBroadcastIdle(userId, true); + stopUserAfterWaitingForBroadcastIdle(userId); SystemClock.sleep(1_000); // 1 second cool-down before re-starting profile. mRunner.resumeTiming(); Log.i(TAG, "Starting timer"); @@ -1019,7 +1019,7 @@ public class UserLifecycleTests { installPreexistingApp(userId, DUMMY_PACKAGE_NAME); startUserInBackgroundAndWaitForUnlock(userId); startApp(userId, DUMMY_PACKAGE_NAME); - stopUserAfterWaitingForBroadcastIdle(userId, true); + stopUserAfterWaitingForBroadcastIdle(userId); SystemClock.sleep(1_000); // 1 second cool-down before re-starting profile. mRunner.resumeTiming(); Log.d(TAG, "Starting timer"); @@ -1144,7 +1144,7 @@ public class UserLifecycleTests { mRunner.resumeTiming(); Log.i(TAG, "Starting timer"); - stopUser(userId, true); + stopUser(userId); mRunner.pauseTiming(); Log.i(TAG, "Stopping timer"); @@ -1168,7 +1168,7 @@ public class UserLifecycleTests { mRunner.resumeTiming(); Log.d(TAG, "Starting timer"); - stopUser(userId, true); + stopUser(userId); mRunner.pauseTiming(); Log.d(TAG, "Stopping timer"); @@ -1294,15 +1294,15 @@ public class UserLifecycleTests { * Do not call this method while timing is on. i.e. between mRunner.resumeTiming() and * mRunner.pauseTiming(). Otherwise it would cause the test results to be spiky. */ - private void stopUserAfterWaitingForBroadcastIdle(int userId, boolean force) + private void stopUserAfterWaitingForBroadcastIdle(int userId) throws RemoteException { waitForBroadcastIdle(); - stopUser(userId, force); + stopUser(userId); } - private void stopUser(int userId, boolean force) throws RemoteException { + private void stopUser(int userId) throws RemoteException { final CountDownLatch latch = new CountDownLatch(1); - mIam.stopUser(userId, force /* force */, new IStopUserCallback.Stub() { + mIam.stopUserWithCallback(userId, new IStopUserCallback.Stub() { @Override public void userStopped(int userId) throws RemoteException { latch.countDown(); @@ -1352,7 +1352,7 @@ public class UserLifecycleTests { attestTrue("Didn't switch back to user, " + origUser, origUser == mAm.getCurrentUser()); if (stopNewUser) { - stopUserAfterWaitingForBroadcastIdle(testUser, true); + stopUserAfterWaitingForBroadcastIdle(testUser); attestFalse("Failed to stop user " + testUser, mAm.isUserRunning(testUser)); } @@ -1471,7 +1471,7 @@ public class UserLifecycleTests { } private void removeUser(int userId) throws RemoteException { - stopUserAfterWaitingForBroadcastIdle(userId, true); + stopUserAfterWaitingForBroadcastIdle(userId); try { ShellHelper.runShellCommandWithTimeout("pm remove-user -w " + userId, TIMEOUT_IN_SECOND); @@ -1512,7 +1512,7 @@ public class UserLifecycleTests { final boolean preStartComplete = mIam.startUserInBackgroundWithListener(userId, preWaiter) && preWaiter.waitForFinish(TIMEOUT_IN_SECOND * 1000); - stopUserAfterWaitingForBroadcastIdle(userId, /* force */true); + stopUserAfterWaitingForBroadcastIdle(userId); assertTrue("Pre start was not performed for user" + userId, preStartComplete); } diff --git a/apex/jobscheduler/OWNERS b/apex/jobscheduler/OWNERS index 58434f165aa2..22b648975e5f 100644 --- a/apex/jobscheduler/OWNERS +++ b/apex/jobscheduler/OWNERS @@ -2,7 +2,6 @@ ctate@android.com ctate@google.com dplotnikov@google.com jji@google.com -kwekua@google.com omakoto@google.com suprabh@google.com varunshah@google.com diff --git a/apex/jobscheduler/framework/java/android/app/job/OWNERS b/apex/jobscheduler/framework/java/android/app/job/OWNERS index b4a45f585157..0b1e559dda15 100644 --- a/apex/jobscheduler/framework/java/android/app/job/OWNERS +++ b/apex/jobscheduler/framework/java/android/app/job/OWNERS @@ -4,4 +4,3 @@ yamasani@google.com omakoto@google.com ctate@android.com ctate@google.com -kwekua@google.com diff --git a/apex/jobscheduler/service/aconfig/Android.bp b/apex/jobscheduler/service/aconfig/Android.bp index 4db39dc1976b..859c67ad8910 100644 --- a/apex/jobscheduler/service/aconfig/Android.bp +++ b/apex/jobscheduler/service/aconfig/Android.bp @@ -2,6 +2,7 @@ aconfig_declarations { name: "service-deviceidle.flags-aconfig", package: "com.android.server.deviceidle", + container: "system", srcs: [ "device_idle.aconfig", ], @@ -17,6 +18,7 @@ java_aconfig_library { aconfig_declarations { name: "service-job.flags-aconfig", package: "com.android.server.job", + container: "system", srcs: [ "job.aconfig", ], @@ -32,6 +34,7 @@ java_aconfig_library { aconfig_declarations { name: "alarm_flags", package: "com.android.server.alarm", + container: "system", srcs: ["alarm.aconfig"], } diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig index bb0f3cbd5257..d3068d7d37e8 100644 --- a/apex/jobscheduler/service/aconfig/alarm.aconfig +++ b/apex/jobscheduler/service/aconfig/alarm.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.alarm" +container: "system" flag { name: "use_frozen_state_to_drop_listener_alarms" diff --git a/apex/jobscheduler/service/aconfig/device_idle.aconfig b/apex/jobscheduler/service/aconfig/device_idle.aconfig index e4cb5ad81ba0..e8c99b12828f 100644 --- a/apex/jobscheduler/service/aconfig/device_idle.aconfig +++ b/apex/jobscheduler/service/aconfig/device_idle.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.deviceidle" +container: "system" flag { name: "disable_wakelocks_in_light_idle" diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index 5e6d3775f6a2..75e2efd2ec99 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.job" +container: "system" flag { name: "batch_active_bucket_jobs" diff --git a/cmds/incident_helper/OWNERS b/cmds/incident_helper/OWNERS index cede4eae50ad..29f44aba75d1 100644 --- a/cmds/incident_helper/OWNERS +++ b/cmds/incident_helper/OWNERS @@ -1,3 +1,2 @@ joeo@google.com -kwekua@google.com yanmin@google.com diff --git a/core/api/current.txt b/core/api/current.txt index b19c3ab96bb5..13d5e03da7ed 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9877,7 +9877,7 @@ package android.companion { ctor public ObservingDevicePresenceRequest.Builder(); method @NonNull public android.companion.ObservingDevicePresenceRequest build(); method @NonNull public android.companion.ObservingDevicePresenceRequest.Builder setAssociationId(int); - method @NonNull @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) public android.companion.ObservingDevicePresenceRequest.Builder setUuid(@NonNull android.os.ParcelUuid); + method @NonNull @RequiresPermission(allOf={android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE, android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN}) public android.companion.ObservingDevicePresenceRequest.Builder setUuid(@NonNull android.os.ParcelUuid); } public final class WifiDeviceFilter implements android.companion.DeviceFilter<android.net.wifi.ScanResult> { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index b7c2ee920c79..b767c52ea9ba 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -319,7 +319,6 @@ package android { field public static final String RECEIVE_DATA_ACTIVITY_CHANGE = "android.permission.RECEIVE_DATA_ACTIVITY_CHANGE"; field public static final String RECEIVE_DEVICE_CUSTOMIZATION_READY = "android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY"; field public static final String RECEIVE_EMERGENCY_BROADCAST = "android.permission.RECEIVE_EMERGENCY_BROADCAST"; - field @FlaggedApi("android.permission.flags.voice_activation_permission_apis") public static final String RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA = "android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA"; field @FlaggedApi("android.permission.flags.voice_activation_permission_apis") public static final String RECEIVE_SANDBOX_TRIGGER_AUDIO = "android.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO"; field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final String RECEIVE_SENSITIVE_NOTIFICATIONS = "android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS"; field public static final String RECEIVE_WIFI_CREDENTIAL_CHANGE = "android.permission.RECEIVE_WIFI_CREDENTIAL_CHANGE"; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index f36aeab2cb17..ac679095faa0 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -157,7 +157,7 @@ package android.app { method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void scheduleApplicationInfoChanged(java.util.List<java.lang.String>, int); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public void setStopUserOnSwitch(int); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundVisibleOnDisplay(int, int); - method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int, boolean); + method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int); method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void unregisterUidFrozenStateChangedCallback(@NonNull android.app.ActivityManager.UidFrozenStateChangedCallback); method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String); method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle(); @@ -1765,15 +1765,12 @@ package android.hardware.input { public final class InputManager { method public void addUniqueIdAssociation(@NonNull String, @NonNull String); method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings(); - method @Nullable public String getCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier); - method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice); + method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptors(); method @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping(); method public int getMousePointerSpeed(); method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int); - method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String); method public void removeUniqueIdAssociation(@NonNull String); - method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void setCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String); field public static final long BLOCK_UNTRUSTED_TOUCHES = 158002302L; // 0x96aec7eL } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 77255617af0a..0dab3de599dc 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -5071,7 +5071,7 @@ public class ActivityManager { * <p><b>NOTE:</b> differently from {@link #switchUser(int)}, which stops the current foreground * user before starting a new one, this method does not stop the previous user running in * background in the display, and it will return {@code false} in this case. It's up to the - * caller to call {@link #stopUser(int, boolean)} before starting a new user. + * caller to call {@link #stopUser(int)} before starting a new user. * * @param userId user to be started in the display. It will return {@code false} if the user is * a profile, the {@link #getCurrentUser()}, the {@link UserHandle#SYSTEM system user}, or @@ -5281,15 +5281,16 @@ public class ActivityManager { * * @hide */ + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. @TestApi @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) - public boolean stopUser(@UserIdInt int userId, boolean force) { + public boolean stopUser(@UserIdInt int userId) { if (userId == UserHandle.USER_SYSTEM) { return false; } try { - return USER_OP_SUCCESS == getService().stopUser( - userId, force, /* callback= */ null); + return USER_OP_SUCCESS == getService().stopUserWithCallback( + userId, /* callback= */ null); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index e094ac61b500..9ea55f5ff84c 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -2205,19 +2205,6 @@ public class ActivityOptions extends ComponentOptions { } /** - * Sets background activity launch logic won't use pending intent creator foreground state. - * - * @hide - * @deprecated use {@link #setPendingIntentCreatorBackgroundActivityStartMode(int)} instead - */ - @Deprecated - public ActivityOptions setIgnorePendingIntentCreatorForegroundState(boolean ignore) { - mPendingIntentCreatorBackgroundActivityStartMode = ignore - ? MODE_BACKGROUND_ACTIVITY_START_DENIED : MODE_BACKGROUND_ACTIVITY_START_ALLOWED; - return this; - } - - /** * Allow a {@link PendingIntent} to use the privilege of its creator to start background * activities. * diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index e53bd395f7fe..3575545e202d 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1082,6 +1082,8 @@ public final class ActivityThread extends ClientTransactionHandler public boolean managed; public boolean mallocInfo; public boolean runGc; + // compression format to dump bitmaps, null if no bitmaps to be dumped + public String dumpBitmaps; String path; ParcelFileDescriptor fd; RemoteCallback finishCallback; @@ -1486,11 +1488,12 @@ public final class ActivityThread extends ClientTransactionHandler } @Override - public void dumpHeap(boolean managed, boolean mallocInfo, boolean runGc, String path, - ParcelFileDescriptor fd, RemoteCallback finishCallback) { + public void dumpHeap(boolean managed, boolean mallocInfo, boolean runGc, String dumpBitmaps, + String path, ParcelFileDescriptor fd, RemoteCallback finishCallback) { DumpHeapData dhd = new DumpHeapData(); dhd.managed = managed; dhd.mallocInfo = mallocInfo; + dhd.dumpBitmaps = dumpBitmaps; dhd.runGc = runGc; dhd.path = path; try { @@ -6859,6 +6862,9 @@ public final class ActivityThread extends ClientTransactionHandler System.runFinalization(); System.gc(); } + if (dhd.dumpBitmaps != null) { + Bitmap.dumpAll(dhd.dumpBitmaps); + } try (ParcelFileDescriptor fd = dhd.fd) { if (dhd.managed) { Debug.dumpHprofData(dhd.path, fd.getFileDescriptor()); @@ -6886,6 +6892,9 @@ public final class ActivityThread extends ClientTransactionHandler if (dhd.finishCallback != null) { dhd.finishCallback.sendResult(null); } + if (dhd.dumpBitmaps != null) { + Bitmap.dumpAll(null); // clear dump + } } final void handleDispatchPackageBroadcast(int cmd, String[] packages) { diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 7ed10e784b1c..7ae514ac2491 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1502,12 +1502,10 @@ public class AppOpsManager { AppProtoEnums.APP_OP_RECEIVE_SANDBOX_TRIGGER_AUDIO; /** - * Allows the privileged assistant app to receive the training data from the sandboxed hotword - * detection service. + * This op has been deprecated. * - * @hide */ - public static final int OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA = + private static final int OP_DEPRECATED_3 = AppProtoEnums.APP_OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA; /** @@ -1735,7 +1733,6 @@ public class AppOpsManager { OPSTR_CAMERA_SANDBOXED, OPSTR_RECORD_AUDIO_SANDBOXED, OPSTR_RECEIVE_SANDBOX_TRIGGER_AUDIO, - OPSTR_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, OPSTR_CREATE_ACCESSIBILITY_OVERLAY, OPSTR_MEDIA_ROUTING_CONTROL, OPSTR_ENABLE_MOBILE_DATA_BY_USER, @@ -2395,13 +2392,10 @@ public class AppOpsManager { "android:receive_sandbox_trigger_audio"; /** - * Allows the privileged assistant app to receive training data from the sandboxed hotword - * detection service. - * + * App op has been deprecated. * @hide */ - public static final String OPSTR_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA = - "android:RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA"; + public static final String OPSTR_DEPRECATED_3 = "android:deprecated_3"; /** * Creation of an overlay using accessibility services @@ -2582,7 +2576,6 @@ public class AppOpsManager { OP_CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD, OP_USE_FULL_SCREEN_INTENT, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, - OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, OP_MEDIA_ROUTING_CONTROL, OP_READ_SYSTEM_GRAMMATICAL_GENDER, OP_RUN_BACKUP_JOBS, @@ -3021,11 +3014,8 @@ public class AppOpsManager { "RECEIVE_SANDBOX_TRIGGER_AUDIO") .setPermission(Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO) .setDefaultMode(AppOpsManager.MODE_DEFAULT).build(), - new AppOpInfo.Builder(OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, - OPSTR_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, - "RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA") - .setPermission(Manifest.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA) - .setDefaultMode(AppOpsManager.MODE_DEFAULT).build(), + new AppOpInfo.Builder(OP_DEPRECATED_3, OPSTR_DEPRECATED_3, "DEPRECATED_3") + .setDefaultMode(AppOpsManager.MODE_IGNORED).build(), new AppOpInfo.Builder(OP_CREATE_ACCESSIBILITY_OVERLAY, OPSTR_CREATE_ACCESSIBILITY_OVERLAY, "CREATE_ACCESSIBILITY_OVERLAY") diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index ee0225fc8918..e3380e0bf12a 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -21,12 +21,14 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.StrictMode.vmIncorrectContextUseEnabled; import static android.view.WindowManager.LayoutParams.WindowType; +import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UiContext; +import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; @@ -2288,7 +2290,35 @@ class ContextImpl extends Context { Log.v(TAG, "Treating renounced permission " + permission + " as denied"); return PERMISSION_DENIED; } - return PermissionManager.checkPermission(permission, pid, uid, getDeviceId()); + + // When checking a device-aware permission on a remote device, if the permission is CAMERA + // or RECORD_AUDIO we need to check remote device's corresponding capability. If the remote + // device doesn't have capability fall back to checking permission on the default device. + // Note: we only perform permission check redirection when the device id is not explicitly + // set in the context. + int deviceId = getDeviceId(); + if (deviceId != Context.DEVICE_ID_DEFAULT + && !mIsExplicitDeviceId + && PermissionManager.DEVICE_AWARE_PERMISSIONS.contains(permission)) { + VirtualDeviceManager virtualDeviceManager = + getSystemService(VirtualDeviceManager.class); + VirtualDevice virtualDevice = virtualDeviceManager.getVirtualDevice(deviceId); + if (virtualDevice != null) { + if ((Objects.equals(permission, Manifest.permission.RECORD_AUDIO) + && !virtualDevice.hasCustomAudioInputSupport()) + || (Objects.equals(permission, Manifest.permission.CAMERA) + && !virtualDevice.hasCustomCameraSupport())) { + deviceId = Context.DEVICE_ID_DEFAULT; + } + } else { + Slog.e( + TAG, + "virtualDevice is not found when device id is not default. deviceId = " + + deviceId); + } + } + + return PermissionManager.checkPermission(permission, pid, uid, deviceId); } /** @hide */ diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 85611e8303bf..dca164d6acc1 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -394,7 +394,7 @@ interface IActivityManager { oneway void getMimeTypeFilterAsync(in Uri uri, int userId, in RemoteCallback resultCallback); // Cause the specified process to dump the specified heap. boolean dumpHeap(in String process, int userId, boolean managed, boolean mallocInfo, - boolean runGc, in String path, in ParcelFileDescriptor fd, + boolean runGc, in String dumpBitmaps, in String path, in ParcelFileDescriptor fd, in RemoteCallback finishCallback); @UnsupportedAppUsage boolean isUserRunning(int userid, int flags); @@ -452,12 +452,14 @@ interface IActivityManager { in IBinder resultTo, in String resultWho, int requestCode, int flags, in ProfilerInfo profilerInfo, in Bundle options, int userId); @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) - int stopUser(int userid, boolean force, in IStopUserCallback callback); + int stopUser(int userid, boolean stopProfileRegardlessOfParent, in IStopUserCallback callback); + int stopUserWithCallback(int userid, in IStopUserCallback callback); + int stopUserExceptCertainProfiles(int userid, boolean stopProfileRegardlessOfParent, in IStopUserCallback callback); /** - * Check {@link com.android.server.am.ActivityManagerService#stopUserWithDelayedLocking(int, boolean, IStopUserCallback)} + * Check {@link com.android.server.am.ActivityManagerService#stopUserWithDelayedLocking(int, IStopUserCallback)} * for details. */ - int stopUserWithDelayedLocking(int userid, boolean force, in IStopUserCallback callback); + int stopUserWithDelayedLocking(int userid, in IStopUserCallback callback); @UnsupportedAppUsage void registerUserSwitchObserver(in IUserSwitchObserver observer, in String name); @@ -499,6 +501,7 @@ interface IActivityManager { in String shareDescription); void requestInteractiveBugReport(); + void requestBugReportWithExtraAttachment(in Uri extraAttachment); void requestFullBugReport(); void requestRemoteBugReport(long nonce); boolean launchBugReportHandlerApp(); diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl index 251e4e8ad834..a64261a77a29 100644 --- a/core/java/android/app/IApplicationThread.aidl +++ b/core/java/android/app/IApplicationThread.aidl @@ -119,7 +119,8 @@ oneway interface IApplicationThread { void scheduleSuicide(); void dispatchPackageBroadcast(int cmd, in String[] packages); void scheduleCrash(in String msg, int typeId, in Bundle extras); - void dumpHeap(boolean managed, boolean mallocInfo, boolean runGc, in String path, + void dumpHeap(boolean managed, boolean mallocInfo, boolean runGc, + in String dumpBitmaps, in String path, in ParcelFileDescriptor fd, in RemoteCallback finishCallback); void dumpActivity(in ParcelFileDescriptor fd, IBinder servicetoken, in String prefix, in String[] args); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 42e82f6d77c0..ea6f45e8e201 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -10284,6 +10284,16 @@ public class DevicePolicyManager { * get the list of app restrictions set by each admin via * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin}. * + * <p>Starting from Android Version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * the device policy management role holder can also set app restrictions on any applications + * in the calling user, as well as the parent user of an organization-owned managed profile via + * the {@link DevicePolicyManager} instance returned by + * {@link #getParentProfileInstance(ComponentName)}. App restrictions set by the device policy + * management role holder are not returned by + * {@link UserManager#getApplicationRestrictions(String)}. The target application should use + * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} to retrieve + * them, alongside any app restrictions the profile or device owner might have set. + * * <p>NOTE: The method performs disk I/O and shouldn't be called on the main thread * * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or @@ -10299,11 +10309,14 @@ public class DevicePolicyManager { @WorkerThread public void setApplicationRestrictions(@Nullable ComponentName admin, String packageName, Bundle settings) { - throwIfParentInstance("setApplicationRestrictions"); + if (!Flags.dmrhCanSetAppRestriction()) { + throwIfParentInstance("setApplicationRestrictions"); + } + if (mService != null) { try { mService.setApplicationRestrictions(admin, mContext.getPackageName(), packageName, - settings); + settings, mParentInstance); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -11704,11 +11717,14 @@ public class DevicePolicyManager { @WorkerThread public @NonNull Bundle getApplicationRestrictions( @Nullable ComponentName admin, String packageName) { - throwIfParentInstance("getApplicationRestrictions"); + if (!Flags.dmrhCanSetAppRestriction()) { + throwIfParentInstance("getApplicationRestrictions"); + } + if (mService != null) { try { return mService.getApplicationRestrictions(admin, mContext.getPackageName(), - packageName); + packageName, mParentInstance); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -13986,8 +14002,15 @@ public class DevicePolicyManager { public @NonNull DevicePolicyManager getParentProfileInstance(@NonNull ComponentName admin) { throwIfParentInstance("getParentProfileInstance"); try { - if (!mService.isManagedProfile(admin)) { - throw new SecurityException("The current user does not have a parent profile."); + if (Flags.dmrhCanSetAppRestriction()) { + UserManager um = mContext.getSystemService(UserManager.class); + if (!um.isManagedProfile()) { + throw new SecurityException("The current user does not have a parent profile."); + } + } else { + if (!mService.isManagedProfile(admin)) { + throw new SecurityException("The current user does not have a parent profile."); + } } return new DevicePolicyManager(mContext, mService, true); } catch (RemoteException e) { diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index d4589dc6d453..2002326d76bd 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -244,8 +244,8 @@ interface IDevicePolicyManager { void setDefaultSmsApplication(in ComponentName admin, String callerPackageName, String packageName, boolean parent); void setDefaultDialerApplication(String packageName); - void setApplicationRestrictions(in ComponentName who, in String callerPackage, in String packageName, in Bundle settings); - Bundle getApplicationRestrictions(in ComponentName who, in String callerPackage, in String packageName); + void setApplicationRestrictions(in ComponentName who, in String callerPackage, in String packageName, in Bundle settings, in boolean parent); + Bundle getApplicationRestrictions(in ComponentName who, in String callerPackage, in String packageName, in boolean parent); boolean setApplicationRestrictionsManagingPackage(in ComponentName admin, in String packageName); String getApplicationRestrictionsManagingPackage(in ComponentName admin); boolean isCallerApplicationRestrictionsManagingPackage(in String callerPackage); diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 6b2baa7ceb61..56fb4aa45fb3 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -204,6 +204,13 @@ flag { } flag { + name: "dmrh_can_set_app_restriction" + namespace: "enterprise" + description: "Allow DMRH to set application restrictions (both on the profile and the parent)" + bug: "328758346" +} + +flag { name: "allow_screen_brightness_control_on_cope" namespace: "enterprise" description: "Allow COPE admin to control screen brightness and timeout." diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java index 75485626d2ef..508077ed43cc 100644 --- a/core/java/android/app/assist/AssistStructure.java +++ b/core/java/android/app/assist/AssistStructure.java @@ -2655,13 +2655,12 @@ public class AssistStructure implements Parcelable { ); } GetCredentialRequest getCredentialRequest = node.getPendingCredentialRequest(); - if (getCredentialRequest == null) { - Log.i(TAG, prefix + " No Credential Manager Request"); - } else { - Log.i(TAG, prefix + " GetCredentialRequest: no. of options= " - + getCredentialRequest.getCredentialOptions().size() - ); - } + Log.i(TAG, prefix + " Credential Manager info:" + + " hasCredentialManagerRequest=" + (getCredentialRequest != null) + + (getCredentialRequest != null + ? ", sizeOfOptions=" + getCredentialRequest.getCredentialOptions().size() + : "") + ); final int NCHILDREN = node.getChildCount(); if (NCHILDREN > 0) { diff --git a/core/java/android/companion/ObservingDevicePresenceRequest.java b/core/java/android/companion/ObservingDevicePresenceRequest.java index f1d594e80bda..11ea735dff4f 100644 --- a/core/java/android/companion/ObservingDevicePresenceRequest.java +++ b/core/java/android/companion/ObservingDevicePresenceRequest.java @@ -183,7 +183,11 @@ public final class ObservingDevicePresenceRequest implements Parcelable { * @param uuid The ParcelUuid for observing device presence. */ @NonNull - @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) + @RequiresPermission(allOf = { + android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE, + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.BLUETOOTH_SCAN + }) public Builder setUuid(@NonNull ParcelUuid uuid) { checkNotUsed(); this.mUuid = uuid; diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java index 915992904a5c..1d4403cb79e8 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -887,7 +887,7 @@ public final class UserProperties implements Parcelable { * <p> Setting this property to true will enable the user's CE storage to remain unlocked when * the user is stopped using * {@link com.android.server.am.ActivityManagerService#stopUserWithDelayedLocking(int, - * boolean, IStopUserCallback)}. + * IStopUserCallback)}. * * <p> When this property is false, delayed locking may still be applicable at a global * level for all users via the {@code config_multiuserDelayUserDataLocking}. That is, delayed diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java index 625d7cb66900..c7237ea35c2a 100644 --- a/core/java/android/content/res/FontScaleConverterFactory.java +++ b/core/java/android/content/res/FontScaleConverterFactory.java @@ -58,6 +58,16 @@ public class FontScaleConverterFactory { synchronized (LOOKUP_TABLES_WRITE_LOCK) { putInto( sLookupTables, + /* scaleKey= */ 1.05f, + new FontScaleConverterImpl( + /* fromSp= */ + new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, + /* toDp= */ + new float[] { 8.4f, 10.5f, 12.6f, 14.8f, 18.6f, 20.6f, 24.4f, 30f, 100}) + ); + + putInto( + sLookupTables, /* scaleKey= */ 1.1f, new FontScaleConverterImpl( /* fromSp= */ @@ -78,6 +88,16 @@ public class FontScaleConverterFactory { putInto( sLookupTables, + /* scaleKey= */ 1.2f, + new FontScaleConverterImpl( + /* fromSp= */ + new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, + /* toDp= */ + new float[] { 9.6f, 12f, 14.4f, 17.2f, 20.4f, 22.4f, 25.6f, 30f, 100}) + ); + + putInto( + sLookupTables, /* scaleKey= */ 1.3f, new FontScaleConverterImpl( /* fromSp= */ @@ -117,7 +137,7 @@ public class FontScaleConverterFactory { ); } - sMinScaleBeforeCurvesApplied = getScaleFromKey(sLookupTables.keyAt(0)) - 0.02f; + sMinScaleBeforeCurvesApplied = getScaleFromKey(sLookupTables.keyAt(0)) - 0.01f; if (sMinScaleBeforeCurvesApplied <= 1.0f) { throw new IllegalStateException( "You should only apply non-linear scaling to font scales > 1" diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 90b7869e1c5d..a0e40f6390ee 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -52,6 +52,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; @@ -70,7 +71,8 @@ import javax.crypto.Mac; public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants { private static final String TAG = "BiometricPrompt"; - private static final int MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER = 30; + @VisibleForTesting + static final int MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER = 30; /** * Error/help message will show for this amount of time. @@ -223,8 +225,8 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * * @param logoDescription The logo description text that will be shown on the prompt. * @return This builder. - * @throws IllegalStateException If logo description is null or exceeds certain character - * limit. + * @throws IllegalArgumentException If logo description is null or exceeds certain character + * limit. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) @@ -232,7 +234,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescription) { if (logoDescription == null || logoDescription.length() > MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER) { - throw new IllegalStateException( + throw new IllegalArgumentException( "Logo description passed in can not be null or exceed " + MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER + " character number."); } @@ -240,7 +242,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan return this; } - /** * Required: Sets the title that will be shown on the prompt. * @param title The title to display. diff --git a/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java b/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java index 853d86cf94dc..a9eca3f87fc3 100644 --- a/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java +++ b/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java @@ -29,19 +29,22 @@ import android.hardware.biometrics.BiometricPrompt.ButtonInfo; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.annotations.VisibleForTesting; + import java.util.concurrent.Executor; /** - * Contains the information of the template of content view with a more options button for Biometric - * Prompt. + * Contains the information of the template of content view with a more options button for + * Biometric Prompt. + * <p> * This button should be used to provide more options for sign in or other purposes, such as when a * user needs to select between multiple app-specific accounts or profiles that are available for - * sign in. This is not common and apps should avoid using it if there is only one choice available - * or if the user has already selected the appropriate account to use before invoking - * BiometricPrompt because it will create additional steps that the user must navigate through. - * Clicking the more options button will dismiss the prompt, provide the app an opportunity to ask - * the user for the correct account, and finally allow the app to decide how to proceed once - * selected. + * sign in. + * <p> + * Apps should avoid using this when possible because it will create additional steps that the user + * must navigate through - clicking the more options button will dismiss the prompt, provide the app + * an opportunity to ask the user for the correct option, and finally allow the app to decide how to + * proceed once selected. * * <p> * Here's how you'd set a <code>PromptContentViewWithMoreOptionsButton</code> on a Biometric @@ -59,7 +62,8 @@ import java.util.concurrent.Executor; */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptContentViewWithMoreOptionsButton implements PromptContentViewParcelable { - private static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225; + @VisibleForTesting + static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225; private final String mDescription; private DialogInterface.OnClickListener mListener; @@ -132,14 +136,16 @@ public final class PromptContentViewWithMoreOptionsButton implements PromptConte } }; + /** + * A builder that collects arguments to be shown on the content view with more options button. + */ public static final class Builder { private String mDescription; private Executor mExecutor; private DialogInterface.OnClickListener mListener; /** - * Optional: Sets a description that will be shown on the content view. Note that there are - * limits on the number of characters allowed for description. + * Optional: Sets a description that will be shown on the content view. * * @param description The description to display. * @return This builder. @@ -149,7 +155,7 @@ public final class PromptContentViewWithMoreOptionsButton implements PromptConte @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) public Builder setDescription(@NonNull String description) { if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) { - throw new IllegalStateException("The character number of description exceeds " + throw new IllegalArgumentException("The character number of description exceeds " + MAX_DESCRIPTION_CHARACTER_NUMBER); } mDescription = description; diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java index 02b2a50ade3c..d8b28673f8ae 100644 --- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java +++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java @@ -24,6 +24,8 @@ import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.List; @@ -47,9 +49,12 @@ import java.util.List; */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptVerticalListContentView implements PromptContentViewParcelable { - private static final int MAX_ITEM_NUMBER = 20; - private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640; - private static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225; + @VisibleForTesting + static final int MAX_ITEM_NUMBER = 20; + @VisibleForTesting + static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640; + @VisibleForTesting + static final int MAX_DESCRIPTION_CHARACTER_NUMBER = 225; private final List<PromptContentItemParcelable> mContentList; private final String mDescription; @@ -155,7 +160,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar @NonNull public Builder setDescription(@NonNull String description) { if (description.length() > MAX_DESCRIPTION_CHARACTER_NUMBER) { - throw new IllegalStateException("The character number of description exceeds " + throw new IllegalArgumentException("The character number of description exceeds " + MAX_DESCRIPTION_CHARACTER_NUMBER); } mDescription = description; @@ -195,12 +200,12 @@ public final class PromptVerticalListContentView implements PromptContentViewPar private void checkItemLimits(@NonNull PromptContentItem listItem) { if (doesListItemExceedsCharLimit(listItem)) { - throw new IllegalStateException( + throw new IllegalArgumentException( "The character number of list item exceeds " + MAX_EACH_ITEM_CHARACTER_NUMBER); } if (mContentList.size() > MAX_ITEM_NUMBER) { - throw new IllegalStateException( + throw new IllegalArgumentException( "The number of list items exceeds " + MAX_ITEM_NUMBER); } } diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index ec9b013c34cc..dca663d206d3 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -23,12 +23,14 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.graphics.SurfaceTexture; import android.hardware.camera2.params.ExtensionSessionConfiguration; import android.hardware.camera2.params.InputConfiguration; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; import android.os.Handler; +import android.util.Size; import android.view.Surface; import com.android.internal.camera.flags.Flags; @@ -530,9 +532,10 @@ public abstract class CameraDevice implements AutoCloseable { * SurfaceTexture}: Set the size of the SurfaceTexture with {@link * android.graphics.SurfaceTexture#setDefaultBufferSize} to be one of the sizes returned by * {@link StreamConfigurationMap#getOutputSizes(Class) getOutputSizes(SurfaceTexture.class)} - * before creating a Surface from the SurfaceTexture with {@link Surface#Surface}. If the size - * is not set by the application, it will be set to be the smallest supported size less than - * 1080p, by the camera device.</li> + * before creating a Surface from the SurfaceTexture with + * {@link Surface#Surface(SurfaceTexture)}. If the size is not set by the application, + * it will be set to be the smallest supported size less than 1080p, by the camera + * device.</li> * * <li>For recording with {@link android.media.MediaCodec}: Call * {@link android.media.MediaCodec#createInputSurface} after configuring @@ -1405,10 +1408,16 @@ public abstract class CameraDevice implements AutoCloseable { * * <p><b>NOTE:</b> * For apps targeting {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, - * this method will ensure session parameters set through calls to - * {@link SessionConfiguration#setSessionParameters} are also supported if the Camera Device - * supports it. For apps targeting {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and - * below, session parameters will be ignored.</p> + * this method will automatically delegate to + * {@link CameraDeviceSetup#isSessionConfigurationSupported} whenever possible. This + * means that the output of this method will consider parameters set through + * {@link SessionConfiguration#setSessionParameters} as well. + * </p> + * + * <p>Session Parameters will be ignored for apps targeting <= + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, or if + * {@link CameraManager#isCameraDeviceSetupSupported} returns false for the camera id + * associated with this {@code CameraDevice}.</p> * * @return {@code true} if the given session configuration is supported by the camera device * {@code false} otherwise. @@ -1419,6 +1428,8 @@ public abstract class CameraDevice implements AutoCloseable { * encountered a fatal error * @throws IllegalStateException if the camera device has been closed * + * @see CameraManager#isCameraDeviceSetupSupported(String) + * @see CameraDeviceSetup#isSessionConfigurationSupported(SessionConfiguration) */ public boolean isSessionConfigurationSupported( @NonNull SessionConfiguration sessionConfig) throws CameraAccessException { @@ -1703,7 +1714,7 @@ public abstract class CameraDevice implements AutoCloseable { * SessionConfiguration} can then be created using the OutputConfiguration objects and * be used to query whether it's supported by the camera device. To create the * CameraCaptureSession, the application still needs to make sure all output surfaces - * are added via {@link OutputConfiguration#addSurfaces} with the exception of deferred + * are added via {@link OutputConfiguration#addSurface} with the exception of deferred * surfaces for {@link android.view.SurfaceView} and * {@link android.graphics.SurfaceTexture}.</li> * </ul> @@ -1751,7 +1762,7 @@ public abstract class CameraDevice implements AutoCloseable { * SessionConfiguration} can then be created using the OutputConfiguration objects and * be used for this function. To create the CameraCaptureSession, the application still * needs to make sure all output surfaces are added via {@link - * OutputConfiguration#addSurfaces} with the exception of deferred surfaces for {@link + * OutputConfiguration#addSurface} with the exception of deferred surfaces for {@link * android.view.SurfaceView} and {@link android.graphics.SurfaceTexture}.</p> * * @param sessionConfig The session configuration for which characteristics are fetched. diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 2816f777e8ab..1c37aa25af5f 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -94,33 +94,8 @@ interface IInputManager { // Keyboard layouts configuration. KeyboardLayout[] getKeyboardLayouts(); - KeyboardLayout[] getKeyboardLayoutsForInputDevice(in InputDeviceIdentifier identifier); - KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor); - String getCurrentKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier); - - @EnforcePermission("SET_KEYBOARD_LAYOUT") - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " - + "android.Manifest.permission.SET_KEYBOARD_LAYOUT)") - void setCurrentKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor); - - String[] getEnabledKeyboardLayoutsForInputDevice(in InputDeviceIdentifier identifier); - - @EnforcePermission("SET_KEYBOARD_LAYOUT") - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " - + "android.Manifest.permission.SET_KEYBOARD_LAYOUT)") - void addKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor); - - @EnforcePermission("SET_KEYBOARD_LAYOUT") - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " - + "android.Manifest.permission.SET_KEYBOARD_LAYOUT)") - void removeKeyboardLayoutForInputDevice(in InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor); - - // New Keyboard layout config APIs KeyboardLayoutSelectionResult getKeyboardLayoutForInputDevice( in InputDeviceIdentifier identifier, int userId, in InputMethodInfo imeInfo, in InputMethodSubtype imeSubtype); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index a1242fb43bbd..f94915876a2f 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -473,22 +473,21 @@ public final class InputManager { } /** - * Returns the descriptors of all supported keyboard layouts appropriate for the specified - * input device. + * Returns the descriptors of all supported keyboard layouts. * <p> * The input manager consults the built-in keyboard layouts as well as all keyboard layouts * advertised by applications using a {@link #ACTION_QUERY_KEYBOARD_LAYOUTS} broadcast receiver. * </p> * - * @param device The input device to query. * @return The ids of all keyboard layouts which are supported by the specified input device. * * @hide */ @TestApi @NonNull - public List<String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull InputDevice device) { - KeyboardLayout[] layouts = getKeyboardLayoutsForInputDevice(device.getIdentifier()); + @SuppressLint("UnflaggedApi") + public List<String> getKeyboardLayoutDescriptors() { + KeyboardLayout[] layouts = getKeyboardLayouts(); List<String> res = new ArrayList<>(); for (KeyboardLayout kl : layouts) { res.add(kl.getDescriptor()); @@ -511,33 +510,18 @@ public final class InputManager { @TestApi @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String layoutDescriptor) { - KeyboardLayout[] layouts = getKeyboardLayouts(); - for (KeyboardLayout kl : layouts) { - if (layoutDescriptor.equals(kl.getDescriptor())) { - return kl.getLayoutType(); - } - } - return ""; + KeyboardLayout layout = getKeyboardLayout(layoutDescriptor); + return layout == null ? "" : layout.getLayoutType(); } /** - * Gets information about all supported keyboard layouts appropriate - * for a specific input device. - * <p> - * The input manager consults the built-in keyboard layouts as well - * as all keyboard layouts advertised by applications using a - * {@link #ACTION_QUERY_KEYBOARD_LAYOUTS} broadcast receiver. - * </p> - * - * @return A list of all supported keyboard layouts for a specific - * input device. - * + * TODO(b/330517633): Cleanup the unsupported API * @hide */ @NonNull public KeyboardLayout[] getKeyboardLayoutsForInputDevice( @NonNull InputDeviceIdentifier identifier) { - return mGlobal.getKeyboardLayoutsForInputDevice(identifier); + return new KeyboardLayout[0]; } /** @@ -549,6 +533,7 @@ public final class InputManager { * * @hide */ + @Nullable public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) { if (keyboardLayoutDescriptor == null) { throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null"); @@ -562,121 +547,45 @@ public final class InputManager { } /** - * Gets the current keyboard layout descriptor for the specified input device. - * - * @param identifier Identifier for the input device - * @return The keyboard layout descriptor, or null if no keyboard layout has been set. - * + * TODO(b/330517633): Cleanup the unsupported API * @hide */ - @TestApi @Nullable public String getCurrentKeyboardLayoutForInputDevice( @NonNull InputDeviceIdentifier identifier) { - try { - return mIm.getCurrentKeyboardLayoutForInputDevice(identifier); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } + return null; } /** - * Sets the current keyboard layout descriptor for the specified input device. - * <p> - * This method may have the side-effect of causing the input device in question to be - * reconfigured. - * </p> - * - * @param identifier The identifier for the input device. - * @param keyboardLayoutDescriptor The keyboard layout descriptor to use, must not be null. - * + * TODO(b/330517633): Cleanup the unsupported API * @hide */ - @TestApi - @RequiresPermission(Manifest.permission.SET_KEYBOARD_LAYOUT) public void setCurrentKeyboardLayoutForInputDevice(@NonNull InputDeviceIdentifier identifier, - @NonNull String keyboardLayoutDescriptor) { - mGlobal.setCurrentKeyboardLayoutForInputDevice(identifier, - keyboardLayoutDescriptor); - } + @NonNull String keyboardLayoutDescriptor) {} /** - * Gets all keyboard layout descriptors that are enabled for the specified input device. - * - * @param identifier The identifier for the input device. - * @return The keyboard layout descriptors. - * + * TODO(b/330517633): Cleanup the unsupported API * @hide */ public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) { - if (identifier == null) { - throw new IllegalArgumentException("inputDeviceDescriptor must not be null"); - } - - try { - return mIm.getEnabledKeyboardLayoutsForInputDevice(identifier); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } + return new String[0]; } /** - * Adds the keyboard layout descriptor for the specified input device. - * <p> - * This method may have the side-effect of causing the input device in question to be - * reconfigured. - * </p> - * - * @param identifier The identifier for the input device. - * @param keyboardLayoutDescriptor The descriptor of the keyboard layout to add. - * + * TODO(b/330517633): Cleanup the unsupported API * @hide */ - @RequiresPermission(Manifest.permission.SET_KEYBOARD_LAYOUT) public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, String keyboardLayoutDescriptor) { - if (identifier == null) { - throw new IllegalArgumentException("inputDeviceDescriptor must not be null"); - } - if (keyboardLayoutDescriptor == null) { - throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null"); - } - - try { - mIm.addKeyboardLayoutForInputDevice(identifier, keyboardLayoutDescriptor); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } } /** - * Removes the keyboard layout descriptor for the specified input device. - * <p> - * This method may have the side-effect of causing the input device in question to be - * reconfigured. - * </p> - * - * @param identifier The identifier for the input device. - * @param keyboardLayoutDescriptor The descriptor of the keyboard layout to remove. - * + * TODO(b/330517633): Cleanup the unsupported API * @hide */ - @TestApi @RequiresPermission(Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull InputDeviceIdentifier identifier, @NonNull String keyboardLayoutDescriptor) { - if (identifier == null) { - throw new IllegalArgumentException("inputDeviceDescriptor must not be null"); - } - if (keyboardLayoutDescriptor == null) { - throw new IllegalArgumentException("keyboardLayoutDescriptor must not be null"); - } - - try { - mIm.removeKeyboardLayoutForInputDevice(identifier, keyboardLayoutDescriptor); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } } /** diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index 7c104a0ca946..7b29666d9a96 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -1068,36 +1068,21 @@ public final class InputManagerGlobal { } /** - * @see InputManager#getKeyboardLayoutsForInputDevice(InputDeviceIdentifier) + * TODO(b/330517633): Cleanup the unsupported API */ @NonNull public KeyboardLayout[] getKeyboardLayoutsForInputDevice( @NonNull InputDeviceIdentifier identifier) { - try { - return mIm.getKeyboardLayoutsForInputDevice(identifier); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } + return new KeyboardLayout[0]; } /** - * @see InputManager#setCurrentKeyboardLayoutForInputDevice - * (InputDeviceIdentifier, String) + * TODO(b/330517633): Cleanup the unsupported API */ - @RequiresPermission(Manifest.permission.SET_KEYBOARD_LAYOUT) public void setCurrentKeyboardLayoutForInputDevice( @NonNull InputDeviceIdentifier identifier, - @NonNull String keyboardLayoutDescriptor) { - Objects.requireNonNull(identifier, "identifier must not be null"); - Objects.requireNonNull(keyboardLayoutDescriptor, - "keyboardLayoutDescriptor must not be null"); - try { - mIm.setCurrentKeyboardLayoutForInputDevice(identifier, - keyboardLayoutDescriptor); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } - } + @NonNull String keyboardLayoutDescriptor) {} + /** * @see InputDevice#getSensorManager() diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 2cd7aeb2d99d..cbfc5d1e5050 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -56,6 +56,7 @@ import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECT import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_OTHER; import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED; import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING; +import static android.view.inputmethod.Flags.ctrlShiftShortcut; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -400,9 +401,14 @@ public class InputMethodService extends AbstractInputMethodService { private long mStylusHwSessionsTimeout = STYLUS_HANDWRITING_IDLE_TIMEOUT_MS; private Runnable mStylusWindowIdleTimeoutRunnable; private long mStylusWindowIdleTimeoutForTest; - /** Tracks last {@link MotionEvent#getToolType(int)} used for {@link MotionEvent#ACTION_DOWN}. + /** + * Tracks last {@link MotionEvent#getToolType(int)} used for {@link MotionEvent#ACTION_DOWN}. **/ private int mLastUsedToolType; + /** + * Tracks the ctrl+shift shortcut + **/ + private boolean mUsingCtrlShiftShortcut = false; /** * Returns whether {@link InputMethodService} is responsible for rendering the back button and @@ -3612,7 +3618,8 @@ public class InputMethodService extends AbstractInputMethodService { // any KeyEvent keyDown should reset last toolType. updateEditorToolTypeInternal(MotionEvent.TOOL_TYPE_UNKNOWN); } - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + + if (keyCode == KeyEvent.KEYCODE_BACK) { final ExtractEditText eet = getExtractEditTextIfVisible(); if (eet != null && eet.handleBackInTextActionModeIfNeeded(event)) { return true; @@ -3622,14 +3629,33 @@ public class InputMethodService extends AbstractInputMethodService { return true; } return false; - } else if (event.getKeyCode() == KeyEvent.KEYCODE_SPACE && KeyEvent.metaStateHasModifiers( + } else if (keyCode == KeyEvent.KEYCODE_SPACE && KeyEvent.metaStateHasModifiers( event.getMetaState() & ~KeyEvent.META_SHIFT_MASK, KeyEvent.META_CTRL_ON)) { if (mDecorViewVisible && mWindowVisible) { int direction = (event.getMetaState() & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1; mPrivOps.switchKeyboardLayoutAsync(direction); + event.startTracking(); return true; } } + + // Check if this may be a ctrl+shift shortcut + if (ctrlShiftShortcut()) { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + // Potentially Ctrl+Shift shortcut if Ctrl is currently pressed + mUsingCtrlShiftShortcut = KeyEvent.metaStateHasModifiers( + event.getMetaState() & ~KeyEvent.META_SHIFT_MASK, KeyEvent.META_CTRL_ON); + } else if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT + || keyCode == KeyEvent.KEYCODE_CTRL_RIGHT) { + // Potentially Ctrl+Shift shortcut if Shift is currently pressed + mUsingCtrlShiftShortcut = KeyEvent.metaStateHasModifiers( + event.getMetaState() & ~KeyEvent.META_CTRL_MASK, KeyEvent.META_SHIFT_ON); + } else { + mUsingCtrlShiftShortcut = false; + } + } + return doMovementKey(keyCode, event, MOVEMENT_DOWN); } @@ -3671,7 +3697,27 @@ public class InputMethodService extends AbstractInputMethodService { * them to perform navigation in the underlying application. */ public boolean onKeyUp(int keyCode, KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + if (ctrlShiftShortcut()) { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT + || keyCode == KeyEvent.KEYCODE_CTRL_LEFT + || keyCode == KeyEvent.KEYCODE_CTRL_RIGHT) { + if (mUsingCtrlShiftShortcut + && event.hasNoModifiers()) { + mUsingCtrlShiftShortcut = false; + if (mDecorViewVisible && mWindowVisible) { + // Move to the next IME + switchToNextInputMethod(false /* onlyCurrentIme */); + // TODO(b/332937629): Make the event stream consistent again + return true; + } + } + } else { + mUsingCtrlShiftShortcut = false; + } + } + + if (keyCode == KeyEvent.KEYCODE_BACK) { final ExtractEditText eet = getExtractEditTextIfVisible(); if (eet != null && eet.handleBackInTextActionModeIfNeeded(event)) { return true; @@ -3679,7 +3725,12 @@ public class InputMethodService extends AbstractInputMethodService { if (event.isTracking() && !event.isCanceled()) { return handleBack(true); } + } else if (keyCode == KeyEvent.KEYCODE_SPACE) { + if (event.isTracking() && !event.isCanceled()) { + return true; + } } + return doMovementKey(keyCode, event, MOVEMENT_UP); } diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index 2fe115f49099..5b711c9d8401 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -25,8 +25,6 @@ import android.util.Printer; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; -import dalvik.annotation.optimization.CriticalNative; - import java.io.FileDescriptor; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -80,7 +78,6 @@ public final class MessageQueue { private native static void nativeDestroy(long ptr); @UnsupportedAppUsage private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ - @CriticalNative private native static void nativeWake(long ptr); private native static boolean nativeIsPolling(long ptr); private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 8a1774291e9f..bb74a3e7f896 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -1436,10 +1436,10 @@ public class RecoverySystem { * @throws IOException if the recovery system service could not be contacted */ private boolean requestLskf(String packageName, IntentSender sender) throws IOException { - Log.i(TAG, String.format("<%s> is requesting LSFK", packageName)); + Log.i(TAG, TextUtils.formatSimple("Package<%s> requesting LSKF", packageName)); try { boolean validRequest = mService.requestLskf(packageName, sender); - Log.i(TAG, String.format("LSKF Request isValid = %b", validRequest)); + Log.i(TAG, TextUtils.formatSimple("LSKF Request isValid = %b", validRequest)); return validRequest; } catch (RemoteException | SecurityException e) { throw new IOException("could not request LSKF capture", e); diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index 3441244d6c58..fe3fa8cf34f5 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -240,6 +240,16 @@ public final class PermissionManager { public static final String EXTRA_PERMISSION_USAGES = "android.permission.extra.PERMISSION_USAGES"; + /** + * Specify what permissions are device aware. Only device aware permissions can be granted to + * a remote device. + * @hide + */ + public static final Set<String> DEVICE_AWARE_PERMISSIONS = + Flags.deviceAwarePermissionsEnabled() + ? Set.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) + : Collections.emptySet(); + private final @NonNull Context mContext; private final IPackageManager mPackageManager; diff --git a/core/java/android/permission/TEST_MAPPING b/core/java/android/permission/TEST_MAPPING index 69113ef8f946..a15d9bc1b485 100644 --- a/core/java/android/permission/TEST_MAPPING +++ b/core/java/android/permission/TEST_MAPPING @@ -11,5 +11,29 @@ } ] } + ], + "postsubmit": [ + { + "name": "CtsVirtualDevicesAudioTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "include-filter": "android.virtualdevice.cts.audio.VirtualAudioPermissionTest" + } + ] + }, + { + "name": "CtsVirtualDevicesAppLaunchTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "include-filter": "android.virtualdevice.cts.applaunch.VirtualDevicePermissionTest" + } + ] + } ] }
\ No newline at end of file diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 92bbadc4d2f2..c26f351bc3a0 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -157,3 +157,13 @@ flag { bug: "266164193" } +flag { + name: "ignore_apex_permissions" + is_fixed_read_only: true + namespace: "permissions" + description: "Ignore APEX pacakges for permissions on V+" + bug: "301320911" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 0fc51e74d570..582c90feb547 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -27,12 +27,15 @@ import static android.graphics.Matrix.MSKEW_Y; import static android.view.View.SYSTEM_UI_FLAG_VISIBLE; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; +import static com.android.window.flags.Flags.FLAG_OFFLOAD_COLOR_EXTRACTION; import static com.android.window.flags.Flags.noConsecutiveVisibilityEvents; +import static com.android.window.flags.Flags.offloadColorExtraction; import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.MainThread; import android.annotation.NonNull; @@ -832,6 +835,15 @@ public abstract class WallpaperService extends Service { } /** + * Called when the dim amount of the wallpaper changed. This can be used to recompute the + * wallpaper colors based on the new dim, and call {@link #notifyColorsChanged()}. + * @hide + */ + @FlaggedApi(FLAG_OFFLOAD_COLOR_EXTRACTION) + public void onDimAmountChanged(float dimAmount) { + } + + /** * Called when an application has changed the desired virtual size of * the wallpaper. */ @@ -1043,6 +1055,10 @@ public abstract class WallpaperService extends Service { } mPreviousWallpaperDimAmount = mWallpaperDimAmount; + + // after the dim changes, allow colors to be immediately recomputed + mLastColorInvalidation = 0; + if (offloadColorExtraction()) onDimAmountChanged(mWallpaperDimAmount); } /** diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 559fa96d0198..a8a0c5b8bdf7 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -53,6 +53,19 @@ flag { } flag { + name: "complete_font_load_in_system_services_ready" + namespace: "text" + description: "Fix to ensure that font loading is complete on system-services-ready boot phase." + # Make read only, as font loading is in the critical boot path which happens before the read-write + # flags propagate to the device. + is_fixed_read_only: true + bug: "327941215" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "phrase_strict_fallback" namespace: "text" description: "Feature flag for automatic fallback from phrase based line break to strict line break." @@ -147,3 +160,14 @@ flag { description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it" bug: "297962571" } + +flag { + name: "fix_font_update_failure" + namespace: "text" + description: "There was a bug of updating system font from Android 13 to 14. This flag for fixing the migration failure." + is_fixed_read_only: true + bug: "331717791" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index b30002228d54..6caf4d6ff992 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -196,11 +196,11 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { if (!super.setControl(control, showTypes, hideTypes)) { return false; } - final boolean hasLeash = control != null && control.getLeash() != null; - if (!hasLeash && !mIsRequestedVisibleAwaitingLeash) { + if (control == null && !mIsRequestedVisibleAwaitingLeash) { mController.setRequestedVisibleTypes(0 /* visibleTypes */, getType()); removeSurface(); } + final boolean hasLeash = control != null && control.getLeash() != null; if (hasLeash) { mIsRequestedVisibleAwaitingLeash = false; } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 2402a0b16b16..b52003f437da 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -31,7 +31,6 @@ import static android.view.WindowInsets.Type.ime; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; -import android.animation.AnimationHandler; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TypeEvaluator; @@ -69,7 +68,6 @@ import android.view.inputmethod.ImeTracker.InputMethodJankContext; import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.internal.inputmethod.ImeTracing; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.util.function.TriFunction; @@ -379,16 +377,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private final WindowInsetsAnimationControlListener mLoggingListener; private final InputMethodJankContext mInputMethodJankContext; - private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = - new ThreadLocal<AnimationHandler>() { - @Override - protected AnimationHandler initialValue() { - AnimationHandler handler = new AnimationHandler(); - handler.setProvider(new SfVsyncFrameCallbackProvider()); - return handler; - } - }; - public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks, @InsetsType int requestedTypes, @Behavior int behavior, boolean disable, int floatingImeBottomInset, WindowInsetsAnimationControlListener loggingListener, @@ -470,9 +458,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation ImeTracker.forJank().onFinishAnimation(getAnimationType()); } }); - if (!mHasAnimationCallbacks) { - mAnimator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get()); - } mAnimator.start(); } diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 0ae3e598f8b9..56a24e4705b7 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -3783,6 +3783,13 @@ public final class MotionEvent extends InputEvent implements Parcelable { throw new IllegalArgumentException( "idBits must contain at least one pointer from this motion event"); } + final int currentBits = getPointerIdBits(); + if ((currentBits & idBits) != idBits) { + throw new IllegalArgumentException( + "idBits must be a non-empty subset of the pointer IDs from this MotionEvent, " + + "got idBits: " + + String.format("0x%x", idBits) + " for " + this); + } MotionEvent event = obtain(); event.mNativePtr = nativeSplit(event.mNativePtr, this.mNativePtr, idBits); return event; diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index cfdf8fab05c2..1cd7d349a9af 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -1272,7 +1272,7 @@ public final class SurfaceControl implements Parcelable { * surface has no buffer or crop, the surface is boundless and only constrained * by the size of its parent bounds. * - * @param session The surface session, must not be null. + * @param session The surface session. * @param name The surface name, must not be null. * @param w The surface initial width. * @param h The surface initial height. diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index bd8e9c6b4bf8..eb7de93b83f3 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -226,6 +226,7 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -3334,16 +3335,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0x00000000; /** - * Live region mode specifying that accessibility services should announce - * changes to this view. + * Live region mode specifying that accessibility services should notify users of changes to + * this view. * <p> * Use with {@link #setAccessibilityLiveRegion(int)}. */ public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001; /** - * Live region mode specifying that accessibility services should interrupt - * ongoing speech to immediately announce changes to this view. + * Live region mode specifying that accessibility services should immediately notify users of + * changes to this view. For example, a screen reader may interrupt ongoing speech to + * immediately announce these changes. * <p> * Use with {@link #setAccessibilityLiveRegion(int)}. */ @@ -8797,14 +8799,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * <p> * When transitioning from one Activity to another, instead of using - * setAccessibilityPaneTitle(), set a descriptive title for its window by using android:label - * for the matching <activity> entry in your application’s manifest or updating the title at - * runtime with{@link android.app.Activity#setTitle(CharSequence)}. + * {@code setAccessibilityPaneTitle()}, set a descriptive title for its window by using + * {@code android:label} + * for the matching Activity entry in your application's manifest or updating the title at + * runtime with {@link android.app.Activity#setTitle(CharSequence)}. * * <p> + * <aside> * <b>Note:</b> Use * {@link androidx.core.view.ViewCompat#setAccessibilityPaneTitle(View, CharSequence)} - * for backwards-compatibility. </aside> + * for backwards-compatibility. + * </aside> * @param accessibilityPaneTitle The pane's title. Setting to {@code null} indicates that this * View is not a pane. * @@ -8912,7 +8917,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * They should not need to specify what exactly is announced to users. * * <p> - * In general, only announce transitions and don’t generate a confirmation message for simple + * In general, only announce transitions and don't generate a confirmation message for simple * actions like a button press. Label your controls concisely and precisely instead, and for * significant UI changes like window changes, use * {@link android.app.Activity#setTitle(CharSequence)} and @@ -15305,33 +15310,56 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * to the view's content description or text, or to the content descriptions * or text of the view's children (where applicable). * <p> - * To indicate that the user should be notified of changes, use - * {@link #ACCESSIBILITY_LIVE_REGION_POLITE}. Announcements from this region are queued and - * do not disrupt ongoing speech. + * Different priority levels are available: + * <ul> + * <li> + * {@link #ACCESSIBILITY_LIVE_REGION_POLITE}: + * Indicates that updates to the region should be presented to the user. Suitable in most + * cases for prominent updates within app content that don't require the user's immediate + * attention. + * </li> + * <li> + * {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}: Indicates that updates to the region have + * the highest priority and should be presented to the user immediately. This may result + * in disruptive notifications from an accessibility service, which may potentially + * interrupt other feedback or user actions, so it should generally be used only for + * critical, time-sensitive information. + * </li> + * <li> + * {@link #ACCESSIBILITY_LIVE_REGION_NONE}: Disables change announcements (the default for + * most views). + * </li> + * </ul> * <p> - * For example, selecting an option in a dropdown menu may update a panel below with the updated - * content. This panel may be marked as a live region with - * {@link #ACCESSIBILITY_LIVE_REGION_POLITE} to notify users of the change. + * Examples: + * <ul> + * <li> + * Selecting an option in a dropdown menu updates a panel below with the updated + * content. This panel may be marked as a live region with + * {@link #ACCESSIBILITY_LIVE_REGION_POLITE} to notify users of the change. A screen + * reader may queue changes as announcements that don't disrupt ongoing speech. + * </li> + * <li> + * An emergency alert may be marked with {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE} + * to immediately inform users of the emergency. + * </li> + * </ul> * <p> - * For notifying users about errors, such as in a login screen with text that displays an - * "incorrect password" notification, that view should send an AccessibilityEvent of type + * For error notifications, like an "incorrect password" warning in a login screen, views + * should send a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} + * {@code AccessibilityEvent} with a content change type * {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_ERROR} and set - * {@link AccessibilityNodeInfo#setError(CharSequence)} instead. Custom widgets should expose - * error-setting methods that support accessibility automatically. For example, instead of - * explicitly sending this event when using a TextView, use - * {@link android.widget.TextView#setError(CharSequence)}. - * <p> - * To disable change notifications for this view, use - * {@link #ACCESSIBILITY_LIVE_REGION_NONE}. This is the default live region - * mode for most views. + * {@link AccessibilityNodeInfo#setError(CharSequence)}. Custom widgets should provide + * error-setting methods that support accessibility. For example, use + * {@link android.widget.TextView#setError(CharSequence)} instead of explicitly sending events. * <p> - * If the view's changes should interrupt ongoing speech and notify the user - * immediately, use {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}. This may result in disruptive - * announcements from an accessibility service, so it should generally be used only to convey - * information that is time-sensitive or critical for use of the application. Examples may - * include an incoming call or an emergency alert. + * Don't use live regions for frequently-updating UI elements (e.g., progress bars), as this can + * overwhelm the user with feedback from accessibility services. If necessary, use + * {@link AccessibilityNodeInfo#setMinDurationBetweenContentChanges(Duration)} to throttle + * feedback and reduce disruptions. * <p> - * <b>Note:</b> Use {@link androidx.core.view.ViewCompat#setAccessibilityLiveRegion(View, int)} + * <aside><b>Note:</b> Use + * {@link androidx.core.view.ViewCompat#setAccessibilityLiveRegion(View, int)} * for backwards-compatibility. </aside> * * @param mode The live region mode for this view, one of: diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c8cb1a7c8773..c5a4d677e70e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -110,6 +110,7 @@ import static android.view.flags.Flags.toolkitFrameRateVelocityMappingReadOnly; import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision; import static android.view.flags.Flags.toolkitSetFrameRateReadOnly; import static android.view.flags.Flags.toolkitFrameRateFunctionEnablingReadOnly; +import static android.view.flags.Flags.toolkitFrameRateViewEnablingReadOnly; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER; @@ -1153,6 +1154,7 @@ public final class ViewRootImpl implements ViewParent, private static boolean sToolkitFrameRateFunctionEnablingReadOnlyFlagValue; private static boolean sToolkitMetricsForFrameRateDecisionFlagValue; private static boolean sToolkitFrameRateTypingReadOnlyFlagValue; + private static final boolean sToolkitFrameRateViewEnablingReadOnlyFlagValue; private static boolean sToolkitFrameRateVelocityMappingReadOnlyFlagValue = toolkitFrameRateVelocityMappingReadOnly();; @@ -1162,6 +1164,8 @@ public final class ViewRootImpl implements ViewParent, sToolkitFrameRateTypingReadOnlyFlagValue = toolkitFrameRateTypingReadOnly(); sToolkitFrameRateFunctionEnablingReadOnlyFlagValue = toolkitFrameRateFunctionEnablingReadOnly(); + sToolkitFrameRateViewEnablingReadOnlyFlagValue = + toolkitFrameRateViewEnablingReadOnly(); } // The latest input event from the gesture that was used to resolve the pointer icon. @@ -2624,8 +2628,10 @@ public final class ViewRootImpl implements ViewParent, // no longer needed if the dVRR feature is disabled. if (shouldEnableDvrr()) { try { - mFrameRateTransaction.setFrameRateSelectionStrategy(sc, + if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) { + mFrameRateTransaction.setFrameRateSelectionStrategy(sc, sc.FRAME_RATE_SELECTION_STRATEGY_SELF).applyAsyncUnsafe(); + } } catch (Exception e) { Log.e(mTag, "Unable to set frame rate selection strategy ", e); } @@ -6378,6 +6384,12 @@ public final class ViewRootImpl implements ViewParent, return "MSG_KEEP_CLEAR_RECTS_CHANGED"; case MSG_REFRESH_POINTER_ICON: return "MSG_REFRESH_POINTER_ICON"; + case MSG_TOUCH_BOOST_TIMEOUT: + return "MSG_TOUCH_BOOST_TIMEOUT"; + case MSG_CHECK_INVALIDATION_IDLE: + return "MSG_CHECK_INVALIDATION_IDLE"; + case MSG_FRAME_RATE_SETTING: + return "MSG_FRAME_RATE_SETTING"; } return super.getMessageName(message); } @@ -9575,6 +9587,8 @@ public final class ViewRootImpl implements ViewParent, } mRemoved = true; mOnBackInvokedDispatcher.detachFromWindow(); + removeVrrMessages(); + if (mAdded) { dispatchDetachedFromWindow(); } @@ -12536,8 +12550,10 @@ public final class ViewRootImpl implements ViewParent, + category + ", reason " + reason + ", " + sourceView); } - mFrameRateTransaction.setFrameRateCategory(mSurfaceControl, + if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) { + mFrameRateTransaction.setFrameRateCategory(mSurfaceControl, frameRateCategory, false).applyAsyncUnsafe(); + } mLastPreferredFrameRateCategory = frameRateCategory; } } catch (Exception e) { @@ -12595,8 +12611,10 @@ public final class ViewRootImpl implements ViewParent, + preferredFrameRate + " compatibility " + mFrameRateCompatibility); } - mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate, + if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) { + mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate, mFrameRateCompatibility).applyAsyncUnsafe(); + } mLastPreferredFrameRate = preferredFrameRate; } } catch (Exception e) { @@ -12824,7 +12842,7 @@ public final class ViewRootImpl implements ViewParent, private boolean shouldEnableDvrr() { // uncomment this when we are ready for enabling dVRR - if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) { + if (sToolkitFrameRateViewEnablingReadOnlyFlagValue) { return sToolkitSetFrameRateReadOnlyFlagValue && isFrameRatePowerSavingsBalanced(); } return false; @@ -12839,4 +12857,10 @@ public final class ViewRootImpl implements ViewParent, mHasIdledMessage = true; } } + + private void removeVrrMessages() { + mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT); + mHandler.removeMessages(MSG_CHECK_INVALIDATION_IDLE); + mHandler.removeMessages(MSG_FRAME_RATE_SETTING); + } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 03ba8aedb730..a5ba294d6a19 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -159,7 +159,7 @@ public class AccessibilityNodeInfo implements Parcelable { * <p> To avoid disconnected trees, this flag will also prefetch the parent. Siblings will be * prefetched before descendants. * - * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags. + * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used. */ public static final int FLAG_PREFETCH_SIBLINGS = 1 << 1; @@ -171,7 +171,7 @@ public class AccessibilityNodeInfo implements Parcelable { * {@link #FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST} or this will trigger an * IllegalArgumentException. * - * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags. + * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used. */ public static final int FLAG_PREFETCH_DESCENDANTS_HYBRID = 1 << 2; @@ -181,7 +181,7 @@ public class AccessibilityNodeInfo implements Parcelable { * {@link #FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST} or this will trigger an * IllegalArgumentException. * - * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags. + * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used. */ public static final int FLAG_PREFETCH_DESCENDANTS_DEPTH_FIRST = 1 << 3; @@ -191,7 +191,7 @@ public class AccessibilityNodeInfo implements Parcelable { * {@link #FLAG_PREFETCH_DESCENDANTS_DEPTH_FIRST} or this will trigger an * IllegalArgumentException. * - * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags. + * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used. */ public static final int FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST = 1 << 4; @@ -199,7 +199,7 @@ public class AccessibilityNodeInfo implements Parcelable { * Prefetching flag that specifies prefetching should not be interrupted by a request to * retrieve a node or perform an action on a node. * - * @see #FLAG_PREFETCH_ANCESTORS for where to use these flags. + * <p> See {@link #FLAG_PREFETCH_ANCESTORS} for information on where these flags can be used. */ public static final int FLAG_PREFETCH_UNINTERRUPTIBLE = 1 << 5; @@ -1295,6 +1295,8 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Get the child at given index. * + * <p> + * See {@link #getParent(int)} for a description of prefetching. * @param index The child index. * @param prefetchingStrategy the prefetching strategy. * @return The child node. @@ -1302,7 +1304,6 @@ public class AccessibilityNodeInfo implements Parcelable { * @throws IllegalStateException If called outside of an {@link AccessibilityService} and before * calling {@link #setQueryFromAppProcessEnabled}. * - * @see AccessibilityNodeInfo#getParent(int) for a description of prefetching. */ @Nullable public AccessibilityNodeInfo getChild(int index, @PrefetchingStrategy int prefetchingStrategy) { @@ -1903,8 +1904,13 @@ public class AccessibilityNodeInfo implements Parcelable { * Accessibility service will throttle those content change events and only handle one event * per minute for that view. * </p> + * <p> + * Example UI elements that frequently update and may benefit from a duration are progress bars, + * timers, and stopwatches. + * </p> * - * @see AccessibilityEvent#getContentChangeTypes for all content change types. + * @see AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED + * @see AccessibilityEvent#getContentChangeTypes * @param duration the minimum duration between content change events. * Negative duration would be treated as zero. */ @@ -3954,7 +3960,7 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Returns the container title. * - * @see #setContainerTitle for details. + * @see #setContainerTitle */ @Nullable public CharSequence getContainerTitle() { @@ -5187,8 +5193,8 @@ public class AccessibilityNodeInfo implements Parcelable { * <p>The node that is focused should return {@code true} for * {@link AccessibilityNodeInfo#isFocused()}. * - * @see #ACTION_ACCESSIBILITY_FOCUS for the difference between system and accessibility - * focus. + * See {@link #ACTION_ACCESSIBILITY_FOCUS} for the difference between system and + * accessibility focus. */ public static final AccessibilityAction ACTION_FOCUS = new AccessibilityAction(AccessibilityNodeInfo.ACTION_FOCUS); diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 80b23969ed73..8174da69becb 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1153,6 +1153,9 @@ public final class InputMethodManager { } final boolean startInput; synchronized (mH) { + if (reason == UnbindReason.DISCONNECT_IME) { + mImeDispatcher.clear(); + } if (getBindSequenceLocked() != sequence) { return; } diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 4c3a290da1d6..d79903bfd06b 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -94,3 +94,15 @@ flag { bug: "322836622" is_fixed_read_only: true } + +flag { + name: "ctrl_shift_shortcut" + namespace: "input_method" + description: "Ctrl+Shift shortcut to switch IMEs" + bug: "327198899" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 356d059ba5dc..6e43d0f1c6cd 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -69,6 +69,7 @@ import android.widget.RemoteViews.RemoteView; import com.android.internal.R; import java.text.NumberFormat; +import java.time.Duration; import java.util.ArrayList; import java.util.Locale; @@ -139,6 +140,14 @@ import java.util.Locale; * <p>The "inverse" styles provide an inverse color scheme for the spinner, which may be necessary * if your application uses a light colored theme (a white background).</p> * + * <h4>Accessibility</h4> + * <p> + * Consider using + * {@link AccessibilityNodeInfo#setMinDurationBetweenContentChanges(Duration)} to + * convey to accessibility services that changes can be throttled. This may reduce the + * frequency of potentially disruptive notifications. + * </p> + * * <p><strong>XML attributes</b></strong> * <p> * See {@link android.R.styleable#ProgressBar ProgressBar Attributes}, diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 42c2d80ea322..b5bf529fadbd 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -568,7 +568,7 @@ public class ScrollView extends FrameLayout { handled = pageScroll(View.FOCUS_DOWN); break; case KeyEvent.KEYCODE_SPACE: - pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + handled = pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); break; } } diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index efe31fff411e..616004b78dba 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -36,3 +36,10 @@ flag { description: "Enables a limit on the number of Tasks shown in Desktop Mode" bug: "332502912" } + +flag { + name: "enable_windowing_edge_drag_resize" + namespace: "lse_desktop_experience" + description: "Enables edge drag resizing for all input sources" + bug: "323383067" +} diff --git a/core/java/android/window/flags/wallpaper_manager.aconfig b/core/java/android/window/flags/wallpaper_manager.aconfig index aa92af228862..150b04e87d97 100644 --- a/core/java/android/window/flags/wallpaper_manager.aconfig +++ b/core/java/android/window/flags/wallpaper_manager.aconfig @@ -21,4 +21,14 @@ flag { namespace: "systemui" description: "Prevent the system from sending consecutive onVisibilityChanged(false) events." bug: "285631818" +} + +flag { + name: "offload_color_extraction" + namespace: "systemui" + description: "Let ImageWallpaper take care of its wallpaper color extraction, instead of system_server" + bug: "328791519" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 4402ac712d42..87c47da16b9a 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -9,6 +9,16 @@ flag { } flag { + name: "wait_for_transition_on_display_switch" + namespace: "windowing_frontend" + description: "Waits for Shell transition to start before unblocking the screen after display switch" + bug: "301420598" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "edge_to_edge_by_default" namespace: "windowing_frontend" description: "Make app go edge-to-edge by default when targeting SDK 35 or greater" @@ -118,9 +128,28 @@ flag { } flag { + name: "fifo_priority_for_major_ui_processes" + namespace: "windowing_frontend" + description: "Use realtime priority for SystemUI and launcher" + bug: "288140556" + is_fixed_read_only: true +} + +flag { name: "insets_decoupled_configuration" namespace: "windowing_frontend" description: "Configuration decoupled from insets" bug: "151861875" is_fixed_read_only: true +} + +flag { + name: "keyguard_appear_transition" + namespace: "windowing_frontend" + description: "Add transition when keyguard appears" + bug: "327970608" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index 9481dc91bcc4..ddb8ee0df668 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -24,8 +24,10 @@ import static com.android.internal.accessibility.dialog.AccessibilityTargetHelpe import static com.android.internal.os.RoSystemProperties.SUPPORT_ONE_HANDED_MODE; import static com.android.internal.util.ArrayUtils.convertToLongArray; +import android.Manifest; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.IntDef; +import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.ActivityThread; import android.app.AlertDialog; @@ -329,11 +331,14 @@ public class AccessibilityShortcutController { warningToast.show(); } + @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY) private AlertDialog createShortcutWarningDialog(int userId) { List<AccessibilityTarget> targets = getTargets(mContext, HARDWARE); if (targets.size() == 0) { return null; } + final AccessibilityManager am = mFrameworkObjectProvider + .getAccessibilityManagerInstance(mContext); // Avoid non-a11y users accidentally turning shortcut on without reading this carefully. // Put "don't turn on" as the primary action. @@ -361,32 +366,34 @@ public class AccessibilityShortcutController { // to the Settings. final ComponentName configDefaultService = ComponentName.unflattenFromString(defaultService); - Settings.Secure.putStringForUser(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, - configDefaultService.flattenToString(), - userId); + if (Flags.a11yQsShortcut()) { + am.enableShortcutsForTargets(true, HARDWARE, + Set.of(configDefaultService.flattenToString()), userId); + } else { + Settings.Secure.putStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, + configDefaultService.flattenToString(), + userId); + } } }) .setPositiveButton(R.string.accessibility_shortcut_off, (DialogInterface d, int which) -> { - if (Flags.updateAlwaysOnA11yService()) { - Set<String> targetServices = - ShortcutUtils.getShortcutTargetsFromSettings( - mContext, - HARDWARE, - userId); - + Set<String> targetServices = + ShortcutUtils.getShortcutTargetsFromSettings( + mContext, + HARDWARE, + userId); + if (Flags.a11yQsShortcut()) { + am.enableShortcutsForTargets( + false, HARDWARE, targetServices, userId); + } else { Settings.Secure.putStringForUser(mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "", userId); ShortcutUtils.updateInvisibleToggleAccessibilityServiceEnableState( mContext, targetServices, userId); - } else { - Settings.Secure.putStringForUser(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "", - userId); } - // If canceled, treat as if the dialog has never been shown Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java index ba1dffcec73f..f0885922e670 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java @@ -23,11 +23,14 @@ import static com.android.internal.accessibility.util.ShortcutUtils.optOutValueF import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; import android.graphics.drawable.Drawable; +import android.os.UserHandle; import android.view.View; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.Flags; import com.android.internal.accessibility.common.ShortcutConstants; import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; @@ -35,6 +38,8 @@ import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutT import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder; import com.android.internal.annotations.VisibleForTesting; +import java.util.Set; + /** * Abstract base class for creating various target related to accessibility service, accessibility * activity, and allowlisting features. @@ -108,13 +113,21 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS } } + @SuppressLint("MissingPermission") @Override public void onCheckedChanged(boolean isChecked) { setShortcutEnabled(isChecked); - if (isChecked) { - optInValueToSettings(getContext(), getShortcutType(), getId()); + if (Flags.a11yQsShortcut()) { + final AccessibilityManager am = + getContext().getSystemService(AccessibilityManager.class); + am.enableShortcutsForTargets( + isChecked, getShortcutType(), Set.of(mId), UserHandle.myUserId()); } else { - optOutValueFromSettings(getContext(), getShortcutType(), getId()); + if (isChecked) { + optInValueToSettings(getContext(), getShortcutType(), getId()); + } else { + optOutValueFromSettings(getContext(), getShortcutType(), getId()); + } } } diff --git a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java index 7831afb8798e..209778808764 100644 --- a/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTarget.java @@ -16,17 +16,11 @@ package com.android.internal.accessibility.dialog; -import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; -import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; -import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState; -import static com.android.internal.accessibility.util.ShortcutUtils.isComponentIdExistingInSettings; - import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; import android.content.ComponentName; import android.content.Context; import android.os.UserHandle; -import android.view.accessibility.Flags; import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; @@ -53,31 +47,9 @@ public class InvisibleToggleAccessibilityServiceTarget extends AccessibilityServ @Override public void onCheckedChanged(boolean isChecked) { + super.onCheckedChanged(isChecked); final ComponentName componentName = ComponentName.unflattenFromString(getId()); - - if (Flags.updateAlwaysOnA11yService()) { - super.onCheckedChanged(isChecked); - ShortcutUtils.updateInvisibleToggleAccessibilityServiceEnableState( - getContext(), Set.of(componentName.flattenToString()), UserHandle.myUserId()); - } else { - if (!isComponentIdExistingInOtherShortcut()) { - setAccessibilityServiceState(getContext(), componentName, isChecked); - } - - super.onCheckedChanged(isChecked); - } - } - - private boolean isComponentIdExistingInOtherShortcut() { - switch (getShortcutType()) { - case SOFTWARE: - return isComponentIdExistingInSettings(getContext(), UserShortcutType.HARDWARE, - getId()); - case HARDWARE: - return isComponentIdExistingInSettings(getContext(), UserShortcutType.SOFTWARE, - getId()); - default: - throw new IllegalStateException("Unexpected shortcut type"); - } + ShortcutUtils.updateInvisibleToggleAccessibilityServiceEnableState( + getContext(), Set.of(componentName.flattenToString()), UserHandle.myUserId()); } } diff --git a/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java index 753597914782..f9efb5059c09 100644 --- a/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/VolumeShortcutToggleAccessibilityServiceTarget.java @@ -17,17 +17,11 @@ package com.android.internal.accessibility.dialog; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; -import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; -import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState; -import static com.android.internal.accessibility.util.ShortcutUtils.optOutValueFromSettings; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.NonNull; -import android.content.ComponentName; import android.content.Context; -import android.widget.Toast; -import com.android.internal.R; import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; @@ -47,30 +41,10 @@ class VolumeShortcutToggleAccessibilityServiceTarget extends AccessibilityServic @Override public void onCheckedChanged(boolean isChecked) { - switch (getShortcutType()) { - case SOFTWARE: - onCheckedFromAccessibilityButton(isChecked); - return; - case HARDWARE: - super.onCheckedChanged(isChecked); - return; - default: - throw new IllegalStateException("Unexpected shortcut type"); - } - } - - private void onCheckedFromAccessibilityButton(boolean isChecked) { - setShortcutEnabled(isChecked); - final ComponentName componentName = ComponentName.unflattenFromString(getId()); - setAccessibilityServiceState(getContext(), componentName, isChecked); - - if (!isChecked) { - optOutValueFromSettings(getContext(), UserShortcutType.HARDWARE, getId()); - - final String warningText = - getContext().getString(R.string.accessibility_uncheck_legacy_item_warning, - getLabel()); - Toast.makeText(getContext(), warningText, Toast.LENGTH_SHORT).show(); + if (getShortcutType() == HARDWARE) { + super.onCheckedChanged(isChecked); + } else { + throw new IllegalStateException("Unexpected shortcut type"); } } } diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 6bd273bc1aeb..4f5544172edf 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -28,6 +28,7 @@ import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER; +import static com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER; import static com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE; import android.annotation.Nullable; @@ -190,7 +191,7 @@ public class IntentForwarderActivity extends Activity { .thenApplyAsync(targetResolveInfo -> { if (isResolverActivityResolveInfo(targetResolveInfo)) { launchResolverActivityWithCorrectTab(intentReceived, className, newIntent, - callingUserId, targetUserId); + callingUserId, targetUserId, false); // When switching to the personal profile, automatically start the activity } else if (className.equals(FORWARD_INTENT_TO_PARENT)) { startActivityAsCaller(newIntent, targetUserId); @@ -218,7 +219,7 @@ public class IntentForwarderActivity extends Activity { .thenAcceptAsync(targetResolveInfo -> { if (isResolverActivityResolveInfo(targetResolveInfo)) { launchResolverActivityWithCorrectTab(intentReceived, className, newIntent, - callingUserId, targetUserId); + callingUserId, targetUserId, true); } else { maybeShowUserConsentMiniResolverPrivate(targetResolveInfo, newIntent, targetUserId); @@ -490,7 +491,7 @@ public class IntentForwarderActivity extends Activity { } private void launchResolverActivityWithCorrectTab(Intent intentReceived, String className, - Intent newIntent, int callingUserId, int targetUserId) { + Intent newIntent, int callingUserId, int targetUserId, boolean singleTabOnly) { // When showing the intent resolver, instead of forwarding to the other profile, // we launch it in the current user and select the other tab. This fixes b/155874820. // @@ -505,6 +506,9 @@ public class IntentForwarderActivity extends Activity { sanitizeIntent(intentReceived); intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile); intentReceived.putExtra(EXTRA_CALLING_USER, UserHandle.of(callingUserId)); + if (singleTabOnly) { + intentReceived.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true); + } startActivityAsCaller(intentReceived, null, false, userId); finish(); } diff --git a/core/java/com/android/internal/compat/Android.bp b/core/java/com/android/internal/compat/Android.bp index 9ff05a6589b7..8253aa89b979 100644 --- a/core/java/com/android/internal/compat/Android.bp +++ b/core/java/com/android/internal/compat/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "compat_logging_flags", package: "com.android.internal.compat.flags", + container: "system", srcs: [ "compat_logging_flags.aconfig", ], 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 a5c31edde473..4f5162693408 100644 --- a/core/java/com/android/internal/compat/compat_logging_flags.aconfig +++ b/core/java/com/android/internal/compat/compat_logging_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.internal.compat.flags" +container: "system" flag { name: "skip_old_and_disabled_compat_logging" @@ -6,4 +7,4 @@ flag { 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 -}
\ No newline at end of file +} diff --git a/core/java/com/android/internal/foldables/Android.bp b/core/java/com/android/internal/foldables/Android.bp index f1d06da98186..53a9be5fdb0e 100644 --- a/core/java/com/android/internal/foldables/Android.bp +++ b/core/java/com/android/internal/foldables/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "fold_lock_setting_flags", package: "com.android.internal.foldables.flags", + container: "system", srcs: [ "fold_lock_setting_flags.aconfig", ], diff --git a/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig b/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig index d73e62373732..03b6a5bcf467 100644 --- a/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig +++ b/core/java/com/android/internal/foldables/fold_lock_setting_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.internal.foldables.flags" +container: "system" flag { name: "fold_lock_setting_enabled" diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java index 2096ba42080f..d24487412313 100644 --- a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java @@ -66,6 +66,7 @@ public class LegacyProtoLogImpl implements IProtoLog { private final TraceBuffer mBuffer; private final LegacyProtoLogViewerConfigReader mViewerConfig; private final TreeMap<String, IProtoLogGroup> mLogGroups; + private final Runnable mCacheUpdater; private final int mPerChunkSize; private boolean mProtoLogEnabled; @@ -73,20 +74,21 @@ public class LegacyProtoLogImpl implements IProtoLog { private final Object mProtoLogEnabledLock = new Object(); public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename, - TreeMap<String, IProtoLogGroup> logGroups) { + TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) { this(new File(outputFile), viewerConfigFilename, BUFFER_CAPACITY, - new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE, logGroups); + new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE, logGroups, cacheUpdater); } public LegacyProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity, LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize, - TreeMap<String, IProtoLogGroup> logGroups) { + TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) { mLogFile = file; mBuffer = new TraceBuffer(bufferCapacity); mLegacyViewerConfigFilename = viewerConfigFilename; mViewerConfig = viewerConfig; mPerChunkSize = perChunkSize; - this.mLogGroups = logGroups; + mLogGroups = logGroups; + mCacheUpdater = cacheUpdater; } /** @@ -285,6 +287,8 @@ public class LegacyProtoLogImpl implements IProtoLog { return -1; } } + + mCacheUpdater.run(); return 0; } @@ -399,5 +403,12 @@ public class LegacyProtoLogImpl implements IProtoLog { public int stopLoggingToLogcat(String[] groups, ILogger logger) { return setLogging(true /* setTextLogging */, false, logger, groups); } + + @Override + public boolean isEnabled(IProtoLogGroup group, LogLevel level) { + // In legacy logging we just enable an entire group at a time without more granular control, + // so we ignore the level argument to this function. + return group.isLogToLogcat() || (group.isLogToProto() && isProtoEnabled()); + } } diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 561ca2178966..9f3ce8163bf3 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -52,6 +52,7 @@ import android.tracing.perfetto.DataSourceParams; import android.tracing.perfetto.InitArguments; import android.tracing.perfetto.Producer; import android.tracing.perfetto.TracingContext; +import android.util.ArrayMap; import android.util.LongArray; import android.util.Slog; import android.util.proto.ProtoInputStream; @@ -70,6 +71,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -83,18 +85,22 @@ public class PerfettoProtoLogImpl implements IProtoLog { private final AtomicInteger mTracingInstances = new AtomicInteger(); private final ProtoLogDataSource mDataSource = new ProtoLogDataSource( - this.mTracingInstances::incrementAndGet, + this::onTracingInstanceStart, this::dumpTransitionTraceConfig, - this.mTracingInstances::decrementAndGet + this::onTracingInstanceStop ); private final ProtoLogViewerConfigReader mViewerConfigReader; private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; private final TreeMap<String, IProtoLogGroup> mLogGroups; + private final Runnable mCacheUpdater; + + private final Map<LogLevel, Integer> mDefaultLogLevelCounts = new ArrayMap<>(); + private final Map<IProtoLogGroup, Map<LogLevel, Integer>> mLogLevelCounts = new ArrayMap<>(); private final ExecutorService mBackgroundLoggingService = Executors.newCachedThreadPool(); public PerfettoProtoLogImpl(String viewerConfigFilePath, - TreeMap<String, IProtoLogGroup> logGroups) { + TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) { this(() -> { try { return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); @@ -102,28 +108,32 @@ public class PerfettoProtoLogImpl implements IProtoLog { Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e); return null; } - }, logGroups); + }, logGroups, cacheUpdater); } public PerfettoProtoLogImpl( ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, - TreeMap<String, IProtoLogGroup> logGroups + TreeMap<String, IProtoLogGroup> logGroups, + Runnable cacheUpdater ) { this(viewerConfigInputStreamProvider, - new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider), logGroups); + new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider), logGroups, + cacheUpdater); } @VisibleForTesting public PerfettoProtoLogImpl( ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, ProtoLogViewerConfigReader viewerConfigReader, - TreeMap<String, IProtoLogGroup> logGroups + TreeMap<String, IProtoLogGroup> logGroups, + Runnable cacheUpdater ) { Producer.init(InitArguments.DEFAULTS); mDataSource.register(DataSourceParams.DEFAULTS); this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; this.mViewerConfigReader = viewerConfigReader; this.mLogGroups = logGroups; + this.mCacheUpdater = cacheUpdater; } /** @@ -494,6 +504,29 @@ public class PerfettoProtoLogImpl implements IProtoLog { return setTextLogging(false, logger, groups); } + @Override + public boolean isEnabled(IProtoLogGroup group, LogLevel level) { + return group.isLogToLogcat() || getLogFromLevel(group).ordinal() <= level.ordinal(); + } + + private LogLevel getLogFromLevel(IProtoLogGroup group) { + if (mLogLevelCounts.containsKey(group)) { + for (LogLevel logLevel : LogLevel.values()) { + if (mLogLevelCounts.get(group).getOrDefault(logLevel, 0) > 0) { + return logLevel; + } + } + } else { + for (LogLevel logLevel : LogLevel.values()) { + if (mDefaultLogLevelCounts.getOrDefault(logLevel, 0) > 0) { + return logLevel; + } + } + } + + return LogLevel.WTF; + } + /** * Start logging the stack trace of the when the log message happened for target groups * @return status code @@ -521,6 +554,8 @@ public class PerfettoProtoLogImpl implements IProtoLog { return -1; } } + + mCacheUpdater.run(); return 0; } @@ -567,6 +602,61 @@ public class PerfettoProtoLogImpl implements IProtoLog { return -1; } + private synchronized void onTracingInstanceStart(ProtoLogDataSource.ProtoLogConfig config) { + this.mTracingInstances.incrementAndGet(); + + final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; + mDefaultLogLevelCounts.put(defaultLogFrom, + mDefaultLogLevelCounts.getOrDefault(defaultLogFrom, 0) + 1); + + final Set<String> overriddenGroupTags = config.getGroupTagsWithOverriddenConfigs(); + + for (String overriddenGroupTag : overriddenGroupTags) { + IProtoLogGroup group = mLogGroups.get(overriddenGroupTag); + + mLogLevelCounts.putIfAbsent(group, new ArrayMap<>()); + final Map<LogLevel, Integer> logLevelsCountsForGroup = mLogLevelCounts.get(group); + + final LogLevel logFromLevel = config.getConfigFor(overriddenGroupTag).logFrom; + logLevelsCountsForGroup.put(logFromLevel, + logLevelsCountsForGroup.getOrDefault(logFromLevel, 0) + 1); + } + + mCacheUpdater.run(); + } + + private synchronized void onTracingInstanceStop(ProtoLogDataSource.ProtoLogConfig config) { + this.mTracingInstances.decrementAndGet(); + + final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; + mDefaultLogLevelCounts.put(defaultLogFrom, + mDefaultLogLevelCounts.get(defaultLogFrom) - 1); + if (mDefaultLogLevelCounts.get(defaultLogFrom) <= 0) { + mDefaultLogLevelCounts.remove(defaultLogFrom); + } + + final Set<String> overriddenGroupTags = config.getGroupTagsWithOverriddenConfigs(); + + for (String overriddenGroupTag : overriddenGroupTags) { + IProtoLogGroup group = mLogGroups.get(overriddenGroupTag); + + mLogLevelCounts.putIfAbsent(group, new ArrayMap<>()); + final Map<LogLevel, Integer> logLevelsCountsForGroup = mLogLevelCounts.get(group); + + final LogLevel logFromLevel = config.getConfigFor(overriddenGroupTag).logFrom; + logLevelsCountsForGroup.put(logFromLevel, + logLevelsCountsForGroup.get(logFromLevel) - 1); + if (logLevelsCountsForGroup.get(logFromLevel) <= 0) { + logLevelsCountsForGroup.remove(logFromLevel); + } + if (logLevelsCountsForGroup.isEmpty()) { + mLogLevelCounts.remove(group); + } + } + + mCacheUpdater.run(); + } + static void logAndPrintln(@Nullable PrintWriter pw, String msg) { Slog.i(LOG_TAG, msg); if (pw != null) { diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java index a2d5e70ee412..e79bf36343db 100644 --- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java +++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java @@ -39,16 +39,19 @@ import com.android.internal.protolog.common.LogLevel; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, ProtoLogDataSource.TlsState, ProtoLogDataSource.IncrementalState> { - private final Runnable mOnStart; + private final Consumer<ProtoLogConfig> mOnStart; private final Runnable mOnFlush; - private final Runnable mOnStop; + private final Consumer<ProtoLogConfig> mOnStop; - public ProtoLogDataSource(Runnable onStart, Runnable onFlush, Runnable onStop) { + public ProtoLogDataSource(Consumer<ProtoLogConfig> onStart, Runnable onFlush, + Consumer<ProtoLogConfig> onStop) { super("android.protolog"); this.mOnStart = onStart; this.mOnFlush = onFlush; @@ -138,7 +141,7 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, public boolean clearReported = false; } - private static class ProtoLogConfig { + public static class ProtoLogConfig { private final LogLevel mDefaultLogFromLevel; private final Map<String, GroupConfig> mGroupConfigs; @@ -151,13 +154,17 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, this.mGroupConfigs = groupConfigs; } - private GroupConfig getConfigFor(String groupTag) { + public GroupConfig getConfigFor(String groupTag) { return mGroupConfigs.getOrDefault(groupTag, getDefaultGroupConfig()); } - private GroupConfig getDefaultGroupConfig() { + public GroupConfig getDefaultGroupConfig() { return new GroupConfig(mDefaultLogFromLevel, false); } + + public Set<String> getGroupTagsWithOverriddenConfigs() { + return mGroupConfigs.keySet(); + } } public static class GroupConfig { @@ -255,18 +262,18 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, public static class Instance extends DataSourceInstance { - private final Runnable mOnStart; + private final Consumer<ProtoLogConfig> mOnStart; private final Runnable mOnFlush; - private final Runnable mOnStop; + private final Consumer<ProtoLogConfig> mOnStop; private final ProtoLogConfig mConfig; public Instance( DataSource<Instance, TlsState, IncrementalState> dataSource, int instanceIdx, ProtoLogConfig config, - Runnable onStart, + Consumer<ProtoLogConfig> onStart, Runnable onFlush, - Runnable onStop + Consumer<ProtoLogConfig> onStop ) { super(dataSource, instanceIdx); this.mOnStart = onStart; @@ -277,7 +284,7 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, @Override public void onStart(StartCallbackArguments args) { - this.mOnStart.run(); + this.mOnStart.accept(this.mConfig); } @Override @@ -287,7 +294,7 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, @Override public void onStop(StopCallbackArguments args) { - this.mOnStop.run(); + this.mOnStop.accept(this.mConfig); } } } diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 487ae8145546..6d142afce626 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -16,6 +16,7 @@ package com.android.internal.protolog; +import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.CACHE_UPDATER; import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_OUTPUT_FILE_PATH; import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LEGACY_VIEWER_CONFIG_PATH; import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LOG_GROUPS; @@ -49,6 +50,9 @@ public class ProtoLogImpl { @ProtoLogToolInjected(LOG_GROUPS) private static TreeMap<String, IProtoLogGroup> sLogGroups; + @ProtoLogToolInjected(CACHE_UPDATER) + private static Runnable sCacheUpdater; + /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ public static void d(IProtoLogGroup group, long messageHash, int paramsMask, @Nullable String messageString, @@ -94,9 +98,12 @@ public class ProtoLogImpl { getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args); } - public static boolean isEnabled(IProtoLogGroup group) { - // TODO: Implement for performance reasons, with optional level parameter? - return true; + /** + * Should return true iff we should be logging to either protolog or logcat for this group + * and log level. + */ + public static boolean isEnabled(IProtoLogGroup group, LogLevel level) { + return getSingleInstance().isEnabled(group, level); } /** @@ -105,11 +112,14 @@ public class ProtoLogImpl { public static synchronized IProtoLog getSingleInstance() { if (sServiceInstance == null) { if (android.tracing.Flags.perfettoProtologTracing()) { - sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sLogGroups); + sServiceInstance = new PerfettoProtoLogImpl( + sViewerConfigPath, sLogGroups, sCacheUpdater); } else { sServiceInstance = new LegacyProtoLogImpl( - sLegacyOutputFilePath, sLegacyViewerConfigPath, sLogGroups); + sLegacyOutputFilePath, sLegacyViewerConfigPath, sLogGroups, sCacheUpdater); } + + sCacheUpdater.run(); } return sServiceInstance; } diff --git a/core/java/com/android/internal/protolog/common/IProtoLog.java b/core/java/com/android/internal/protolog/common/IProtoLog.java index c06d14b2075e..f72d9f79958d 100644 --- a/core/java/com/android/internal/protolog/common/IProtoLog.java +++ b/core/java/com/android/internal/protolog/common/IProtoLog.java @@ -52,4 +52,12 @@ public interface IProtoLog { * @return status code */ int stopLoggingToLogcat(String[] groups, ILogger logger); + + /** + * Should return true iff logging is enabled to ProtoLog or to Logcat for this group and level. + * @param group ProtoLog group to check for. + * @param level ProtoLog level to check for. + * @return If we need to log this group and level to either ProtoLog or Logcat. + */ + boolean isEnabled(IProtoLogGroup group, LogLevel level); } diff --git a/core/java/com/android/internal/protolog/common/IProtoLogGroup.java b/core/java/com/android/internal/protolog/common/IProtoLogGroup.java index 4e9686f99f4b..149aa7aa7170 100644 --- a/core/java/com/android/internal/protolog/common/IProtoLogGroup.java +++ b/core/java/com/android/internal/protolog/common/IProtoLogGroup.java @@ -38,6 +38,7 @@ public interface IProtoLogGroup { /** * returns true is any logging is enabled for this group. + * @deprecated TODO(b/324128613) remove once we migrate fully to Perfetto */ default boolean isLogToAny() { return isLogToLogcat() || isLogToProto(); @@ -50,6 +51,7 @@ public interface IProtoLogGroup { /** * set binary logging for this group. + * @deprecated TODO(b/324128613) remove once we migrate fully to Perfetto */ void setLogToProto(boolean logToProto); diff --git a/core/java/com/android/internal/protolog/common/ProtoLog.java b/core/java/com/android/internal/protolog/common/ProtoLog.java index 18e3f66c4795..8149cd5bbd3b 100644 --- a/core/java/com/android/internal/protolog/common/ProtoLog.java +++ b/core/java/com/android/internal/protolog/common/ProtoLog.java @@ -135,7 +135,7 @@ public class ProtoLog { * @param group Group to check enable status of. * @return true iff this is being logged. */ - public static boolean isEnabled(IProtoLogGroup group) { + public static boolean isEnabled(IProtoLogGroup group, LogLevel level) { if (REQUIRE_PROTOLOGTOOL) { throw new UnsupportedOperationException( "ProtoLog calls MUST be processed with ProtoLogTool"); diff --git a/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java index 17c82d76408e..2d39f3b4e152 100644 --- a/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java +++ b/core/java/com/android/internal/protolog/common/ProtoLogToolInjected.java @@ -23,7 +23,11 @@ import java.lang.annotation.Target; @Target({ElementType.FIELD, ElementType.PARAMETER}) public @interface ProtoLogToolInjected { enum Value { - VIEWER_CONFIG_PATH, LEGACY_OUTPUT_FILE_PATH, LEGACY_VIEWER_CONFIG_PATH, LOG_GROUPS + VIEWER_CONFIG_PATH, + LEGACY_OUTPUT_FILE_PATH, + LEGACY_VIEWER_CONFIG_PATH, + LOG_GROUPS, + CACHE_UPDATER } Value value(); diff --git a/core/jni/OWNERS b/core/jni/OWNERS index 593bdf0812aa..163f32e022e6 100644 --- a/core/jni/OWNERS +++ b/core/jni/OWNERS @@ -110,3 +110,6 @@ per-file android_database_SQLite* = file:/SQLITE_OWNERS # PerformanceHintManager per-file android_os_PerformanceHintManager.cpp = file:/ADPF_OWNERS + +# IF Tools +per-file android_tracing_Perfetto* = file:platform/development:/tools/winscope/OWNERS diff --git a/core/jni/android_os_MessageQueue.cpp b/core/jni/android_os_MessageQueue.cpp index 9525605a6a8c..30d9ea19be39 100644 --- a/core/jni/android_os_MessageQueue.cpp +++ b/core/jni/android_os_MessageQueue.cpp @@ -225,7 +225,7 @@ static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj, nativeMessageQueue->pollOnce(env, obj, timeoutMillis); } -static void android_os_MessageQueue_nativeWake(jlong ptr) { +static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) { NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr); nativeMessageQueue->wake(); } diff --git a/core/jni/android_text_Hyphenator.cpp b/core/jni/android_text_Hyphenator.cpp index 7c976b7473f6..b6bf617b40ae 100644 --- a/core/jni/android_text_Hyphenator.cpp +++ b/core/jni/android_text_Hyphenator.cpp @@ -36,43 +36,41 @@ static std::string buildFileName(const std::string& locale) { return SYSTEM_HYPHENATOR_PREFIX + lowerLocale + SYSTEM_HYPHENATOR_SUFFIX; } -static std::pair<const uint8_t*, uint32_t> mmapPatternFile(const std::string& locale) { +static const uint8_t* mmapPatternFile(const std::string& locale) { const std::string hyFilePath = buildFileName(locale); const int fd = open(hyFilePath.c_str(), O_RDONLY | O_CLOEXEC); if (fd == -1) { - return std::make_pair(nullptr, 0); // Open failed. + return nullptr; // Open failed. } struct stat st = {}; if (fstat(fd, &st) == -1) { // Unlikely to happen. close(fd); - return std::make_pair(nullptr, 0); + return nullptr; } void* ptr = mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd, 0 /* offset */); close(fd); if (ptr == MAP_FAILED) { - return std::make_pair(nullptr, 0); + return nullptr; } - return std::make_pair(reinterpret_cast<const uint8_t*>(ptr), st.st_size); + return reinterpret_cast<const uint8_t*>(ptr); } static void addHyphenatorWithoutPatternFile(const std::string& locale, int minPrefix, int minSuffix) { - minikin::addHyphenator(locale, - minikin::Hyphenator::loadBinary(nullptr, 0, minPrefix, minSuffix, - locale)); + minikin::addHyphenator(locale, minikin::Hyphenator::loadBinary( + nullptr, minPrefix, minSuffix, locale)); } static void addHyphenator(const std::string& locale, int minPrefix, int minSuffix) { - auto [ptr, size] = mmapPatternFile(locale); + const uint8_t* ptr = mmapPatternFile(locale); if (ptr == nullptr) { ALOGE("Unable to find pattern file or unable to map it for %s", locale.c_str()); return; } - minikin::addHyphenator(locale, - minikin::Hyphenator::loadBinary(ptr, size, minPrefix, minSuffix, - locale)); + minikin::addHyphenator(locale, minikin::Hyphenator::loadBinary( + ptr, minPrefix, minSuffix, locale)); } static void addHyphenatorAlias(const std::string& from, const std::string& to) { diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp index 1eff5ce8eaa3..25ff853ae7e4 100644 --- a/core/jni/android_tracing_PerfettoDataSource.cpp +++ b/core/jni/android_tracing_PerfettoDataSource.cpp @@ -213,7 +213,7 @@ void PerfettoDataSource::flushAll() { PerfettoDataSource::~PerfettoDataSource() { JNIEnv* env = AndroidRuntime::getJNIEnv(); - env->DeleteWeakGlobalRef(mJavaDataSource); + env->DeleteGlobalRef(mJavaDataSource); } jlong nativeCreate(JNIEnv* env, jclass clazz, jobject javaDataSource, jstring name) { diff --git a/core/proto/OWNERS b/core/proto/OWNERS index b900fa60ff70..b51f72dee260 100644 --- a/core/proto/OWNERS +++ b/core/proto/OWNERS @@ -11,7 +11,6 @@ zhouwenjie@google.com # Frameworks ogunwale@google.com jjaggi@google.com -kwekua@google.com roosa@google.com per-file package_item_info.proto = file:/PACKAGE_MANAGER_OWNERS per-file usagestatsservice.proto, usagestatsservice_v2.proto = file:/core/java/android/app/usage/OWNERS diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d4256ca316c2..f74329903690 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7507,16 +7507,6 @@ <permission android:name="android.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO" android:protectionLevel="signature|privileged|appop" /> - <!-- @SystemApi Required for the privileged assistant apps targeting - {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} - that receive training data from a sandboxed {@link HotwordDetectionService} or - {@link VisualQueryDetectionService}. - <p>Protection level: internal|appop - @FlaggedApi("android.permission.flags.voice_activation_permission_apis") - @hide --> - <permission android:name="android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA" - android:protectionLevel="internal|appop" /> - <!-- @SystemApi Allows requesting the framework broadcast the {@link Intent#ACTION_DEVICE_CUSTOMIZATION_READY} intent. @hide --> diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index a2a5433eca24..c7d5825733ae 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -175,10 +175,12 @@ class FontScaleConverterFactoryTest { assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(-1f)).isFalse() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0.85f)).isFalse() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.02f)).isFalse() + assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.05f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.15f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.1499999f)) .isTrue() + assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.2f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.5f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(2f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(3f)).isTrue() diff --git a/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java b/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java index 66f3bca72aeb..ca9154280a10 100644 --- a/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java +++ b/core/tests/coretests/src/android/hardware/biometrics/BiometricPromptTest.java @@ -16,6 +16,14 @@ package android.hardware.biometrics; +import static android.hardware.biometrics.BiometricPrompt.MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER; +import static android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.MAX_DESCRIPTION_CHARACTER_NUMBER; +import static android.hardware.biometrics.PromptVerticalListContentView.MAX_EACH_ITEM_CHARACTER_NUMBER; +import static android.hardware.biometrics.PromptVerticalListContentView.MAX_ITEM_NUMBER; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -40,6 +48,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoRule; +import java.util.Random; import java.util.concurrent.Executor; @@ -83,10 +92,11 @@ public class BiometricPromptTest { ArgumentCaptor.forClass(IBiometricServiceReceiver.class); BiometricPrompt.AuthenticationCallback callback = new BiometricPrompt.AuthenticationCallback() { - @Override - public void onAuthenticationError(int errorCode, CharSequence errString) { - super.onAuthenticationError(errorCode, errString); - }}; + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + } + }; mBiometricPrompt.authenticate(mCancellationSignal, mExecutor, callback); mLooper.dispatchAll(); @@ -99,4 +109,112 @@ public class BiometricPromptTest { verify(mService).cancelAuthentication(any(), anyString(), anyLong()); } + + @Test + public void testLogoDescription_null() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new BiometricPrompt.Builder(mContext).setLogoDescription(null) + ); + + assertThat(e).hasMessageThat().contains( + "Logo description passed in can not be null or exceed"); + } + + @Test + public void testLogoDescription_charLimit() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new BiometricPrompt.Builder(mContext).setLogoDescription( + generateRandomString(MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER + 1)) + ); + + assertThat(e).hasMessageThat().contains( + "Logo description passed in can not be null or exceed"); + } + + @Test + public void testMoreOptionsButton_descriptionCharLimit() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new PromptContentViewWithMoreOptionsButton.Builder().setDescription( + generateRandomString(MAX_DESCRIPTION_CHARACTER_NUMBER + 1)) + ); + + assertThat(e).hasMessageThat().contains( + "The character number of description exceeds "); + } + + @Test + public void testMoreOptionsButton_ExecutorNull() { + PromptContentViewWithMoreOptionsButton.Builder builder = + new PromptContentViewWithMoreOptionsButton.Builder().setMoreOptionsButtonListener( + null, null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + builder::build + ); + + assertThat(e).hasMessageThat().contains( + "The executor for the listener of more options button on prompt content must be " + + "set"); + } + + @Test + public void testMoreOptionsButton_ListenerNull() { + PromptContentViewWithMoreOptionsButton.Builder builder = + new PromptContentViewWithMoreOptionsButton.Builder().setMoreOptionsButtonListener( + mExecutor, null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + builder::build + ); + + assertThat(e).hasMessageThat().contains( + "The listener of more options button on prompt content must be set"); + } + + @Test + public void testVerticalList_descriptionCharLimit() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new PromptVerticalListContentView.Builder().setDescription( + generateRandomString(MAX_DESCRIPTION_CHARACTER_NUMBER + 1)) + ); + + assertThat(e).hasMessageThat().contains( + "The character number of description exceeds "); + } + + @Test + public void testVerticalList_itemCharLimit() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new PromptVerticalListContentView.Builder().addListItem( + new PromptContentItemBulletedText( + generateRandomString(MAX_EACH_ITEM_CHARACTER_NUMBER + 1))) + ); + + assertThat(e).hasMessageThat().contains( + "The character number of list item exceeds "); + } + + @Test + public void testVerticalList_itemNumLimit() { + PromptVerticalListContentView.Builder builder = new PromptVerticalListContentView.Builder(); + + for (int i = 0; i < MAX_ITEM_NUMBER; i++) { + builder.addListItem(new PromptContentItemBulletedText(generateRandomString(10))); + } + + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> builder.addListItem( + new PromptContentItemBulletedText(generateRandomString(10))) + ); + + assertThat(e).hasMessageThat().contains( + "The number of list items exceeds "); + } + + private String generateRandomString(int charNum) { + final Random random = new Random(); + final StringBuilder longString = new StringBuilder(charNum); + for (int j = 0; j < charNum; j++) { + longString.append(random.nextInt(10)); + } + return longString.toString(); + } } diff --git a/core/tests/coretests/src/android/view/MotionEventTest.java b/core/tests/coretests/src/android/view/MotionEventTest.java index c4c983d24af9..bad048523053 100644 --- a/core/tests/coretests/src/android/view/MotionEventTest.java +++ b/core/tests/coretests/src/android/view/MotionEventTest.java @@ -26,6 +26,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import android.graphics.Matrix; import android.platform.test.annotations.Presubmit; @@ -47,21 +48,25 @@ import java.util.Set; public class MotionEventTest { private static final int ID_SOURCE_MASK = 0x3 << 30; + private PointerCoords pointerCoords(float x, float y) { + final var coords = new PointerCoords(); + coords.x = x; + coords.y = y; + return coords; + } + + private PointerProperties fingerProperties(int id) { + final var props = new PointerProperties(); + props.id = id; + props.toolType = TOOL_TYPE_FINGER; + return props; + } + @Test public void testObtainWithDisplayId() { final int pointerCount = 1; - PointerProperties[] properties = new PointerProperties[pointerCount]; - final PointerCoords[] coords = new PointerCoords[pointerCount]; - for (int i = 0; i < pointerCount; i++) { - final PointerCoords c = new PointerCoords(); - c.x = i * 10; - c.y = i * 20; - coords[i] = c; - final PointerProperties p = new PointerProperties(); - p.id = i; - p.toolType = TOOL_TYPE_FINGER; - properties[i] = p; - } + final var properties = new PointerProperties[]{fingerProperties(0)}; + final var coords = new PointerCoords[]{pointerCoords(10, 20)}; int displayId = 2; MotionEvent motionEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, @@ -125,18 +130,8 @@ public class MotionEventTest { @Test public void testCalculatesCursorPositionForMultiTouchMouseEvents() { final int pointerCount = 2; - final PointerProperties[] properties = new PointerProperties[pointerCount]; - final PointerCoords[] coords = new PointerCoords[pointerCount]; - - for (int i = 0; i < pointerCount; ++i) { - properties[i] = new PointerProperties(); - properties[i].id = i; - properties[i].toolType = MotionEvent.TOOL_TYPE_FINGER; - - coords[i] = new PointerCoords(); - coords[i].x = 20 + i * 20; - coords[i].y = 60 - i * 20; - } + final var properties = new PointerProperties[]{fingerProperties(0), fingerProperties(1)}; + final var coords = new PointerCoords[]{pointerCoords(20, 60), pointerCoords(40, 40)}; final MotionEvent event = MotionEvent.obtain(0 /* downTime */, 0 /* eventTime */, ACTION_POINTER_DOWN, pointerCount, properties, coords, @@ -238,4 +233,66 @@ public class MotionEventTest { assertEquals(10, (int) event.getX()); assertEquals(20, (int) event.getY()); } + + @Test + public void testSplit() { + final int pointerCount = 2; + final var properties = new PointerProperties[]{fingerProperties(0), fingerProperties(1)}; + final var coords = new PointerCoords[]{pointerCoords(20, 60), pointerCoords(40, 40)}; + + final MotionEvent event = MotionEvent.obtain(0 /* downTime */, + 0 /* eventTime */, MotionEvent.ACTION_MOVE, pointerCount, properties, coords, + 0 /* metaState */, 0 /* buttonState */, 1 /* xPrecision */, 1 /* yPrecision */, + 0 /* deviceId */, 0 /* edgeFlags */, InputDevice.SOURCE_TOUCHSCREEN, + 0 /* flags */); + + final int idBits = ~0b1 & event.getPointerIdBits(); + final MotionEvent splitEvent = event.split(idBits); + assertEquals(1, splitEvent.getPointerCount()); + assertEquals(1, splitEvent.getPointerId(0)); + assertEquals(40, (int) splitEvent.getX()); + assertEquals(40, (int) splitEvent.getY()); + } + + @Test + public void testSplitFailsWhenNoIdsSpecified() { + final int pointerCount = 2; + final var properties = new PointerProperties[]{fingerProperties(0), fingerProperties(1)}; + final var coords = new PointerCoords[]{pointerCoords(20, 60), pointerCoords(40, 40)}; + + final MotionEvent event = MotionEvent.obtain(0 /* downTime */, + 0 /* eventTime */, MotionEvent.ACTION_MOVE, pointerCount, properties, coords, + 0 /* metaState */, 0 /* buttonState */, 1 /* xPrecision */, 1 /* yPrecision */, + 0 /* deviceId */, 0 /* edgeFlags */, InputDevice.SOURCE_TOUCHSCREEN, + 0 /* flags */); + + try { + final MotionEvent splitEvent = event.split(0); + fail("Splitting event with id bits 0 should throw: " + splitEvent); + } catch (IllegalArgumentException e) { + // Expected + } + } + + @Test + public void testSplitFailsWhenIdBitsDoNotMatch() { + final int pointerCount = 2; + final var properties = new PointerProperties[]{fingerProperties(0), fingerProperties(1)}; + final var coords = new PointerCoords[]{pointerCoords(20, 60), pointerCoords(40, 40)}; + + final MotionEvent event = MotionEvent.obtain(0 /* downTime */, + 0 /* eventTime */, MotionEvent.ACTION_MOVE, pointerCount, properties, coords, + 0 /* metaState */, 0 /* buttonState */, 1 /* xPrecision */, 1 /* yPrecision */, + 0 /* deviceId */, 0 /* edgeFlags */, InputDevice.SOURCE_TOUCHSCREEN, + 0 /* flags */); + + try { + final int idBits = 0b100; + final MotionEvent splitEvent = event.split(idBits); + fail("Splitting event with id bits that do not match any pointers should throw: " + + splitEvent); + } catch (IllegalArgumentException e) { + // Expected + } + } } diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java index 180521ba7b70..5fab1a0f3c8e 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java @@ -64,9 +64,9 @@ import android.os.Build; import android.os.Handler; import android.os.Message; import android.os.Vibrator; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.speech.tts.TextToSpeech; import android.speech.tts.Voice; @@ -91,6 +91,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; @@ -98,14 +99,14 @@ import org.mockito.invocation.InvocationOnMock; import java.lang.reflect.Field; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @RunWith(AndroidJUnit4.class) public class AccessibilityShortcutControllerTest { @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final String SERVICE_NAME_STRING = "fake.package/fake.service.name"; private static final CharSequence PACKAGE_NAME_STRING = "Service name"; private static final String SERVICE_NAME_SUMMARY = "Summary"; @@ -134,6 +135,7 @@ public class AccessibilityShortcutControllerTest { private @Mock TextToSpeech mTextToSpeech; private @Mock Voice mVoice; private @Mock Ringtone mRingtone; + private @Captor ArgumentCaptor<List<String>> mListCaptor; private MockContentResolver mContentResolver; private WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams(); @@ -418,6 +420,7 @@ public class AccessibilityShortcutControllerTest { } @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void testClickingDisableButtonInDialog_shouldClearShortcutId() throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureValidShortcutService(); @@ -431,6 +434,29 @@ public class AccessibilityShortcutControllerTest { captor.capture()); captor.getValue().onClick(null, DialogInterface.BUTTON_POSITIVE); + verify(mAccessibilityManagerService).enableShortcutsForTargets( + eq(false), eq(HARDWARE), mListCaptor.capture(), anyInt()); + assertThat(mListCaptor.getValue()).containsExactly(SERVICE_NAME_STRING); + assertThat(Settings.Secure.getInt( + mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)).isEqualTo( + AccessibilityShortcutController.DialogStatus.NOT_SHOWN); + } + + @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void testClickingDisableButtonInDialog_shouldClearShortcutId_old() throws Exception { + configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); + configureValidShortcutService(); + Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, + AccessibilityShortcutController.DialogStatus.NOT_SHOWN); + getController().performAccessibilityShortcut(); + + ArgumentCaptor<DialogInterface.OnClickListener> captor = + ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mAlertDialogBuilder).setPositiveButton(eq(R.string.accessibility_shortcut_off), + captor.capture()); + captor.getValue().onClick(null, DialogInterface.BUTTON_POSITIVE); + assertThat( Settings.Secure.getString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE) ).isEmpty(); @@ -440,7 +466,8 @@ public class AccessibilityShortcutControllerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_UPDATE_ALWAYS_ON_A11Y_SERVICE) + @EnableFlags(Flags.FLAG_UPDATE_ALWAYS_ON_A11Y_SERVICE) + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void turnOffVolumeShortcutForAlwaysOnA11yService_shouldTurnOffA11yService() throws Exception { configureApplicationTargetSdkVersion(Build.VERSION_CODES.R); @@ -452,7 +479,8 @@ public class AccessibilityShortcutControllerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_UPDATE_ALWAYS_ON_A11Y_SERVICE) + @EnableFlags(Flags.FLAG_UPDATE_ALWAYS_ON_A11Y_SERVICE) + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void turnOffVolumeShortcutForAlwaysOnA11yService_hasOtherTypesShortcut_shouldNotTurnOffA11yService() throws Exception { configureApplicationTargetSdkVersion(Build.VERSION_CODES.R); @@ -499,6 +527,7 @@ public class AccessibilityShortcutControllerTest { } @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void testTurnOnDefaultA11yServiceInDialog_defaultServiceShortcutTurnsOn() throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); @@ -513,15 +542,39 @@ public class AccessibilityShortcutControllerTest { captor.capture()); captor.getValue().onClick(null, DialogInterface.BUTTON_NEGATIVE); - assertThat( - Settings.Secure.getString(mContentResolver, - ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)).isEqualTo(SERVICE_NAME_STRING); + verify(mAccessibilityManagerService).enableShortcutsForTargets( + eq(true), eq(HARDWARE), mListCaptor.capture(), anyInt()); + assertThat(mListCaptor.getValue()).containsExactly(SERVICE_NAME_STRING); assertThat(Settings.Secure.getInt( mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)).isEqualTo( AccessibilityShortcutController.DialogStatus.SHOWN); } @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void testTurnOnDefaultA11yServiceInDialog_defaultServiceShortcutTurnsOn_old() + throws Exception { + configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); + configureDefaultAccessibilityService(); + Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, + AccessibilityShortcutController.DialogStatus.NOT_SHOWN); + getController().performAccessibilityShortcut(); + + ArgumentCaptor<DialogInterface.OnClickListener> captor = + ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mAlertDialogBuilder).setNegativeButton(eq(R.string.accessibility_shortcut_on), + captor.capture()); + captor.getValue().onClick(null, DialogInterface.BUTTON_NEGATIVE); + + assertThat(Settings.Secure.getString(mContentResolver, + ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)).isEqualTo(SERVICE_NAME_STRING); + assertThat(Settings.Secure.getInt( + mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)).isEqualTo( + AccessibilityShortcutController.DialogStatus.SHOWN); + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void testTurnOffDefaultA11yServiceInDialog_defaultServiceShortcutTurnsOff() throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); @@ -536,9 +589,32 @@ public class AccessibilityShortcutControllerTest { captor.capture()); captor.getValue().onClick(null, DialogInterface.BUTTON_POSITIVE); - assertThat( - Settings.Secure.getString(mContentResolver, - ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)).isEmpty(); + verify(mAccessibilityManagerService).enableShortcutsForTargets( + eq(false), eq(HARDWARE), mListCaptor.capture(), anyInt()); + assertThat(mListCaptor.getValue()).isEmpty(); + assertThat(Settings.Secure.getInt( + mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)).isEqualTo( + AccessibilityShortcutController.DialogStatus.NOT_SHOWN); + } + + @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void testTurnOffDefaultA11yServiceInDialog_defaultServiceShortcutTurnsOff_old() + throws Exception { + configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); + configureDefaultAccessibilityService(); + Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, + AccessibilityShortcutController.DialogStatus.NOT_SHOWN); + getController().performAccessibilityShortcut(); + + ArgumentCaptor<DialogInterface.OnClickListener> captor = + ArgumentCaptor.forClass(DialogInterface.OnClickListener.class); + verify(mAlertDialogBuilder).setPositiveButton(eq(R.string.accessibility_shortcut_off), + captor.capture()); + captor.getValue().onClick(null, DialogInterface.BUTTON_POSITIVE); + + assertThat(Settings.Secure.getString(mContentResolver, + ACCESSIBILITY_SHORTCUT_TARGET_SERVICE)).isEmpty(); assertThat(Settings.Secure.getInt( mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN)).isEqualTo( AccessibilityShortcutController.DialogStatus.NOT_SHOWN); diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTargetTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTargetTest.java index 2ea044ccfb52..a14d8e034b32 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTargetTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/InvisibleToggleAccessibilityServiceTargetTest.java @@ -19,8 +19,10 @@ package com.android.internal.accessibility.dialog; 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.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.accessibilityservice.AccessibilityServiceInfo; @@ -31,8 +33,12 @@ import android.content.pm.ParceledListSlice; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.Flags; import android.view.accessibility.IAccessibilityManager; import androidx.test.platform.app.InstrumentationRegistry; @@ -47,10 +53,13 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Collections; +import java.util.List; /** * Unit Tests for @@ -59,9 +68,13 @@ import java.util.Collections; @RunWith(AndroidJUnit4.class) public class InvisibleToggleAccessibilityServiceTargetTest { @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); @Mock private IAccessibilityManager mAccessibilityManagerService; + @Captor + private ArgumentCaptor<List<String>> mListCaptor; private static final String ALWAYS_ON_SERVICE_PACKAGE_LABEL = "always on a11y service"; private static final String ALWAYS_ON_SERVICE_COMPONENT_NAME = @@ -104,6 +117,32 @@ public class InvisibleToggleAccessibilityServiceTargetTest { } @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void onCheckedChanged_true_callA11yManagerToUpdateShortcuts() throws Exception { + mSut.onCheckedChanged(true); + + verify(mAccessibilityManagerService).enableShortcutsForTargets( + eq(true), + eq(ShortcutConstants.UserShortcutType.HARDWARE), + mListCaptor.capture(), + anyInt()); + assertThat(mListCaptor.getValue()).containsExactly(ALWAYS_ON_SERVICE_COMPONENT_NAME); + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void onCheckedChanged_false_callA11yManagerToUpdateShortcuts() throws Exception { + mSut.onCheckedChanged(false); + verify(mAccessibilityManagerService).enableShortcutsForTargets( + eq(false), + eq(ShortcutConstants.UserShortcutType.HARDWARE), + mListCaptor.capture(), + anyInt()); + assertThat(mListCaptor.getValue()).containsExactly(ALWAYS_ON_SERVICE_COMPONENT_NAME); + } + + @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void onCheckedChanged_turnOnShortcut_hasOtherShortcut_serviceKeepsOn() { enableA11yService(/* enable= */ true); addShortcutForA11yService( @@ -116,6 +155,7 @@ public class InvisibleToggleAccessibilityServiceTargetTest { } @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void onCheckedChanged_turnOnShortcut_noOtherShortcut_shouldTurnOnService() { enableA11yService(/* enable= */ false); addShortcutForA11yService( @@ -128,6 +168,7 @@ public class InvisibleToggleAccessibilityServiceTargetTest { } @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void onCheckedChanged_turnOffShortcut_hasOtherShortcut_serviceKeepsOn() { enableA11yService(/* enable= */ true); addShortcutForA11yService( @@ -140,6 +181,7 @@ public class InvisibleToggleAccessibilityServiceTargetTest { } @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) public void onCheckedChanged_turnOffShortcut_noOtherShortcut_shouldTurnOffService() { enableA11yService(/* enable= */ true); addShortcutForA11yService( diff --git a/data/etc/core.protolog.pb b/data/etc/core.protolog.pb Binary files differindex 826adc39c19a..97147a01b259 100644 --- a/data/etc/core.protolog.pb +++ b/data/etc/core.protolog.pb diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index d410d5f5400e..6cf12deea928 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -709,18 +709,6 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" }, - "2959074735946674755": { - "message": "Trying to update display configuration for system\/invalid process.", - "level": "WARN", - "group": "WM_DEBUG_CONFIGURATION", - "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" - }, - "5668810920995272206": { - "message": "Trying to update display configuration for invalid process, pid=%d", - "level": "WARN", - "group": "WM_DEBUG_CONFIGURATION", - "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" - }, "-1123414663662718691": { "message": "setVr2dDisplayId called for: %d", "level": "DEBUG", @@ -3469,6 +3457,12 @@ "group": "WM_DEBUG_WALLPAPER", "at": "com\/android\/server\/wm\/WallpaperController.java" }, + "257349083882992098": { + "message": "updateWallpaperTokens requestedVisibility=%b on keyguardLocked=%b", + "level": "VERBOSE", + "group": "WM_DEBUG_WALLPAPER", + "at": "com\/android\/server\/wm\/WallpaperController.java" + }, "7408402065665963407": { "message": "Wallpaper at display %d - visibility: %b, keyguardLocked: %b", "level": "VERBOSE", diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl index f9347ee6ea09..e8b4104a33bb 100644 --- a/data/keyboards/Generic.kl +++ b/data/keyboards/Generic.kl @@ -424,12 +424,14 @@ key 580 APP_SWITCH key 582 VOICE_ASSIST # Linux KEY_ASSISTANT key 583 ASSIST +key 585 EMOJI_PICKER key 656 MACRO_1 key 657 MACRO_2 key 658 MACRO_3 key 659 MACRO_4 # Keys defined by HID usages +key usage 0x0c0065 SCREENSHOT FALLBACK_USAGE_MAPPING key usage 0x0c0067 WINDOW FALLBACK_USAGE_MAPPING key usage 0x0c006F BRIGHTNESS_UP FALLBACK_USAGE_MAPPING key usage 0x0c0070 BRIGHTNESS_DOWN FALLBACK_USAGE_MAPPING diff --git a/graphics/java/Android.bp b/graphics/java/Android.bp index ece453d1a70e..f4abd0ab582c 100644 --- a/graphics/java/Android.bp +++ b/graphics/java/Android.bp @@ -11,6 +11,7 @@ package { aconfig_declarations { name: "framework_graphics_flags", package: "com.android.graphics.flags", + container: "system", srcs: ["android/framework_graphics.aconfig"], } diff --git a/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig index 1e41b4d9ed1b..4ab09eb7e9f8 100644 --- a/graphics/java/android/framework_graphics.aconfig +++ b/graphics/java/android/framework_graphics.aconfig @@ -1,4 +1,5 @@ package: "com.android.graphics.flags" +container: "system" flag { name: "exact_compute_bounds" @@ -14,4 +15,4 @@ flag { namespace: "core_graphics" description: "Feature flag for YUV image compress to Ultra HDR." bug: "308978825" -}
\ No newline at end of file +} diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 250362b1e1e3..319f115d7427 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -41,12 +41,15 @@ import dalvik.annotation.optimization.CriticalNative; import libcore.util.NativeAllocationRegistry; import java.io.IOException; +import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.WeakHashMap; public final class Bitmap implements Parcelable { private static final String TAG = "Bitmap"; @@ -120,6 +123,11 @@ public final class Bitmap implements Parcelable { } /** + * @hide + */ + private static final WeakHashMap<Bitmap, Void> sAllBitmaps = new WeakHashMap<>(); + + /** * Private constructor that must receive an already allocated native bitmap * int (pointer). */ @@ -162,6 +170,9 @@ public final class Bitmap implements Parcelable { Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount); } registry.registerNativeAllocation(this, nativeBitmap); + synchronized (Bitmap.class) { + sAllBitmaps.put(this, null); + } } /** @@ -1510,6 +1521,86 @@ public final class Bitmap implements Parcelable { } /** + * @hide + */ + private static final class DumpData { + private int count; + private int format; + private long[] natives; + private byte[][] buffers; + private int max; + + public DumpData(@NonNull CompressFormat format, int max) { + this.max = max; + this.format = format.nativeInt; + this.natives = new long[max]; + this.buffers = new byte[max][]; + this.count = 0; + } + + public void add(long nativePtr, byte[] buffer) { + natives[count] = nativePtr; + buffers[count] = buffer; + count = (count >= max) ? max : count + 1; + } + + public int size() { + return count; + } + } + + /** + * @hide + */ + private static DumpData dumpData = null; + + + /** + * @hide + * + * Dump all the bitmaps with their contents compressed into dumpData + * + * @param format format of the compressed image, null to clear dump data + */ + public static void dumpAll(@Nullable String format) { + if (format == null) { + /* release the dump data */ + dumpData = null; + return; + } + final CompressFormat fmt; + if (format.equals("jpg") || format.equals("jpeg")) { + fmt = CompressFormat.JPEG; + } else if (format.equals("png")) { + fmt = CompressFormat.PNG; + } else if (format.equals("webp")) { + fmt = CompressFormat.WEBP_LOSSLESS; + } else { + Log.w(TAG, "No bitmaps dumped: unrecognized format " + format); + return; + } + + final ArrayList<Bitmap> allBitmaps; + synchronized (Bitmap.class) { + allBitmaps = new ArrayList<>(sAllBitmaps.size()); + for (Bitmap bitmap : sAllBitmaps.keySet()) { + if (bitmap != null && !bitmap.isRecycled()) { + allBitmaps.add(bitmap); + } + } + } + + dumpData = new DumpData(fmt, allBitmaps.size()); + for (Bitmap bitmap : allBitmaps) { + ByteArrayOutputStream bas = new ByteArrayOutputStream(); + if (bitmap.compress(fmt, 90, bas)) { + dumpData.add(bitmap.getNativeInstance(), bas.toByteArray()); + } + } + Log.i(TAG, dumpData.size() + "/" + allBitmaps.size() + " bitmaps dumped"); + } + + /** * Number of bytes of temp storage we use for communicating between the * native compressor and the java OutputStream. */ diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java index 2cac2e150919..2f2215fd51a2 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.StrictMode; /** * This class provides some constants and helper methods related to Android's Keystore service. @@ -38,17 +37,4 @@ public class KeyStore { public static KeyStore getInstance() { return KEY_STORE; } - - /** - * 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 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); - } } diff --git a/keystore/java/android/security/Authorization.java b/keystore/java/android/security/KeyStoreAuthorization.java index 6404c4bc33d6..14d715f03ae1 100644 --- a/keystore/java/android/security/Authorization.java +++ b/keystore/java/android/security/KeyStoreAuthorization.java @@ -33,15 +33,21 @@ import android.util.Log; * @hide This is the client side for IKeystoreAuthorization AIDL. * It shall only be used by biometric authentication providers and Gatekeeper. */ -public class Authorization { - private static final String TAG = "KeystoreAuthorization"; +public class KeyStoreAuthorization { + private static final String TAG = "KeyStoreAuthorization"; public static final int SYSTEM_ERROR = ResponseCode.SYSTEM_ERROR; + private static final KeyStoreAuthorization sInstance = new KeyStoreAuthorization(); + + public static KeyStoreAuthorization getInstance() { + return sInstance; + } + /** * @return an instance of IKeystoreAuthorization */ - public static IKeystoreAuthorization getService() { + private IKeystoreAuthorization getService() { return IKeystoreAuthorization.Stub.asInterface( ServiceManager.checkService("android.security.authorization")); } @@ -52,7 +58,7 @@ public class Authorization { * @param authToken created by Android authenticators. * @return 0 if successful or {@code ResponseCode.SYSTEM_ERROR}. */ - public static int addAuthToken(@NonNull HardwareAuthToken authToken) { + public int addAuthToken(@NonNull HardwareAuthToken authToken) { StrictMode.noteSlowCall("addAuthToken"); try { getService().addAuthToken(authToken); @@ -70,7 +76,7 @@ public class Authorization { * @param authToken * @return 0 if successful or a {@code ResponseCode}. */ - public static int addAuthToken(@NonNull byte[] authToken) { + public int addAuthToken(@NonNull byte[] authToken) { return addAuthToken(AuthTokenUtils.toHardwareAuthToken(authToken)); } @@ -82,7 +88,7 @@ public class Authorization { * is LSKF (or equivalent) and thus has made the synthetic password available * @return 0 if successful or a {@code ResponseCode}. */ - public static int onDeviceUnlocked(int userId, @Nullable byte[] password) { + public int onDeviceUnlocked(int userId, @Nullable byte[] password) { StrictMode.noteDiskWrite(); try { getService().onDeviceUnlocked(userId, password); @@ -103,7 +109,7 @@ public class Authorization { * @param weakUnlockEnabled - true if non-strong biometric or trust agent unlock is enabled * @return 0 if successful or a {@code ResponseCode}. */ - public static int onDeviceLocked(int userId, @NonNull long[] unlockingSids, + public int onDeviceLocked(int userId, @NonNull long[] unlockingSids, boolean weakUnlockEnabled) { StrictMode.noteDiskWrite(); try { @@ -125,14 +131,17 @@ public class Authorization { * @return the last authentication time or * {@link BiometricConstants#BIOMETRIC_NO_AUTHENTICATION}. */ - public static long getLastAuthenticationTime( - long userId, @HardwareAuthenticatorType int[] authenticatorTypes) { + public long getLastAuthTime(long userId, @HardwareAuthenticatorType int[] authenticatorTypes) { try { return getService().getLastAuthTime(userId, authenticatorTypes); } catch (RemoteException | NullPointerException e) { - Log.w(TAG, "Can not connect to keystore", e); + Log.w(TAG, "Error getting last auth time: " + e); return BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; } catch (ServiceSpecificException e) { + // This is returned when the feature flag test fails in keystore2 + if (e.errorCode == ResponseCode.PERMISSION_DENIED) { + throw new UnsupportedOperationException(); + } return BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; } } diff --git a/libs/WindowManager/Shell/aconfig/Android.bp b/libs/WindowManager/Shell/aconfig/Android.bp index 1a98ffcea9e7..7f8f57b172ff 100644 --- a/libs/WindowManager/Shell/aconfig/Android.bp +++ b/libs/WindowManager/Shell/aconfig/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "com_android_wm_shell_flags", package: "com.android.wm.shell", + container: "system", srcs: [ "multitasking.aconfig", ], diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index b61dda4c4e53..7ff204c695f8 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -1,4 +1,5 @@ package: "com.android.wm.shell" +container: "system" flag { name: "enable_app_pairs" diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index a541c590575f..c2ba064ac7b6 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -45,9 +45,6 @@ <!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu --> <bool name="config_pipEnableResizeForMenu">true</bool> - <!-- PiP minimum size, which is a % based off the shorter side of display width and height --> - <fraction name="config_pipShortestEdgePercent">40%</fraction> - <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself if a custom action is present before closing it. --> <integer name="config_pipForceCloseDelay">1000</integer> @@ -91,11 +88,45 @@ 16x16 </string> + <!-- Default percentages for the PIP size logic. + 1. Determine max widths + Subtract width of system UI and default padding from the shortest edge of the device. + This is the max width. + 2. Calculate Default and Mins + Default is config_pipSystemPreferredDefaultSizePercent of max-width/height. + Min is config_pipSystemPreferredMinimumSizePercent of it. --> + <item name="config_pipSystemPreferredDefaultSizePercent" format="float" type="dimen">0.6</item> + <item name="config_pipSystemPreferredMinimumSizePercent" format="float" type="dimen">0.5</item> + <!-- Default percentages for the PIP size logic when the Display is close to square. + This is used instead when the display is square-ish, like fold-ables when unfolded, + to make sure that default PiP does not cover the hinge (halfway of the display). + 0. Determine if the display is square-ish + If min(displayWidth, displayHeight) / max(displayWidth, displayHeight) is greater than + config_pipSquareDisplayThresholdForSystemPreferredSize, we use the percent for + square display listed below. + 1. Determine max widths + Subtract width of system UI and default padding from the shortest edge of the device. + This is the max width. + 2. Calculate Default and Mins + Default is config_pipSystemPreferredDefaultSizePercentForSquareDisplay of max-width/height. + Min is config_pipSystemPreferredMinimumSizePercentForSquareDisplay of it. --> + <item name="config_pipSquareDisplayThresholdForSystemPreferredSize" + format="float" type="dimen">0.95</item> + <item name="config_pipSystemPreferredDefaultSizePercentForSquareDisplay" + format="float" type="dimen">0.5</item> + <item name="config_pipSystemPreferredMinimumSizePercentForSquareDisplay" + format="float" type="dimen">0.4</item> + <!-- The percentage of the screen width to use for the default width or height of picture-in-picture windows. Regardless of the percent set here, calculated size will never - be smaller than @dimen/default_minimal_size_pip_resizable_task. --> + be smaller than @dimen/default_minimal_size_pip_resizable_task. + This is used in legacy spec, use config_pipSystemPreferredDefaultSizePercent instead. --> <item name="config_pictureInPictureDefaultSizePercent" format="float" type="dimen">0.23</item> + <!-- PiP minimum size, which is a % based off the shorter side of display width and height. + This is used in legacy spec, use config_pipSystemPreferredMinimumSizePercent instead. --> + <fraction name="config_pipShortestEdgePercent">40%</fraction> + <!-- The default aspect ratio for picture-in-picture windows. --> <item name="config_pictureInPictureDefaultAspectRatio" format="float" type="dimen"> 1.777778 diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index fa6dd3914ddd..bf654d979856 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -282,6 +282,6 @@ <string name="expand_menu_text">Open Menu</string> <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> - <!-- Maximize menu maximize button string. --> + <!-- Maximize menu snap buttons string. --> <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 0867a44c1998..d2958779c0d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -455,8 +455,7 @@ public class BubbleController implements ConfigurationChangeListener, ProtoLog.d(WM_SHELL_BUBBLES, "onActivityRestartAttempt - taskId=%d selecting matching bubble=%s", task.taskId, b.getKey()); - mBubbleData.setSelectedBubble(b); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(b); return; } } @@ -593,13 +592,6 @@ public class BubbleController implements ConfigurationChangeListener, } } - private void openBubbleOverflow() { - ensureBubbleViewsAndWindowCreated(); - mBubbleData.setShowingOverflow(true); - mBubbleData.setSelectedBubble(mBubbleData.getOverflow()); - mBubbleData.setExpanded(true); - } - /** * Called when the status bar has become visible or invisible (either permanently or * temporarily). @@ -1247,8 +1239,7 @@ public class BubbleController implements ConfigurationChangeListener, } if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) { // already in the stack - mBubbleData.setSelectedBubble(b); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(b); } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) { // promote it out of the overflow promoteBubbleFromOverflow(b); @@ -1273,8 +1264,7 @@ public class BubbleController implements ConfigurationChangeListener, String key = entry.getKey(); Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); if (bubble != null) { - mBubbleData.setSelectedBubble(bubble); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(bubble); } else { bubble = mBubbleData.getOverflowBubbleWithKey(key); if (bubble != null) { @@ -1367,8 +1357,7 @@ public class BubbleController implements ConfigurationChangeListener, } else { // App bubble is not selected, select it & expand Log.i(TAG, " showOrHideAppBubble, expand and select existing app bubble"); - mBubbleData.setSelectedBubble(existingAppBubble); - mBubbleData.setExpanded(true); + mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble); } } else { // Check if it exists in the overflow @@ -2335,6 +2324,11 @@ public class BubbleController implements ConfigurationChangeListener, mMainExecutor.execute(() -> mController.setBubbleBarLocation(location)); } + + @Override + public void setBubbleBarBounds(Rect bubbleBarBounds) { + mMainExecutor.execute(() -> mBubblePositioner.setBubbleBarBounds(bubbleBarBounds)); + } } private class BubblesImpl implements Bubbles { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 61f0ed22b537..ae3d0c559014 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -365,6 +365,19 @@ public class BubbleData { mSelectedBubble = bubble; } + /** + * Sets the selected bubble and expands it. + * + * <p>This dispatches a single state update for both changes and should be used instead of + * calling {@link #setSelectedBubble(BubbleViewProvider)} followed by + * {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates. + */ + public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) { + setSelectedBubbleInternal(bubble); + setExpandedInternal(true); + dispatchPendingChanges(); + } + public void setSelectedBubble(BubbleViewProvider bubble) { setSelectedBubbleInternal(bubble); dispatchPendingChanges(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 16134d3e5a70..c9f0f0d61713 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -44,4 +44,6 @@ interface IBubbles { oneway void showUserEducation(in int positionX, in int positionY) = 8; oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9; + + oneway void setBubbleBarBounds(in Rect bubbleBarBounds) = 10; }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 8af4c75b5733..45ad6319bbf8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -166,13 +166,8 @@ public class BubbleBarAnimationHelper { bbev.setTaskViewAlpha(0f); bbev.setVisibility(VISIBLE); - // Set the pivot point for the scale, so the view animates out from the bubble bar. - Rect bubbleBarBounds = mPositioner.getBubbleBarBounds(); - mExpandedViewContainerMatrix.setScale( - 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - bubbleBarBounds.centerX(), - bubbleBarBounds.top); + setScaleFromBubbleBar(mExpandedViewContainerMatrix, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT); bbev.setAnimationMatrix(mExpandedViewContainerMatrix); @@ -214,8 +209,8 @@ public class BubbleBarAnimationHelper { } bbev.setScaleX(1f); bbev.setScaleY(1f); - mExpandedViewContainerMatrix.setScaleX(1f); - mExpandedViewContainerMatrix.setScaleY(1f); + + setScaleFromBubbleBar(mExpandedViewContainerMatrix, 1f); PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) @@ -240,6 +235,16 @@ public class BubbleBarAnimationHelper { mExpandedViewAlphaAnimator.reverse(); } + private void setScaleFromBubbleBar(AnimatableScaleMatrix matrix, float scale) { + // Set the pivot point for the scale, so the view animates out from the bubble bar. + Rect bubbleBarBounds = mPositioner.getBubbleBarBounds(); + matrix.setScale( + scale, + scale, + bubbleBarBounds.centerX(), + bubbleBarBounds.top); + } + /** * Animate the expanded bubble when it is being dragged */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt index 18c7bdd6d5ba..7eb0f267b312 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.common.pip import android.content.Context import android.content.res.Resources -import android.os.SystemProperties import android.util.Size import com.android.wm.shell.R import java.io.PrintWriter @@ -36,30 +35,81 @@ class PhoneSizeSpecSource( private var mOverrideMinSize: Size? = null - /** Default and minimum percentages for the PIP size logic. */ - private val mDefaultSizePercent: Float - private val mMinimumSizePercent: Float + /** + * Default percentages for the PIP size logic. + * 1. Determine max widths + * Subtract width of system UI and default padding from the shortest edge of the device. + * This is the max width. + * 2. Calculate Default and Mins + * Default is mSystemPreferredDefaultSizePercent of max-width/height. + * Min is mSystemPreferredMinimumSizePercent of it. + * + * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead. + */ + private var mSystemPreferredDefaultSizePercent = 0.6f + /** Minimum percentages for the PIP size logic. */ + private var mSystemPreferredMinimumSizePercent = 0.5f + + /** Threshold to categorize the Display as square, calculated as min(w, h) / max(w, h). */ + private var mSquareDisplayThresholdForSystemPreferredSize = 0.95f + /** + * Default percentages for the PIP size logic when the Display is square-ish. + * This is used instead when the display is square-ish, like fold-ables when unfolded, + * to make sure that default PiP does not cover the hinge (halfway of the display). + * 1. Determine max widths + * Subtract width of system UI and default padding from the shortest edge of the device. + * This is the max width. + * 2. Calculate Default and Mins + * Default is mSystemPreferredDefaultSizePercent of max-width/height. + * Min is mSystemPreferredMinimumSizePercent of it. + * + * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead. + */ + private var mSystemPreferredDefaultSizePercentForSquareDisplay = 0.5f + /** Minimum percentages for the PIP size logic. */ + private var mSystemPreferredMinimumSizePercentForSquareDisplay = 0.4f + + private val mIsSquareDisplay + get() = minOf(pipDisplayLayoutState.displayLayout.width(), + pipDisplayLayoutState.displayLayout.height()).toFloat() / + maxOf(pipDisplayLayoutState.displayLayout.width(), + pipDisplayLayoutState.displayLayout.height()) > + mSquareDisplayThresholdForSystemPreferredSize + private val mPreferredDefaultSizePercent + get() = if (mIsSquareDisplay) mSystemPreferredDefaultSizePercentForSquareDisplay else + mSystemPreferredDefaultSizePercent + + private val mPreferredMinimumSizePercent + get() = if (mIsSquareDisplay) mSystemPreferredMinimumSizePercentForSquareDisplay else + mSystemPreferredMinimumSizePercent /** Aspect ratio that the PIP size spec logic optimizes for. */ private var mOptimizedAspectRatio = 0f init { - mDefaultSizePercent = SystemProperties - .get("com.android.wm.shell.pip.phone.def_percentage", "0.6").toFloat() - mMinimumSizePercent = SystemProperties - .get("com.android.wm.shell.pip.phone.min_percentage", "0.5").toFloat() - reloadResources() } private fun reloadResources() { - val res: Resources = context.getResources() + val res: Resources = context.resources mDefaultMinSize = res.getDimensionPixelSize( R.dimen.default_minimal_size_pip_resizable_task) mOverridableMinSize = res.getDimensionPixelSize( R.dimen.overridable_minimal_size_pip_resizable_task) + mSystemPreferredDefaultSizePercent = res.getFloat( + R.dimen.config_pipSystemPreferredDefaultSizePercent) + mSystemPreferredMinimumSizePercent = res.getFloat( + R.dimen.config_pipSystemPreferredMinimumSizePercent) + + mSquareDisplayThresholdForSystemPreferredSize = res.getFloat( + R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize) + mSystemPreferredDefaultSizePercentForSquareDisplay = res.getFloat( + R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay) + mSystemPreferredMinimumSizePercentForSquareDisplay = res.getFloat( + R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay) + val requestedOptAspRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio) // make sure the optimized aspect ratio is valid with a default value to fall back to mOptimizedAspectRatio = if (requestedOptAspRatio > 1) { @@ -128,7 +178,7 @@ class PhoneSizeSpecSource( return minSize } val maxSize = getMaxSize(aspectRatio) - val defaultWidth = Math.max(Math.round(maxSize.width * mDefaultSizePercent), + val defaultWidth = Math.max(Math.round(maxSize.width * mPreferredDefaultSizePercent), minSize.width) val defaultHeight = Math.round(defaultWidth / aspectRatio) return Size(defaultWidth, defaultHeight) @@ -146,8 +196,8 @@ class PhoneSizeSpecSource( return adjustOverrideMinSizeToAspectRatio(aspectRatio)!! } val maxSize = getMaxSize(aspectRatio) - var minWidth = Math.round(maxSize.width * mMinimumSizePercent) - var minHeight = Math.round(maxSize.height * mMinimumSizePercent) + var minWidth = Math.round(maxSize.width * mPreferredMinimumSizePercent) + var minHeight = Math.round(maxSize.height * mPreferredMinimumSizePercent) // make sure the calculated min size is not smaller than the allowed default min size if (aspectRatio > 1f) { @@ -244,8 +294,8 @@ class PhoneSizeSpecSource( pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize) pw.println(innerPrefix + "mOverridableMinSize=" + mOverridableMinSize) pw.println(innerPrefix + "mDefaultMinSize=" + mDefaultMinSize) - pw.println(innerPrefix + "mDefaultSizePercent=" + mDefaultSizePercent) - pw.println(innerPrefix + "mMinimumSizePercent=" + mMinimumSizePercent) + pw.println(innerPrefix + "mDefaultSizePercent=" + mPreferredDefaultSizePercent) + pw.println(innerPrefix + "mMinimumSizePercent=" + mPreferredMinimumSizePercent) pw.println(innerPrefix + "mOptimizedAspectRatio=" + mOptimizedAspectRatio) } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index b86e39fca742..4eff3f03670e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -23,19 +23,25 @@ import android.os.Handler; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipPerfHintController; +import com.android.wm.shell.common.pip.PipSnapAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; +import com.android.wm.shell.pip2.phone.PipMotionHelper; import com.android.wm.shell.pip2.phone.PipScheduler; +import com.android.wm.shell.pip2.phone.PipTouchHandler; import com.android.wm.shell.pip2.phone.PipTransition; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellController; @@ -62,6 +68,7 @@ public abstract class Pip2Module { PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, + PipTouchHandler pipTouchHandler, @NonNull PipScheduler pipScheduler) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipScheduler); @@ -109,4 +116,34 @@ public abstract class Pip2Module { return new PhonePipMenuController(context, pipBoundsState, pipMediaController, systemWindows, pipUiEventLogger, mainExecutor, mainHandler); } + + + @WMSingleton + @Provides + static PipTouchHandler providePipTouchHandler(Context context, + ShellInit shellInit, + PhonePipMenuController menuPhoneController, + PipBoundsAlgorithm pipBoundsAlgorithm, + @NonNull PipBoundsState pipBoundsState, + @NonNull SizeSpecSource sizeSpecSource, + PipMotionHelper pipMotionHelper, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger, + @ShellMainThread ShellExecutor mainExecutor, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm, + pipBoundsState, sizeSpecSource, pipMotionHelper, floatingContentCoordinator, + pipUiEventLogger, mainExecutor, pipPerfHintControllerOptional); + } + + @WMSingleton + @Provides + static PipMotionHelper providePipMotionHelper(Context context, + PipBoundsState pipBoundsState, PhonePipMenuController menuController, + PipSnapAlgorithm pipSnapAlgorithm, + FloatingContentCoordinator floatingContentCoordinator, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + return new PipMotionHelper(context, pipBoundsState, menuController, pipSnapAlgorithm, + floatingContentCoordinator, pipPerfHintControllerOptional); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java new file mode 100644 index 000000000000..e7e797096c0e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java @@ -0,0 +1,311 @@ +/* + * 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.wm.shell.pip2.phone; + +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.DismissViewUtils; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.bubbles.DismissCircleView; +import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.common.pip.PipUiEventLogger; + +import kotlin.Unit; + +/** + * Handler of all Magnetized Object related code for PiP. + */ +public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListener { + + /* The multiplier to apply scale the target size by when applying the magnetic field radius */ + private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f; + + /** + * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move + * PIP. + */ + private MagnetizedObject<Rect> mMagnetizedPip; + + /** + * Container for the dismiss circle, so that it can be animated within the container via + * translation rather than within the WindowManager via slow layout animations. + */ + private DismissView mTargetViewContainer; + + /** Circle view used to render the dismiss target. */ + private DismissCircleView mTargetView; + + /** + * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius. + */ + private MagnetizedObject.MagneticTarget mMagneticTarget; + + // Allow dragging the PIP to a location to close it + private boolean mEnableDismissDragToEdge; + + private int mTargetSize; + private int mDismissAreaHeight; + private float mMagneticFieldRadiusPercent = 1f; + private WindowInsets mWindowInsets; + + private SurfaceControl mTaskLeash; + private boolean mHasDismissTargetSurface; + + private final Context mContext; + private final PipMotionHelper mMotionHelper; + private final PipUiEventLogger mPipUiEventLogger; + private final WindowManager mWindowManager; + private final ShellExecutor mMainExecutor; + + public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger, + PipMotionHelper motionHelper, ShellExecutor mainExecutor) { + mContext = context; + mPipUiEventLogger = pipUiEventLogger; + mMotionHelper = motionHelper; + mMainExecutor = mainExecutor; + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + } + + void init() { + Resources res = mContext.getResources(); + mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge); + mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + + if (mTargetViewContainer != null) { + // init can be called multiple times, remove the old one from view hierarchy first. + cleanUpDismissTarget(); + } + + mTargetViewContainer = new DismissView(mContext); + DismissViewUtils.setup(mTargetViewContainer); + mTargetView = mTargetViewContainer.getCircle(); + mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> { + if (!windowInsets.equals(mWindowInsets)) { + mWindowInsets = windowInsets; + updateMagneticTargetSize(); + } + return windowInsets; + }); + + mMagnetizedPip = mMotionHelper.getMagnetizedPip(); + mMagnetizedPip.clearAllTargets(); + mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); + updateMagneticTargetSize(); + + mMagnetizedPip.setAnimateStuckToTarget( + (target, velX, velY, flung, after) -> { + if (mEnableDismissDragToEdge) { + mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after); + } + return Unit.INSTANCE; + }); + mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { + // Show the dismiss target, in case the initial touch event occurred within + // the magnetic field radius. + if (mEnableDismissDragToEdge) { + showDismissTargetMaybe(); + } + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject, + float velX, float velY, boolean wasFlungOut) { + if (wasFlungOut) { + mMotionHelper.flingToSnapTarget(velX, velY, null /* endAction */); + hideDismissTargetMaybe(); + } else { + mMotionHelper.setSpringingToTouch(true); + } + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, + @NonNull MagnetizedObject<?> draggedObject) { + if (mEnableDismissDragToEdge) { + mMainExecutor.executeDelayed(() -> { + mMotionHelper.notifyDismissalPending(); + mMotionHelper.animateDismiss(); + hideDismissTargetMaybe(); + + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); + }, 0); + } + } + }); + + } + + @Override + public boolean onPreDraw() { + mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); + mHasDismissTargetSurface = true; + updateDismissTargetLayer(); + return true; + } + + /** + * Potentially start consuming future motion events if PiP is currently near the magnetized + * object. + */ + public boolean maybeConsumeMotionEvent(MotionEvent ev) { + return mMagnetizedPip.maybeConsumeMotionEvent(ev); + } + + /** + * Update the magnet size. + */ + public void updateMagneticTargetSize() { + if (mTargetView == null) { + return; + } + if (mTargetViewContainer != null) { + mTargetViewContainer.updateResources(); + } + + final Resources res = mContext.getResources(); + mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); + mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + + // Set the magnetic field radius equal to the target size from the center of the target + setMagneticFieldRadiusPercent(mMagneticFieldRadiusPercent); + } + + /** + * Increase or decrease the field radius of the magnet object, e.g. with larger percent, + * PiP will magnetize to the field sooner. + */ + public void setMagneticFieldRadiusPercent(float percent) { + mMagneticFieldRadiusPercent = percent; + mMagneticTarget.setMagneticFieldRadiusPx((int) (mMagneticFieldRadiusPercent * mTargetSize + * MAGNETIC_FIELD_RADIUS_MULTIPLIER)); + } + + public void setTaskLeash(SurfaceControl taskLeash) { + mTaskLeash = taskLeash; + } + + private void updateDismissTargetLayer() { + if (!mHasDismissTargetSurface || mTaskLeash == null) { + // No dismiss target surface, can just return + return; + } + + final SurfaceControl targetViewLeash = + mTargetViewContainer.getViewRootImpl().getSurfaceControl(); + if (!targetViewLeash.isValid()) { + // The surface of mTargetViewContainer is somehow not ready, bail early + return; + } + + // Put the dismiss target behind the task + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setRelativeLayer(targetViewLeash, mTaskLeash, -1); + t.apply(); + } + + /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */ + public void createOrUpdateDismissTarget() { + if (mTargetViewContainer.getParent() == null) { + mTargetViewContainer.cancelAnimators(); + + mTargetViewContainer.setVisibility(View.INVISIBLE); + mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this); + mHasDismissTargetSurface = false; + + mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams()); + } else { + mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams()); + } + } + + /** Returns layout params for the dismiss target, using the latest display metrics. */ + private WindowManager.LayoutParams getDismissTargetLayoutParams() { + final Point windowSize = new Point(); + mWindowManager.getDefaultDisplay().getRealSize(windowSize); + int height = Math.min(windowSize.y, mDismissAreaHeight); + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + height, + 0, windowSize.y - height, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + + lp.setTitle("pip-dismiss-overlay"); + lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + lp.setFitInsetsTypes(0 /* types */); + + return lp; + } + + /** Makes the dismiss target visible and animates it in, if it isn't already visible. */ + public void showDismissTargetMaybe() { + if (!mEnableDismissDragToEdge) { + return; + } + + createOrUpdateDismissTarget(); + + if (mTargetViewContainer.getVisibility() != View.VISIBLE) { + mTargetViewContainer.getViewTreeObserver().addOnPreDrawListener(this); + } + // always invoke show, since the target might still be VISIBLE while playing hide animation, + // so we want to ensure it will show back again + mTargetViewContainer.show(); + } + + /** Animates the magnetic dismiss target out and then sets it to GONE. */ + public void hideDismissTargetMaybe() { + if (!mEnableDismissDragToEdge) { + return; + } + mTargetViewContainer.hide(); + } + + /** + * Removes the dismiss target and cancels any pending callbacks to show it. + */ + public void cleanUpDismissTarget() { + if (mTargetViewContainer.getParent() != null) { + mWindowManager.removeViewImmediate(mTargetViewContainer); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java new file mode 100644 index 000000000000..03547a55fa27 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java @@ -0,0 +1,188 @@ +/* + * 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.wm.shell.pip2.phone; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.os.Binder; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.view.BatchedInputEventReceiver; +import android.view.Choreographer; +import android.view.IWindowManager; +import android.view.InputChannel; +import android.view.InputEvent; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; + +/** + * Manages the input consumer that allows the Shell to directly receive input. + */ +public class PipInputConsumer { + + private static final String TAG = PipInputConsumer.class.getSimpleName(); + + /** + * Listener interface for callers to subscribe to input events. + */ + public interface InputListener { + /** Handles any input event. */ + boolean onInputEvent(InputEvent ev); + } + + /** + * Listener interface for callers to learn when this class is registered or unregistered with + * window manager + */ + private interface RegistrationListener { + void onRegistrationChanged(boolean isRegistered); + } + + /** + * Input handler used for the input consumer. Input events are batched and consumed with the + * SurfaceFlinger vsync. + */ + private final class InputEventReceiver extends BatchedInputEventReceiver { + + InputEventReceiver(InputChannel inputChannel, Looper looper, + Choreographer choreographer) { + super(inputChannel, looper, choreographer); + } + + @Override + public void onInputEvent(InputEvent event) { + boolean handled = true; + try { + if (mListener != null) { + handled = mListener.onInputEvent(event); + } + } finally { + finishInputEvent(event, handled); + } + } + } + + private final IWindowManager mWindowManager; + private final IBinder mToken; + private final String mName; + private final ShellExecutor mMainExecutor; + + private InputEventReceiver mInputEventReceiver; + private InputListener mListener; + private RegistrationListener mRegistrationListener; + + /** + * @param name the name corresponding to the input consumer that is defined in the system. + */ + public PipInputConsumer(IWindowManager windowManager, String name, + ShellExecutor mainExecutor) { + mWindowManager = windowManager; + mToken = new Binder(); + mName = name; + mMainExecutor = mainExecutor; + } + + /** + * Sets the input listener. + */ + public void setInputListener(InputListener listener) { + mListener = listener; + } + + /** + * Sets the registration listener. + */ + public void setRegistrationListener(RegistrationListener listener) { + mRegistrationListener = listener; + mMainExecutor.execute(() -> { + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(mInputEventReceiver != null); + } + }); + } + + /** + * Check if the InputConsumer is currently registered with WindowManager + * + * @return {@code true} if registered, {@code false} if not. + */ + public boolean isRegistered() { + return mInputEventReceiver != null; + } + + /** + * Registers the input consumer. + */ + public void registerInputConsumer() { + if (mInputEventReceiver != null) { + return; + } + final InputChannel inputChannel = new InputChannel(); + try { + // TODO(b/113087003): Support Picture-in-picture in multi-display. + mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY); + mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel); + } catch (RemoteException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to create input consumer, %s", TAG, e); + } + mMainExecutor.execute(() -> { + mInputEventReceiver = new InputEventReceiver(inputChannel, + Looper.myLooper(), Choreographer.getInstance()); + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(true /* isRegistered */); + } + }); + } + + /** + * Unregisters the input consumer. + */ + public void unregisterInputConsumer() { + if (mInputEventReceiver == null) { + return; + } + try { + // TODO(b/113087003): Support Picture-in-picture in multi-display. + mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY); + } catch (RemoteException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to destroy input consumer, %s", TAG, e); + } + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + mMainExecutor.execute(() -> { + if (mRegistrationListener != null) { + mRegistrationListener.onRegistrationChanged(false /* isRegistered */); + } + }); + } + + /** + * Dumps the {@link PipInputConsumer} state. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "registered=" + (mInputEventReceiver != null)); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java new file mode 100644 index 000000000000..619bed4e19ca --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -0,0 +1,719 @@ +/* + * 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.wm.shell.pip2.phone; + +import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY; +import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW; +import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM; + +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT; +import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_DISMISS; +import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Debug; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.animation.FloatProperties; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import com.android.wm.shell.common.pip.PipAppOpsListener; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipPerfHintController; +import com.android.wm.shell.common.pip.PipSnapAlgorithm; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.animation.PhysicsAnimator; + +import kotlin.Unit; +import kotlin.jvm.functions.Function0; + +import java.util.Optional; +import java.util.function.Consumer; + +/** + * A helper to animate and manipulate the PiP. + */ +public class PipMotionHelper implements PipAppOpsListener.Callback, + FloatingContentCoordinator.FloatingContent { + private static final String TAG = "PipMotionHelper"; + private static final boolean DEBUG = false; + + private static final int SHRINK_STACK_FROM_MENU_DURATION = 250; + private static final int EXPAND_STACK_TO_MENU_DURATION = 250; + private static final int UNSTASH_DURATION = 250; + private static final int LEAVE_PIP_DURATION = 300; + private static final int SHIFT_DURATION = 300; + + /** Friction to use for PIP when it moves via physics fling animations. */ + private static final float DEFAULT_FRICTION = 1.9f; + /** How much of the dismiss circle size to use when scaling down PIP. **/ + private static final float DISMISS_CIRCLE_PERCENT = 0.85f; + + private final Context mContext; + private @NonNull PipBoundsState mPipBoundsState; + + private PhonePipMenuController mMenuController; + private PipSnapAlgorithm mSnapAlgorithm; + + /** The region that all of PIP must stay within. */ + private final Rect mFloatingAllowedArea = new Rect(); + + /** Coordinator instance for resolving conflicts with other floating content. */ + private FloatingContentCoordinator mFloatingContentCoordinator; + + @Nullable private final PipPerfHintController mPipPerfHintController; + @Nullable private PipPerfHintController.PipHighPerfSession mPipHighPerfSession; + + /** + * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()} + * using physics animations. + */ + private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator; + + private MagnetizedObject<Rect> mMagnetizedPip; + + /** + * Update listener that resizes the PIP to {@link PipBoundsState#getMotionBoundsState()}. + */ + private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener; + + /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */ + private PhysicsAnimator.FlingConfig mFlingConfigX; + private PhysicsAnimator.FlingConfig mFlingConfigY; + /** FlingConfig instances provided to PhysicsAnimator for stashing. */ + private PhysicsAnimator.FlingConfig mStashConfigX; + + /** SpringConfig to use for fling-then-spring animations. */ + private final PhysicsAnimator.SpringConfig mSpringConfig = + new PhysicsAnimator.SpringConfig(700f, DAMPING_RATIO_NO_BOUNCY); + + /** SpringConfig used for animating into the dismiss region, matches the one in + * {@link MagnetizedObject}. */ + private final PhysicsAnimator.SpringConfig mAnimateToDismissSpringConfig = + new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_NO_BOUNCY); + + /** SpringConfig used for animating the pip to catch up to the finger once it leaves the dismiss + * drag region. */ + private final PhysicsAnimator.SpringConfig mCatchUpSpringConfig = + new PhysicsAnimator.SpringConfig(5000f, DAMPING_RATIO_NO_BOUNCY); + + /** SpringConfig to use for springing PIP away from conflicting floating content. */ + private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig = + new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY); + + private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> { + if (mPipBoundsState.getBounds().equals(newBounds)) { + return; + } + + mMenuController.updateMenuLayout(newBounds); + mPipBoundsState.setBounds(newBounds); + }; + + /** + * Whether we're springing to the touch event location (vs. moving it to that position + * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was + * 'stuck' in the target and needs to catch up to the touch location. + */ + private boolean mSpringingToTouch = false; + + /** + * Whether PIP was released in the dismiss target, and will be animated out and dismissed + * shortly. + */ + private boolean mDismissalPending = false; + + /** + * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is + * used to show menu activity when the expand animation is completed. + */ + private Runnable mPostPipTransitionCallback; + + public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, + PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, + FloatingContentCoordinator floatingContentCoordinator, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + mContext = context; + mPipBoundsState = pipBoundsState; + mMenuController = menuController; + mSnapAlgorithm = snapAlgorithm; + mFloatingContentCoordinator = floatingContentCoordinator; + mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); + mResizePipUpdateListener = (target, values) -> { + if (mPipBoundsState.getMotionBoundsState().isInMotion()) { + /* + mPipTaskOrganizer.scheduleUserResizePip(getBounds(), + mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), null); + */ + } + }; + } + + void init() { + mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + } + + @NonNull + @Override + public Rect getFloatingBoundsOnScreen() { + return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty() + ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds(); + } + + @NonNull + @Override + public Rect getAllowedFloatingBoundsRegion() { + return mFloatingAllowedArea; + } + + @Override + public void moveToBounds(@NonNull Rect bounds) { + animateToBounds(bounds, mConflictResolutionSpringConfig); + } + + /** + * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations. + */ + void synchronizePinnedStackBounds() { + cancelPhysicsAnimation(); + mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); + + /* + if (mPipTaskOrganizer.isInPip()) { + mFloatingContentCoordinator.onContentMoved(this); + } + */ + } + + /** + * Tries to move the pinned stack to the given {@param bounds}. + */ + void movePip(Rect toBounds) { + movePip(toBounds, false /* isDragging */); + } + + /** + * Tries to move the pinned stack to the given {@param bounds}. + * + * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we + * won't notify the floating content coordinator of this move, since that will + * happen when the gesture ends. + */ + void movePip(Rect toBounds, boolean isDragging) { + if (!isDragging) { + mFloatingContentCoordinator.onContentMoved(this); + } + + if (!mSpringingToTouch) { + // If we are moving PIP directly to the touch event locations, cancel any animations and + // move PIP to the given bounds. + cancelPhysicsAnimation(); + + if (!isDragging) { + resizePipUnchecked(toBounds); + mPipBoundsState.setBounds(toBounds); + } else { + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(toBounds); + /* + mPipTaskOrganizer.scheduleUserResizePip(getBounds(), toBounds, + (Rect newBounds) -> { + mMenuController.updateMenuLayout(newBounds); + }); + */ + } + } else { + // If PIP is 'catching up' after being stuck in the dismiss target, update the animation + // to spring towards the new touch location. + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mCatchUpSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mCatchUpSpringConfig) + .spring(FloatProperties.RECT_X, toBounds.left, mCatchUpSpringConfig) + .spring(FloatProperties.RECT_Y, toBounds.top, mCatchUpSpringConfig); + + startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */); + } + } + + /** Animates the PIP into the dismiss target, scaling it down. */ + void animateIntoDismissTarget( + MagnetizedObject.MagneticTarget target, + float velX, float velY, + boolean flung, Function0<Unit> after) { + final PointF targetCenter = target.getCenterOnScreen(); + + // PIP should fit in the circle + final float dismissCircleSize = mContext.getResources().getDimensionPixelSize( + R.dimen.dismiss_circle_size); + + final float width = getBounds().width(); + final float height = getBounds().height(); + final float ratio = width / height; + + // Width should be a little smaller than the circle size. + final float desiredWidth = dismissCircleSize * DISMISS_CIRCLE_PERCENT; + final float desiredHeight = desiredWidth / ratio; + final float destinationX = targetCenter.x - (desiredWidth / 2f); + final float destinationY = targetCenter.y - (desiredHeight / 2f); + + // If we're already in the dismiss target area, then there won't be a move to set the + // temporary bounds, so just initialize it to the current bounds. + if (!mPipBoundsState.getMotionBoundsState().isInMotion()) { + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); + } + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_X, destinationX, velX, mAnimateToDismissSpringConfig) + .spring(FloatProperties.RECT_Y, destinationY, velY, mAnimateToDismissSpringConfig) + .spring(FloatProperties.RECT_WIDTH, desiredWidth, mAnimateToDismissSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mAnimateToDismissSpringConfig) + .withEndActions(after); + + startBoundsAnimator(destinationX, destinationY); + } + + /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */ + void setSpringingToTouch(boolean springingToTouch) { + mSpringingToTouch = springingToTouch; + } + + /** + * Resizes the pinned stack back to unknown windowing mode, which could be freeform or + * * fullscreen depending on the display area's windowing mode. + */ + void expandLeavePip(boolean skipAnimation) { + expandLeavePip(skipAnimation, false /* enterSplit */); + } + + /** + * Resizes the pinned task to split-screen mode. + */ + void expandIntoSplit() { + expandLeavePip(false, true /* enterSplit */); + } + + /** + * Resizes the pinned stack back to unknown windowing mode, which could be freeform or + * fullscreen depending on the display area's windowing mode. + */ + private void expandLeavePip(boolean skipAnimation, boolean enterSplit) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: exitPip: skipAnimation=%s" + + " callers=\n%s", TAG, skipAnimation, Debug.getCallers(5, " ")); + } + cancelPhysicsAnimation(); + mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); + // mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION, enterSplit); + } + + /** + * Dismisses the pinned stack. + */ + @Override + public void dismissPip() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: removePip: callers=\n%s", TAG, Debug.getCallers(5, " ")); + } + cancelPhysicsAnimation(); + mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */); + // mPipTaskOrganizer.removePip(); + } + + /** Sets the movement bounds to use to constrain PIP position animations. */ + void onMovementBoundsChanged() { + rebuildFlingConfigs(); + + // The movement bounds represent the area within which we can move PIP's top-left position. + // The allowed area for all of PIP is those bounds plus PIP's width and height. + mFloatingAllowedArea.set(mPipBoundsState.getMovementBounds()); + mFloatingAllowedArea.right += getBounds().width(); + mFloatingAllowedArea.bottom += getBounds().height(); + } + + /** + * @return the PiP bounds. + */ + private Rect getBounds() { + return mPipBoundsState.getBounds(); + } + + /** + * Flings the PiP to the closest snap target. + */ + void flingToSnapTarget( + float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback) { + movetoTarget(velocityX, velocityY, postBoundsUpdateCallback, false /* isStash */); + } + + /** + * Stash PiP to the closest edge. We set velocityY to 0 to limit pure horizontal motion. + */ + void stashToEdge(float velX, float velY, @Nullable Runnable postBoundsUpdateCallback) { + velY = mPipBoundsState.getStashedState() == STASH_TYPE_NONE ? 0 : velY; + movetoTarget(velX, velY, postBoundsUpdateCallback, true /* isStash */); + } + + private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {} + + private void cleanUpHighPerfSessionMaybe() { + if (mPipHighPerfSession != null) { + // Close the high perf session once pointer interactions are over; + mPipHighPerfSession.close(); + mPipHighPerfSession = null; + } + } + + private void movetoTarget( + float velocityX, + float velocityY, + @Nullable Runnable postBoundsUpdateCallback, + boolean isStash) { + // If we're flinging to a snap target now, we're not springing to catch up to the touch + // location now. + mSpringingToTouch = false; + + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mSpringConfig) + .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig) + .flingThenSpring( + FloatProperties.RECT_X, velocityX, + isStash ? mStashConfigX : mFlingConfigX, + mSpringConfig, true /* flingMustReachMinOrMax */) + .flingThenSpring( + FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig); + + final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); + final float leftEdge = isStash + ? mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() + + insetBounds.left + : mPipBoundsState.getMovementBounds().left; + final float rightEdge = isStash + ? mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() + - insetBounds.right + : mPipBoundsState.getMovementBounds().right; + + final float xEndValue = velocityX < 0 ? leftEdge : rightEdge; + + final int startValueY = mPipBoundsState.getMotionBoundsState().getBoundsInMotion().top; + final float estimatedFlingYEndValue = + PhysicsAnimator.estimateFlingEndValue(startValueY, velocityY, mFlingConfigY); + + startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */, + postBoundsUpdateCallback); + } + + /** + * Animates PIP to the provided bounds, using physics animations and the given spring + * configuration + */ + void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) { + if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { + // Animate from the current bounds if we're not already animating. + mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds()); + } + + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_X, bounds.left, springConfig) + .spring(FloatProperties.RECT_Y, bounds.top, springConfig); + startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */); + } + + /** + * Animates the dismissal of the PiP off the edge of the screen. + */ + void animateDismiss() { + // Animate off the bottom of the screen, then dismiss PIP. + mTemporaryBoundsPhysicsAnimator + .spring(FloatProperties.RECT_Y, + mPipBoundsState.getMovementBounds().bottom + getBounds().height() * 2, + 0, + mSpringConfig) + .withEndActions(this::dismissPip); + + startBoundsAnimator( + getBounds().left /* toX */, getBounds().bottom + getBounds().height() /* toY */); + + mDismissalPending = false; + } + + /** + * Animates the PiP to the expanded state to show the menu. + */ + float animateToExpandedState(Rect expandedBounds, Rect movementBounds, + Rect expandedMovementBounds, Runnable callback) { + float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()), + movementBounds); + mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction); + mPostPipTransitionCallback = callback; + resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION); + return savedSnapFraction; + } + + /** + * Animates the PiP from the expanded state to the normal state after the menu is hidden. + */ + void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction, + Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) { + if (savedSnapFraction < 0f) { + // If there are no saved snap fractions, then just use the current bounds + savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()), + currentMovementBounds, mPipBoundsState.getStashedState()); + } + + mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction, + mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), + mPipBoundsState.getDisplayBounds(), + mPipBoundsState.getDisplayLayout().stableInsets()); + + if (immediate) { + movePip(normalBounds); + } else { + resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION); + } + } + + /** + * Animates the PiP to the stashed state, choosing the closest edge. + */ + void animateToStashedClosestEdge() { + Rect tmpBounds = new Rect(); + final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); + final int stashType = + mPipBoundsState.getBounds().left == mPipBoundsState.getMovementBounds().left + ? STASH_TYPE_LEFT : STASH_TYPE_RIGHT; + final float leftEdge = stashType == STASH_TYPE_LEFT + ? mPipBoundsState.getStashOffset() + - mPipBoundsState.getBounds().width() + insetBounds.left + : mPipBoundsState.getDisplayBounds().right + - mPipBoundsState.getStashOffset() - insetBounds.right; + tmpBounds.set((int) leftEdge, + mPipBoundsState.getBounds().top, + (int) (leftEdge + mPipBoundsState.getBounds().width()), + mPipBoundsState.getBounds().bottom); + resizeAndAnimatePipUnchecked(tmpBounds, UNSTASH_DURATION); + mPipBoundsState.setStashed(stashType); + } + + /** + * Animates the PiP from stashed state into un-stashed, popping it out from the edge. + */ + void animateToUnStashedBounds(Rect unstashedBounds) { + resizeAndAnimatePipUnchecked(unstashedBounds, UNSTASH_DURATION); + } + + /** + * Animates the PiP to offset it from the IME or shelf. + */ + void animateToOffset(Rect originalBounds, int offset) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: animateToOffset: originalBounds=%s offset=%s" + + " callers=\n%s", TAG, originalBounds, offset, + Debug.getCallers(5, " ")); + } + cancelPhysicsAnimation(); + /* + mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION, + mUpdateBoundsCallback); + */ + } + + /** + * Cancels all existing animations. + */ + private void cancelPhysicsAnimation() { + mTemporaryBoundsPhysicsAnimator.cancel(); + mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); + mSpringingToTouch = false; + } + + /** Set new fling configs whose min/max values respect the given movement bounds. */ + private void rebuildFlingConfigs() { + mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, + mPipBoundsState.getMovementBounds().left, + mPipBoundsState.getMovementBounds().right); + mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION, + mPipBoundsState.getMovementBounds().top, + mPipBoundsState.getMovementBounds().bottom); + final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets(); + mStashConfigX = new PhysicsAnimator.FlingConfig( + DEFAULT_FRICTION, + mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width() + + insetBounds.left, + mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset() + - insetBounds.right); + } + + private void startBoundsAnimator(float toX, float toY) { + startBoundsAnimator(toX, toY, null /* postBoundsUpdateCallback */); + } + + /** + * Starts the physics animator which will update the animated PIP bounds using physics + * animations, as well as the TimeAnimator which will apply those bounds to PIP. + * + * This will also add end actions to the bounds animator that cancel the TimeAnimator and update + * the 'real' bounds to equal the final animated bounds. + * + * If one wishes to supply a callback after all the 'real' bounds update has happened, + * pass @param postBoundsUpdateCallback. + */ + private void startBoundsAnimator(float toX, float toY, Runnable postBoundsUpdateCallback) { + if (!mSpringingToTouch) { + cancelPhysicsAnimation(); + } + + setAnimatingToBounds(new Rect( + (int) toX, + (int) toY, + (int) toX + getBounds().width(), + (int) toY + getBounds().height())); + + if (!mTemporaryBoundsPhysicsAnimator.isRunning()) { + if (mPipPerfHintController != null) { + // Start a high perf session with a timeout callback. + mPipHighPerfSession = mPipPerfHintController.startSession( + this::onHighPerfSessionTimeout, "startBoundsAnimator"); + } + if (postBoundsUpdateCallback != null) { + mTemporaryBoundsPhysicsAnimator + .addUpdateListener(mResizePipUpdateListener) + .withEndActions(this::onBoundsPhysicsAnimationEnd, + postBoundsUpdateCallback); + } else { + mTemporaryBoundsPhysicsAnimator + .addUpdateListener(mResizePipUpdateListener) + .withEndActions(this::onBoundsPhysicsAnimationEnd); + } + } + + mTemporaryBoundsPhysicsAnimator.start(); + } + + /** + * Notify that PIP was released in the dismiss target and will be animated out and dismissed + * shortly. + */ + void notifyDismissalPending() { + mDismissalPending = true; + } + + private void onBoundsPhysicsAnimationEnd() { + // The physics animation ended, though we may not necessarily be done animating, such as + // when we're still dragging after moving out of the magnetic target. + if (!mDismissalPending + && !mSpringingToTouch + && !mMagnetizedPip.getObjectStuckToTarget()) { + // All motion operations have actually finished. + mPipBoundsState.setBounds( + mPipBoundsState.getMotionBoundsState().getBoundsInMotion()); + mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded(); + if (!mDismissalPending) { + // do not schedule resize if PiP is dismissing, which may cause app re-open to + // mBounds instead of its normal bounds. + // mPipTaskOrganizer.scheduleFinishResizePip(getBounds()); + } + } + mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded(); + mSpringingToTouch = false; + mDismissalPending = false; + cleanUpHighPerfSessionMaybe(); + } + + /** + * Notifies the floating coordinator that we're moving, and sets the animating to bounds so + * we return these bounds from + * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}. + */ + private void setAnimatingToBounds(Rect bounds) { + mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds); + mFloatingContentCoordinator.onContentMoved(this); + } + + /** + * Directly resizes the PiP to the given {@param bounds}. + */ + private void resizePipUnchecked(Rect toBounds) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: resizePipUnchecked: toBounds=%s" + + " callers=\n%s", TAG, toBounds, Debug.getCallers(5, " ")); + } + if (!toBounds.equals(getBounds())) { + // mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback); + } + } + + /** + * Directly resizes the PiP to the given {@param bounds}. + */ + private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: resizeAndAnimatePipUnchecked: toBounds=%s" + + " duration=%s callers=\n%s", TAG, toBounds, duration, + Debug.getCallers(5, " ")); + } + + // Intentionally resize here even if the current bounds match the destination bounds. + // This is so all the proper callbacks are performed. + + // mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, + // TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */); + // setAnimatingToBounds(toBounds); + } + + /** + * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the + * magnetic dismiss target so it can calculate PIP's size and position. + */ + MagnetizedObject<Rect> getMagnetizedPip() { + if (mMagnetizedPip == null) { + mMagnetizedPip = new MagnetizedObject<Rect>( + mContext, mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), + FloatProperties.RECT_X, FloatProperties.RECT_Y) { + @Override + public float getWidth(@NonNull Rect animatedPipBounds) { + return animatedPipBounds.width(); + } + + @Override + public float getHeight(@NonNull Rect animatedPipBounds) { + return animatedPipBounds.height(); + } + + @Override + public void getLocationOnScreen( + @NonNull Rect animatedPipBounds, @NonNull int[] loc) { + loc[0] = animatedPipBounds.left; + loc[1] = animatedPipBounds.top; + } + }; + mMagnetizedPip.setFlingToTargetEnabled(false); + } + + return mMagnetizedPip; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java new file mode 100644 index 000000000000..04cf350ddd3e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -0,0 +1,538 @@ +/* + * 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.wm.shell.pip2.phone; + +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.hardware.input.InputManager; +import android.os.Looper; +import android.view.BatchedInputEventReceiver; +import android.view.Choreographer; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.InputMonitor; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipPerfHintController; +import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; +import com.android.wm.shell.common.pip.PipUiEventLogger; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +/** + * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to + * trigger dynamic resize. + */ +public class PipResizeGestureHandler { + + private static final String TAG = "PipResizeGestureHandler"; + private static final int PINCH_RESIZE_SNAP_DURATION = 250; + private static final float PINCH_RESIZE_AUTO_MAX_RATIO = 0.9f; + + private final Context mContext; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipBoundsState mPipBoundsState; + private final PipTouchState mPipTouchState; + private final PhonePipMenuController mPhonePipMenuController; + private final PipUiEventLogger mPipUiEventLogger; + private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; + private final int mDisplayId; + private final ShellExecutor mMainExecutor; + + private final PointF mDownPoint = new PointF(); + private final PointF mDownSecondPoint = new PointF(); + private final PointF mLastPoint = new PointF(); + private final PointF mLastSecondPoint = new PointF(); + private final Point mMaxSize = new Point(); + private final Point mMinSize = new Point(); + private final Rect mLastResizeBounds = new Rect(); + private final Rect mUserResizeBounds = new Rect(); + private final Rect mDownBounds = new Rect(); + private final Runnable mUpdateMovementBoundsRunnable; + private final Consumer<Rect> mUpdateResizeBoundsCallback; + + private float mTouchSlop; + + private boolean mAllowGesture; + private boolean mIsAttached; + private boolean mIsEnabled; + private boolean mEnablePinchResize; + private boolean mIsSysUiStateValid; + private boolean mThresholdCrossed; + private boolean mOngoingPinchToResize = false; + private float mAngle = 0; + int mFirstIndex = -1; + int mSecondIndex = -1; + + private InputMonitor mInputMonitor; + private InputEventReceiver mInputEventReceiver; + + @Nullable + private final PipPerfHintController mPipPerfHintController; + + @Nullable + private PipPerfHintController.PipHighPerfSession mPipHighPerfSession; + + private int mCtrlType; + private int mOhmOffset; + + public PipResizeGestureHandler(Context context, PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, PipTouchState pipTouchState, + Runnable updateMovementBoundsRunnable, + PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, + ShellExecutor mainExecutor, @Nullable PipPerfHintController pipPerfHintController) { + mContext = context; + mDisplayId = context.getDisplayId(); + mMainExecutor = mainExecutor; + mPipPerfHintController = pipPerfHintController; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipBoundsState = pipBoundsState; + mPipTouchState = pipTouchState; + mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; + mPhonePipMenuController = menuActivityController; + mPipUiEventLogger = pipUiEventLogger; + mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + + mUpdateResizeBoundsCallback = (rect) -> { + mUserResizeBounds.set(rect); + // mMotionHelper.synchronizePinnedStackBounds(); + mUpdateMovementBoundsRunnable.run(); + resetState(); + }; + } + + void init() { + mContext.getDisplay().getRealSize(mMaxSize); + reloadResources(); + + final Resources res = mContext.getResources(); + mEnablePinchResize = res.getBoolean(R.bool.config_pipEnablePinchResize); + } + + void onConfigurationChanged() { + reloadResources(); + } + + /** + * Called when SysUI state changed. + * + * @param isSysUiStateValid Is SysUI valid or not. + */ + public void onSystemUiStateChanged(boolean isSysUiStateValid) { + mIsSysUiStateValid = isSysUiStateValid; + } + + private void reloadResources() { + mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + } + + private void disposeInputChannel() { + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + mInputEventReceiver = null; + } + if (mInputMonitor != null) { + mInputMonitor.dispose(); + mInputMonitor = null; + } + } + + void onActivityPinned() { + mIsAttached = true; + updateIsEnabled(); + } + + void onActivityUnpinned() { + mIsAttached = false; + mUserResizeBounds.setEmpty(); + updateIsEnabled(); + } + + private void updateIsEnabled() { + boolean isEnabled = mIsAttached; + if (isEnabled == mIsEnabled) { + return; + } + mIsEnabled = isEnabled; + disposeInputChannel(); + + if (mIsEnabled) { + // Register input event receiver + mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput( + "pip-resize", mDisplayId); + try { + mMainExecutor.executeBlocking(() -> { + mInputEventReceiver = new PipResizeInputEventReceiver( + mInputMonitor.getInputChannel(), Looper.myLooper()); + }); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to create input event receiver", e); + } + } + } + + @VisibleForTesting + void onInputEvent(InputEvent ev) { + if (!mEnablePinchResize) { + // No need to handle anything if neither form of resizing is enabled. + return; + } + + if (!mPipTouchState.getAllowInputEvents()) { + // No need to handle anything if touches are not enabled + return; + } + + // Don't allow resize when PiP is stashed. + if (mPipBoundsState.isStashed()) { + return; + } + + if (ev instanceof MotionEvent) { + MotionEvent mv = (MotionEvent) ev; + int action = mv.getActionMasked(); + final Rect pipBounds = mPipBoundsState.getBounds(); + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + if (!pipBounds.contains((int) mv.getRawX(), (int) mv.getRawY()) + && mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); + } + } + + if (mEnablePinchResize && mOngoingPinchToResize) { + onPinchResize(mv); + } + } + } + + /** + * Checks if there is currently an on-going gesture, either drag-resize or pinch-resize. + */ + public boolean hasOngoingGesture() { + return mCtrlType != CTRL_NONE || mOngoingPinchToResize; + } + + public boolean isUsingPinchToZoom() { + return mEnablePinchResize; + } + + public boolean isResizing() { + return mAllowGesture; + } + + boolean willStartResizeGesture(MotionEvent ev) { + if (isInValidSysUiState()) { + if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + if (mEnablePinchResize && ev.getPointerCount() == 2) { + onPinchResize(ev); + mOngoingPinchToResize = mAllowGesture; + return mAllowGesture; + } + } + } + return false; + } + + private boolean isInValidSysUiState() { + return mIsSysUiStateValid; + } + + private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {} + + private void cleanUpHighPerfSessionMaybe() { + if (mPipHighPerfSession != null) { + // Close the high perf session once pointer interactions are over; + mPipHighPerfSession.close(); + mPipHighPerfSession = null; + } + } + + @VisibleForTesting + void onPinchResize(MotionEvent ev) { + int action = ev.getActionMasked(); + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mFirstIndex = -1; + mSecondIndex = -1; + mAllowGesture = false; + finishResize(); + cleanUpHighPerfSessionMaybe(); + } + + if (ev.getPointerCount() != 2) { + return; + } + + final Rect pipBounds = mPipBoundsState.getBounds(); + if (action == MotionEvent.ACTION_POINTER_DOWN) { + if (mFirstIndex == -1 && mSecondIndex == -1 + && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0)) + && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) { + mAllowGesture = true; + mFirstIndex = 0; + mSecondIndex = 1; + mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); + mDownSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); + mDownBounds.set(pipBounds); + + mLastPoint.set(mDownPoint); + mLastSecondPoint.set(mLastSecondPoint); + mLastResizeBounds.set(mDownBounds); + + // start the high perf session as the second pointer gets detected + if (mPipPerfHintController != null) { + mPipHighPerfSession = mPipPerfHintController.startSession( + this::onHighPerfSessionTimeout, "onPinchResize"); + } + } + } + + if (action == MotionEvent.ACTION_MOVE) { + if (mFirstIndex == -1 || mSecondIndex == -1) { + return; + } + + float x0 = ev.getRawX(mFirstIndex); + float y0 = ev.getRawY(mFirstIndex); + float x1 = ev.getRawX(mSecondIndex); + float y1 = ev.getRawY(mSecondIndex); + mLastPoint.set(x0, y0); + mLastSecondPoint.set(x1, y1); + + // Capture inputs + if (!mThresholdCrossed + && (distanceBetween(mDownSecondPoint, mLastSecondPoint) > mTouchSlop + || distanceBetween(mDownPoint, mLastPoint) > mTouchSlop)) { + pilferPointers(); + mThresholdCrossed = true; + // Reset the down to begin resizing from this point + mDownPoint.set(mLastPoint); + mDownSecondPoint.set(mLastSecondPoint); + + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); + } + } + + if (mThresholdCrossed) { + mAngle = mPinchResizingAlgorithm.calculateBoundsAndAngle(mDownPoint, + mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize, + mDownBounds, mLastResizeBounds); + + /* + mPipTaskOrganizer.scheduleUserResizePip(mDownBounds, mLastResizeBounds, + mAngle, null); + */ + mPipBoundsState.setHasUserResizedPip(true); + } + } + } + + private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { + final int leftEdge = bounds.left; + + + final int fromLeft = Math.abs(leftEdge - movementBounds.left); + final int fromRight = Math.abs(movementBounds.right - leftEdge); + + // The PIP will be snapped to either the right or left edge, so calculate which one + // is closest to the current position. + final int newLeft = fromLeft < fromRight + ? movementBounds.left : movementBounds.right; + + bounds.offsetTo(newLeft, mLastResizeBounds.top); + } + + /** + * Resizes the pip window and updates user-resized bounds. + * + * @param bounds target bounds to resize to + * @param snapFraction snap fraction to apply after resizing + */ + void userResizeTo(Rect bounds, float snapFraction) { + Rect finalBounds = new Rect(bounds); + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds); + + // snap the target bounds to the either left or right edge, by choosing the closer one + snapToMovementBoundsEdge(finalBounds, movementBounds); + + // apply the requested snap fraction onto the target bounds + mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction); + + // resize from current bounds to target bounds without animation + // mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null); + // set the flag that pip has been resized + mPipBoundsState.setHasUserResizedPip(true); + + // finish the resize operation and update the state of the bounds + // mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback); + } + + private void finishResize() { + if (!mLastResizeBounds.isEmpty()) { + // Pinch-to-resize needs to re-calculate snap fraction and animate to the snapped + // position correctly. Drag-resize does not need to move, so just finalize resize. + if (mOngoingPinchToResize) { + final Rect startBounds = new Rect(mLastResizeBounds); + // If user resize is pretty close to max size, just auto resize to max. + if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x + || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { + resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y); + } + + // If user resize is smaller than min size, auto resize to min + if (mLastResizeBounds.width() < mMinSize.x + || mLastResizeBounds.height() < mMinSize.y) { + resizeRectAboutCenter(mLastResizeBounds, mMinSize.x, mMinSize.y); + } + + // get the current movement bounds + final Rect movementBounds = mPipBoundsAlgorithm + .getMovementBounds(mLastResizeBounds); + + // snap mLastResizeBounds to the correct edge based on movement bounds + snapToMovementBoundsEdge(mLastResizeBounds, movementBounds); + + final float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mLastResizeBounds, movementBounds); + mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); + + // disable any touch events beyond resizing too + mPipTouchState.setAllowInputEvents(false); + + /* + mPipTaskOrganizer.scheduleAnimateResizePip(startBounds, mLastResizeBounds, + PINCH_RESIZE_SNAP_DURATION, mAngle, mUpdateResizeBoundsCallback, () -> { + // enable touch events + mPipTouchState.setAllowInputEvents(true); + }); + */ + } else { + /* + mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, + TRANSITION_DIRECTION_USER_RESIZE, + mUpdateResizeBoundsCallback); + */ + } + final float magnetRadiusPercent = (float) mLastResizeBounds.width() / mMinSize.x / 2.f; + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); + } else { + resetState(); + } + } + + private void resetState() { + mCtrlType = CTRL_NONE; + mAngle = 0; + mOngoingPinchToResize = false; + mAllowGesture = false; + mThresholdCrossed = false; + } + + void setUserResizeBounds(Rect bounds) { + mUserResizeBounds.set(bounds); + } + + void invalidateUserResizeBounds() { + mUserResizeBounds.setEmpty(); + } + + Rect getUserResizeBounds() { + return mUserResizeBounds; + } + + @VisibleForTesting + Rect getLastResizeBounds() { + return mLastResizeBounds; + } + + @VisibleForTesting + void pilferPointers() { + mInputMonitor.pilferPointers(); + } + + + void updateMaxSize(int maxX, int maxY) { + mMaxSize.set(maxX, maxY); + } + + void updateMinSize(int minX, int minY) { + mMinSize.set(minX, minY); + } + + void setOhmOffset(int offset) { + mOhmOffset = offset; + } + + private float distanceBetween(PointF p1, PointF p2) { + return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y); + } + + private void resizeRectAboutCenter(Rect rect, int w, int h) { + int cx = rect.centerX(); + int cy = rect.centerY(); + int l = cx - w / 2; + int r = l + w; + int t = cy - h / 2; + int b = t + h; + rect.set(l, t, r, b); + } + + /** + * Dumps the {@link PipResizeGestureHandler} state. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture); + pw.println(innerPrefix + "mIsAttached=" + mIsAttached); + pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); + pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize); + pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); + pw.println(innerPrefix + "mOhmOffset=" + mOhmOffset); + pw.println(innerPrefix + "mMinSize=" + mMinSize); + pw.println(innerPrefix + "mMaxSize=" + mMaxSize); + } + + class PipResizeInputEventReceiver extends BatchedInputEventReceiver { + PipResizeInputEventReceiver(InputChannel channel, Looper looper) { + super(channel, looper, Choreographer.getInstance()); + } + + public void onInputEvent(InputEvent event) { + PipResizeGestureHandler.this.onInputEvent(event); + finishInputEvent(event, true); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java new file mode 100644 index 000000000000..efa5fc8bf8b1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java @@ -0,0 +1,47 @@ +/* + * 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.wm.shell.pip2.phone; + +/** + * A generic interface for a touch gesture. + */ +public abstract class PipTouchGesture { + + /** + * Handle the touch down. + */ + public void onDown(PipTouchState touchState) {} + + /** + * Handle the touch move, and return whether the event was consumed. + */ + public boolean onMove(PipTouchState touchState) { + return false; + } + + /** + * Handle the touch up, and return whether the gesture was consumed. + */ + public boolean onUp(PipTouchState touchState) { + return false; + } + + /** + * Cleans up the high performance hint session if needed. + */ + public void cleanUpHighPerfSessionMaybe() {} +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java new file mode 100644 index 000000000000..cc8e3e0934e6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -0,0 +1,1081 @@ +/* + * 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.wm.shell.pip2.phone; + +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASH_MINIMUM_VELOCITY_THRESHOLD; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE; +import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip2.phone.PhonePipMenuController.MENU_STATE_FULL; +import static com.android.wm.shell.pip2.phone.PhonePipMenuController.MENU_STATE_NONE; +import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.provider.DeviceConfig; +import android.util.Size; +import android.view.DisplayCutout; +import android.view.InputEvent; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDoubleTapHelper; +import com.android.wm.shell.common.pip.PipPerfHintController; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.common.pip.SizeSpecSource; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.sysui.ShellInit; + +import java.io.PrintWriter; +import java.util.Optional; + +/** + * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding + * the PIP. + */ +public class PipTouchHandler { + + private static final String TAG = "PipTouchHandler"; + private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f; + + // Allow PIP to resize to a slightly bigger state upon touch + private boolean mEnableResize; + private final Context mContext; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + @NonNull private final PipBoundsState mPipBoundsState; + @NonNull private final SizeSpecSource mSizeSpecSource; + private final PipUiEventLogger mPipUiEventLogger; + private final PipDismissTargetHandler mPipDismissTargetHandler; + private final ShellExecutor mMainExecutor; + @Nullable private final PipPerfHintController mPipPerfHintController; + + private PipResizeGestureHandler mPipResizeGestureHandler; + + private final PhonePipMenuController mMenuController; + private final AccessibilityManager mAccessibilityManager; + + /** + * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the + * screen, it will be shown in "stashed" mode, where PIP will only show partially. + */ + private boolean mEnableStash = true; + + private float mStashVelocityThreshold; + + // The reference inset bounds, used to determine the dismiss fraction + private final Rect mInsetBounds = new Rect(); + + // Used to workaround an issue where the WM rotation happens before we are notified, allowing + // us to send stale bounds + private int mDeferResizeToNormalBoundsUntilRotation = -1; + private int mDisplayRotation; + + // Behaviour states + private int mMenuState = MENU_STATE_NONE; + private boolean mIsImeShowing; + private int mImeHeight; + private int mImeOffset; + private boolean mIsShelfShowing; + private int mShelfHeight; + private int mMovementBoundsExtraOffsets; + private int mBottomOffsetBufferPx; + private float mSavedSnapFraction = -1f; + private boolean mSendingHoverAccessibilityEvents; + private boolean mMovementWithinDismiss; + + // Touch state + private final PipTouchState mTouchState; + private final FloatingContentCoordinator mFloatingContentCoordinator; + private PipMotionHelper mMotionHelper; + private PipTouchGesture mGesture; + + // Temp vars + private final Rect mTmpBounds = new Rect(); + + /** + * A listener for the PIP menu activity. + */ + private class PipMenuListener implements PhonePipMenuController.Listener { + @Override + public void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { + PipTouchHandler.this.onPipMenuStateChangeStart(menuState, resize, callback); + } + + @Override + public void onPipMenuStateChangeFinish(int menuState) { + setMenuState(menuState); + } + + @Override + public void onPipExpand() { + mMotionHelper.expandLeavePip(false /* skipAnimation */); + } + + @Override + public void onPipDismiss() { + mTouchState.removeDoubleTapTimeoutCallback(); + mMotionHelper.dismissPip(); + } + + @Override + public void onPipShowMenu() { + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()); + } + } + + @SuppressLint("InflateParams") + public PipTouchHandler(Context context, + ShellInit shellInit, + PhonePipMenuController menuController, + PipBoundsAlgorithm pipBoundsAlgorithm, + @NonNull PipBoundsState pipBoundsState, + @NonNull SizeSpecSource sizeSpecSource, + PipMotionHelper pipMotionHelper, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger, + ShellExecutor mainExecutor, + Optional<PipPerfHintController> pipPerfHintControllerOptional) { + mContext = context; + mMainExecutor = mainExecutor; + mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipBoundsState = pipBoundsState; + mSizeSpecSource = sizeSpecSource; + mMenuController = menuController; + mPipUiEventLogger = pipUiEventLogger; + mFloatingContentCoordinator = floatingContentCoordinator; + mMenuController.addListener(new PipMenuListener()); + mGesture = new DefaultPipTouchGesture(); + mMotionHelper = pipMotionHelper; + mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger, + mMotionHelper, mainExecutor); + mTouchState = new PipTouchState(ViewConfiguration.get(context), + () -> { + if (mPipBoundsState.isStashed()) { + animateToUnStashedState(); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + } else { + mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL, + mPipBoundsState.getBounds(), true /* allowMenuTimeout */, + willResizeMenu(), + shouldShowResizeHandle()); + } + }, + menuController::hideMenu, + mainExecutor); + mPipResizeGestureHandler = + new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, + mTouchState, this::updateMovementBounds, pipUiEventLogger, + menuController, mainExecutor, mPipPerfHintController); + + if (PipUtils.isPip2ExperimentEnabled()) { + shellInit.addInitCallback(this::onInit, this); + } + } + + /** + * Called when the touch handler is initialized. + */ + public void onInit() { + Resources res = mContext.getResources(); + mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu); + reloadResources(); + + mMotionHelper.init(); + mPipResizeGestureHandler.init(); + mPipDismissTargetHandler.init(); + + mEnableStash = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_STASHING, + /* defaultValue = */ true); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mMainExecutor, + properties -> { + if (properties.getKeyset().contains(PIP_STASHING)) { + mEnableStash = properties.getBoolean( + PIP_STASHING, /* defaultValue = */ true); + } + }); + mStashVelocityThreshold = DeviceConfig.getFloat( + DeviceConfig.NAMESPACE_SYSTEMUI, + PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, + DEFAULT_STASH_VELOCITY_THRESHOLD); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mMainExecutor, + properties -> { + if (properties.getKeyset().contains(PIP_STASH_MINIMUM_VELOCITY_THRESHOLD)) { + mStashVelocityThreshold = properties.getFloat( + PIP_STASH_MINIMUM_VELOCITY_THRESHOLD, + DEFAULT_STASH_VELOCITY_THRESHOLD); + } + }); + } + + public PipTransitionController getTransitionHandler() { + // return mPipTaskOrganizer.getTransitionController(); + return null; + } + + private void reloadResources() { + final Resources res = mContext.getResources(); + mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer); + mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); + mPipDismissTargetHandler.updateMagneticTargetSize(); + } + + void onOverlayChanged() { + // onOverlayChanged is triggered upon theme change, update the dismiss target accordingly. + mPipDismissTargetHandler.init(); + } + + private boolean shouldShowResizeHandle() { + return false; + } + + void setTouchGesture(PipTouchGesture gesture) { + mGesture = gesture; + } + + void setTouchEnabled(boolean enabled) { + mTouchState.setAllowTouches(enabled); + } + + void showPictureInPictureMenu() { + // Only show the menu if the user isn't currently interacting with the PiP + if (!mTouchState.isUserInteracting()) { + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + false /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + } + + void onActivityPinned() { + mPipDismissTargetHandler.createOrUpdateDismissTarget(); + + mPipResizeGestureHandler.onActivityPinned(); + mFloatingContentCoordinator.onContentAdded(mMotionHelper); + } + + void onActivityUnpinned(ComponentName topPipActivity) { + if (topPipActivity == null) { + // Clean up state after the last PiP activity is removed + mPipDismissTargetHandler.cleanUpDismissTarget(); + + mFloatingContentCoordinator.onContentRemoved(mMotionHelper); + } + mPipResizeGestureHandler.onActivityUnpinned(); + } + + void onPinnedStackAnimationEnded( + @PipAnimationController.TransitionDirection int direction) { + // Always synchronize the motion helper bounds once PiP animations finish + mMotionHelper.synchronizePinnedStackBounds(); + updateMovementBounds(); + if (direction == TRANSITION_DIRECTION_TO_PIP) { + // Set the initial bounds as the user resize bounds. + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + } + } + + void onConfigurationChanged() { + mPipResizeGestureHandler.onConfigurationChanged(); + mMotionHelper.synchronizePinnedStackBounds(); + reloadResources(); + + /* + if (mPipTaskOrganizer.isInPip()) { + // Recreate the dismiss target for the new orientation. + mPipDismissTargetHandler.createOrUpdateDismissTarget(); + } + */ + } + + void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mIsImeShowing = imeVisible; + mImeHeight = imeHeight; + } + + void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { + mIsShelfShowing = shelfVisible; + mShelfHeight = shelfHeight; + } + + /** + * Called when SysUI state changed. + * + * @param isSysUiStateValid Is SysUI valid or not. + */ + public void onSystemUiStateChanged(boolean isSysUiStateValid) { + mPipResizeGestureHandler.onSystemUiStateChanged(isSysUiStateValid); + } + + void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) { + final Rect toMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(outBounds, insetBounds, toMovementBounds, 0); + final int prevBottom = mPipBoundsState.getMovementBounds().bottom + - mMovementBoundsExtraOffsets; + if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) { + outBounds.offsetTo(outBounds.left, toMovementBounds.bottom); + } + } + + /** + * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window. + */ + public void onAspectRatioChanged() { + mPipResizeGestureHandler.invalidateUserResizeBounds(); + } + + void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, + boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) { + // Set the user resized bounds equal to the new normal bounds in case they were + // invalidated (e.g. by an aspect ratio change). + if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) { + mPipResizeGestureHandler.setUserResizeBounds(normalBounds); + } + + final int bottomOffset = mIsImeShowing ? mImeHeight : 0; + final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation); + if (fromDisplayRotationChanged) { + mTouchState.reset(); + } + + // Re-calculate the expanded bounds + Rect normalMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(normalBounds, insetBounds, + normalMovementBounds, bottomOffset); + + if (mPipBoundsState.getMovementBounds().isEmpty()) { + // mMovementBounds is not initialized yet and a clean movement bounds without + // bottom offset shall be used later in this function. + mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds, + mPipBoundsState.getMovementBounds(), 0 /* bottomOffset */); + } + + // Calculate the expanded size + float aspectRatio = (float) normalBounds.width() / normalBounds.height(); + Size expandedSize = mSizeSpecSource.getDefaultSize(aspectRatio); + mPipBoundsState.setExpandedBounds( + new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight())); + Rect expandedMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds( + mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds, + bottomOffset); + + updatePipSizeConstraints(normalBounds, aspectRatio); + + // The extra offset does not really affect the movement bounds, but are applied based on the + // current state (ime showing, or shelf offset) when we need to actually shift + int extraOffset = Math.max( + mIsImeShowing ? mImeOffset : 0, + !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0); + + // Update the movement bounds after doing the calculations based on the old movement bounds + // above + mPipBoundsState.setNormalMovementBounds(normalMovementBounds); + mPipBoundsState.setExpandedMovementBounds(expandedMovementBounds); + mDisplayRotation = displayRotation; + mInsetBounds.set(insetBounds); + updateMovementBounds(); + mMovementBoundsExtraOffsets = extraOffset; + + // If we have a deferred resize, apply it now + if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) { + mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction, + mPipBoundsState.getNormalMovementBounds(), mPipBoundsState.getMovementBounds(), + true /* immediate */); + mSavedSnapFraction = -1f; + mDeferResizeToNormalBoundsUntilRotation = -1; + } + } + + /** + * Update the values for min/max allowed size of picture in picture window based on the aspect + * ratio. + * @param aspectRatio aspect ratio to use for the calculation of min/max size + */ + public void updateMinMaxSize(float aspectRatio) { + updatePipSizeConstraints(mPipBoundsState.getNormalBounds(), + aspectRatio); + } + + private void updatePipSizeConstraints(Rect normalBounds, + float aspectRatio) { + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + updatePinchResizeSizeConstraints(aspectRatio); + } else { + mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height()); + mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(), + mPipBoundsState.getExpandedBounds().height()); + } + } + + private void updatePinchResizeSizeConstraints(float aspectRatio) { + mPipBoundsState.updateMinMaxSize(aspectRatio); + mPipResizeGestureHandler.updateMinSize(mPipBoundsState.getMinSize().x, + mPipBoundsState.getMinSize().y); + mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getMaxSize().x, + mPipBoundsState.getMaxSize().y); + } + + /** + * TODO Add appropriate description + */ + public void onRegistrationChanged(boolean isRegistered) { + if (isRegistered) { + // Register the accessibility connection. + } else { + mAccessibilityManager.setPictureInPictureActionReplacingConnection(null); + } + if (!isRegistered && mTouchState.isUserInteracting()) { + // If the input consumer is unregistered while the user is interacting, then we may not + // get the final TOUCH_UP event, so clean up the dismiss target as well + mPipDismissTargetHandler.cleanUpDismissTarget(); + } + } + + private void onAccessibilityShowMenu() { + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + + /** + * TODO Add appropriate description + */ + public boolean handleTouchEvent(InputEvent inputEvent) { + // Skip any non motion events + if (!(inputEvent instanceof MotionEvent)) { + return true; + } + + // do not process input event if not allowed + if (!mTouchState.getAllowInputEvents()) { + return true; + } + + MotionEvent ev = (MotionEvent) inputEvent; + if (!mPipBoundsState.isStashed() && mPipResizeGestureHandler.willStartResizeGesture(ev)) { + // Initialize the touch state for the gesture, but immediately reset to invalidate the + // gesture + mTouchState.onTouchEvent(ev); + mTouchState.reset(); + return true; + } + + if (mPipResizeGestureHandler.hasOngoingGesture()) { + mGesture.cleanUpHighPerfSessionMaybe(); + mPipDismissTargetHandler.hideDismissTargetMaybe(); + return true; + } + + if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting()) + && mPipDismissTargetHandler.maybeConsumeMotionEvent(ev)) { + // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event + // to the touch state. Touch state needs a DOWN event in order to later process MOVE + // events it'll receive if the object is dragged out of the magnetic field. + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mTouchState.onTouchEvent(ev); + } + + // Continue tracking velocity when the object is in the magnetic field, since we want to + // respect touch input velocity if the object is dragged out and then flung. + mTouchState.addMovementToVelocityTracker(ev); + + return true; + } + + if (!mTouchState.isUserInteracting()) { + ProtoLog.wtf(WM_SHELL_PICTURE_IN_PICTURE, + "%s: Waiting to start the entry animation, skip the motion event.", TAG); + return true; + } + + // Update the touch state + mTouchState.onTouchEvent(ev); + + boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE; + + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: { + mGesture.onDown(mTouchState); + break; + } + case MotionEvent.ACTION_MOVE: { + if (mGesture.onMove(mTouchState)) { + break; + } + + shouldDeliverToMenu = !mTouchState.isDragging(); + break; + } + case MotionEvent.ACTION_UP: { + // Update the movement bounds again if the state has changed since the user started + // dragging (ie. when the IME shows) + updateMovementBounds(); + + if (mGesture.onUp(mTouchState)) { + break; + } + } + // Fall through to clean up + case MotionEvent.ACTION_CANCEL: { + shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging(); + mTouchState.reset(); + break; + } + case MotionEvent.ACTION_HOVER_ENTER: { + // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably + // on and changing MotionEvents into HoverEvents. + // Let's not enable menu show/hide for a11y services. + if (!mAccessibilityManager.isTouchExplorationEnabled()) { + mTouchState.removeHoverExitTimeoutCallback(); + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + false /* allowMenuTimeout */, false /* willResizeMenu */, + shouldShowResizeHandle()); + } + } + // Fall through + case MotionEvent.ACTION_HOVER_MOVE: { + if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) { + sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + mSendingHoverAccessibilityEvents = true; + } + break; + } + case MotionEvent.ACTION_HOVER_EXIT: { + // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably + // on and changing MotionEvents into HoverEvents. + // Let's not enable menu show/hide for a11y services. + if (!mAccessibilityManager.isTouchExplorationEnabled()) { + mTouchState.scheduleHoverExitTimeoutCallback(); + } + if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) { + sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + mSendingHoverAccessibilityEvents = false; + } + break; + } + } + + shouldDeliverToMenu &= !mPipBoundsState.isStashed(); + + // Deliver the event to PipMenuActivity to handle button click if the menu has shown. + if (shouldDeliverToMenu) { + final MotionEvent cloneEvent = MotionEvent.obtain(ev); + // Send the cancel event and cancel menu timeout if it starts to drag. + if (mTouchState.startedDragging()) { + cloneEvent.setAction(MotionEvent.ACTION_CANCEL); + mMenuController.pokeMenu(); + } + + mMenuController.handlePointerEvent(cloneEvent); + cloneEvent.recycle(); + } + + return true; + } + + private void sendAccessibilityHoverEvent(int type) { + if (!mAccessibilityManager.isEnabled()) { + return; + } + + AccessibilityEvent event = AccessibilityEvent.obtain(type); + event.setImportantForAccessibility(true); + event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID); + event.setWindowId( + AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); + mAccessibilityManager.sendAccessibilityEvent(event); + } + + /** + * Called when the PiP menu state is in the process of animating/changing from one to another. + */ + private void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { + if (mMenuState == menuState && !resize) { + return; + } + + if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) { + // Save the current snap fraction and if we do not drag or move the PiP, then + // we store back to this snap fraction. Otherwise, we'll reset the snap + // fraction and snap to the closest edge. + if (resize) { + // PIP is too small to show the menu actions and thus needs to be resized to a + // size that can fit them all. Resize to the default size. + animateToNormalSize(callback); + } + } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) { + // Try and restore the PiP to the closest edge, using the saved snap fraction + // if possible + if (resize && !mPipResizeGestureHandler.isResizing()) { + if (mDeferResizeToNormalBoundsUntilRotation == -1) { + // This is a very special case: when the menu is expanded and visible, + // navigating to another activity can trigger auto-enter PiP, and if the + // revealed activity has a forced rotation set, then the controller will get + // updated with the new rotation of the display. However, at the same time, + // SystemUI will try to hide the menu by creating an animation to the normal + // bounds which are now stale. In such a case we defer the animation to the + // normal bounds until after the next onMovementBoundsChanged() call to get the + // bounds in the new orientation + int displayRotation = mContext.getDisplay().getRotation(); + if (mDisplayRotation != displayRotation) { + mDeferResizeToNormalBoundsUntilRotation = displayRotation; + } + } + + if (mDeferResizeToNormalBoundsUntilRotation == -1) { + animateToUnexpandedState(getUserResizeBounds()); + } + } else { + mSavedSnapFraction = -1f; + } + } + } + + private void setMenuState(int menuState) { + mMenuState = menuState; + updateMovementBounds(); + // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip + // as well, or it can't handle a11y focus and pip menu can't perform any action. + onRegistrationChanged(menuState == MENU_STATE_NONE); + if (menuState == MENU_STATE_NONE) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU); + } else if (menuState == MENU_STATE_FULL) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU); + } + } + + private void animateToMaximizedState(Runnable callback) { + Rect maxMovementBounds = new Rect(); + Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x, + mPipBoundsState.getMaxSize().y); + mPipBoundsAlgorithm.getMovementBounds(maxBounds, mInsetBounds, maxMovementBounds, + mIsImeShowing ? mImeHeight : 0); + mSavedSnapFraction = mMotionHelper.animateToExpandedState(maxBounds, + mPipBoundsState.getMovementBounds(), maxMovementBounds, + callback); + } + + private void animateToNormalSize(Runnable callback) { + // Save the current bounds as the user-resize bounds. + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + + final Size minMenuSize = mMenuController.getEstimatedMinMenuSize(); + final Rect normalBounds = mPipBoundsState.getNormalBounds(); + final Rect destBounds = mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, + minMenuSize); + Rect restoredMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(destBounds, + mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); + mSavedSnapFraction = mMotionHelper.animateToExpandedState(destBounds, + mPipBoundsState.getMovementBounds(), restoredMovementBounds, callback); + } + + private void animateToUnexpandedState(Rect restoreBounds) { + Rect restoredMovementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(restoreBounds, + mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); + mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction, + restoredMovementBounds, mPipBoundsState.getMovementBounds(), false /* immediate */); + mSavedSnapFraction = -1f; + } + + private void animateToUnStashedState() { + final Rect pipBounds = mPipBoundsState.getBounds(); + final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left; + final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom); + unStashedBounds.left = onLeftEdge ? mInsetBounds.left + : mInsetBounds.right - pipBounds.width(); + unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width() + : mInsetBounds.right; + mMotionHelper.animateToUnStashedBounds(unStashedBounds); + } + + /** + * @return the motion helper. + */ + public PipMotionHelper getMotionHelper() { + return mMotionHelper; + } + + @VisibleForTesting + public PipResizeGestureHandler getPipResizeGestureHandler() { + return mPipResizeGestureHandler; + } + + @VisibleForTesting + public void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) { + mPipResizeGestureHandler = pipResizeGestureHandler; + } + + @VisibleForTesting + public void setPipMotionHelper(PipMotionHelper pipMotionHelper) { + mMotionHelper = pipMotionHelper; + } + + Rect getUserResizeBounds() { + return mPipResizeGestureHandler.getUserResizeBounds(); + } + + /** + * Sets the user resize bounds tracked by {@link PipResizeGestureHandler} + */ + void setUserResizeBounds(Rect bounds) { + mPipResizeGestureHandler.setUserResizeBounds(bounds); + } + + /** + * Gesture controlling normal movement of the PIP. + */ + private class DefaultPipTouchGesture extends PipTouchGesture { + private final Point mStartPosition = new Point(); + private final PointF mDelta = new PointF(); + private boolean mShouldHideMenuAfterFling; + + @Nullable private PipPerfHintController.PipHighPerfSession mPipHighPerfSession; + + private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {} + + @Override + public void cleanUpHighPerfSessionMaybe() { + if (mPipHighPerfSession != null) { + // Close the high perf session once pointer interactions are over; + mPipHighPerfSession.close(); + mPipHighPerfSession = null; + } + } + + @Override + public void onDown(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return; + } + + if (mPipPerfHintController != null) { + // Cache the PiP high perf session to close it upon touch up. + mPipHighPerfSession = mPipPerfHintController.startSession( + this::onHighPerfSessionTimeout, "DefaultPipTouchGesture#onDown"); + } + + Rect bounds = getPossiblyMotionBounds(); + mDelta.set(0f, 0f); + mStartPosition.set(bounds.left, bounds.top); + mMovementWithinDismiss = touchState.getDownTouchPosition().y + >= mPipBoundsState.getMovementBounds().bottom; + mMotionHelper.setSpringingToTouch(false); + // mPipDismissTargetHandler.setTaskLeash(mPipTaskOrganizer.getSurfaceControl()); + + // If the menu is still visible then just poke the menu + // so that it will timeout after the user stops touching it + if (mMenuState != MENU_STATE_NONE && !mPipBoundsState.isStashed()) { + mMenuController.pokeMenu(); + } + } + + @Override + public boolean onMove(PipTouchState touchState) { + if (!touchState.isUserInteracting()) { + return false; + } + + if (touchState.startedDragging()) { + mSavedSnapFraction = -1f; + mPipDismissTargetHandler.showDismissTargetMaybe(); + } + + if (touchState.isDragging()) { + mPipBoundsState.setHasUserMovedPip(true); + + // Move the pinned stack freely + final PointF lastDelta = touchState.getLastTouchDelta(); + float lastX = mStartPosition.x + mDelta.x; + float lastY = mStartPosition.y + mDelta.y; + float left = lastX + lastDelta.x; + float top = lastY + lastDelta.y; + + // Add to the cumulative delta after bounding the position + mDelta.x += left - lastX; + mDelta.y += top - lastY; + + mTmpBounds.set(getPossiblyMotionBounds()); + mTmpBounds.offsetTo((int) left, (int) top); + mMotionHelper.movePip(mTmpBounds, true /* isDragging */); + + final PointF curPos = touchState.getLastTouchPosition(); + if (mMovementWithinDismiss) { + // Track if movement remains near the bottom edge to identify swipe to dismiss + mMovementWithinDismiss = curPos.y >= mPipBoundsState.getMovementBounds().bottom; + } + return true; + } + return false; + } + + @Override + public boolean onUp(PipTouchState touchState) { + mPipDismissTargetHandler.hideDismissTargetMaybe(); + mPipDismissTargetHandler.setTaskLeash(null); + + if (!touchState.isUserInteracting()) { + return false; + } + + final PointF vel = touchState.getVelocity(); + + if (touchState.isDragging()) { + if (mMenuState != MENU_STATE_NONE) { + // If the menu is still visible, then just poke the menu so that + // it will timeout after the user stops touching it + mMenuController.showMenu(mMenuState, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } + mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE; + + // Reset the touch state on up before the fling settles + mTouchState.reset(); + if (mEnableStash && shouldStash(vel, getPossiblyMotionBounds())) { + mMotionHelper.stashToEdge(vel.x, vel.y, this::stashEndAction /* endAction */); + } else { + if (mPipBoundsState.isStashed()) { + // Reset stashed state if previously stashed + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + } + mMotionHelper.flingToSnapTarget(vel.x, vel.y, + this::flingEndAction /* endAction */); + } + } else if (mTouchState.isDoubleTap() && !mPipBoundsState.isStashed() + && mMenuState != MENU_STATE_FULL) { + // If using pinch to zoom, double-tap functions as resizing between max/min size + if (mPipResizeGestureHandler.isUsingPinchToZoom()) { + final boolean toExpand = mPipBoundsState.getBounds().width() + < mPipBoundsState.getMaxSize().x + && mPipBoundsState.getBounds().height() + < mPipBoundsState.getMaxSize().y; + if (mMenuController.isMenuVisible()) { + mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); + } + + // the size to toggle to after a double tap + int nextSize = PipDoubleTapHelper + .nextSizeSpec(mPipBoundsState, getUserResizeBounds()); + + // actually toggle to the size chosen + if (nextSize == PipDoubleTapHelper.SIZE_SPEC_MAX) { + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + animateToMaximizedState(null); + } else if (nextSize == PipDoubleTapHelper.SIZE_SPEC_DEFAULT) { + mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); + animateToNormalSize(null); + } else { + animateToUnexpandedState(getUserResizeBounds()); + } + } else { + // Expand to fullscreen if this is a double tap + // the PiP should be frozen until the transition ends + setTouchEnabled(false); + mMotionHelper.expandLeavePip(false /* skipAnimation */); + } + } else if (mMenuState != MENU_STATE_FULL) { + if (mPipBoundsState.isStashed()) { + // Unstash immediately if stashed, and don't wait for the double tap timeout + animateToUnStashedState(); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED); + mPipBoundsState.setStashed(STASH_TYPE_NONE); + mTouchState.removeDoubleTapTimeoutCallback(); + } else if (!mTouchState.isWaitingForDoubleTap()) { + // User has stalled long enough for this not to be a drag or a double tap, + // just expand the menu + mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(), + true /* allowMenuTimeout */, willResizeMenu(), + shouldShowResizeHandle()); + } else { + // Next touch event _may_ be the second tap for the double-tap, schedule a + // fallback runnable to trigger the menu if no touch event occurs before the + // next tap + mTouchState.scheduleDoubleTapTimeoutCallback(); + } + } + cleanUpHighPerfSessionMaybe(); + return true; + } + + private void stashEndAction() { + if (mPipBoundsState.getBounds().left < 0 + && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) { + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_LEFT); + mPipBoundsState.setStashed(STASH_TYPE_LEFT); + } else if (mPipBoundsState.getBounds().left >= 0 + && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) { + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_RIGHT); + mPipBoundsState.setStashed(STASH_TYPE_RIGHT); + } + mMenuController.hideMenu(); + } + + private void flingEndAction() { + if (mShouldHideMenuAfterFling) { + // If the menu is not visible, then we can still be showing the activity for the + // dismiss overlay, so just finish it after the animation completes + mMenuController.hideMenu(); + } + } + + private boolean shouldStash(PointF vel, Rect motionBounds) { + final boolean flingToLeft = vel.x < -mStashVelocityThreshold; + final boolean flingToRight = vel.x > mStashVelocityThreshold; + final int offset = motionBounds.width() / 2; + final boolean droppingOnLeft = + motionBounds.left < mPipBoundsState.getDisplayBounds().left - offset; + final boolean droppingOnRight = + motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset; + + // Do not allow stash if the destination edge contains display cutout. We only + // compare the left and right edges since we do not allow stash on top / bottom. + final DisplayCutout displayCutout = + mPipBoundsState.getDisplayLayout().getDisplayCutout(); + if (displayCutout != null) { + if ((flingToLeft || droppingOnLeft) + && !displayCutout.getBoundingRectLeft().isEmpty()) { + return false; + } else if ((flingToRight || droppingOnRight) + && !displayCutout.getBoundingRectRight().isEmpty()) { + return false; + } + } + + // If user flings the PIP window above the minimum velocity, stash PIP. + // Only allow stashing to the edge if PIP wasn't previously stashed on the opposite + // edge. + final boolean stashFromFlingToEdge = + (flingToLeft && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) + || (flingToRight && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT); + + // If User releases the PIP window while it's out of the display bounds, put + // PIP into stashed mode. + final boolean stashFromDroppingOnEdge = droppingOnLeft || droppingOnRight; + + return stashFromFlingToEdge || stashFromDroppingOnEdge; + } + } + + /** + * Updates the current movement bounds based on whether the menu is currently visible and + * resized. + */ + private void updateMovementBounds() { + mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(), + mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0); + mMotionHelper.onMovementBoundsChanged(); + } + + private Rect getMovementBounds(Rect curBounds) { + Rect movementBounds = new Rect(); + mPipBoundsAlgorithm.getMovementBounds(curBounds, mInsetBounds, + movementBounds, mIsImeShowing ? mImeHeight : 0); + return movementBounds; + } + + /** + * @return {@code true} if the menu should be resized on tap because app explicitly specifies + * PiP window size that is too small to hold all the actions. + */ + private boolean willResizeMenu() { + if (!mEnableResize) { + return false; + } + final Size estimatedMinMenuSize = mMenuController.getEstimatedMinMenuSize(); + if (estimatedMinMenuSize == null) { + ProtoLog.wtf(WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to get estimated menu size", TAG); + return false; + } + final Rect currentBounds = mPipBoundsState.getBounds(); + return currentBounds.width() < estimatedMinMenuSize.getWidth() + || currentBounds.height() < estimatedMinMenuSize.getHeight(); + } + + /** + * Returns the PIP bounds if we're not in the middle of a motion operation, or the current, + * temporary motion bounds otherwise. + */ + Rect getPossiblyMotionBounds() { + return mPipBoundsState.getMotionBoundsState().isInMotion() + ? mPipBoundsState.getMotionBoundsState().getBoundsInMotion() + : mPipBoundsState.getBounds(); + } + + void setOhmOffset(int offset) { + mPipResizeGestureHandler.setOhmOffset(offset); + } + + /** + * Dumps the {@link PipTouchHandler} state. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mMenuState=" + mMenuState); + pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); + pw.println(innerPrefix + "mImeHeight=" + mImeHeight); + pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); + pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); + pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction); + pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets); + mPipBoundsAlgorithm.dump(pw, innerPrefix); + mTouchState.dump(pw, innerPrefix); + if (mPipResizeGestureHandler != null) { + mPipResizeGestureHandler.dump(pw, innerPrefix); + } + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java new file mode 100644 index 000000000000..d093f1e5ccc1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java @@ -0,0 +1,427 @@ +/* + * 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.wm.shell.pip2.phone; + +import android.graphics.PointF; +import android.view.Display; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; + +/** + * This keeps track of the touch state throughout the current touch gesture. + */ +public class PipTouchState { + private static final String TAG = "PipTouchState"; + private static final boolean DEBUG = false; + + @VisibleForTesting + public static final long DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); + static final long HOVER_EXIT_TIMEOUT = 50; + + private final ShellExecutor mMainExecutor; + private final ViewConfiguration mViewConfig; + private final Runnable mDoubleTapTimeoutCallback; + private final Runnable mHoverExitTimeoutCallback; + + private VelocityTracker mVelocityTracker; + private long mDownTouchTime = 0; + private long mLastDownTouchTime = 0; + private long mUpTouchTime = 0; + private final PointF mDownTouch = new PointF(); + private final PointF mDownDelta = new PointF(); + private final PointF mLastTouch = new PointF(); + private final PointF mLastDelta = new PointF(); + private final PointF mVelocity = new PointF(); + private boolean mAllowTouches = true; + + // Set to false to block both PipTouchHandler and PipResizeGestureHandler's input processing + private boolean mAllowInputEvents = true; + private boolean mIsUserInteracting = false; + // Set to true only if the multiple taps occur within the double tap timeout + private boolean mIsDoubleTap = false; + // Set to true only if a gesture + private boolean mIsWaitingForDoubleTap = false; + private boolean mIsDragging = false; + // The previous gesture was a drag + private boolean mPreviouslyDragging = false; + private boolean mStartedDragging = false; + private boolean mAllowDraggingOffscreen = false; + private int mActivePointerId; + private int mLastTouchDisplayId = Display.INVALID_DISPLAY; + + public PipTouchState(ViewConfiguration viewConfig, Runnable doubleTapTimeoutCallback, + Runnable hoverExitTimeoutCallback, ShellExecutor mainExecutor) { + mViewConfig = viewConfig; + mDoubleTapTimeoutCallback = doubleTapTimeoutCallback; + mHoverExitTimeoutCallback = hoverExitTimeoutCallback; + mMainExecutor = mainExecutor; + } + + /** + * @return true if input processing is enabled for PiP in general. + */ + public boolean getAllowInputEvents() { + return mAllowInputEvents; + } + + /** + * @param allowInputEvents true to enable input processing for PiP in general. + */ + public void setAllowInputEvents(boolean allowInputEvents) { + mAllowInputEvents = allowInputEvents; + } + + /** + * Resets this state. + */ + public void reset() { + mAllowDraggingOffscreen = false; + mIsDragging = false; + mStartedDragging = false; + mIsUserInteracting = false; + mLastTouchDisplayId = Display.INVALID_DISPLAY; + } + + /** + * Processes a given touch event and updates the state. + */ + public void onTouchEvent(MotionEvent ev) { + mLastTouchDisplayId = ev.getDisplayId(); + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + if (!mAllowTouches) { + return; + } + + // Initialize the velocity tracker + initOrResetVelocityTracker(); + addMovementToVelocityTracker(ev); + + mActivePointerId = ev.getPointerId(0); + if (DEBUG) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Setting active pointer id on DOWN: %d", TAG, mActivePointerId); + } + mLastTouch.set(ev.getRawX(), ev.getRawY()); + mDownTouch.set(mLastTouch); + mAllowDraggingOffscreen = true; + mIsUserInteracting = true; + mDownTouchTime = ev.getEventTime(); + mIsDoubleTap = !mPreviouslyDragging + && (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT; + mIsWaitingForDoubleTap = false; + mIsDragging = false; + mLastDownTouchTime = mDownTouchTime; + if (mDoubleTapTimeoutCallback != null) { + mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback); + } + break; + } + case MotionEvent.ACTION_MOVE: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid active pointer id on MOVE: %d", TAG, mActivePointerId); + break; + } + + float x = ev.getRawX(pointerIndex); + float y = ev.getRawY(pointerIndex); + mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y); + mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y); + + boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop(); + if (!mIsDragging) { + if (hasMovedBeyondTap) { + mIsDragging = true; + mStartedDragging = true; + } + } else { + mStartedDragging = false; + } + mLastTouch.set(x, y); + break; + } + case MotionEvent.ACTION_POINTER_UP: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + + int pointerIndex = ev.getActionIndex(); + int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // Select a new active pointer id and reset the movement state + final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + if (DEBUG) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Relinquish active pointer id on POINTER_UP: %d", + TAG, mActivePointerId); + } + mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex)); + } + break; + } + case MotionEvent.ACTION_UP: { + // Skip event if we did not start processing this touch gesture + if (!mIsUserInteracting) { + break; + } + + // Update the velocity tracker + addMovementToVelocityTracker(ev); + mVelocityTracker.computeCurrentVelocity(1000, + mViewConfig.getScaledMaximumFlingVelocity()); + mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + + int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Invalid active pointer id on UP: %d", TAG, mActivePointerId); + break; + } + + mUpTouchTime = ev.getEventTime(); + mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex)); + mPreviouslyDragging = mIsDragging; + mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging + && (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT; + + } + // fall through to clean up + case MotionEvent.ACTION_CANCEL: { + recycleVelocityTracker(); + break; + } + case MotionEvent.ACTION_BUTTON_PRESS: { + removeHoverExitTimeoutCallback(); + break; + } + } + } + + /** + * @return the velocity of the active touch pointer at the point it is lifted off the screen. + */ + public PointF getVelocity() { + return mVelocity; + } + + /** + * @return the last touch position of the active pointer. + */ + public PointF getLastTouchPosition() { + return mLastTouch; + } + + /** + * @return the movement delta between the last handled touch event and the previous touch + * position. + */ + public PointF getLastTouchDelta() { + return mLastDelta; + } + + /** + * @return the down touch position. + */ + public PointF getDownTouchPosition() { + return mDownTouch; + } + + /** + * @return the movement delta between the last handled touch event and the down touch + * position. + */ + public PointF getDownTouchDelta() { + return mDownDelta; + } + + /** + * @return whether the user has started dragging. + */ + public boolean isDragging() { + return mIsDragging; + } + + /** + * @return whether the user is currently interacting with the PiP. + */ + public boolean isUserInteracting() { + return mIsUserInteracting; + } + + /** + * @return whether the user has started dragging just in the last handled touch event. + */ + public boolean startedDragging() { + return mStartedDragging; + } + + /** + * @return Display ID of the last touch event. + */ + public int getLastTouchDisplayId() { + return mLastTouchDisplayId; + } + + /** + * Sets whether touching is currently allowed. + */ + public void setAllowTouches(boolean allowTouches) { + mAllowTouches = allowTouches; + + // If the user happens to touch down before this is sent from the system during a transition + // then block any additional handling by resetting the state now + if (mIsUserInteracting) { + reset(); + } + } + + /** + * Disallows dragging offscreen for the duration of the current gesture. + */ + public void setDisallowDraggingOffscreen() { + mAllowDraggingOffscreen = false; + } + + /** + * @return whether dragging offscreen is allowed during this gesture. + */ + public boolean allowDraggingOffscreen() { + return mAllowDraggingOffscreen; + } + + /** + * @return whether this gesture is a double-tap. + */ + public boolean isDoubleTap() { + return mIsDoubleTap; + } + + /** + * @return whether this gesture will potentially lead to a following double-tap. + */ + public boolean isWaitingForDoubleTap() { + return mIsWaitingForDoubleTap; + } + + /** + * Schedules the callback to run if the next double tap does not occur. Only runs if + * isWaitingForDoubleTap() is true. + */ + public void scheduleDoubleTapTimeoutCallback() { + if (mIsWaitingForDoubleTap) { + long delay = getDoubleTapTimeoutCallbackDelay(); + mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback); + mMainExecutor.executeDelayed(mDoubleTapTimeoutCallback, delay); + } + } + + long getDoubleTapTimeoutCallbackDelay() { + if (mIsWaitingForDoubleTap) { + return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime)); + } + return -1; + } + + /** + * Removes the timeout callback if it's in queue. + */ + public void removeDoubleTapTimeoutCallback() { + mIsWaitingForDoubleTap = false; + mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback); + } + + void scheduleHoverExitTimeoutCallback() { + mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback); + mMainExecutor.executeDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT); + } + + void removeHoverExitTimeoutCallback() { + mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback); + } + + void addMovementToVelocityTracker(MotionEvent event) { + if (mVelocityTracker == null) { + return; + } + + // Add movement to velocity tracker using raw screen X and Y coordinates instead + // of window coordinates because the window frame may be moving at the same time. + float deltaX = event.getRawX() - event.getX(); + float deltaY = event.getRawY() - event.getY(); + event.offsetLocation(deltaX, deltaY); + mVelocityTracker.addMovement(event); + event.offsetLocation(-deltaX, -deltaY); + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * Dumps the {@link PipTouchState}. + */ + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches); + pw.println(innerPrefix + "mAllowInputEvents=" + mAllowInputEvents); + pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId); + pw.println(innerPrefix + "mLastTouchDisplayId=" + mLastTouchDisplayId); + pw.println(innerPrefix + "mDownTouch=" + mDownTouch); + pw.println(innerPrefix + "mDownDelta=" + mDownDelta); + pw.println(innerPrefix + "mLastTouch=" + mLastTouch); + pw.println(innerPrefix + "mLastDelta=" + mLastDelta); + pw.println(innerPrefix + "mVelocity=" + mVelocity); + pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting); + pw.println(innerPrefix + "mIsDragging=" + mIsDragging); + pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging); + pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen); + } +} 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 f790d2abcdb4..d0879434657d 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 @@ -425,7 +425,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } boolean shouldResizeListenerHandleEvent(MotionEvent e, Point offset) { - return mDragResizeListener.shouldHandleEvent(e, offset); + return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset); } boolean isHandlingDragResize() { @@ -795,7 +795,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private Region getGlobalExclusionRegion() { Region exclusionRegion; - if (mTaskInfo.isResizeable) { + if (mDragResizeListener != null && mTaskInfo.isResizeable) { exclusionRegion = mDragResizeListener.getCornersRegion(); } else { exclusionRegion = new Region(); diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml index b3eb2bfd9e9d..f5a8655b81f0 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml @@ -38,8 +38,6 @@ <!-- Increase trace size: 20mb for WM and 80mb for SF --> <option name="run-command" value="cmd window tracing size 20480"/> <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> - <!-- uninstall Maps, so that latest version can be installed from pStash directly --> - <option name="run-command" value="su root pm uninstall -k --user 0 com.google.android.apps.maps"/> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> @@ -71,7 +69,6 @@ <option name="install-arg" value="-g"/> <option name="install-arg" value="-r"/> <option name="test-file-name" value="pstash://com.netflix.mediaclient"/> - <option name="test-file-name" value="pstash://com.google.android.apps.maps"/> </target_preparer> <!-- Enable mocking GPS location by the test app --> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt index b94989d98e97..12e395db8396 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.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 @@ -143,6 +144,10 @@ class FromSplitScreenAutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : } } + @FlakyTest(bugId = 293133362) + @Test + override fun entireScreenCovered() = super.entireScreenCovered() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 48e396a4817f..6be411dd81d0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -1222,6 +1222,19 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.bubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT); } + @Test + public void setSelectedBubbleAndExpandStack() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setListener(mListener); + + mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1); + + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + assertExpandedChangedTo(true); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java index 3d5cd6939d1b..85f1da5322ea 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java @@ -16,33 +16,26 @@ package com.android.wm.shell.pip.phone; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; -import android.os.SystemProperties; import android.testing.AndroidTestingRunner; import android.util.Size; import android.view.DisplayInfo; -import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.pip.PhoneSizeSpecSource; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.SizeSpecSource; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.exceptions.misusing.InvalidUseOfMatchersException; import java.util.HashMap; import java.util.Map; @@ -63,15 +56,24 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { private static final float DEFAULT_PERCENT = 0.6f; /** Minimum sizing percentage */ private static final float MIN_PERCENT = 0.5f; + /** Threshold to determine if a Display is square-ish. */ + private static final float SQUARE_DISPLAY_THRESHOLD = 0.95f; + /** Default sizing percentage for square-ish Display. */ + private static final float SQUARE_DISPLAY_DEFAULT_PERCENT = 0.5f; + /** Minimum sizing percentage for square-ish Display. */ + private static final float SQUARE_DISPLAY_MIN_PERCENT = 0.4f; /** Aspect ratio that the new PIP size spec logic optimizes for. */ private static final float OPTIMIZED_ASPECT_RATIO = 9f / 16; - /** A map of aspect ratios to be tested to expected sizes */ - private static Map<Float, Size> sExpectedMaxSizes; - private static Map<Float, Size> sExpectedDefaultSizes; - private static Map<Float, Size> sExpectedMinSizes; - /** A static mockito session object to mock {@link SystemProperties} */ - private static StaticMockitoSession sStaticMockitoSession; + /** Maps of aspect ratios to be tested to expected sizes on non-square Display. */ + private static Map<Float, Size> sNonSquareDisplayExpectedMaxSizes; + private static Map<Float, Size> sNonSquareDisplayExpectedDefaultSizes; + private static Map<Float, Size> sNonSquareDisplayExpectedMinSizes; + + /** Maps of aspect ratios to be tested to expected sizes on square Display. */ + private static Map<Float, Size> sSquareDisplayExpectedMaxSizes; + private static Map<Float, Size> sSquareDisplayExpectedDefaultSizes; + private static Map<Float, Size> sSquareDisplayExpectedMinSizes; @Mock private Context mContext; @Mock private Resources mResources; @@ -80,49 +82,55 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { private SizeSpecSource mSizeSpecSource; /** - * Sets up static Mockito session for SystemProperties and mocks necessary static methods. + * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes. + * This is to initialize the expectations on non-square Display only. */ - private static void setUpStaticSystemPropertiesSession() { - sStaticMockitoSession = mockitoSession() - .mockStatic(SystemProperties.class).startMocking(); - when(SystemProperties.get(anyString(), anyString())).thenAnswer(invocation -> { - String property = invocation.getArgument(0); - if (property.equals("com.android.wm.shell.pip.phone.def_percentage")) { - return Float.toString(DEFAULT_PERCENT); - } else if (property.equals("com.android.wm.shell.pip.phone.min_percentage")) { - return Float.toString(MIN_PERCENT); - } - - // throw an exception if illegal arguments are used for these tests - throw new InvalidUseOfMatchersException( - String.format("Argument %s does not match", property) - ); - }); + private static void initNonSquareDisplayExpectedSizes() { + sNonSquareDisplayExpectedMaxSizes = new HashMap<>(); + sNonSquareDisplayExpectedDefaultSizes = new HashMap<>(); + sNonSquareDisplayExpectedMinSizes = new HashMap<>(); + + sNonSquareDisplayExpectedMaxSizes.put(16f / 9, new Size(1000, 563)); + sNonSquareDisplayExpectedDefaultSizes.put(16f / 9, new Size(600, 338)); + sNonSquareDisplayExpectedMinSizes.put(16f / 9, new Size(501, 282)); + + sNonSquareDisplayExpectedMaxSizes.put(4f / 3, new Size(893, 670)); + sNonSquareDisplayExpectedDefaultSizes.put(4f / 3, new Size(536, 402)); + sNonSquareDisplayExpectedMinSizes.put(4f / 3, new Size(447, 335)); + + sNonSquareDisplayExpectedMaxSizes.put(3f / 4, new Size(670, 893)); + sNonSquareDisplayExpectedDefaultSizes.put(3f / 4, new Size(402, 536)); + sNonSquareDisplayExpectedMinSizes.put(3f / 4, new Size(335, 447)); + + sNonSquareDisplayExpectedMaxSizes.put(9f / 16, new Size(563, 1001)); + sNonSquareDisplayExpectedDefaultSizes.put(9f / 16, new Size(338, 601)); + sNonSquareDisplayExpectedMinSizes.put(9f / 16, new Size(282, 501)); } /** * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes. + * This is to initialize the expectations on square Display only. */ - private static void initExpectedSizes() { - sExpectedMaxSizes = new HashMap<>(); - sExpectedDefaultSizes = new HashMap<>(); - sExpectedMinSizes = new HashMap<>(); - - sExpectedMaxSizes.put(16f / 9, new Size(1000, 563)); - sExpectedDefaultSizes.put(16f / 9, new Size(600, 338)); - sExpectedMinSizes.put(16f / 9, new Size(501, 282)); - - sExpectedMaxSizes.put(4f / 3, new Size(893, 670)); - sExpectedDefaultSizes.put(4f / 3, new Size(536, 402)); - sExpectedMinSizes.put(4f / 3, new Size(447, 335)); - - sExpectedMaxSizes.put(3f / 4, new Size(670, 893)); - sExpectedDefaultSizes.put(3f / 4, new Size(402, 536)); - sExpectedMinSizes.put(3f / 4, new Size(335, 447)); - - sExpectedMaxSizes.put(9f / 16, new Size(563, 1001)); - sExpectedDefaultSizes.put(9f / 16, new Size(338, 601)); - sExpectedMinSizes.put(9f / 16, new Size(282, 501)); + private static void initSquareDisplayExpectedSizes() { + sSquareDisplayExpectedMaxSizes = new HashMap<>(); + sSquareDisplayExpectedDefaultSizes = new HashMap<>(); + sSquareDisplayExpectedMinSizes = new HashMap<>(); + + sSquareDisplayExpectedMaxSizes.put(16f / 9, new Size(1000, 563)); + sSquareDisplayExpectedDefaultSizes.put(16f / 9, new Size(500, 281)); + sSquareDisplayExpectedMinSizes.put(16f / 9, new Size(400, 225)); + + sSquareDisplayExpectedMaxSizes.put(4f / 3, new Size(893, 670)); + sSquareDisplayExpectedDefaultSizes.put(4f / 3, new Size(447, 335)); + sSquareDisplayExpectedMinSizes.put(4f / 3, new Size(357, 268)); + + sSquareDisplayExpectedMaxSizes.put(3f / 4, new Size(670, 893)); + sSquareDisplayExpectedDefaultSizes.put(3f / 4, new Size(335, 447)); + sSquareDisplayExpectedMinSizes.put(3f / 4, new Size(268, 357)); + + sSquareDisplayExpectedMaxSizes.put(9f / 16, new Size(563, 1001)); + sSquareDisplayExpectedDefaultSizes.put(9f / 16, new Size(282, 501)); + sSquareDisplayExpectedMinSizes.put(9f / 16, new Size(225, 400)); } private void forEveryTestCaseCheck(Map<Float, Size> expectedSizes, @@ -137,20 +145,38 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { @Before public void setUp() { - initExpectedSizes(); - - when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE); - when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO); - when(mResources.getString(anyInt())).thenReturn("0x0"); + initNonSquareDisplayExpectedSizes(); + initSquareDisplayExpectedSizes(); + + when(mResources.getFloat(R.dimen.config_pipSystemPreferredDefaultSizePercent)) + .thenReturn(DEFAULT_PERCENT); + when(mResources.getFloat(R.dimen.config_pipSystemPreferredMinimumSizePercent)) + .thenReturn(MIN_PERCENT); + when(mResources.getDimensionPixelSize(R.dimen.default_minimal_size_pip_resizable_task)) + .thenReturn(DEFAULT_MIN_EDGE_SIZE); + when(mResources.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio)) + .thenReturn(OPTIMIZED_ASPECT_RATIO); + when(mResources.getString(R.string.config_defaultPictureInPictureScreenEdgeInsets)) + .thenReturn("0x0"); when(mResources.getDisplayMetrics()) .thenReturn(getContext().getResources().getDisplayMetrics()); + when(mResources.getFloat(R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize)) + .thenReturn(SQUARE_DISPLAY_THRESHOLD); + when(mResources.getFloat( + R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay)) + .thenReturn(SQUARE_DISPLAY_DEFAULT_PERCENT); + when(mResources.getFloat( + R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay)) + .thenReturn(SQUARE_DISPLAY_MIN_PERCENT); // set up the mock context for spec handler specifically when(mContext.getResources()).thenReturn(mResources); + } + private void setupSizeSpecWithDisplayDimension(int width, int height) { DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.logicalWidth = DISPLAY_EDGE_SIZE; - displayInfo.logicalHeight = DISPLAY_EDGE_SIZE; + displayInfo.logicalWidth = width; + displayInfo.logicalHeight = height; // use the parent context (not the mocked one) to obtain the display layout // this is done to avoid unnecessary mocking while allowing for custom display dimensions @@ -159,38 +185,57 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { mPipDisplayLayoutState = new PipDisplayLayoutState(mContext); mPipDisplayLayoutState.setDisplayLayout(displayLayout); - setUpStaticSystemPropertiesSession(); mSizeSpecSource = new PhoneSizeSpecSource(mContext, mPipDisplayLayoutState); // no overridden min edge size by default mSizeSpecSource.setOverrideMinSize(null); } - @After - public void cleanUp() { - sStaticMockitoSession.finishMocking(); + @Test + public void testGetMaxSize_nonSquareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sNonSquareDisplayExpectedMaxSizes, + (aspectRatio) -> mSizeSpecSource.getMaxSize(aspectRatio)); + } + + @Test + public void testGetDefaultSize_nonSquareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sNonSquareDisplayExpectedDefaultSizes, + (aspectRatio) -> mSizeSpecSource.getDefaultSize(aspectRatio)); + } + + @Test + public void testGetMinSize_nonSquareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sNonSquareDisplayExpectedMinSizes, + (aspectRatio) -> mSizeSpecSource.getMinSize(aspectRatio)); } @Test - public void testGetMaxSize() { - forEveryTestCaseCheck(sExpectedMaxSizes, + public void testGetMaxSize_squareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sSquareDisplayExpectedMaxSizes, (aspectRatio) -> mSizeSpecSource.getMaxSize(aspectRatio)); } @Test - public void testGetDefaultSize() { - forEveryTestCaseCheck(sExpectedDefaultSizes, + public void testGetDefaultSize_squareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sSquareDisplayExpectedDefaultSizes, (aspectRatio) -> mSizeSpecSource.getDefaultSize(aspectRatio)); } @Test - public void testGetMinSize() { - forEveryTestCaseCheck(sExpectedMinSizes, + public void testGetMinSize_squareDisplay() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE); + forEveryTestCaseCheck(sSquareDisplayExpectedMinSizes, (aspectRatio) -> mSizeSpecSource.getMinSize(aspectRatio)); } @Test public void testGetSizeForAspectRatio_noOverrideMinSize() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); // an initial size with 16:9 aspect ratio Size initSize = new Size(600, 337); @@ -202,6 +247,7 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase { @Test public void testGetSizeForAspectRatio_withOverrideMinSize() { + setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE); // an initial size with a 1:1 aspect ratio Size initSize = new Size(OVERRIDE_MIN_EDGE_SIZE, OVERRIDE_MIN_EDGE_SIZE); mSizeSpecSource.setOverrideMinSize(initSize); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 341c3e8cf373..29bb1b9846b4 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -31,6 +31,7 @@ license { aconfig_declarations { name: "hwui_flags", package: "com.android.graphics.hwui.flags", + container: "system", srcs: [ "aconfig/hwui_flags.aconfig", ], @@ -78,13 +79,13 @@ cc_defaults { include_dirs: [ "external/skia/include/private", "external/skia/src/core", + "external/skia/src/utils", ], target: { android: { include_dirs: [ "external/skia/src/image", - "external/skia/src/utils", "external/skia/src/gpu", "external/skia/src/shaders", ], @@ -529,7 +530,9 @@ cc_defaults { "effects/GainmapRenderer.cpp", "pipeline/skia/BackdropFilterDrawable.cpp", "pipeline/skia/HolePunch.cpp", + "pipeline/skia/SkiaCpuPipeline.cpp", "pipeline/skia/SkiaDisplayList.cpp", + "pipeline/skia/SkiaPipeline.cpp", "pipeline/skia/SkiaRecordingCanvas.cpp", "pipeline/skia/StretchMask.cpp", "pipeline/skia/RenderNodeDrawable.cpp", @@ -568,6 +571,7 @@ cc_defaults { "HWUIProperties.sysprop", "Interpolator.cpp", "JankTracker.cpp", + "LayerUpdateQueue.cpp", "LightingInfo.cpp", "Matrix.cpp", "Mesh.cpp", @@ -604,9 +608,9 @@ cc_defaults { "pipeline/skia/GLFunctorDrawable.cpp", "pipeline/skia/LayerDrawable.cpp", "pipeline/skia/ShaderCache.cpp", + "pipeline/skia/SkiaGpuPipeline.cpp", "pipeline/skia/SkiaMemoryTracer.cpp", "pipeline/skia/SkiaOpenGLPipeline.cpp", - "pipeline/skia/SkiaPipeline.cpp", "pipeline/skia/SkiaProfileRenderer.cpp", "pipeline/skia/SkiaVulkanPipeline.cpp", "pipeline/skia/VkFunctorDrawable.cpp", @@ -630,7 +634,6 @@ cc_defaults { "DeferredLayerUpdater.cpp", "HardwareBitmapUploader.cpp", "Layer.cpp", - "LayerUpdateQueue.cpp", "ProfileDataContainer.cpp", "Readback.cpp", "TreeInfo.cpp", diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index ec53070f6cb8..c1510d96461f 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -242,7 +242,7 @@ enum class ProfileType { None, Console, Bars }; enum class OverdrawColorSet { Default = 0, Deuteranomaly }; -enum class RenderPipelineType { SkiaGL, SkiaVulkan, NotInitialized = 128 }; +enum class RenderPipelineType { SkiaGL, SkiaVulkan, SkiaCpu, NotInitialized = 128 }; enum class StretchEffectBehavior { ShaderHWUI, // Stretch shader in HWUI only, matrix scale in SF diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 659bcdc6852d..50f8b3929e1e 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.graphics.hwui.flags" +container: "system" flag { name: "clip_shader" diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp new file mode 100644 index 000000000000..5bbbc1009541 --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pipeline/skia/SkiaCpuPipeline.h" + +#include <system/window.h> + +#include "DeviceInfo.h" +#include "LightingInfo.h" +#include "renderthread/Frame.h" +#include "utils/Color.h" + +using namespace android::uirenderer::renderthread; + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +void SkiaCpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { + // Render all layers that need to be updated, in order. + for (size_t i = 0; i < layers.entries().size(); i++) { + RenderNode* layerNode = layers.entries()[i].renderNode.get(); + // only schedule repaint if node still on layer - possible it may have been + // removed during a dropped frame, but layers may still remain scheduled so + // as not to lose info on what portion is damaged + if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) { + continue; + } + bool rendered = renderLayerImpl(layerNode, layers.entries()[i].damage); + if (!rendered) { + return; + } + } +} + +// If the given node didn't have a layer surface, or had one of the wrong size, this method +// creates a new one and returns true. Otherwise does nothing and returns false. +bool SkiaCpuPipeline::createOrUpdateLayer(RenderNode* node, + const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) { + // compute the size of the surface (i.e. texture) to be allocated for this layer + const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; + const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; + + SkSurface* layer = node->getLayerSurface(); + if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { + SkImageInfo info; + info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), + kPremul_SkAlphaType, getSurfaceColorSpace()); + SkSurfaceProps props(0, kUnknown_SkPixelGeometry); + node->setLayerSurface(SkSurfaces::Raster(info, &props)); + if (node->getLayerSurface()) { + // update the transform in window of the layer to reset its origin wrt light source + // position + Matrix4 windowTransform; + damageAccumulator.computeCurrentTransform(&windowTransform); + node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); + } else { + String8 cachesOutput; + mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, + &mRenderThread.renderState()); + ALOGE("%s", cachesOutput.c_str()); + if (errorHandler) { + std::ostringstream err; + err << "Unable to create layer for " << node->getName(); + const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); + err << ", size " << info.width() << "x" << info.height() << " max size " + << maxTextureSize << " color type " << (int)info.colorType() << " has context " + << (int)(mRenderThread.getGrContext() != nullptr); + errorHandler->onError(err.str()); + } + } + return true; + } + return false; +} + +MakeCurrentResult SkiaCpuPipeline::makeCurrent() { + return MakeCurrentResult::AlreadyCurrent; +} + +Frame SkiaCpuPipeline::getFrame() { + return Frame(mSurface->width(), mSurface->height(), 0); +} + +IRenderPipeline::DrawResult SkiaCpuPipeline::draw( + const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const HardwareBufferRenderParams& bufferParams, std::mutex& profilerLock) { + LightingInfo::updateLighting(lightGeometry, lightInfo); + renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, mSurface, + SkMatrix::I()); + return {true, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd{}}; +} + +bool SkiaCpuPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) { + if (surface) { + ANativeWindowBuffer* buffer; + surface->dequeueBuffer(surface, &buffer, nullptr); + int width, height; + surface->query(surface, NATIVE_WINDOW_WIDTH, &width); + surface->query(surface, NATIVE_WINDOW_HEIGHT, &height); + SkImageInfo imageInfo = + SkImageInfo::Make(width, height, mSurfaceColorType, + SkAlphaType::kPremul_SkAlphaType, mSurfaceColorSpace); + size_t widthBytes = width * imageInfo.bytesPerPixel(); + void* pixels = buffer->reserved[0]; + mSurface = SkSurfaces::WrapPixels(imageInfo, pixels, widthBytes); + } else { + mSurface = sk_sp<SkSurface>(); + } + return true; +} + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.h b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h new file mode 100644 index 000000000000..5a1014c2c2de --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaCpuPipeline : public SkiaPipeline { +public: + SkiaCpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {} + ~SkiaCpuPipeline() {} + + bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; } + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override {} + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override; + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override; + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {} + bool hasHardwareBuffer() override { return false; } + + renderthread::MakeCurrentResult makeCurrent() override; + renderthread::Frame getFrame() override; + renderthread::IRenderPipeline::DrawResult draw( + const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const renderthread::HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) override; + bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult, + const SkRect& screenDirty, FrameInfo* currentFrameInfo, + bool* requireSwap) override { + return false; + } + DeferredLayerUpdater* createTextureLayer() override { return nullptr; } + bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override; + [[nodiscard]] android::base::unique_fd flush() override { + return android::base::unique_fd(-1); + }; + void onStop() override {} + bool isSurfaceReady() override { return mSurface.get() != nullptr; } + bool isContextReady() override { return true; } + + const SkM44& getPixelSnapMatrix() const override { + static const SkM44 sSnapMatrix = SkM44(); + return sSnapMatrix; + } + +private: + sk_sp<SkSurface> mSurface; +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp new file mode 100644 index 000000000000..7bfbfdc4b96b --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pipeline/skia/SkiaGpuPipeline.h" + +#include <SkImageAndroid.h> +#include <gui/TraceUtils.h> +#include <include/android/SkSurfaceAndroid.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> + +using namespace android::uirenderer::renderthread; + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +SkiaGpuPipeline::SkiaGpuPipeline(RenderThread& thread) : SkiaPipeline(thread) {} + +SkiaGpuPipeline::~SkiaGpuPipeline() { + unpinImages(); +} + +void SkiaGpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { + sk_sp<GrDirectContext> cachedContext; + + // Render all layers that need to be updated, in order. + for (size_t i = 0; i < layers.entries().size(); i++) { + RenderNode* layerNode = layers.entries()[i].renderNode.get(); + // only schedule repaint if node still on layer - possible it may have been + // removed during a dropped frame, but layers may still remain scheduled so + // as not to lose info on what portion is damaged + if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) { + continue; + } + bool rendered = renderLayerImpl(layerNode, layers.entries()[i].damage); + if (!rendered) { + return; + } + // cache the current context so that we can defer flushing it until + // either all the layers have been rendered or the context changes + GrDirectContext* currentContext = + GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext()); + if (cachedContext.get() != currentContext) { + if (cachedContext.get()) { + ATRACE_NAME("flush layers (context changed)"); + cachedContext->flushAndSubmit(); + } + cachedContext.reset(SkSafeRef(currentContext)); + } + } + if (cachedContext.get()) { + ATRACE_NAME("flush layers"); + cachedContext->flushAndSubmit(); + } +} + +// If the given node didn't have a layer surface, or had one of the wrong size, this method +// creates a new one and returns true. Otherwise does nothing and returns false. +bool SkiaGpuPipeline::createOrUpdateLayer(RenderNode* node, + const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) { + // compute the size of the surface (i.e. texture) to be allocated for this layer + const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; + const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; + + SkSurface* layer = node->getLayerSurface(); + if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { + SkImageInfo info; + info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), + kPremul_SkAlphaType, getSurfaceColorSpace()); + SkSurfaceProps props(0, kUnknown_SkPixelGeometry); + SkASSERT(mRenderThread.getGrContext() != nullptr); + node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, info, 0, + this->getSurfaceOrigin(), &props)); + if (node->getLayerSurface()) { + // update the transform in window of the layer to reset its origin wrt light source + // position + Matrix4 windowTransform; + damageAccumulator.computeCurrentTransform(&windowTransform); + node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); + } else { + String8 cachesOutput; + mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, + &mRenderThread.renderState()); + ALOGE("%s", cachesOutput.c_str()); + if (errorHandler) { + std::ostringstream err; + err << "Unable to create layer for " << node->getName(); + const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); + err << ", size " << info.width() << "x" << info.height() << " max size " + << maxTextureSize << " color type " << (int)info.colorType() << " has context " + << (int)(mRenderThread.getGrContext() != nullptr); + errorHandler->onError(err.str()); + } + } + return true; + } + return false; +} + +bool SkiaGpuPipeline::pinImages(std::vector<SkImage*>& mutableImages) { + if (!mRenderThread.getGrContext()) { + ALOGD("Trying to pin an image with an invalid GrContext"); + return false; + } + for (SkImage* image : mutableImages) { + if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) { + mPinnedImages.emplace_back(sk_ref_sp(image)); + } else { + return false; + } + } + return true; +} + +void SkiaGpuPipeline::unpinImages() { + for (auto& image : mPinnedImages) { + skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get()); + } + mPinnedImages.clear(); +} + +void SkiaGpuPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { + GrDirectContext* context = thread.getGrContext(); + if (context && !bitmap->isHardware()) { + ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height()); + auto image = bitmap->makeImage(); + if (image.get()) { + skgpu::ganesh::PinAsTexture(context, image.get()); + skgpu::ganesh::UnpinTexture(context, image.get()); + // A submit is necessary as there may not be a frame coming soon, so without a call + // to submit these texture uploads can just sit in the queue building up until + // we run out of RAM + context->flushAndSubmit(); + } + } +} + +sk_sp<SkSurface> SkiaGpuPipeline::getBufferSkSurface( + const renderthread::HardwareBufferRenderParams& bufferParams) { + auto bufferColorSpace = bufferParams.getColorSpace(); + if (mBufferSurface == nullptr || mBufferColorSpace == nullptr || + !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) { + mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer( + mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin, + bufferColorSpace, nullptr, true); + mBufferColorSpace = bufferColorSpace; + } + return mBufferSurface; +} + +void SkiaGpuPipeline::dumpResourceCacheUsage() const { + int resources; + size_t bytes; + mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes); + size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit(); + + SkString log("Resource Cache Usage:\n"); + log.appendf("%8d items\n", resources); + log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes, + bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f))); + + ALOGD("%s", log.c_str()); +} + +void SkiaGpuPipeline::setHardwareBuffer(AHardwareBuffer* buffer) { + if (mHardwareBuffer) { + AHardwareBuffer_release(mHardwareBuffer); + mHardwareBuffer = nullptr; + } + + if (buffer) { + AHardwareBuffer_acquire(buffer); + mHardwareBuffer = buffer; + } +} + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index c8d598702a7c..e4b1f916b4d6 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -14,25 +14,25 @@ * limitations under the License. */ -#include "SkiaOpenGLPipeline.h" +#include "pipeline/skia/SkiaOpenGLPipeline.h" -#include <include/gpu/ganesh/SkSurfaceGanesh.h> -#include <include/gpu/ganesh/gl/GrGLBackendSurface.h> -#include <include/gpu/gl/GrGLTypes.h> #include <GrBackendSurface.h> #include <SkBlendMode.h> #include <SkImageInfo.h> #include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> +#include <include/gpu/ganesh/gl/GrGLBackendSurface.h> +#include <include/gpu/gl/GrGLTypes.h> #include <strings.h> #include "DeferredLayerUpdater.h" #include "FrameInfo.h" -#include "LayerDrawable.h" #include "LightingInfo.h" -#include "SkiaPipeline.h" -#include "SkiaProfileRenderer.h" #include "hwui/Bitmap.h" +#include "pipeline/skia/LayerDrawable.h" +#include "pipeline/skia/SkiaGpuPipeline.h" +#include "pipeline/skia/SkiaProfileRenderer.h" #include "private/hwui/DrawGlInfo.h" #include "renderstate/RenderState.h" #include "renderthread/EglManager.h" @@ -47,7 +47,7 @@ namespace uirenderer { namespace skiapipeline { SkiaOpenGLPipeline::SkiaOpenGLPipeline(RenderThread& thread) - : SkiaPipeline(thread), mEglManager(thread.eglManager()) { + : SkiaGpuPipeline(thread), mEglManager(thread.eglManager()) { thread.renderState().registerContextCallback(this); } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 99469d1e3628..34932b1b1e25 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -14,11 +14,8 @@ * limitations under the License. */ -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaPipeline.h" -#include <include/android/SkSurfaceAndroid.h> -#include <include/gpu/ganesh/SkSurfaceGanesh.h> -#include <include/encode/SkPngEncoder.h> #include <SkCanvas.h> #include <SkColor.h> #include <SkColorSpace.h> @@ -40,6 +37,9 @@ #include <SkTypeface.h> #include <android-base/properties.h> #include <gui/TraceUtils.h> +#include <include/android/SkSurfaceAndroid.h> +#include <include/encode/SkPngEncoder.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <unistd.h> #include <sstream> @@ -62,37 +62,13 @@ SkiaPipeline::SkiaPipeline(RenderThread& thread) : mRenderThread(thread) { setSurfaceColorProperties(mColorMode); } -SkiaPipeline::~SkiaPipeline() { - unpinImages(); -} +SkiaPipeline::~SkiaPipeline() {} void SkiaPipeline::onDestroyHardwareResources() { unpinImages(); mRenderThread.cacheManager().trimStaleResources(); } -bool SkiaPipeline::pinImages(std::vector<SkImage*>& mutableImages) { - if (!mRenderThread.getGrContext()) { - ALOGD("Trying to pin an image with an invalid GrContext"); - return false; - } - for (SkImage* image : mutableImages) { - if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) { - mPinnedImages.emplace_back(sk_ref_sp(image)); - } else { - return false; - } - } - return true; -} - -void SkiaPipeline::unpinImages() { - for (auto& image : mPinnedImages) { - skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get()); - } - mPinnedImages.clear(); -} - void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, bool opaque, const LightInfo& lightInfo) { @@ -102,136 +78,48 @@ void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry, layerUpdateQueue->clear(); } -void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { - sk_sp<GrDirectContext> cachedContext; - - // Render all layers that need to be updated, in order. - for (size_t i = 0; i < layers.entries().size(); i++) { - RenderNode* layerNode = layers.entries()[i].renderNode.get(); - // only schedule repaint if node still on layer - possible it may have been - // removed during a dropped frame, but layers may still remain scheduled so - // as not to lose info on what portion is damaged - if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) { - continue; - } - SkASSERT(layerNode->getLayerSurface()); - SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl(); - if (!displayList || displayList->isEmpty()) { - ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()); - return; - } - - const Rect& layerDamage = layers.entries()[i].damage; - - SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas(); - - int saveCount = layerCanvas->save(); - SkASSERT(saveCount == 1); - - layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect()); - - // TODO: put localized light center calculation and storage to a drawable related code. - // It does not seem right to store something localized in a global state - // fix here and in recordLayers - const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw()); - Vector3 transformedLightCenter(savedLightCenter); - // map current light center into RenderNode's coordinate space - layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter); - LightingInfo::setLightCenterRaw(transformedLightCenter); - - const RenderProperties& properties = layerNode->properties(); - const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); - if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) { - return; - } - - ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(), - bounds.height()); +bool SkiaPipeline::renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage) { + SkASSERT(layerNode->getLayerSurface()); + SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl(); + if (!displayList || displayList->isEmpty()) { + ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()); + return false; + } - layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false; - layerCanvas->clear(SK_ColorTRANSPARENT); + SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas(); - RenderNodeDrawable root(layerNode, layerCanvas, false); - root.forceDraw(layerCanvas); - layerCanvas->restoreToCount(saveCount); + int saveCount = layerCanvas->save(); + SkASSERT(saveCount == 1); - LightingInfo::setLightCenterRaw(savedLightCenter); + layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect()); - // cache the current context so that we can defer flushing it until - // either all the layers have been rendered or the context changes - GrDirectContext* currentContext = - GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext()); - if (cachedContext.get() != currentContext) { - if (cachedContext.get()) { - ATRACE_NAME("flush layers (context changed)"); - cachedContext->flushAndSubmit(); - } - cachedContext.reset(SkSafeRef(currentContext)); - } + // TODO: put localized light center calculation and storage to a drawable related code. + // It does not seem right to store something localized in a global state + // fix here and in recordLayers + const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw()); + Vector3 transformedLightCenter(savedLightCenter); + // map current light center into RenderNode's coordinate space + layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter); + LightingInfo::setLightCenterRaw(transformedLightCenter); + + const RenderProperties& properties = layerNode->properties(); + const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); + if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) { + return false; } - if (cachedContext.get()) { - ATRACE_NAME("flush layers"); - cachedContext->flushAndSubmit(); - } -} + ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(), + bounds.height()); -bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, - ErrorHandler* errorHandler) { - // compute the size of the surface (i.e. texture) to be allocated for this layer - const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; - const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; - - SkSurface* layer = node->getLayerSurface(); - if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { - SkImageInfo info; - info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), - kPremul_SkAlphaType, getSurfaceColorSpace()); - SkSurfaceProps props(0, kUnknown_SkPixelGeometry); - SkASSERT(mRenderThread.getGrContext() != nullptr); - node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(), - skgpu::Budgeted::kYes, info, 0, - this->getSurfaceOrigin(), &props)); - if (node->getLayerSurface()) { - // update the transform in window of the layer to reset its origin wrt light source - // position - Matrix4 windowTransform; - damageAccumulator.computeCurrentTransform(&windowTransform); - node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); - } else { - String8 cachesOutput; - mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, - &mRenderThread.renderState()); - ALOGE("%s", cachesOutput.c_str()); - if (errorHandler) { - std::ostringstream err; - err << "Unable to create layer for " << node->getName(); - const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); - err << ", size " << info.width() << "x" << info.height() << " max size " - << maxTextureSize << " color type " << (int)info.colorType() << " has context " - << (int)(mRenderThread.getGrContext() != nullptr); - errorHandler->onError(err.str()); - } - } - return true; - } - return false; -} + layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false; + layerCanvas->clear(SK_ColorTRANSPARENT); -void SkiaPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { - GrDirectContext* context = thread.getGrContext(); - if (context && !bitmap->isHardware()) { - ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height()); - auto image = bitmap->makeImage(); - if (image.get()) { - skgpu::ganesh::PinAsTexture(context, image.get()); - skgpu::ganesh::UnpinTexture(context, image.get()); - // A submit is necessary as there may not be a frame coming soon, so without a call - // to submit these texture uploads can just sit in the queue building up until - // we run out of RAM - context->flushAndSubmit(); - } - } + RenderNodeDrawable root(layerNode, layerCanvas, false); + root.forceDraw(layerCanvas); + layerCanvas->restoreToCount(saveCount); + + LightingInfo::setLightCenterRaw(savedLightCenter); + return true; } static void savePictureAsync(const sk_sp<SkData>& data, const std::string& filename) { @@ -599,45 +487,6 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip, } } -void SkiaPipeline::dumpResourceCacheUsage() const { - int resources; - size_t bytes; - mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes); - size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit(); - - SkString log("Resource Cache Usage:\n"); - log.appendf("%8d items\n", resources); - log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes, - bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f))); - - ALOGD("%s", log.c_str()); -} - -void SkiaPipeline::setHardwareBuffer(AHardwareBuffer* buffer) { - if (mHardwareBuffer) { - AHardwareBuffer_release(mHardwareBuffer); - mHardwareBuffer = nullptr; - } - - if (buffer) { - AHardwareBuffer_acquire(buffer); - mHardwareBuffer = buffer; - } -} - -sk_sp<SkSurface> SkiaPipeline::getBufferSkSurface( - const renderthread::HardwareBufferRenderParams& bufferParams) { - auto bufferColorSpace = bufferParams.getColorSpace(); - if (mBufferSurface == nullptr || mBufferColorSpace == nullptr || - !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) { - mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer( - mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin, - bufferColorSpace, nullptr, true); - mBufferColorSpace = bufferColorSpace; - } - return mBufferSurface; -} - void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mColorMode = colorMode; switch (colorMode) { diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index befee8989383..cf14b1f9ebe3 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -42,18 +42,9 @@ public: void onDestroyHardwareResources() override; - bool pinImages(std::vector<SkImage*>& mutableImages) override; - bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } - void unpinImages() override; - void renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, bool opaque, const LightInfo& lightInfo) override; - // If the given node didn't have a layer surface, or had one of the wrong size, this method - // creates a new one and returns true. Otherwise does nothing and returns false. - bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, - ErrorHandler* errorHandler) override; - void setSurfaceColorProperties(ColorMode colorMode) override; SkColorType getSurfaceColorType() const override { return mSurfaceColorType; } sk_sp<SkColorSpace> getSurfaceColorSpace() override { return mSurfaceColorSpace; } @@ -63,9 +54,8 @@ public: const Rect& contentDrawBounds, sk_sp<SkSurface> surface, const SkMatrix& preTransform); - static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap); - - void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque); + bool renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage); + virtual void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) = 0; // Sets the recording callback to the provided function and the recording mode // to CallbackAPI @@ -75,19 +65,11 @@ public: mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None; } - virtual void setHardwareBuffer(AHardwareBuffer* buffer) override; - bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; } - void setTargetSdrHdrRatio(float ratio) override; protected: - sk_sp<SkSurface> getBufferSkSurface( - const renderthread::HardwareBufferRenderParams& bufferParams); - void dumpResourceCacheUsage() const; - renderthread::RenderThread& mRenderThread; - AHardwareBuffer* mHardwareBuffer = nullptr; sk_sp<SkSurface> mBufferSurface = nullptr; sk_sp<SkColorSpace> mBufferColorSpace = nullptr; @@ -125,8 +107,6 @@ private: // Set up a multi frame capture. bool setupMultiFrameCapture(); - std::vector<sk_sp<SkImage>> mPinnedImages; - // Block of properties used only for debugging to record a SkPicture and save it in a file. // There are three possible ways of recording drawing commands. enum class CaptureMode { diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index fd0a8e06f39c..d06dba05ee88 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "SkiaVulkanPipeline.h" +#include "pipeline/skia/SkiaVulkanPipeline.h" #include <GrDirectContext.h> #include <GrTypes.h> @@ -28,10 +28,10 @@ #include "DeferredLayerUpdater.h" #include "LightingInfo.h" #include "Readback.h" -#include "ShaderCache.h" -#include "SkiaPipeline.h" -#include "SkiaProfileRenderer.h" -#include "VkInteropFunctorDrawable.h" +#include "pipeline/skia/ShaderCache.h" +#include "pipeline/skia/SkiaGpuPipeline.h" +#include "pipeline/skia/SkiaProfileRenderer.h" +#include "pipeline/skia/VkInteropFunctorDrawable.h" #include "renderstate/RenderState.h" #include "renderthread/Frame.h" #include "renderthread/IRenderPipeline.h" @@ -42,7 +42,8 @@ namespace android { namespace uirenderer { namespace skiapipeline { -SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) { +SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread) + : SkiaGpuPipeline(thread) { thread.renderState().registerContextCallback(this); } diff --git a/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h new file mode 100644 index 000000000000..9159eae46065 --- /dev/null +++ b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +class SkiaGpuPipeline : public SkiaPipeline { +public: + SkiaGpuPipeline(renderthread::RenderThread& thread); + virtual ~SkiaGpuPipeline(); + + virtual GrSurfaceOrigin getSurfaceOrigin() = 0; + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override; + + bool pinImages(std::vector<SkImage*>& mutableImages) override; + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override; + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override; + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override; + bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; } + + static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap); + +protected: + sk_sp<SkSurface> getBufferSkSurface( + const renderthread::HardwareBufferRenderParams& bufferParams); + void dumpResourceCacheUsage() const; + + AHardwareBuffer* mHardwareBuffer = nullptr; + +private: + std::vector<sk_sp<SkImage>> mPinnedImages; +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h index ebe8b6e15d44..6e7478288777 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h @@ -19,7 +19,7 @@ #include <EGL/egl.h> #include <system/window.h> -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "renderstate/RenderState.h" #include "renderthread/HardwareBufferRenderParams.h" @@ -30,7 +30,7 @@ class Bitmap; namespace uirenderer { namespace skiapipeline { -class SkiaOpenGLPipeline : public SkiaPipeline, public IGpuContextCallback { +class SkiaOpenGLPipeline : public SkiaGpuPipeline, public IGpuContextCallback { public: SkiaOpenGLPipeline(renderthread::RenderThread& thread); virtual ~SkiaOpenGLPipeline(); diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h index 624eaa51a584..0d30df48baee 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h @@ -17,7 +17,7 @@ #pragma once #include "SkRefCnt.h" -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "renderstate/RenderState.h" #include "renderthread/HardwareBufferRenderParams.h" #include "renderthread/VulkanManager.h" @@ -30,7 +30,7 @@ namespace android { namespace uirenderer { namespace skiapipeline { -class SkiaVulkanPipeline : public SkiaPipeline, public IGpuContextCallback { +class SkiaVulkanPipeline : public SkiaGpuPipeline, public IGpuContextCallback { public: explicit SkiaVulkanPipeline(renderthread::RenderThread& thread); virtual ~SkiaVulkanPipeline(); diff --git a/libs/hwui/platform/host/android/api-level.h b/libs/hwui/platform/host/android/api-level.h new file mode 120000 index 000000000000..4fb4784f9f60 --- /dev/null +++ b/libs/hwui/platform/host/android/api-level.h @@ -0,0 +1 @@ +../../../../../../../bionic/libc/include/android/api-level.h
\ No newline at end of file diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h new file mode 100644 index 000000000000..a71726585081 --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" +#include "renderthread/Frame.h" + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +class SkiaGpuPipeline : public SkiaPipeline { +public: + SkiaGpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {} + ~SkiaGpuPipeline() {} + + bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; } + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override {} + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override { + return false; + } + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override {} + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {} + bool hasHardwareBuffer() override { return false; } + + renderthread::MakeCurrentResult makeCurrent() override { + return renderthread::MakeCurrentResult::Failed; + } + renderthread::Frame getFrame() override { return renderthread::Frame(0, 0, 0); } + renderthread::IRenderPipeline::DrawResult draw( + const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const renderthread::HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) override { + return {false, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd(-1)}; + } + bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult, + const SkRect& screenDirty, FrameInfo* currentFrameInfo, + bool* requireSwap) override { + return false; + } + DeferredLayerUpdater* createTextureLayer() override { return nullptr; } + bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override { + return false; + } + [[nodiscard]] android::base::unique_fd flush() override { + return android::base::unique_fd(-1); + }; + void onStop() override {} + bool isSurfaceReady() override { return false; } + bool isContextReady() override { return false; } + + const SkM44& getPixelSnapMatrix() const override { + static const SkM44 sSnapMatrix = SkM44(); + return sSnapMatrix; + } + static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h new file mode 100644 index 000000000000..4fafbcc4748d --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "pipeline/skia/SkiaGpuPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaOpenGLPipeline : public SkiaGpuPipeline { +public: + SkiaOpenGLPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {} + + static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h new file mode 100644 index 000000000000..d54caef45bb5 --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "pipeline/skia/SkiaGpuPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaVulkanPipeline : public SkiaGpuPipeline { +public: + SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {} + + static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 1fbd580874f6..22de2f29792d 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -35,8 +35,8 @@ #include "Properties.h" #include "RenderThread.h" #include "hwui/Canvas.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "pipeline/skia/SkiaOpenGLPipeline.h" -#include "pipeline/skia/SkiaPipeline.h" #include "pipeline/skia/SkiaVulkanPipeline.h" #include "thread/CommonPool.h" #include "utils/GLUtils.h" @@ -108,7 +108,7 @@ void CanvasContext::invokeFunctor(const RenderThread& thread, Functor* functor) } void CanvasContext::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { - skiapipeline::SkiaPipeline::prepareToDraw(thread, bitmap); + skiapipeline::SkiaGpuPipeline::prepareToDraw(thread, bitmap); } CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index b8c3a4de2bd4..ee1d1f8789d9 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -30,8 +30,6 @@ #include "SwapBehavior.h" #include "hwui/Bitmap.h" -class GrDirectContext; - struct ANativeWindow; namespace android { @@ -94,7 +92,6 @@ public: virtual void setSurfaceColorProperties(ColorMode colorMode) = 0; virtual SkColorType getSurfaceColorType() const = 0; virtual sk_sp<SkColorSpace> getSurfaceColorSpace() = 0; - virtual GrSurfaceOrigin getSurfaceOrigin() = 0; virtual void setPictureCapturedCallback( const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0; diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java index eaafa595ae4f..6d84e7066839 100644 --- a/media/java/android/media/FadeManagerConfiguration.java +++ b/media/java/android/media/FadeManagerConfiguration.java @@ -836,7 +836,7 @@ public final class FadeManagerConfiguration implements Parcelable { */ public Builder(@NonNull FadeManagerConfiguration fmc) { mFadeState = fmc.mFadeState; - mUsageToFadeWrapperMap = fmc.mUsageToFadeWrapperMap.clone(); + copyUsageToFadeWrapperMapInternal(fmc.mUsageToFadeWrapperMap); mAttrToFadeWrapperMap = new ArrayMap<AudioAttributes, FadeVolumeShaperConfigsWrapper>( fmc.mAttrToFadeWrapperMap); mFadeableUsages = fmc.mFadeableUsages.clone(); @@ -1459,6 +1459,14 @@ public final class FadeManagerConfiguration implements Parcelable { } } + private void copyUsageToFadeWrapperMapInternal( + SparseArray<FadeVolumeShaperConfigsWrapper> usageToFadeWrapperMap) { + for (int index = 0; index < usageToFadeWrapperMap.size(); index++) { + mUsageToFadeWrapperMap.put(usageToFadeWrapperMap.keyAt(index), + new FadeVolumeShaperConfigsWrapper(usageToFadeWrapperMap.valueAt(index))); + } + } + private void validateFadeState(int state) { switch(state) { case FADE_STATE_DISABLED: @@ -1551,6 +1559,12 @@ public final class FadeManagerConfiguration implements Parcelable { FadeVolumeShaperConfigsWrapper() {} + FadeVolumeShaperConfigsWrapper(@NonNull FadeVolumeShaperConfigsWrapper wrapper) { + Objects.requireNonNull(wrapper, "Fade volume shaper configs wrapper cannot be null"); + this.mFadeOutVolShaperConfig = wrapper.mFadeOutVolShaperConfig; + this.mFadeInVolShaperConfig = wrapper.mFadeInVolShaperConfig; + } + public void setFadeOutVolShaperConfig(@Nullable VolumeShaper.Configuration fadeOutConfig) { mFadeOutVolShaperConfig = fadeOutConfig; } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3f9440b60202..5aa006bce000 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -1309,18 +1309,24 @@ public final class MediaRouter2 { return; } - RoutingController newController; - if (sessionInfo.isSystemSession()) { - newController = getSystemController(); - newController.setRoutingSessionInfo(sessionInfo); + RoutingController newController = addRoutingController(sessionInfo); + notifyTransfer(oldController, newController); + } + + @NonNull + private RoutingController addRoutingController(@NonNull RoutingSessionInfo session) { + RoutingController controller; + if (session.isSystemSession()) { + // mSystemController is never released, so we only need to update its status. + mSystemController.setRoutingSessionInfo(session); + controller = mSystemController; } else { - newController = new RoutingController(sessionInfo); + controller = new RoutingController(session); synchronized (mLock) { - mNonSystemRoutingControllers.put(newController.getId(), newController); + mNonSystemRoutingControllers.put(controller.getId(), controller); } } - - notifyTransfer(oldController, newController); + return controller; } void updateControllerOnHandler(RoutingSessionInfo sessionInfo) { @@ -1329,40 +1335,12 @@ public final class MediaRouter2 { return; } - if (sessionInfo.isSystemSession()) { - // The session info is sent from SystemMediaRoute2Provider. - RoutingController systemController = getSystemController(); - systemController.setRoutingSessionInfo(sessionInfo); - notifyControllerUpdated(systemController); - return; - } - - RoutingController matchingController; - synchronized (mLock) { - matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId()); - } - - if (matchingController == null) { - Log.w( - TAG, - "updateControllerOnHandler: Matching controller not found. uniqueSessionId=" - + sessionInfo.getId()); - return; - } - - RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo(); - if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) { - Log.w( - TAG, - "updateControllerOnHandler: Provider IDs are not matched. old=" - + oldInfo.getProviderId() - + ", new=" - + sessionInfo.getProviderId()); - return; + RoutingController controller = + getMatchingController(sessionInfo, /* logPrefix */ "updateControllerOnHandler"); + if (controller != null) { + controller.setRoutingSessionInfo(sessionInfo); + notifyControllerUpdated(controller); } - - matchingController.setRoutingSessionInfo(sessionInfo); - notifyControllerUpdated(matchingController); } void releaseControllerOnHandler(RoutingSessionInfo sessionInfo) { @@ -1371,34 +1349,47 @@ public final class MediaRouter2 { return; } - RoutingController matchingController; - synchronized (mLock) { - matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId()); + RoutingController matchingController = + getMatchingController(sessionInfo, /* logPrefix */ "releaseControllerOnHandler"); + + if (matchingController != null) { + matchingController.releaseInternal(/* shouldReleaseSession= */ false); } + } - if (matchingController == null) { - if (DEBUG) { - Log.d( + @Nullable + private RoutingController getMatchingController( + RoutingSessionInfo sessionInfo, String logPrefix) { + if (sessionInfo.isSystemSession()) { + return getSystemController(); + } else { + RoutingController controller; + synchronized (mLock) { + controller = mNonSystemRoutingControllers.get(sessionInfo.getId()); + } + + if (controller == null) { + Log.w( TAG, - "releaseControllerOnHandler: Matching controller not found. " - + "uniqueSessionId=" + logPrefix + + ": Matching controller not found. uniqueSessionId=" + sessionInfo.getId()); + return null; } - return; - } - RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo(); - if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) { - Log.w( - TAG, - "releaseControllerOnHandler: Provider IDs are not matched. old=" - + oldInfo.getProviderId() - + ", new=" - + sessionInfo.getProviderId()); - return; + RoutingSessionInfo oldInfo = controller.getRoutingSessionInfo(); + if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) { + Log.w( + TAG, + logPrefix + + ": Provider IDs are not matched. old=" + + oldInfo.getProviderId() + + ", new=" + + sessionInfo.getProviderId()); + return null; + } + return controller; } - - matchingController.releaseInternal(/* shouldReleaseSession= */ false); } void onRequestCreateControllerByManagerOnHandler( @@ -3129,20 +3120,8 @@ public final class MediaRouter2 { private void onTransferred( @NonNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession) { - if (!oldSession.isSystemSession() - && !TextUtils.equals( - getClientPackageName(), oldSession.getClientPackageName())) { - return; - } - - if (!newSession.isSystemSession() - && !TextUtils.equals( - getClientPackageName(), newSession.getClientPackageName())) { - return; - } - - // For successful in-session transfer, onControllerUpdated() handles it. - if (TextUtils.equals(oldSession.getId(), newSession.getId())) { + if (!isSessionRelatedToTargetPackageName(oldSession) + || !isSessionRelatedToTargetPackageName(newSession)) { return; } @@ -3169,16 +3148,14 @@ public final class MediaRouter2 { private void onTransferFailed( @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) { - if (!session.isSystemSession() - && !TextUtils.equals(getClientPackageName(), session.getClientPackageName())) { + if (!isSessionRelatedToTargetPackageName(session)) { return; } notifyTransferFailure(route); } private void onSessionUpdated(@NonNull RoutingSessionInfo session) { - if (!session.isSystemSession() - && !TextUtils.equals(getClientPackageName(), session.getClientPackageName())) { + if (!isSessionRelatedToTargetPackageName(session)) { return; } @@ -3193,6 +3170,15 @@ public final class MediaRouter2 { notifyControllerUpdated(controller); } + /** + * Returns {@code true} if the session is a system session or if its client package name + * matches the proxy router's target package name. + */ + private boolean isSessionRelatedToTargetPackageName(@NonNull RoutingSessionInfo session) { + return session.isSystemSession() + || TextUtils.equals(getClientPackageName(), session.getClientPackageName()); + } + private void onSessionCreatedOnHandler( int requestId, @NonNull RoutingSessionInfo sessionInfo) { MediaRouter2Manager.TransferRequest matchingRequest = null; @@ -3237,19 +3223,19 @@ public final class MediaRouter2 { } } - private void onSessionUpdatedOnHandler(@NonNull RoutingSessionInfo sessionInfo) { + private void onSessionUpdatedOnHandler(@NonNull RoutingSessionInfo updatedSession) { for (MediaRouter2Manager.TransferRequest request : mTransferRequests) { String sessionId = request.mOldSessionInfo.getId(); - if (!TextUtils.equals(sessionId, sessionInfo.getId())) { + if (!TextUtils.equals(sessionId, updatedSession.getId())) { continue; } - if (sessionInfo.getSelectedRoutes().contains(request.mTargetRoute.getId())) { + + if (updatedSession.getSelectedRoutes().contains(request.mTargetRoute.getId())) { mTransferRequests.remove(request); - this.onTransferred(request.mOldSessionInfo, sessionInfo); break; } } - this.onSessionUpdated(sessionInfo); + this.onSessionUpdated(updatedSession); } private void onSessionReleasedOnHandler(@NonNull RoutingSessionInfo session) { diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index c1be6b5473d4..91c4f118658a 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -25,13 +25,6 @@ flag { } flag { - name: "disable_screen_off_broadcast_receiver" - namespace: "media_solutions" - description: "Disables the broadcast receiver that prevents scanning when the screen is off." - bug: "304234628" -} - -flag { name: "fallback_to_default_handling_when_media_session_has_fixed_volume_handling" namespace: "media_solutions" description: "Fallbacks to the default handling for volume adjustment when media session has fixed volume handling and its app is in the foreground and setting a media controller." @@ -108,6 +101,16 @@ flag { } flag { + name: "enable_mr2_service_non_main_bg_thread" + namespace: "media_solutions" + description: "Enables the use of a background thread in the media routing framework, instead of using the main thread." + bug: "310145678" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_screen_off_scanning" is_exported: true namespace: "media_solutions" @@ -121,3 +124,13 @@ flag { description: "Enables apps owning a MediaBrowserService to disconnect all connected browsers." bug: "185136506" } + +flag { + name: "enable_prevention_of_manager_scans_when_no_apps_scan" + namespace: "media_solutions" + description: "Prevents waking up route providers when no apps are scanning, even if SysUI or Settings are scanning." + bug: "319604673" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 7ed67dcde913..2a0648d87c85 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -23,9 +23,6 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.app.Activity; import android.app.ActivityOptions.LaunchCookie; -import android.compat.annotation.ChangeId; -import android.compat.annotation.Disabled; -import android.compat.annotation.Overridable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -69,18 +66,6 @@ public final class MediaProjectionManager { private static final String TAG = "MediaProjectionManager"; /** - * This change id ensures that users are presented with a choice of capturing a single app - * or the entire screen when initiating a MediaProjection session, overriding the usage of - * MediaProjectionConfig#createConfigForDefaultDisplay. - * - * @hide - */ - @ChangeId - @Overridable - @Disabled - public static final long OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION = 316897322L; - - /** * Intent extra to customize the permission dialog based on the host app's preferences. * @hide */ diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl index 207ccbee0b50..871e9ab87299 100644 --- a/media/java/android/media/session/ISessionManager.aidl +++ b/media/java/android/media/session/ISessionManager.aidl @@ -80,4 +80,7 @@ interface ISessionManager { boolean hasCustomMediaSessionPolicyProvider(String componentName); int getSessionPolicies(in MediaSession.Token token); void setSessionPolicies(in MediaSession.Token token, int policies); + + // For testing of temporarily engaged sessions. + void expireTempEngagedSessions(); } diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java index c48a95625e8f..74b5afe9b65b 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/FadeManagerConfigurationUnitTest.java @@ -196,8 +196,7 @@ public final class FadeManagerConfigurationUnitTest { FadeManagerConfiguration fmcObj = new FadeManagerConfiguration .Builder(TEST_FADE_OUT_DURATION_MS, TEST_FADE_IN_DURATION_MS).build(); - FadeManagerConfiguration fmc = new FadeManagerConfiguration - .Builder(fmcObj).build(); + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder(fmcObj).build(); expect.withMessage("Fade state for copy builder").that(fmc.getFadeState()) .isEqualTo(fmcObj.getFadeState()); @@ -249,6 +248,45 @@ public final class FadeManagerConfigurationUnitTest { } @Test + public void build_withCopyConstructor_doesnotChangeOriginal() { + FadeManagerConfiguration copyConstructedFmc = new FadeManagerConfiguration.Builder(mFmc) + .setFadeOutDurationForUsage(AudioAttributes.USAGE_MEDIA, TEST_FADE_OUT_DURATION_MS) + .setFadeInDurationForUsage(AudioAttributes.USAGE_MEDIA, TEST_FADE_IN_DURATION_MS) + .build(); + + expect.withMessage("Fade out duration for media usage of default constructor") + .that(mFmc.getFadeOutDurationForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(DEFAULT_FADE_OUT_DURATION_MS); + expect.withMessage("Fade out duration for media usage of default constructor") + .that(mFmc.getFadeInDurationForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(DEFAULT_FADE_IN_DURATION_MS); + expect.withMessage("Fade out duration for media usage of copy constructor") + .that(copyConstructedFmc.getFadeOutDurationForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(TEST_FADE_OUT_DURATION_MS); + expect.withMessage("Fade out duration for media usage of copy constructor") + .that(copyConstructedFmc.getFadeInDurationForUsage(AudioAttributes.USAGE_MEDIA)) + .isEqualTo(TEST_FADE_IN_DURATION_MS); + } + + @Test + public void build_withCopyConstructor_equals() { + FadeManagerConfiguration fmc = new FadeManagerConfiguration.Builder() + .setFadeableUsages(List.of(AudioAttributes.USAGE_MEDIA, + AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + AudioAttributes.USAGE_ASSISTANT, + AudioAttributes.USAGE_EMERGENCY)) + .setFadeOutDurationForUsage(AudioAttributes.USAGE_MEDIA, TEST_FADE_OUT_DURATION_MS) + .setFadeInDurationForUsage(AudioAttributes.USAGE_MEDIA, TEST_FADE_IN_DURATION_MS) + .build(); + + FadeManagerConfiguration copyConstructedFmc = + new FadeManagerConfiguration.Builder(fmc).build(); + + expect.withMessage("Fade manager config constructed using copy constructor").that(fmc) + .isEqualTo(copyConstructedFmc); + } + + @Test public void testGetDefaultFadeOutDuration() { expect.withMessage("Default fade out duration") .that(FadeManagerConfiguration.getDefaultFadeOutDurationMillis()) diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index 8227bdbd14a7..882afcab6290 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -41,16 +41,15 @@ using namespace aidl::android::os; using namespace std::chrono_literals; -using HalSessionHint = aidl::android::hardware::power::SessionHint; -using HalSessionMode = aidl::android::hardware::power::SessionMode; -using HalWorkDuration = aidl::android::hardware::power::WorkDuration; +// Namespace for AIDL types coming from the PowerHAL +namespace hal = aidl::android::hardware::power; using android::base::StringPrintf; struct APerformanceHintSession; constexpr int64_t SEND_HINT_TIMEOUT = std::chrono::nanoseconds(100ms).count(); -struct AWorkDuration : public HalWorkDuration {}; +struct AWorkDuration : public hal::WorkDuration {}; struct APerformanceHintManager { public: @@ -115,7 +114,7 @@ private: // Last hint reported from sendHint indexed by hint value std::vector<int64_t> mLastHintSentTimestamp; // Cached samples - std::vector<HalWorkDuration> mActualWorkDurations; + std::vector<hal::WorkDuration> mActualWorkDurations; std::string mSessionName; static int32_t sIDCounter; // The most recent set of thread IDs @@ -207,8 +206,9 @@ APerformanceHintSession::APerformanceHintSession(std::shared_ptr<IHintManager> h mTargetDurationNanos(targetDurationNanos), mFirstTargetMetTimestamp(0), mLastTargetMetTimestamp(0) { - const std::vector<HalSessionHint> sessionHintRange{ndk::enum_range<HalSessionHint>().begin(), - ndk::enum_range<HalSessionHint>().end()}; + const std::vector<hal::SessionHint> sessionHintRange{ndk::enum_range<hal::SessionHint>() + .begin(), + ndk::enum_range<hal::SessionHint>().end()}; mLastHintSentTimestamp = std::vector<int64_t>(sessionHintRange.size(), 0); mSessionName = android::base::StringPrintf("ADPF Session %" PRId32, ++sIDCounter); @@ -246,10 +246,10 @@ int APerformanceHintSession::updateTargetWorkDuration(int64_t targetDurationNano } int APerformanceHintSession::reportActualWorkDuration(int64_t actualDurationNanos) { - HalWorkDuration workDuration{.durationNanos = actualDurationNanos, - .workPeriodStartTimestampNanos = 0, - .cpuDurationNanos = actualDurationNanos, - .gpuDurationNanos = 0}; + hal::WorkDuration workDuration{.durationNanos = actualDurationNanos, + .workPeriodStartTimestampNanos = 0, + .cpuDurationNanos = actualDurationNanos, + .gpuDurationNanos = 0}; return reportActualWorkDurationInternal(static_cast<AWorkDuration*>(&workDuration)); } @@ -323,7 +323,8 @@ int APerformanceHintSession::getThreadIds(int32_t* const threadIds, size_t* size int APerformanceHintSession::setPreferPowerEfficiency(bool enabled) { ndk::ScopedAStatus ret = - mHintSession->setMode(static_cast<int32_t>(HalSessionMode::POWER_EFFICIENCY), enabled); + mHintSession->setMode(static_cast<int32_t>(hal::SessionMode::POWER_EFFICIENCY), + enabled); if (!ret.isOk()) { ALOGE("%s: HintSession setPreferPowerEfficiency failed: %s", __FUNCTION__, diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 9b1330fc048a..6ce83cd7b765 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -367,7 +367,7 @@ void ASurfaceTransaction_reparent(ASurfaceTransaction* aSurfaceTransaction, void ASurfaceTransaction_setVisibility(ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl, - int8_t visibility) { + ASurfaceTransactionVisibility visibility) { CHECK_NOT_NULL(aSurfaceTransaction); CHECK_NOT_NULL(aSurfaceControl); @@ -496,7 +496,7 @@ void ASurfaceTransaction_setScale(ASurfaceTransaction* aSurfaceTransaction, void ASurfaceTransaction_setBufferTransparency(ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl, - int8_t transparency) { + ASurfaceTransactionTransparency transparency) { CHECK_NOT_NULL(aSurfaceTransaction); CHECK_NOT_NULL(aSurfaceControl); diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java index a8d8f9a1a55d..8891b50352d1 100644 --- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java @@ -39,15 +39,15 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; +import android.util.BackgroundThread; +import android.util.LongArrayQueue; import android.util.Slog; import android.util.Xml; -import android.utils.BackgroundThread; -import android.utils.LongArrayQueue; -import android.utils.XmlUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -137,17 +137,6 @@ public class PackageWatchdog { static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5; 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 = @@ -253,15 +242,8 @@ public class PackageWatchdog { mConnectivityModuleConnector = connectivityModuleConnector; mSystemClock = clock; mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS; - 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); - } + mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS); loadFromFile(); sPackageWatchdog = this; @@ -526,10 +508,16 @@ public class PackageWatchdog { /** * 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. + * + * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots + * are not counted in bootloop. */ @SuppressWarnings("GuardedBy") public void noteBoot() { synchronized (mLock) { + // if boot count has reached threshold, start mitigation. + // We wait until threshold number of restarts only for the first time. Perform + // mitigations for every restart after that. boolean mitigate = mBootThreshold.incrementAndTest(); if (mitigate) { if (!Flags.recoverabilityDetection()) { @@ -557,17 +545,13 @@ public class PackageWatchdog { } if (currentObserverToNotify != null) { 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); - } + int currentObserverMitigationCount = + currentObserverInternal.getBootMitigationCount() + 1; + currentObserverInternal.setBootMitigationCount( + currentObserverMitigationCount); + saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); + currentObserverToNotify.executeBootLoopMitigation( + currentObserverMitigationCount); } else { mBootThreshold.setMitigationCount(mitigationCount); mBootThreshold.saveMitigationCountToMetadata(); @@ -647,11 +631,6 @@ public class PackageWatchdog { 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, @@ -1827,16 +1806,10 @@ 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() { @@ -1915,6 +1888,7 @@ public class PackageWatchdog { } else { readMitigationCountFromMetadataIfNecessary(); } + final long now = mSystemClock.uptimeMillis(); if (now - getStart() < 0) { Slog.e(TAG, "Window was less than zero. Resetting start to current time."); @@ -1939,20 +1913,32 @@ public class PackageWatchdog { setCount(count); EventLogTags.writeRescueNote(Process.ROOT_UID, count, window); if (Flags.recoverabilityDetection()) { - boolean mitigate = (count >= mBootTriggerCount) - && (count - mBootTriggerCount) % mBootMitigationIncrement == 0; - return mitigate; + // After a reboot (e.g. by WARM_REBOOT or mainline rollback) we apply + // mitigations without waiting for DEFAULT_BOOT_LOOP_TRIGGER_COUNT. + return (count >= mBootTriggerCount) + || (performedMitigationsDuringWindow() && count > 1); } return count >= mBootTriggerCount; } } @GuardedBy("mLock") + private boolean performedMitigationsDuringWindow() { + for (ObserverInternal observerInternal: mAllObservers.values()) { + if (observerInternal.getBootMitigationCount() > 0) { + return true; + } + } + return false; + } + + @GuardedBy("mLock") private void resetAllObserversBootMitigationCount() { for (int i = 0; i < mAllObservers.size(); i++) { final ObserverInternal observer = mAllObservers.valueAt(i); observer.setBootMitigationCount(0); } + saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); } @GuardedBy("mLock") diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java index 7093ba42f40d..271d552fc574 100644 --- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java +++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java @@ -31,6 +31,7 @@ import android.content.pm.VersionedPackage; import android.crashrecovery.flags.Flags; import android.os.Build; import android.os.Environment; +import android.os.FileUtils; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.SystemClock; @@ -43,11 +44,10 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Slog; -import android.utils.ArrayUtils; -import android.utils.FileUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.server.PackageWatchdog.FailureReasons; import com.android.server.PackageWatchdog.PackageHealthObserver; import com.android.server.PackageWatchdog.PackageHealthObserverImpact; @@ -139,7 +139,7 @@ public class RescueParty { static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG = "namespace_to_package_mapping"; @VisibleForTesting - static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 10; + static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440; private static final String NAME = "rescue-party-observer"; diff --git a/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java b/packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java index afcf6895fd0d..a6ae68f62f10 100644 --- a/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java +++ b/packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.utils; +package android.util; import android.annotation.NonNull; import android.os.Handler; diff --git a/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java b/packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java index fdb15e2333d5..948ebcca0263 100644 --- a/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java +++ b/packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.utils; +package android.util; import android.annotation.NonNull; import android.os.Handler; diff --git a/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java b/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java deleted file mode 100644 index fa4d6afc03d3..000000000000 --- a/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java +++ /dev/null @@ -1,115 +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 android.utils; - -import android.annotation.NonNull; -import android.annotation.Nullable; - -import java.io.File; -import java.util.List; -import java.util.Objects; - -/** - * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java - * - * @hide - */ -public class ArrayUtils { - private ArrayUtils() { /* cannot be instantiated */ } - public static final File[] EMPTY_FILE = new File[0]; - - - /** - * Return first index of {@code value} in {@code array}, or {@code -1} if - * not found. - */ - public static <T> int indexOf(@Nullable T[] array, T value) { - if (array == null) return -1; - for (int i = 0; i < array.length; i++) { - if (Objects.equals(array[i], value)) return i; - } - return -1; - } - - /** @hide */ - public static @NonNull File[] defeatNullable(@Nullable File[] val) { - return (val != null) ? val : EMPTY_FILE; - } - - /** - * Checks if given array is null or has zero elements. - */ - public static boolean isEmpty(@Nullable int[] array) { - return array == null || array.length == 0; - } - - /** - * True if the byte array is null or has length 0. - */ - public static boolean isEmpty(@Nullable byte[] array) { - return array == null || array.length == 0; - } - - /** - * Converts from List of bytes to byte array - * @param list - * @return byte[] - */ - public static byte[] toPrimitive(List<byte[]> list) { - if (list.size() == 0) { - return new byte[0]; - } - int byteLen = list.get(0).length; - byte[] array = new byte[list.size() * byteLen]; - for (int i = 0; i < list.size(); i++) { - for (int j = 0; j < list.get(i).length; j++) { - array[i * byteLen + j] = list.get(i)[j]; - } - } - return array; - } - - /** - * Adds value to given array if not already present, providing set-like - * behavior. - */ - public static @NonNull int[] appendInt(@Nullable int[] cur, int val) { - return appendInt(cur, val, false); - } - - /** - * Adds value to given array. - */ - public static @NonNull int[] appendInt(@Nullable int[] cur, int val, - boolean allowDuplicates) { - if (cur == null) { - return new int[] { val }; - } - final int n = cur.length; - if (!allowDuplicates) { - for (int i = 0; i < n; i++) { - if (cur[i] == val) { - return cur; - } - } - } - int[] ret = new int[n + 1]; - System.arraycopy(cur, 0, ret, 0, n); - ret[n] = val; - return ret; - } -} diff --git a/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java b/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java deleted file mode 100644 index e4923bfc4ecb..000000000000 --- a/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java +++ /dev/null @@ -1,128 +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 android.utils; - -import android.annotation.NonNull; -import android.annotation.Nullable; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Bits and pieces copied from hidden API of android.os.FileUtils. - * - * @hide - */ -public class FileUtils { - /** - * Read a text file into a String, optionally limiting the length. - * - * @param file to read (will not seek, so things like /proc files are OK) - * @param max length (positive for head, negative of tail, 0 for no limit) - * @param ellipsis to add of the file was truncated (can be null) - * @return the contents of the file, possibly truncated - * @throws IOException if something goes wrong reading the file - * @hide - */ - public static @Nullable String readTextFile(@Nullable File file, @Nullable int max, - @Nullable String ellipsis) throws IOException { - InputStream input = new FileInputStream(file); - // wrapping a BufferedInputStream around it because when reading /proc with unbuffered - // input stream, bytes read not equal to buffer size is not necessarily the correct - // indication for EOF; but it is true for BufferedInputStream due to its implementation. - BufferedInputStream bis = new BufferedInputStream(input); - try { - long size = file.length(); - if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes - if (size > 0 && (max == 0 || size < max)) max = (int) size; - byte[] data = new byte[max + 1]; - int length = bis.read(data); - if (length <= 0) return ""; - if (length <= max) return new String(data, 0, length); - if (ellipsis == null) return new String(data, 0, max); - return new String(data, 0, max) + ellipsis; - } else if (max < 0) { // "tail" mode: keep the last N - int len; - boolean rolled = false; - byte[] last = null; - byte[] data = null; - do { - if (last != null) rolled = true; - byte[] tmp = last; - last = data; - data = tmp; - if (data == null) data = new byte[-max]; - len = bis.read(data); - } while (len == data.length); - - if (last == null && len <= 0) return ""; - if (last == null) return new String(data, 0, len); - if (len > 0) { - rolled = true; - System.arraycopy(last, len, last, 0, last.length - len); - System.arraycopy(data, 0, last, last.length - len, len); - } - if (ellipsis == null || !rolled) return new String(last); - return ellipsis + new String(last); - } else { // "cat" mode: size unknown, read it all in streaming fashion - ByteArrayOutputStream contents = new ByteArrayOutputStream(); - int len; - byte[] data = new byte[1024]; - do { - len = bis.read(data); - if (len > 0) contents.write(data, 0, len); - } while (len == data.length); - return contents.toString(); - } - } finally { - bis.close(); - input.close(); - } - } - - /** - * Perform an fsync on the given FileOutputStream. The stream at this - * point must be flushed but not yet closed. - * - * @hide - */ - public static boolean sync(FileOutputStream stream) { - try { - if (stream != null) { - stream.getFD().sync(); - } - return true; - } catch (IOException e) { - } - return false; - } - - /** - * List the files in the directory or return empty file. - * - * @hide - */ - public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) { - return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles()) - : ArrayUtils.EMPTY_FILE; - } -} diff --git a/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java b/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java deleted file mode 100644 index 5cdc2536129a..000000000000 --- a/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java +++ /dev/null @@ -1,188 +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 android.utils; - -import libcore.util.EmptyArray; - -import java.util.NoSuchElementException; - -/** - * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java - * - * @hide - */ -public class LongArrayQueue { - - private long[] mValues; - private int mSize; - private int mHead; - private int mTail; - - private long[] newUnpaddedLongArray(int num) { - return new long[num]; - } - /** - * Initializes a queue with the given starting capacity. - * - * @param initialCapacity the capacity. - */ - public LongArrayQueue(int initialCapacity) { - if (initialCapacity == 0) { - mValues = EmptyArray.LONG; - } else { - mValues = newUnpaddedLongArray(initialCapacity); - } - mSize = 0; - mHead = mTail = 0; - } - - /** - * Initializes a queue with default starting capacity. - */ - public LongArrayQueue() { - this(16); - } - - /** @hide */ - public static int growSize(int currentSize) { - return currentSize <= 4 ? 8 : currentSize * 2; - } - - private void grow() { - if (mSize < mValues.length) { - throw new IllegalStateException("Queue not full yet!"); - } - final int newSize = growSize(mSize); - final long[] newArray = newUnpaddedLongArray(newSize); - final int r = mValues.length - mHead; // Number of elements on and to the right of head. - System.arraycopy(mValues, mHead, newArray, 0, r); - System.arraycopy(mValues, 0, newArray, r, mHead); - mValues = newArray; - mHead = 0; - mTail = mSize; - } - - /** - * Returns the number of elements in the queue. - */ - public int size() { - return mSize; - } - - /** - * Removes all elements from this queue. - */ - public void clear() { - mSize = 0; - mHead = mTail = 0; - } - - /** - * Adds a value to the tail of the queue. - * - * @param value the value to be added. - */ - public void addLast(long value) { - if (mSize == mValues.length) { - grow(); - } - mValues[mTail] = value; - mTail = (mTail + 1) % mValues.length; - mSize++; - } - - /** - * Removes an element from the head of the queue. - * - * @return the element at the head of the queue. - * @throws NoSuchElementException if the queue is empty. - */ - public long removeFirst() { - if (mSize == 0) { - throw new NoSuchElementException("Queue is empty!"); - } - final long ret = mValues[mHead]; - mHead = (mHead + 1) % mValues.length; - mSize--; - return ret; - } - - /** - * Returns the element at the given position from the head of the queue, where 0 represents the - * head of the queue. - * - * @param position the position from the head of the queue. - * @return the element found at the given position. - * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or - * {@code position} >= {@link #size()} - */ - public long get(int position) { - if (position < 0 || position >= mSize) { - throw new IndexOutOfBoundsException("Index " + position - + " not valid for a queue of size " + mSize); - } - final int index = (mHead + position) % mValues.length; - return mValues[index]; - } - - /** - * Returns the element at the head of the queue, without removing it. - * - * @return the element at the head of the queue. - * @throws NoSuchElementException if the queue is empty - */ - public long peekFirst() { - if (mSize == 0) { - throw new NoSuchElementException("Queue is empty!"); - } - return mValues[mHead]; - } - - /** - * Returns the element at the tail of the queue. - * - * @return the element at the tail of the queue. - * @throws NoSuchElementException if the queue is empty. - */ - public long peekLast() { - if (mSize == 0) { - throw new NoSuchElementException("Queue is empty!"); - } - final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1; - return mValues[index]; - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - if (mSize <= 0) { - return "{}"; - } - - final StringBuilder buffer = new StringBuilder(mSize * 64); - buffer.append('{'); - buffer.append(get(0)); - for (int i = 1; i < mSize; i++) { - buffer.append(", "); - buffer.append(get(i)); - } - buffer.append('}'); - return buffer.toString(); - } -} diff --git a/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java b/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java deleted file mode 100644 index dbbef61f6777..000000000000 --- a/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java +++ /dev/null @@ -1,118 +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 android.utils; - -import android.annotation.NonNull; -import android.system.ErrnoException; -import android.system.Os; - -import com.android.modules.utils.TypedXmlPullParser; - -import libcore.util.XmlObjectFactory; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * Copied over partly from frameworks/base/core/java/com/android/internal/util/XmlUtils.java - * - * @hide - */ -public class XmlUtils { - - private static final String STRING_ARRAY_SEPARATOR = ":"; - - /** @hide */ - public static final void beginDocument(XmlPullParser parser, String firstElementName) - throws XmlPullParserException, IOException { - int type; - while ((type = parser.next()) != parser.START_TAG - && type != parser.END_DOCUMENT) { - // Do nothing - } - - if (type != parser.START_TAG) { - throw new XmlPullParserException("No start tag found"); - } - - if (!parser.getName().equals(firstElementName)) { - throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() - + ", expected " + firstElementName); - } - } - - /** @hide */ - public static boolean nextElementWithin(XmlPullParser parser, int outerDepth) - throws IOException, XmlPullParserException { - for (;;) { - int type = parser.next(); - if (type == XmlPullParser.END_DOCUMENT - || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) { - return false; - } - if (type == XmlPullParser.START_TAG - && parser.getDepth() == outerDepth + 1) { - return true; - } - } - } - - private static XmlPullParser newPullParser() { - try { - XmlPullParser parser = XmlObjectFactory.newXmlPullParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - return parser; - } catch (XmlPullParserException e) { - throw new AssertionError(); - } - } - - /** @hide */ - public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in) - throws IOException { - final byte[] magic = new byte[4]; - if (in instanceof FileInputStream) { - try { - Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0); - } catch (ErrnoException e) { - throw e.rethrowAsIOException(); - } - } else { - if (!in.markSupported()) { - in = new BufferedInputStream(in); - } - in.mark(8); - in.read(magic); - in.reset(); - } - - final TypedXmlPullParser xml; - xml = (TypedXmlPullParser) newPullParser(); - try { - xml.setInput(in, "UTF_8"); - } catch (XmlPullParserException e) { - throw new IOException(e); - } - return xml; - } -} diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index bc35a85e48f8..9fd386f38684 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -68,6 +68,13 @@ <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="app_name" example="Tribank">%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 create passkey flow. [CHAR LIMIT=200] --> + <string name="choose_create_single_tap_passkey_title">Use your screen lock to create a passkey for <xliff:g id="app_name" example="Shrine">%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 create password flow. [CHAR LIMIT=200] --> + <string name="choose_create_single_tap_password_title">Use your screen lock to create a password for <xliff:g id="app_name" example="Shrine">%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 create flow when the credential type is others. [CHAR LIMIT=200] --> + <!-- TODO(b/326243891) : Confirm with team on dynamically setting this based on recent product and ux discussions (does not disrupt e2e) --> + <string name="choose_create_single_tap_sign_in_title">Use your screen lock to save sign in info for <xliff:g id="app_name" example="Shrine">%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> diff --git a/packages/CredentialManager/shared/project.config b/packages/CredentialManager/shared/project.config new file mode 100644 index 000000000000..f748d6cb9f2e --- /dev/null +++ b/packages/CredentialManager/shared/project.config @@ -0,0 +1,9 @@ +[notify "team"] + header = cc + email = sgjerry@google.com + email = helenqin@google.com + email = hemnani@google.com + email = shuanghao@google.com + email = harinirajan@google.com + type = new_changes + type = submitted_changes
\ No newline at end of file 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 f2c252ec6422..b408c1553d94 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt @@ -51,6 +51,8 @@ import com.android.credentialmanager.TAG import com.android.credentialmanager.model.BiometricRequestInfo import com.android.credentialmanager.model.EntryInfo +const val CREDENTIAL_ENTRY_PREFIX = "androidx.credentials.provider.credentialEntry." + fun EntryInfo.getIntentSenderRequest( isAutoSelected: Boolean = false ): IntentSenderRequest? { @@ -140,7 +142,8 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), - biometricRequest = predetermineAndValidateBiometricFlow(it), + biometricRequest = predetermineAndValidateBiometricFlow(it, + CREDENTIAL_ENTRY_PREFIX), ) ) } @@ -169,7 +172,8 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), - biometricRequest = predetermineAndValidateBiometricFlow(it), + biometricRequest = predetermineAndValidateBiometricFlow(it, + CREDENTIAL_ENTRY_PREFIX), ) ) } @@ -197,7 +201,8 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), - biometricRequest = predetermineAndValidateBiometricFlow(it), + biometricRequest = predetermineAndValidateBiometricFlow(it, + CREDENTIAL_ENTRY_PREFIX), ) ) } @@ -217,21 +222,26 @@ private fun getCredentialOptionInfoList( * 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. + * + * @param hintPrefix a string prefix indicating the type of entry being utilized, since both create + * and get flows utilize slice params; includes the final '.' before the name of the type (e.g. + * androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS must have + * 'hintPrefix' up to "androidx.credentials.provider.credentialEntry.") * // 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 +fun predetermineAndValidateBiometricFlow( + entry: Entry, + hintPrefix: String, ): 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") + val allowedAuthenticators: Int? = entry.slice.items.firstOrNull { + it.hasHint(hintPrefix + "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") + val opId: Int? = entry.slice.items.firstOrNull { + it.hasHint(hintPrefix + "SLICE_HINT_CRYPTO_OP_ID") }?.int if (allowedAuthenticators != null) { return BiometricRequestInfo(opId = opId, allowedAuthenticators = allowedAuthenticators) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 28c40479962e..a03975375e8a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -18,6 +18,7 @@ package com.android.credentialmanager import android.app.Activity import android.hardware.biometrics.BiometricPrompt +import android.hardware.biometrics.BiometricPrompt.AuthenticationResult import android.os.IBinder import android.text.TextUtils import android.util.Log @@ -39,6 +40,7 @@ import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.createflow.ActiveEntry import com.android.credentialmanager.createflow.CreateCredentialUiState import com.android.credentialmanager.createflow.CreateScreenState +import com.android.credentialmanager.createflow.findBiometricFlowEntry import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.GetScreenState import com.android.credentialmanager.logging.LifecycleEvent @@ -304,7 +306,11 @@ class CredentialSelectorViewModel( uiState = uiState.copy( createCredentialUiState = uiState.createCredentialUiState?.copy( currentScreenState = - if (uiState.createCredentialUiState?.requestDisplayInfo?.userSetDefaultProviderIds + // An autoselect flow never makes it to the more options screen + if (findBiometricFlowEntry(activeEntry = activeEntry, + isAutoSelectFlow = false) != null) CreateScreenState.BIOMETRIC_SELECTION + else if ( + uiState.createCredentialUiState?.requestDisplayInfo?.userSetDefaultProviderIds ?.contains(activeEntry.activeProvider.id) ?: true || !(uiState.createCredentialUiState?.foundCandidateFromUserDefaultProvider ?: false) || @@ -330,7 +336,10 @@ class CredentialSelectorViewModel( ) } - fun createFlowOnEntrySelected(selectedEntry: EntryInfo) { + fun createFlowOnEntrySelected( + selectedEntry: EntryInfo, + authResult: AuthenticationResult? = null + ) { val providerId = selectedEntry.providerId val entryKey = selectedEntry.entryKey val entrySubkey = selectedEntry.entrySubkey @@ -341,6 +350,9 @@ class CredentialSelectorViewModel( uiState = uiState.copy( selectedEntry = selectedEntry, providerActivityState = ProviderActivityState.READY_TO_LAUNCH, + biometricState = if (authResult == null) uiState.biometricState else uiState + .biometricState.copy(biometricResult = BiometricResult( + biometricAuthenticationResult = authResult)) ) } else { credManRepo.onOptionSelected( @@ -367,9 +379,4 @@ 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 fd6fc6a44c7c..358ebfa1ec90 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -52,10 +52,11 @@ import androidx.credentials.provider.CreateEntry import androidx.credentials.provider.RemoteEntry import org.json.JSONObject import android.credentials.flags.Flags +import com.android.credentialmanager.createflow.isBiometricFlow import com.android.credentialmanager.getflow.TopBrandingContent +import com.android.credentialmanager.ktx.predetermineAndValidateBiometricFlow import java.time.Instant - fun getAppLabel( pm: PackageManager, appPackageName: String @@ -237,6 +238,9 @@ class GetFlowUtils { class CreateFlowUtils { companion object { + + private const val CREATE_ENTRY_PREFIX = "androidx.credentials.provider.createEntry." + /** * Note: caller required handle empty list due to parsing error. */ @@ -417,12 +421,21 @@ class CreateFlowUtils { } } val defaultProvider = defaultProviderPreferredByApp ?: defaultProviderSetByUser + val sortedCreateOptionsPairs = createOptionsPairs.sortedWith( + compareByDescending { it.first.lastUsedTime } + ) + val activeEntry = toActiveEntry( + defaultProvider = defaultProvider, + sortedCreateOptionsPairs = sortedCreateOptionsPairs, + remoteEntry = remoteEntry, + remoteEntryProvider = remoteEntryProvider, + ) + val isBiometricFlow = if (activeEntry == null) false else isBiometricFlow(activeEntry, + sortedCreateOptionsPairs, requestDisplayInfo) val initialScreenState = toCreateScreenState( createOptionSize = createOptionsPairs.size, remoteEntry = remoteEntry, - ) - val sortedCreateOptionsPairs = createOptionsPairs.sortedWith( - compareByDescending { it.first.lastUsedTime } + isBiometricFlow = isBiometricFlow ) return CreateCredentialUiState( enabledProviders = enabledProviders, @@ -430,12 +443,7 @@ class CreateFlowUtils { currentScreenState = initialScreenState, requestDisplayInfo = requestDisplayInfo, sortedCreateOptionsPairs = sortedCreateOptionsPairs, - activeEntry = toActiveEntry( - defaultProvider = defaultProvider, - sortedCreateOptionsPairs = sortedCreateOptionsPairs, - remoteEntry = remoteEntry, - remoteEntryProvider = remoteEntryProvider, - ), + activeEntry = activeEntry, remoteEntry = remoteEntry, foundCandidateFromUserDefaultProvider = defaultProviderSetByUser != null, ) @@ -444,9 +452,12 @@ class CreateFlowUtils { fun toCreateScreenState( createOptionSize: Int, remoteEntry: RemoteInfo?, + isBiometricFlow: Boolean, ): CreateScreenState { return if (createOptionSize == 0 && remoteEntry != null) { CreateScreenState.EXTERNAL_ONLY_SELECTION + } else if (isBiometricFlow) { + CreateScreenState.BIOMETRIC_SELECTION } else { CreateScreenState.CREATION_OPTION_SELECTION } @@ -503,8 +514,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 + biometricRequest = predetermineAndValidateBiometricFlow(it, + CREATE_ENTRY_PREFIX), ) ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt index d21077ee7c5a..fa177351be30 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt @@ -26,11 +26,14 @@ 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.createflow.getCreateTitleResCode 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.CredentialType import com.android.credentialmanager.model.EntryInfo +import com.android.credentialmanager.model.creation.CreateOptionInfo import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.model.get.ProviderInfo import java.lang.Exception @@ -39,14 +42,30 @@ 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 + * the large text displaying the flow in progress, and the [descriptionForCredential], which * describes details of where the credential is being saved, and how. + * (E.g. assume a hypothetical provider 'Any Provider' for *passkey* flows with Your@Email.com: + * + * 'get' flow: + * - [providerIcon] and [providerName] = 'Any Provider' (and it's icon) + * - [displayTitleText] = "Use your saved passkey for Any Provider?" + * - [descriptionForCredential] = "Use your screen lock to sign in to Any Provider with + * Your@Email.com" + * + * 'create' flow: + * - [providerIcon] and [providerName] = 'Any Provider' (and it's icon) + * - [displayTitleText] = "Create passkey to sign in to Any Provider?" + * - [descriptionForCredential] = "Use your screen lock to create a passkey for Any Provider?" + * ). + * + * The above are examples; the credential type can change depending on scenario. + * // TODO(b/326243891) : Finalize once all the strings and create flow is iterated to completion */ data class BiometricDisplayInfo( val providerIcon: Bitmap, val providerName: String, val displayTitleText: String, - val descriptionAboveBiometricButton: String, + val descriptionForCredential: String, val biometricRequestInfo: BiometricRequestInfo, ) @@ -56,10 +75,7 @@ data class BiometricDisplayInfo( * 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, + val biometricResult: BiometricResult? = null ) /** @@ -98,7 +114,8 @@ fun runBiometricFlow( context: Context, openMoreOptionsPage: () -> Unit, sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, - onCancelFlowAndFinish: (String) -> Unit, + onCancelFlowAndFinish: () -> Unit, + onIllegalStateAndFinish: (String) -> Unit, getRequestDisplayInfo: RequestDisplayInfo? = null, getProviderInfoList: List<ProviderInfo>? = null, getProviderDisplayInfo: ProviderDisplayInfo? = null, @@ -107,18 +124,20 @@ fun runBiometricFlow( .RequestDisplayInfo? = null, createProviderInfo: EnabledProviderInfo? = null, ) { + // TODO(b/330396089) : Add rotation configuration fix with state machine var biometricDisplayInfo: BiometricDisplayInfo? = null + var flowType = FlowType.GET 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( + flowType = FlowType.CREATE + biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo( createRequestDisplayInfo, - createProviderInfo - ) + createProviderInfo, + context, biometricEntry) } if (biometricDisplayInfo == null) { @@ -127,11 +146,11 @@ fun runBiometricFlow( } val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage, - biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators) + biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators, flowType) val callback: BiometricPrompt.AuthenticationCallback = setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry, - onCancelFlowAndFinish) + onCancelFlowAndFinish, onIllegalStateAndFinish) val cancellationSignal = CancellationSignal() cancellationSignal.setOnCancelListener { @@ -153,23 +172,21 @@ fun runBiometricFlow( /** * 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, + flowType: FlowType, ): 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), + .setNegativeButton(context.getString(if (flowType == FlowType.GET) R.string + .dropdown_presentation_more_sign_in_options_text else R.string.string_more_options), getMainExecutor(context)) { _, _ -> openMoreOptionsPage() } @@ -177,7 +194,7 @@ private fun setupBiometricPrompt( .setConfirmationRequired(true) .setLogoBitmap(biometricDisplayInfo.providerIcon) .setLogoDescription(biometricDisplayInfo.providerName) - .setDescription(biometricDisplayInfo.descriptionAboveBiometricButton) + .setDescription(biometricDisplayInfo.descriptionForCredential) .build() return biometricPrompt @@ -211,7 +228,8 @@ private fun removeDeviceCredential(requestAllowedAuthenticators: Int): Int { private fun setupBiometricAuthenticationCallback( sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, selectedEntry: EntryInfo, - onCancelFlowAndFinish: (String) -> Unit + onCancelFlowAndFinish: () -> Unit, + onIllegalStateAndFinish: (String) -> Unit, ): BiometricPrompt.AuthenticationCallback { val callback: BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { @@ -224,14 +242,12 @@ private fun setupBiometricAuthenticationCallback( if (authResult != null) { sendDataToProvider(selectedEntry, authResult) } else { - onCancelFlowAndFinish("The biometric flow succeeded but unexpectedly " + + onIllegalStateAndFinish("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 " + + onIllegalStateAndFinish("The biometric flow succeeded but failed on handling " + "the result. See: \n$e\n") - // TODO(b/326243754) : Propagate to provider } } @@ -245,6 +261,12 @@ private fun setupBiometricAuthenticationCallback( override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { super.onAuthenticationError(errorCode, errString) Log.d(TAG, "Authentication error-ed out: $errorCode and $errString") + if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED) { + // Note that because the biometric prompt is imbued directly + // into the selector, parity applies to the selector's cancellation instead + // of the provider's biometric prompt cancellation. + onCancelFlowAndFinish() + } // TODO(b/326243754) : Propagate to provider } @@ -288,14 +310,16 @@ private fun validateAndRetrieveBiometricGetDisplayInfo( * 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( +private fun validateAndRetrieveBiometricCreateDisplayInfo( createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo?, createProviderInfo: EnabledProviderInfo?, + context: Context, + selectedEntry: EntryInfo, ): 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() + if (selectedEntry !is CreateOptionInfo) { return null } + return createBiometricDisplayValues(createRequestDisplayInfo, createProviderInfo, context, + selectedEntry) } return null } @@ -340,17 +364,47 @@ private fun getBiometricDisplayValues( username ) return BiometricDisplayInfo(providerIcon = icon, providerName = providerName, - displayTitleText = displayTitleText, descriptionAboveBiometricButton = descriptionText, + displayTitleText = displayTitleText, descriptionForCredential = descriptionText, biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo) } /** - * Handles the biometric sign in via the 'create credentials' flow, or early validates this flow - * needs to fallback. + * Handles the biometric sign in via the create credentials flow. Stricter in the get flow in that + * if this is called, a result is guaranteed. Specifically, this is guaranteed to return a non-null + * value unlike the get counterpart. */ -private fun createFlowDisplayValues(): BiometricDisplayInfo? { - // TODO(b/326243754) : Create Flow to be implemented in follow up - return null +private fun createBiometricDisplayValues( + createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo, + createProviderInfo: EnabledProviderInfo, + context: Context, + selectedEntry: CreateOptionInfo, +): BiometricDisplayInfo { + val icon: Bitmap? + val providerName: String? + val displayTitleText: String? + icon = createProviderInfo.icon.toBitmap() + providerName = createProviderInfo.displayName + displayTitleText = context.getString( + getCreateTitleResCode(createRequestDisplayInfo), + createRequestDisplayInfo.appName + ) + val descriptionText: String = context.getString( + when (createRequestDisplayInfo.type) { + CredentialType.PASSKEY -> + R.string.choose_create_single_tap_passkey_title + + CredentialType.PASSWORD -> + R.string.choose_create_single_tap_password_title + + CredentialType.UNKNOWN -> + R.string.choose_create_single_tap_sign_in_title + }, + createRequestDisplayInfo.appName, + ) + // TODO(b/327620327) : Add a subtitle and any other recently aligned ideas + return BiometricDisplayInfo(providerIcon = icon, providerName = providerName, + displayTitleText = displayTitleText, descriptionForCredential = descriptionText, + biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo) } /** diff --git a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AllTests.java b/packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt index 7ebb7a1c44bf..f6140f51b7b5 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AllTests.java +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/FlowType.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,9 @@ * limitations under the License. */ -package com.android.aslgen; +package com.android.credentialmanager.common -import org.junit.runner.RunWith; -import org.junit.runners.Suite; - -@RunWith(Suite.class) -@Suite.SuiteClasses({ - AslgenTests.class, -}) -public class AllTests {} +enum class FlowType { + GET, + CREATE +}
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index af78573ee9e9..25fb477cbf38 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager.createflow import android.credentials.flags.Flags.selectorUiImprovementsEnabled +import android.hardware.biometrics.BiometricPrompt import android.text.TextUtils import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.result.ActivityResult @@ -26,7 +27,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Divider import androidx.compose.material.icons.Icons @@ -38,6 +38,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -49,6 +50,7 @@ import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.CredentialType 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.BodyMediumText import com.android.credentialmanager.common.ui.BodySmallText @@ -95,6 +97,22 @@ fun CreateCredentialScreen( viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection, onLog = { viewModel.logUiEvent(it) }, ) + CreateScreenState.BIOMETRIC_SELECTION -> + BiometricSelectionPage( + biometricEntry = createCredentialUiState + .activeEntry?.activeEntryInfo, + onCancelFlowAndFinish = viewModel::onUserCancel, + onIllegalScreenStateAndFinish = viewModel::onIllegalUiState, + onMoreOptionSelected = + viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection, + requestDisplayInfo = createCredentialUiState.requestDisplayInfo, + enabledProviderInfo = createCredentialUiState + .activeEntry?.activeProvider!!, + onBiometricEntrySelected = + viewModel::createFlowOnEntrySelected, + fallbackToOriginalFlow = + viewModel::getFlowOnBackToPrimarySelectionScreen, + ) CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard( requestDisplayInfo = createCredentialUiState.requestDisplayInfo, enabledProviderList = createCredentialUiState.enabledProviders, @@ -313,20 +331,9 @@ fun CreationSelectionCard( item { Divider(thickness = 16.dp, color = Color.Transparent) } item { HeadlineText( - text = when (requestDisplayInfo.type) { - CredentialType.PASSKEY -> stringResource( - R.string.choose_create_option_passkey_title, - requestDisplayInfo.appName - ) - CredentialType.PASSWORD -> stringResource( - R.string.choose_create_option_password_title, - requestDisplayInfo.appName - ) - CredentialType.UNKNOWN -> stringResource( - R.string.choose_create_option_sign_in_title, - requestDisplayInfo.appName - ) - } + text = stringResource( + getCreateTitleResCode(requestDisplayInfo), + requestDisplayInfo.appName) ) } item { Divider(thickness = 24.dp, color = Color.Transparent) } @@ -560,4 +567,32 @@ fun RemoteEntryRow( iconImageVector = Icons.Outlined.QrCodeScanner, entryHeadlineText = stringResource(R.string.another_device), ) -}
\ No newline at end of file +} + +@Composable +internal fun BiometricSelectionPage( + biometricEntry: EntryInfo?, + onMoreOptionSelected: () -> Unit, + requestDisplayInfo: RequestDisplayInfo, + enabledProviderInfo: EnabledProviderInfo, + onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, + onCancelFlowAndFinish: () -> Unit, + onIllegalScreenStateAndFinish: (String) -> Unit, + fallbackToOriginalFlow: () -> Unit, +) { + if (biometricEntry == null) { + fallbackToOriginalFlow() + return + } + runBiometricFlow( + biometricEntry = biometricEntry, + context = LocalContext.current, + openMoreOptionsPage = onMoreOptionSelected, + sendDataToProvider = onBiometricEntrySelected, + onCancelFlowAndFinish = onCancelFlowAndFinish, + createRequestDisplayInfo = requestDisplayInfo, + createProviderInfo = enabledProviderInfo, + onBiometricFailureFallback = fallbackToOriginalFlow, + onIllegalStateAndFinish = onIllegalScreenStateAndFinish, + ) +} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt index 617a981fc4ba..1d262ba5261a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt @@ -16,9 +16,11 @@ package com.android.credentialmanager.createflow +import android.credentials.flags.Flags.credmanBiometricApiEnabled import android.graphics.drawable.Drawable -import com.android.credentialmanager.model.EntryInfo +import com.android.credentialmanager.R import com.android.credentialmanager.model.CredentialType +import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.creation.CreateOptionInfo import com.android.credentialmanager.model.creation.RemoteInfo @@ -33,14 +35,99 @@ data class CreateCredentialUiState( val foundCandidateFromUserDefaultProvider: Boolean, ) +/** + * Checks if this create flow is a biometric flow. Note that this flow differs slightly from the + * autoselect 'get' flow. Namely, given there can be multiple providers, rather than multiple + * accounts, the idea is that autoselect is ever only enabled for a single provider (or even, in + * that case, a single 'type' (family only, or work only) for a provider). However, for all other + * cases, the biometric screen should always show up if that entry contains the biometric bit. + */ +internal fun findBiometricFlowEntry( + activeEntry: ActiveEntry, + isAutoSelectFlow: Boolean, +): CreateOptionInfo? { + if (!credmanBiometricApiEnabled()) { + return null + } + if (isAutoSelectFlow) { + // Since this is the create flow, auto select will only ever be true for a single provider. + // However, for all other cases, biometric should be used if that bit is opted into. If + // they clash, autoselect is always preferred, but that's only if there's a single provider. + return null + } + val biometricEntry = getCreateEntry(activeEntry) + return if (biometricEntry?.biometricRequest != null) biometricEntry else null +} + +/** + * Retrieves the activeEntry by validating it is a [CreateOptionInfo]. This is done by ensuring + * that the [activeEntry] exists as a [CreateOptionInfo] to retrieve its [EntryInfo]. + */ +internal fun getCreateEntry( + activeEntry: ActiveEntry?, +): CreateOptionInfo? { + val entry = activeEntry?.activeEntryInfo + if (entry !is CreateOptionInfo) { + return null + } + return entry +} + +/** +* Determines if the flow is a biometric flow by taking into account autoselect criteria. +*/ +internal fun isBiometricFlow( + activeEntry: ActiveEntry, + sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>, + requestDisplayInfo: RequestDisplayInfo, +) = findBiometricFlowEntry(activeEntry, isFlowAutoSelectable( + requestDisplayInfo = requestDisplayInfo, + activeEntry = activeEntry, + sortedCreateOptionsPairs = sortedCreateOptionsPairs +)) != null + +/** + * This utility presents the correct resource string for the create flows title conditionally. + * Similar to generateDisplayTitleTextResCode in the 'get' flow, but for the create flow instead. + * This is for the title, and is a shared resource, unlike the specific unlock request text. + * E.g. this will look something like: "Create passkey to sign in to Tribank." + * // TODO(b/330396140) : Validate approach and add dynamic auth strings + */ +internal fun getCreateTitleResCode(createRequestDisplayInfo: RequestDisplayInfo): Int = + when (createRequestDisplayInfo.type) { + CredentialType.PASSKEY -> + R.string.choose_create_option_passkey_title + + CredentialType.PASSWORD -> + R.string.choose_create_option_password_title + + CredentialType.UNKNOWN -> + R.string.choose_create_option_sign_in_title + } + internal fun isFlowAutoSelectable( uiState: CreateCredentialUiState ): Boolean { - return uiState.requestDisplayInfo.isAutoSelectRequest && - uiState.sortedCreateOptionsPairs.size == 1 && - uiState.activeEntry?.activeEntryInfo?.let { - it is CreateOptionInfo && it.allowAutoSelect - } ?: false + return isFlowAutoSelectable(uiState.requestDisplayInfo, uiState.activeEntry, + uiState.sortedCreateOptionsPairs) +} + +/** + * When initializing, the [CreateCredentialUiState] is generated after the initial screen is set. + * This overloaded method allows identifying if the flow is auto selectable prior to the creation + * of the [CreateCredentialUiState]. + */ +internal fun isFlowAutoSelectable( + requestDisplayInfo: RequestDisplayInfo, + activeEntry: ActiveEntry?, + sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>> +): Boolean { + val isAutoSelectRequest = requestDisplayInfo.isAutoSelectRequest + if (sortedCreateOptionsPairs.size != 1) { + return false + } + val singleEntry = getCreateEntry(activeEntry) + return isAutoSelectRequest && singleEntry?.allowAutoSelect == true } internal fun hasContentToDisplay(state: CreateCredentialUiState): Boolean { @@ -95,6 +182,7 @@ data class ActiveEntry ( /** The name of the current screen. */ enum class CreateScreenState { CREATION_OPTION_SELECTION, + BIOMETRIC_SELECTION, MORE_OPTIONS_SELECTION, DEFAULT_PROVIDER_CONFIRMATION, EXTERNAL_ONLY_SELECTION, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index b59ccc264630..6d1a3dd98210 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -144,11 +144,10 @@ fun GetCredentialScreen( } 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, + onCancelFlowAndFinish = viewModel::onUserCancel, + onIllegalStateAndFinish = viewModel::onIllegalUiState, requestDisplayInfo = getCredentialUiState.requestDisplayInfo, providerInfoList = getCredentialUiState.providerInfoList, providerDisplayInfo = getCredentialUiState.providerDisplayInfo, @@ -212,7 +211,8 @@ fun GetCredentialScreen( @Composable internal fun BiometricSelectionPage( biometricEntry: EntryInfo?, - onCancelFlowAndFinish: (String) -> Unit, + onCancelFlowAndFinish: () -> Unit, + onIllegalStateAndFinish: (String) -> Unit, onMoreOptionSelected: () -> Unit, requestDisplayInfo: RequestDisplayInfo, providerInfoList: List<ProviderInfo>, @@ -230,6 +230,7 @@ internal fun BiometricSelectionPage( openMoreOptionsPage = onMoreOptionSelected, sendDataToProvider = onBiometricEntrySelected, onCancelFlowAndFinish = onCancelFlowAndFinish, + onIllegalStateAndFinish = onIllegalStateAndFinish, getRequestDisplayInfo = requestDisplayInfo, getProviderInfoList = providerInfoList, getProviderDisplayInfo = providerDisplayInfo, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index 6d5b52a7a5f9..ac776af4f627 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -238,6 +238,7 @@ 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*". + * TODO(b/330396140) : Validate approach and add dynamic auth strings */ internal fun generateDisplayTitleTextResCode( singleEntryType: CredentialType, diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp index 6f4f9ca2e8e1..ec1fe39840f6 100644 --- a/packages/EasterEgg/Android.bp +++ b/packages/EasterEgg/Android.bp @@ -83,6 +83,7 @@ java_aconfig_library { aconfig_declarations { name: "easter_egg_flags", package: "com.android.egg.flags", + container: "system", srcs: [ "easter_egg_flags.aconfig", ], diff --git a/packages/EasterEgg/easter_egg_flags.aconfig b/packages/EasterEgg/easter_egg_flags.aconfig index 3268a4f5728c..7ddc2389bc92 100644 --- a/packages/EasterEgg/easter_egg_flags.aconfig +++ b/packages/EasterEgg/easter_egg_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.egg.flags" +container: "system" flag { name: "flag_flag" diff --git a/packages/InputDevices/res/raw/keyboard_layout_french.kcm b/packages/InputDevices/res/raw/keyboard_layout_french.kcm index 636f98d8bb95..4f4fb1b02d1a 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_french.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_french.kcm @@ -35,85 +35,97 @@ key GRAVE { } key 1 { - label: '1' + label: '&' base: '&' - shift: '1' + shift, capslock: '1' + shift+capslock: '&' } key 2 { - label: '2' + label: '\u00e9' base: '\u00e9' - shift: '2' + shift, capslock: '2' + shift+capslock: '\u00e9' ralt: '\u0303' } key 3 { - label: '3' + label: '"' base: '"' - shift: '3' + shift, capslock: '3' + shift+capslock: '"' ralt: '#' } key 4 { - label: '4' + label: '\'' base: '\'' - shift: '4' + shift, capslock: '4' + shift+capslock: '\'' ralt: '{' } key 5 { - label: '5' + label: '(' base: '(' - shift: '5' + shift, capslock: '5' + shift+capslock: '(' ralt: '[' } key 6 { - label: '6' + label: '-' base: '-' - shift: '6' + shift, capslock: '6' + shift+capslock: '-' ralt: '|' } key 7 { - label: '7' + label: '\u00e8' base: '\u00e8' - shift: '7' + shift, capslock: '7' + shift+capslock: '\u00e8' ralt: '\u0300' } key 8 { - label: '8' + label: '_' base: '_' - shift: '8' + shift, capslock: '8' + shift+capslock: '_' ralt: '\\' } key 9 { - label: '9' + label: '\u00e7' base: '\u00e7' - shift: '9' + shift, capslock: '9' + shift+capslock: '\u00e7' ralt: '^' } key 0 { - label: '0' + label: '\u00e0' base: '\u00e0' - shift: '0' + shift, capslock: '0' + shift+capslock: '\u00e0' ralt: '@' } key MINUS { label: ')' base: ')' - shift: '\u00b0' + shift, capslock: '\u00b0' + shift+capslock: ')' ralt: ']' } key EQUALS { label: '=' base: '=' - shift: '+' + shift, capslock: '+' + shift+capslock: '=' ralt: '}' } @@ -193,13 +205,15 @@ key P { key LEFT_BRACKET { label: '\u02c6' base: '\u0302' - shift: '\u0308' + shift, capslock: '\u0308' + shift+capslock: '\u0302' } key RIGHT_BRACKET { label: '$' base: '$' - shift: '\u00a3' + shift, capslock: '\u00a3' + shift+capslock: '$' ralt: '\u00a4' } @@ -278,13 +292,15 @@ key M { key APOSTROPHE { label: '\u00f9' base: '\u00f9' - shift: '%' + shift, capslock: '%' + shift+capslock: '\u00f9' } key BACKSLASH { label: '*' base: '*' - shift: '\u00b5' + shift, capslock: '\u00b5' + shift+capslock: '*' } ### ROW 4 @@ -340,23 +356,27 @@ key N { key COMMA { label: ',' base: ',' - shift: '?' + shift, capslock: '?' + shift+capslock: ',' } key SEMICOLON { label: ';' base: ';' - shift: '.' + shift, capslock: '.' + shift+capslock: ';' } key PERIOD { label: ':' base: ':' - shift: '/' + shift, capslock: '/' + shift+capslock: ':' } key SLASH { label: '!' base: '!' - shift: '\u00a7' + shift, capslock: '\u00a7' + shift+capslock: '!' } diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp index 79c810ca2611..bd84b58aa0f4 100644 --- a/packages/PackageInstaller/Android.bp +++ b/packages/PackageInstaller/Android.bp @@ -46,7 +46,6 @@ android_app { sdk_version: "system_current", rename_resources_package: false, static_libs: [ - "xz-java", "androidx.leanback_leanback", "androidx.annotation_annotation", "androidx.fragment_fragment", @@ -79,7 +78,6 @@ android_app { overrides: ["PackageInstaller"], static_libs: [ - "xz-java", "androidx.leanback_leanback", "androidx.fragment_fragment", "androidx.lifecycle_lifecycle-livedata", @@ -112,7 +110,6 @@ android_app { overrides: ["PackageInstaller"], static_libs: [ - "xz-java", "androidx.leanback_leanback", "androidx.annotation_annotation", "androidx.fragment_fragment", diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index bf69d3ba7603..05f4d6954a00 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -146,17 +146,6 @@ android:configChanges="mnc|mnc|touchscreen|navigation|screenLayout|screenSize|smallestScreenSize|orientation|locale|keyboard|keyboardHidden|fontScale|uiMode|layoutDirection|density" android:exported="false" /> - <!-- Wearable Components --> - <service android:name=".wear.WearPackageInstallerService" - android:permission="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" - android:foregroundServiceType="systemExempted" - android:exported="true"/> - - <provider android:name=".wear.WearPackageIconProvider" - android:authorities="com.google.android.packageinstaller.wear.provider" - android:grantUriPermissions="true" - android:exported="false" /> - <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" tools:node="remove" /> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java deleted file mode 100644 index 53a460dc18ca..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.packageinstaller.wear; - -import android.content.Context; -import android.content.IntentSender; -import android.content.pm.PackageInstaller; -import android.os.Looper; -import android.os.ParcelFileDescriptor; -import android.text.TextUtils; -import android.util.Log; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Task that installs an APK. This must not be called on the main thread. - * This code is based off the Finsky/Wearsky implementation - */ -public class InstallTask { - private static final String TAG = "InstallTask"; - - private static final int DEFAULT_BUFFER_SIZE = 8192; - - private final Context mContext; - private String mPackageName; - private ParcelFileDescriptor mParcelFileDescriptor; - private PackageInstallerImpl.InstallListener mCallback; - private PackageInstaller.Session mSession; - private IntentSender mCommitCallback; - - private Exception mException = null; - private int mErrorCode = 0; - private String mErrorDesc = null; - - public InstallTask(Context context, String packageName, - ParcelFileDescriptor parcelFileDescriptor, - PackageInstallerImpl.InstallListener callback, PackageInstaller.Session session, - IntentSender commitCallback) { - mContext = context; - mPackageName = packageName; - mParcelFileDescriptor = parcelFileDescriptor; - mCallback = callback; - mSession = session; - mCommitCallback = commitCallback; - } - - public boolean isError() { - return mErrorCode != InstallerConstants.STATUS_SUCCESS || !TextUtils.isEmpty(mErrorDesc); - } - - public void execute() { - if (Looper.myLooper() == Looper.getMainLooper()) { - throw new IllegalStateException("This method cannot be called from the UI thread."); - } - - OutputStream sessionStream = null; - try { - sessionStream = mSession.openWrite(mPackageName, 0, -1); - - // 2b: Stream the asset to the installer. Note: - // Note: writeToOutputStreamFromAsset() always safely closes the input stream - writeToOutputStreamFromAsset(sessionStream); - mSession.fsync(sessionStream); - } catch (Exception e) { - mException = e; - mErrorCode = InstallerConstants.ERROR_INSTALL_COPY_STREAM; - mErrorDesc = "Could not write to stream"; - } finally { - if (sessionStream != null) { - // 2c: close output stream - try { - sessionStream.close(); - } catch (Exception e) { - // Ignore otherwise - if (mException == null) { - mException = e; - mErrorCode = InstallerConstants.ERROR_INSTALL_CLOSE_STREAM; - mErrorDesc = "Could not close session stream"; - } - } - } - } - - if (mErrorCode != InstallerConstants.STATUS_SUCCESS) { - // An error occurred, we're done - Log.e(TAG, "Exception while installing " + mPackageName + ": " + mErrorCode + ", " - + mErrorDesc + ", " + mException); - mSession.close(); - mCallback.installFailed(mErrorCode, "[" + mPackageName + "]" + mErrorDesc); - } else { - // 3. Commit the session (this actually installs it.) Session map - // will be cleaned up in the callback. - mCallback.installBeginning(); - mSession.commit(mCommitCallback); - mSession.close(); - } - } - - /** - * {@code PackageInstaller} works with streams. Get the {@code FileDescriptor} - * corresponding to the {@code Asset} and then write the contents into an - * {@code OutputStream} that is passed in. - * <br> - * The {@code FileDescriptor} is closed but the {@code OutputStream} is not closed. - */ - private boolean writeToOutputStreamFromAsset(OutputStream outputStream) { - if (outputStream == null) { - mErrorCode = InstallerConstants.ERROR_INSTALL_COPY_STREAM_EXCEPTION; - mErrorDesc = "Got a null OutputStream."; - return false; - } - - if (mParcelFileDescriptor == null || mParcelFileDescriptor.getFileDescriptor() == null) { - mErrorCode = InstallerConstants.ERROR_COULD_NOT_GET_FD; - mErrorDesc = "Could not get FD"; - return false; - } - - InputStream inputStream = null; - try { - byte[] inputBuf = new byte[DEFAULT_BUFFER_SIZE]; - int bytesRead; - inputStream = new ParcelFileDescriptor.AutoCloseInputStream(mParcelFileDescriptor); - - while ((bytesRead = inputStream.read(inputBuf)) > -1) { - if (bytesRead > 0) { - outputStream.write(inputBuf, 0, bytesRead); - } - } - - outputStream.flush(); - } catch (IOException e) { - mErrorCode = InstallerConstants.ERROR_INSTALL_APK_COPY_FAILURE; - mErrorDesc = "Reading from Asset FD or writing to temp file failed: " + e; - return false; - } finally { - safeClose(inputStream); - } - - return true; - } - - /** - * Quietly close a closeable resource (e.g. a stream or file). The input may already - * be closed and it may even be null. - */ - public static void safeClose(Closeable resource) { - if (resource != null) { - try { - resource.close(); - } catch (IOException ioe) { - // Catch and discard the error - } - } - } -}
\ No newline at end of file diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java deleted file mode 100644 index 3daf3d831d97..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.packageinstaller.wear; - -/** - * Constants for Installation / Uninstallation requests. - * Using the same values as Finsky/Wearsky code for consistency in user analytics of failures - */ -public class InstallerConstants { - /** Request succeeded */ - public static final int STATUS_SUCCESS = 0; - - /** - * The new PackageInstaller also returns a small set of less granular error codes, which - * we'll remap to the range -500 and below to keep away from existing installer codes - * (which run from -1 to -110). - */ - public final static int ERROR_PACKAGEINSTALLER_BASE = -500; - - public static final int ERROR_COULD_NOT_GET_FD = -603; - /** This node is not targeted by this request. */ - - /** The install did not complete because could not create PackageInstaller session */ - public final static int ERROR_INSTALL_CREATE_SESSION = -612; - /** The install did not complete because could not open PackageInstaller session */ - public final static int ERROR_INSTALL_OPEN_SESSION = -613; - /** The install did not complete because could not open PackageInstaller output stream */ - public final static int ERROR_INSTALL_OPEN_STREAM = -614; - /** The install did not complete because of an exception while streaming bytes */ - public final static int ERROR_INSTALL_COPY_STREAM_EXCEPTION = -615; - /** The install did not complete because of an unexpected exception from PackageInstaller */ - public final static int ERROR_INSTALL_SESSION_EXCEPTION = -616; - /** The install did not complete because of an unexpected userActionRequired callback */ - public final static int ERROR_INSTALL_USER_ACTION_REQUIRED = -617; - /** The install did not complete because of an unexpected broadcast (missing fields) */ - public final static int ERROR_INSTALL_MALFORMED_BROADCAST = -618; - /** The install did not complete because of an error while copying from downloaded file */ - public final static int ERROR_INSTALL_APK_COPY_FAILURE = -619; - /** The install did not complete because of an error while copying to the PackageInstaller - * output stream */ - public final static int ERROR_INSTALL_COPY_STREAM = -620; - /** The install did not complete because of an error while closing the PackageInstaller - * output stream */ - public final static int ERROR_INSTALL_CLOSE_STREAM = -621; -}
\ No newline at end of file diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java deleted file mode 100644 index bdc22cf0e276..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.packageinstaller.wear; - -import android.content.Context; - -/** - * Factory that creates a Package Installer. - */ -public class PackageInstallerFactory { - private static PackageInstallerImpl sPackageInstaller; - - /** - * Return the PackageInstaller shared object. {@code init} should have already been called. - */ - public synchronized static PackageInstallerImpl getPackageInstaller(Context context) { - if (sPackageInstaller == null) { - sPackageInstaller = new PackageInstallerImpl(context); - } - return sPackageInstaller; - } -}
\ No newline at end of file diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java deleted file mode 100644 index 1e37f15f714d..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java +++ /dev/null @@ -1,327 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.packageinstaller.wear; - -import android.annotation.TargetApi; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.IntentSender; -import android.content.pm.PackageInstaller; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Implementation of package manager installation using modern PackageInstaller api. - * - * Heavily copied from Wearsky/Finsky implementation - */ -@TargetApi(Build.VERSION_CODES.LOLLIPOP) -public class PackageInstallerImpl { - private static final String TAG = "PackageInstallerImpl"; - - /** Intent actions used for broadcasts from PackageInstaller back to the local receiver */ - private static final String ACTION_INSTALL_COMMIT = - "com.android.vending.INTENT_PACKAGE_INSTALL_COMMIT"; - - private final Context mContext; - private final PackageInstaller mPackageInstaller; - private final Map<String, PackageInstaller.SessionInfo> mSessionInfoMap; - private final Map<String, PackageInstaller.Session> mOpenSessionMap; - - public PackageInstallerImpl(Context context) { - mContext = context.getApplicationContext(); - mPackageInstaller = mContext.getPackageManager().getPackageInstaller(); - - // Capture a map of known sessions - // This list will be pruned a bit later (stale sessions will be canceled) - mSessionInfoMap = new HashMap<String, PackageInstaller.SessionInfo>(); - List<PackageInstaller.SessionInfo> mySessions = mPackageInstaller.getMySessions(); - for (int i = 0; i < mySessions.size(); i++) { - PackageInstaller.SessionInfo sessionInfo = mySessions.get(i); - String packageName = sessionInfo.getAppPackageName(); - PackageInstaller.SessionInfo oldInfo = mSessionInfoMap.put(packageName, sessionInfo); - - // Checking for old info is strictly for logging purposes - if (oldInfo != null) { - Log.w(TAG, "Multiple sessions for " + packageName + " found. Removing " + oldInfo - .getSessionId() + " & keeping " + mySessions.get(i).getSessionId()); - } - } - mOpenSessionMap = new HashMap<String, PackageInstaller.Session>(); - } - - /** - * This callback will be made after an installation attempt succeeds or fails. - */ - public interface InstallListener { - /** - * This callback signals that preflight checks have succeeded and installation - * is beginning. - */ - void installBeginning(); - - /** - * This callback signals that installation has completed. - */ - void installSucceeded(); - - /** - * This callback signals that installation has failed. - */ - void installFailed(int errorCode, String errorDesc); - } - - /** - * This is a placeholder implementation that bundles an entire "session" into a single - * call. This will be replaced by more granular versions that allow longer session lifetimes, - * download progress tracking, etc. - * - * This must not be called on main thread. - */ - public void install(final String packageName, ParcelFileDescriptor parcelFileDescriptor, - final InstallListener callback) { - // 0. Generic try/catch block because I am not really sure what exceptions (other than - // IOException) might be thrown by PackageInstaller and I want to handle them - // at least slightly gracefully. - try { - // 1. Create or recover a session, and open it - // Try recovery first - PackageInstaller.Session session = null; - PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName); - if (sessionInfo != null) { - // See if it's openable, or already held open - session = getSession(packageName); - } - // If open failed, or there was no session, create a new one and open it. - // If we cannot create or open here, the failure is terminal. - if (session == null) { - try { - innerCreateSession(packageName); - } catch (IOException ioe) { - Log.e(TAG, "Can't create session for " + packageName + ": " + ioe.getMessage()); - callback.installFailed(InstallerConstants.ERROR_INSTALL_CREATE_SESSION, - "Could not create session"); - mSessionInfoMap.remove(packageName); - return; - } - sessionInfo = mSessionInfoMap.get(packageName); - try { - session = mPackageInstaller.openSession(sessionInfo.getSessionId()); - mOpenSessionMap.put(packageName, session); - } catch (SecurityException se) { - Log.e(TAG, "Can't open session for " + packageName + ": " + se.getMessage()); - callback.installFailed(InstallerConstants.ERROR_INSTALL_OPEN_SESSION, - "Can't open session"); - mSessionInfoMap.remove(packageName); - return; - } - } - - // 2. Launch task to handle file operations. - InstallTask task = new InstallTask( mContext, packageName, parcelFileDescriptor, - callback, session, - getCommitCallback(packageName, sessionInfo.getSessionId(), callback)); - task.execute(); - if (task.isError()) { - cancelSession(sessionInfo.getSessionId(), packageName); - } - } catch (Exception e) { - Log.e(TAG, "Unexpected exception while installing: " + packageName + ": " - + e.getMessage()); - callback.installFailed(InstallerConstants.ERROR_INSTALL_SESSION_EXCEPTION, - "Unexpected exception while installing " + packageName); - } - } - - /** - * Retrieve an existing session. Will open if needed, but does not attempt to create. - */ - private PackageInstaller.Session getSession(String packageName) { - // Check for already-open session - PackageInstaller.Session session = mOpenSessionMap.get(packageName); - if (session != null) { - try { - // Probe the session to ensure that it's still open. This may or may not - // throw (if non-open), but it may serve as a canary for stale sessions. - session.getNames(); - return session; - } catch (IOException ioe) { - Log.e(TAG, "Stale open session for " + packageName + ": " + ioe.getMessage()); - mOpenSessionMap.remove(packageName); - } catch (SecurityException se) { - Log.e(TAG, "Stale open session for " + packageName + ": " + se.getMessage()); - mOpenSessionMap.remove(packageName); - } - } - // Check to see if this is a known session - PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName); - if (sessionInfo == null) { - return null; - } - // Try to open it. If we fail here, assume that the SessionInfo was stale. - try { - session = mPackageInstaller.openSession(sessionInfo.getSessionId()); - } catch (SecurityException se) { - Log.w(TAG, "SessionInfo was stale for " + packageName + " - deleting info"); - mSessionInfoMap.remove(packageName); - return null; - } catch (IOException ioe) { - Log.w(TAG, "IOException opening old session for " + ioe.getMessage() - + " - deleting info"); - mSessionInfoMap.remove(packageName); - return null; - } - mOpenSessionMap.put(packageName, session); - return session; - } - - /** This version throws an IOException when the session cannot be created */ - private void innerCreateSession(String packageName) throws IOException { - if (mSessionInfoMap.containsKey(packageName)) { - Log.w(TAG, "Creating session for " + packageName + " when one already exists"); - return; - } - PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( - PackageInstaller.SessionParams.MODE_FULL_INSTALL); - params.setAppPackageName(packageName); - - // IOException may be thrown at this point - int sessionId = mPackageInstaller.createSession(params); - PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); - mSessionInfoMap.put(packageName, sessionInfo); - } - - /** - * Cancel a session based on its sessionId. Package name is for logging only. - */ - private void cancelSession(int sessionId, String packageName) { - // Close if currently held open - closeSession(packageName); - // Remove local record - mSessionInfoMap.remove(packageName); - try { - mPackageInstaller.abandonSession(sessionId); - } catch (SecurityException se) { - // The session no longer exists, so we can exit quietly. - return; - } - } - - /** - * Close a session if it happens to be held open. - */ - private void closeSession(String packageName) { - PackageInstaller.Session session = mOpenSessionMap.remove(packageName); - if (session != null) { - // Unfortunately close() is not idempotent. Try our best to make this safe. - try { - session.close(); - } catch (Exception e) { - Log.w(TAG, "Unexpected error closing session for " + packageName + ": " - + e.getMessage()); - } - } - } - - /** - * Creates a commit callback for the package install that's underway. This will be called - * some time after calling session.commit() (above). - */ - private IntentSender getCommitCallback(final String packageName, final int sessionId, - final InstallListener callback) { - // Create a single-use broadcast receiver - BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - mContext.unregisterReceiver(this); - handleCommitCallback(intent, packageName, sessionId, callback); - } - }; - // Create a matching intent-filter and register the receiver - String action = ACTION_INSTALL_COMMIT + "." + packageName; - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(action); - mContext.registerReceiver(broadcastReceiver, intentFilter, - Context.RECEIVER_EXPORTED); - - // Create a matching PendingIntent and use it to generate the IntentSender - Intent broadcastIntent = new Intent(action).setPackage(mContext.getPackageName()); - PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, packageName.hashCode(), - broadcastIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT - | PendingIntent.FLAG_MUTABLE); - return pendingIntent.getIntentSender(); - } - - /** - * Examine the extras to determine information about the package update/install, decode - * the result, and call the appropriate callback. - * - * @param intent The intent, which the PackageInstaller will have added Extras to - * @param packageName The package name we created the receiver for - * @param sessionId The session Id we created the receiver for - * @param callback The callback to report success/failure to - */ - private void handleCommitCallback(Intent intent, String packageName, int sessionId, - InstallListener callback) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Installation of " + packageName + " finished with extras " - + intent.getExtras()); - } - String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); - int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE); - if (status == PackageInstaller.STATUS_SUCCESS) { - cancelSession(sessionId, packageName); - callback.installSucceeded(); - } else if (status == -1 /*PackageInstaller.STATUS_USER_ACTION_REQUIRED*/) { - // TODO - use the constant when the correct/final name is in the SDK - // TODO This is unexpected, so we are treating as failure for now - cancelSession(sessionId, packageName); - callback.installFailed(InstallerConstants.ERROR_INSTALL_USER_ACTION_REQUIRED, - "Unexpected: user action required"); - } else { - cancelSession(sessionId, packageName); - int errorCode = getPackageManagerErrorCode(status); - Log.e(TAG, "Error " + errorCode + " while installing " + packageName + ": " - + statusMessage); - callback.installFailed(errorCode, null); - } - } - - private int getPackageManagerErrorCode(int status) { - // This is a hack: because PackageInstaller now reports error codes - // with small positive values, we need to remap them into a space - // that is more compatible with the existing package manager error codes. - // See https://sites.google.com/a/google.com/universal-store/documentation - // /android-client/download-error-codes - int errorCode; - if (status == Integer.MIN_VALUE) { - errorCode = InstallerConstants.ERROR_INSTALL_MALFORMED_BROADCAST; - } else { - errorCode = InstallerConstants.ERROR_PACKAGEINSTALLER_BASE - status; - } - return errorCode; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java deleted file mode 100644 index 2c289b2a6f94..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.packageinstaller.wear; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; - -/** - * Installation Util that contains a list of parameters that are needed for - * installing/uninstalling. - */ -public class WearPackageArgs { - private static final String KEY_PACKAGE_NAME = - "com.google.android.clockwork.EXTRA_PACKAGE_NAME"; - private static final String KEY_ASSET_URI = - "com.google.android.clockwork.EXTRA_ASSET_URI"; - private static final String KEY_START_ID = - "com.google.android.clockwork.EXTRA_START_ID"; - private static final String KEY_PERM_URI = - "com.google.android.clockwork.EXTRA_PERM_URI"; - private static final String KEY_CHECK_PERMS = - "com.google.android.clockwork.EXTRA_CHECK_PERMS"; - private static final String KEY_SKIP_IF_SAME_VERSION = - "com.google.android.clockwork.EXTRA_SKIP_IF_SAME_VERSION"; - private static final String KEY_COMPRESSION_ALG = - "com.google.android.clockwork.EXTRA_KEY_COMPRESSION_ALG"; - private static final String KEY_COMPANION_SDK_VERSION = - "com.google.android.clockwork.EXTRA_KEY_COMPANION_SDK_VERSION"; - private static final String KEY_COMPANION_DEVICE_VERSION = - "com.google.android.clockwork.EXTRA_KEY_COMPANION_DEVICE_VERSION"; - private static final String KEY_SHOULD_CHECK_GMS_DEPENDENCY = - "com.google.android.clockwork.EXTRA_KEY_SHOULD_CHECK_GMS_DEPENDENCY"; - private static final String KEY_SKIP_IF_LOWER_VERSION = - "com.google.android.clockwork.EXTRA_SKIP_IF_LOWER_VERSION"; - - public static String getPackageName(Bundle b) { - return b.getString(KEY_PACKAGE_NAME); - } - - public static Bundle setPackageName(Bundle b, String packageName) { - b.putString(KEY_PACKAGE_NAME, packageName); - return b; - } - - public static Uri getAssetUri(Bundle b) { - return b.getParcelable(KEY_ASSET_URI); - } - - public static Uri getPermUri(Bundle b) { - return b.getParcelable(KEY_PERM_URI); - } - - public static boolean checkPerms(Bundle b) { - return b.getBoolean(KEY_CHECK_PERMS); - } - - public static boolean skipIfSameVersion(Bundle b) { - return b.getBoolean(KEY_SKIP_IF_SAME_VERSION); - } - - public static int getCompanionSdkVersion(Bundle b) { - return b.getInt(KEY_COMPANION_SDK_VERSION); - } - - public static int getCompanionDeviceVersion(Bundle b) { - return b.getInt(KEY_COMPANION_DEVICE_VERSION); - } - - public static String getCompressionAlg(Bundle b) { - return b.getString(KEY_COMPRESSION_ALG); - } - - public static int getStartId(Bundle b) { - return b.getInt(KEY_START_ID); - } - - public static boolean skipIfLowerVersion(Bundle b) { - return b.getBoolean(KEY_SKIP_IF_LOWER_VERSION, false); - } - - public static Bundle setStartId(Bundle b, int startId) { - b.putInt(KEY_START_ID, startId); - return b; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java deleted file mode 100644 index 02b9d298db0e..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.packageinstaller.wear; - -import android.annotation.TargetApi; -import android.app.ActivityManager; -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.List; - -import static android.content.pm.PackageManager.PERMISSION_GRANTED; - -public class WearPackageIconProvider extends ContentProvider { - private static final String TAG = "WearPackageIconProvider"; - public static final String AUTHORITY = "com.google.android.packageinstaller.wear.provider"; - - private static final String REQUIRED_PERMISSION = - "com.google.android.permission.INSTALL_WEARABLE_PACKAGES"; - - /** MIME types. */ - public static final String ICON_TYPE = "vnd.android.cursor.item/cw_package_icon"; - - @Override - public boolean onCreate() { - return true; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - throw new UnsupportedOperationException("Query is not supported."); - } - - @Override - public String getType(Uri uri) { - if (uri == null) { - throw new IllegalArgumentException("URI passed in is null."); - } - - if (AUTHORITY.equals(uri.getEncodedAuthority())) { - return ICON_TYPE; - } - return null; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - throw new UnsupportedOperationException("Insert is not supported."); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - if (uri == null) { - throw new IllegalArgumentException("URI passed in is null."); - } - - enforcePermissions(uri); - - if (ICON_TYPE.equals(getType(uri))) { - final File file = WearPackageUtil.getIconFile( - this.getContext().getApplicationContext(), getPackageNameFromUri(uri)); - if (file != null) { - file.delete(); - } - } - - return 0; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException("Update is not supported."); - } - - @Override - public ParcelFileDescriptor openFile( - Uri uri, @SuppressWarnings("unused") String mode) throws FileNotFoundException { - if (uri == null) { - throw new IllegalArgumentException("URI passed in is null."); - } - - enforcePermissions(uri); - - if (ICON_TYPE.equals(getType(uri))) { - final File file = WearPackageUtil.getIconFile( - this.getContext().getApplicationContext(), getPackageNameFromUri(uri)); - if (file != null) { - return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); - } - } - return null; - } - - public static Uri getUriForPackage(final String packageName) { - return Uri.parse("content://" + AUTHORITY + "/icons/" + packageName + ".icon"); - } - - private String getPackageNameFromUri(Uri uri) { - if (uri == null) { - return null; - } - List<String> pathSegments = uri.getPathSegments(); - String packageName = pathSegments.get(pathSegments.size() - 1); - - if (packageName.endsWith(".icon")) { - packageName = packageName.substring(0, packageName.lastIndexOf(".")); - } - return packageName; - } - - /** - * Make sure the calling app is either a system app or the same app or has the right permission. - * @throws SecurityException if the caller has insufficient permissions. - */ - @TargetApi(Build.VERSION_CODES.BASE_1_1) - private void enforcePermissions(Uri uri) { - // Redo some of the permission check in {@link ContentProvider}. Just add an extra check to - // allow System process to access this provider. - Context context = getContext(); - final int pid = Binder.getCallingPid(); - final int uid = Binder.getCallingUid(); - final int myUid = android.os.Process.myUid(); - - if (uid == myUid || isSystemApp(context, pid)) { - return; - } - - if (context.checkPermission(REQUIRED_PERMISSION, pid, uid) == PERMISSION_GRANTED) { - return; - } - - // last chance, check against any uri grants - if (context.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION) - == PERMISSION_GRANTED) { - return; - } - - throw new SecurityException("Permission Denial: reading " - + getClass().getName() + " uri " + uri + " from pid=" + pid - + ", uid=" + uid); - } - - /** - * From the pid of the calling process, figure out whether this is a system app or not. We do - * this by checking the application information corresponding to the pid and then checking if - * FLAG_SYSTEM is set. - */ - @TargetApi(Build.VERSION_CODES.CUPCAKE) - private boolean isSystemApp(Context context, int pid) { - // Get the Activity Manager Object - ActivityManager aManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - // Get the list of running Applications - List<ActivityManager.RunningAppProcessInfo> rapInfoList = - aManager.getRunningAppProcesses(); - for (ActivityManager.RunningAppProcessInfo rapInfo : rapInfoList) { - if (rapInfo.pid == pid) { - try { - PackageInfo pkgInfo = context.getPackageManager().getPackageInfo( - rapInfo.pkgList[0], 0); - if (pkgInfo != null && pkgInfo.applicationInfo != null && - (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - Log.d(TAG, pid + " is a system app."); - return true; - } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Could not find package information.", e); - return false; - } - } - } - return false; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java deleted file mode 100644 index ae0f4ece1c17..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java +++ /dev/null @@ -1,621 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.packageinstaller.wear; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.FeatureInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageInstaller; -import android.content.pm.PackageManager; -import android.content.pm.VersionedPackage; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.ParcelFileDescriptor; -import android.os.PowerManager; -import android.os.Process; -import android.util.ArrayMap; -import android.util.Log; -import android.util.Pair; -import androidx.annotation.Nullable; -import com.android.packageinstaller.DeviceUtils; -import com.android.packageinstaller.PackageUtil; -import com.android.packageinstaller.R; -import com.android.packageinstaller.common.EventResultPersister; -import com.android.packageinstaller.common.UninstallEventReceiver; -import java.io.File; -import java.io.FileNotFoundException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Service that will install/uninstall packages. It will check for permissions and features as well. - * - * ----------- - * - * Debugging information: - * - * Install Action example: - * adb shell am startservice -a com.android.packageinstaller.wear.INSTALL_PACKAGE \ - * -d package://com.google.android.gms \ - * --eu com.google.android.clockwork.EXTRA_ASSET_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/wearable/com.google.android.gms/apk \ - * --es android.intent.extra.INSTALLER_PACKAGE_NAME com.google.android.gms \ - * --ez com.google.android.clockwork.EXTRA_CHECK_PERMS false \ - * --eu com.google.android.clockwork.EXTRA_PERM_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/permissions \ - * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService - * - * Uninstall Action example: - * adb shell am startservice -a com.android.packageinstaller.wear.UNINSTALL_PACKAGE \ - * -d package://com.google.android.gms \ - * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService - * - * Retry GMS: - * adb shell am startservice -a com.android.packageinstaller.wear.RETRY_GMS \ - * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService - */ -public class WearPackageInstallerService extends Service - implements EventResultPersister.EventResultObserver { - private static final String TAG = "WearPkgInstallerService"; - - private static final String WEAR_APPS_CHANNEL = "wear_app_install_uninstall"; - private static final String BROADCAST_ACTION = - "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT"; - - private final int START_INSTALL = 1; - private final int START_UNINSTALL = 2; - - private int mInstallNotificationId = 1; - private final Map<String, Integer> mNotifIdMap = new ArrayMap<>(); - private final Map<Integer, UninstallParams> mServiceIdToParams = new HashMap<>(); - - private class UninstallParams { - public String mPackageName; - public PowerManager.WakeLock mLock; - - UninstallParams(String packageName, PowerManager.WakeLock lock) { - mPackageName = packageName; - mLock = lock; - } - } - - private final class ServiceHandler extends Handler { - public ServiceHandler(Looper looper) { - super(looper); - } - - public void handleMessage(Message msg) { - switch (msg.what) { - case START_INSTALL: - installPackage(msg.getData()); - break; - case START_UNINSTALL: - uninstallPackage(msg.getData()); - break; - } - } - } - private ServiceHandler mServiceHandler; - private NotificationChannel mNotificationChannel; - private static volatile PowerManager.WakeLock lockStatic = null; - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onCreate() { - super.onCreate(); - HandlerThread thread = new HandlerThread("PackageInstallerThread", - Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - - mServiceHandler = new ServiceHandler(thread.getLooper()); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (!DeviceUtils.isWear(this)) { - Log.w(TAG, "Not running on wearable."); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - - if (intent == null) { - Log.w(TAG, "Got null intent."); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Got install/uninstall request " + intent); - } - - Uri packageUri = intent.getData(); - if (packageUri == null) { - Log.e(TAG, "No package URI in intent"); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - - final String packageName = WearPackageUtil.getSanitizedPackageName(packageUri); - if (packageName == null) { - Log.e(TAG, "Invalid package name in URI (expected package:<pkgName>): " + packageUri); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - - PowerManager.WakeLock lock = getLock(this.getApplicationContext()); - if (!lock.isHeld()) { - lock.acquire(); - } - - Bundle intentBundle = intent.getExtras(); - if (intentBundle == null) { - intentBundle = new Bundle(); - } - WearPackageArgs.setStartId(intentBundle, startId); - WearPackageArgs.setPackageName(intentBundle, packageName); - Message msg; - String notifTitle; - if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) { - msg = mServiceHandler.obtainMessage(START_INSTALL); - notifTitle = getString(R.string.installing); - } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) { - msg = mServiceHandler.obtainMessage(START_UNINSTALL); - notifTitle = getString(R.string.uninstalling); - } else { - Log.e(TAG, "Unknown action : " + intent.getAction()); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - Pair<Integer, Notification> notifPair = buildNotification(packageName, notifTitle); - startForeground(notifPair.first, notifPair.second); - msg.setData(intentBundle); - mServiceHandler.sendMessage(msg); - return START_NOT_STICKY; - } - - private void installPackage(Bundle argsBundle) { - int startId = WearPackageArgs.getStartId(argsBundle); - final String packageName = WearPackageArgs.getPackageName(argsBundle); - final Uri assetUri = WearPackageArgs.getAssetUri(argsBundle); - final Uri permUri = WearPackageArgs.getPermUri(argsBundle); - boolean checkPerms = WearPackageArgs.checkPerms(argsBundle); - boolean skipIfSameVersion = WearPackageArgs.skipIfSameVersion(argsBundle); - int companionSdkVersion = WearPackageArgs.getCompanionSdkVersion(argsBundle); - int companionDeviceVersion = WearPackageArgs.getCompanionDeviceVersion(argsBundle); - String compressionAlg = WearPackageArgs.getCompressionAlg(argsBundle); - boolean skipIfLowerVersion = WearPackageArgs.skipIfLowerVersion(argsBundle); - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Installing package: " + packageName + ", assetUri: " + assetUri + - ",permUri: " + permUri + ", startId: " + startId + ", checkPerms: " + - checkPerms + ", skipIfSameVersion: " + skipIfSameVersion + - ", compressionAlg: " + compressionAlg + ", companionSdkVersion: " + - companionSdkVersion + ", companionDeviceVersion: " + companionDeviceVersion + - ", skipIfLowerVersion: " + skipIfLowerVersion); - } - final PackageManager pm = getPackageManager(); - File tempFile = null; - PowerManager.WakeLock lock = getLock(this.getApplicationContext()); - boolean messageSent = false; - try { - PackageInfo existingPkgInfo = null; - try { - existingPkgInfo = pm.getPackageInfo(packageName, - PackageManager.MATCH_ANY_USER | PackageManager.GET_PERMISSIONS); - if (existingPkgInfo != null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Replacing package:" + packageName); - } - } - } catch (PackageManager.NameNotFoundException e) { - // Ignore this exception. We could not find the package, will treat as a new - // installation. - } - // TODO(28021618): This was left as a temp file due to the fact that this code is being - // deprecated and that we need the bare minimum to continue working moving forward - // If this code is used as reference, this permission logic might want to be - // reworked to use a stream instead of a file so that we don't need to write a - // file at all. Note that there might be some trickiness with opening a stream - // for multiple users. - ParcelFileDescriptor parcelFd = getContentResolver() - .openFileDescriptor(assetUri, "r"); - tempFile = WearPackageUtil.getFileFromFd(WearPackageInstallerService.this, - parcelFd, packageName, compressionAlg); - if (tempFile == null) { - Log.e(TAG, "Could not create a temp file from FD for " + packageName); - return; - } - PackageInfo pkgInfo = PackageUtil.getPackageInfo(this, tempFile, - PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS); - if (pkgInfo == null) { - Log.e(TAG, "Could not parse apk information for " + packageName); - return; - } - - if (!pkgInfo.packageName.equals(packageName)) { - Log.e(TAG, "Wearable Package Name has to match what is provided for " + - packageName); - return; - } - - ApplicationInfo appInfo = pkgInfo.applicationInfo; - appInfo.sourceDir = tempFile.getPath(); - appInfo.publicSourceDir = tempFile.getPath(); - getLabelAndUpdateNotification(packageName, - getString(R.string.installing_app, appInfo.loadLabel(pm))); - - List<String> wearablePerms = Arrays.asList(pkgInfo.requestedPermissions); - - // Log if the installed pkg has a higher version number. - if (existingPkgInfo != null) { - long longVersionCode = pkgInfo.getLongVersionCode(); - if (existingPkgInfo.getLongVersionCode() == longVersionCode) { - if (skipIfSameVersion) { - Log.w(TAG, "Version number (" + longVersionCode + - ") of new app is equal to existing app for " + packageName + - "; not installing due to versionCheck"); - return; - } else { - Log.w(TAG, "Version number of new app (" + longVersionCode + - ") is equal to existing app for " + packageName); - } - } else if (existingPkgInfo.getLongVersionCode() > longVersionCode) { - if (skipIfLowerVersion) { - // Starting in Feldspar, we are not going to allow downgrades of any app. - Log.w(TAG, "Version number of new app (" + longVersionCode + - ") is lower than existing app ( " - + existingPkgInfo.getLongVersionCode() + - ") for " + packageName + "; not installing due to versionCheck"); - return; - } else { - Log.w(TAG, "Version number of new app (" + longVersionCode + - ") is lower than existing app ( " - + existingPkgInfo.getLongVersionCode() + ") for " + packageName); - } - } - - // Following the Android Phone model, we should only check for permissions for any - // newly defined perms. - if (existingPkgInfo.requestedPermissions != null) { - for (int i = 0; i < existingPkgInfo.requestedPermissions.length; ++i) { - // If the permission is granted, then we will not ask to request it again. - if ((existingPkgInfo.requestedPermissionsFlags[i] & - PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, existingPkgInfo.requestedPermissions[i] + - " is already granted for " + packageName); - } - wearablePerms.remove(existingPkgInfo.requestedPermissions[i]); - } - } - } - } - - // Check that the wearable has all the features. - boolean hasAllFeatures = true; - for (FeatureInfo feature : pkgInfo.reqFeatures) { - if (feature.name != null && !pm.hasSystemFeature(feature.name) && - (feature.flags & FeatureInfo.FLAG_REQUIRED) != 0) { - Log.e(TAG, "Wearable does not have required feature: " + feature + - " for " + packageName); - hasAllFeatures = false; - } - } - - if (!hasAllFeatures) { - return; - } - - // Check permissions on both the new wearable package and also on the already installed - // wearable package. - // If the app is targeting API level 23, we will also start a service in ClockworkHome - // which will ultimately prompt the user to accept/reject permissions. - if (checkPerms && !checkPermissions(pkgInfo, companionSdkVersion, - companionDeviceVersion, permUri, wearablePerms, tempFile)) { - Log.w(TAG, "Wearable does not have enough permissions."); - return; - } - - // Finally install the package. - ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(assetUri, "r"); - PackageInstallerFactory.getPackageInstaller(this).install(packageName, fd, - new PackageInstallListener(this, lock, startId, packageName)); - - messageSent = true; - Log.i(TAG, "Sent installation request for " + packageName); - } catch (FileNotFoundException e) { - Log.e(TAG, "Could not find the file with URI " + assetUri, e); - } finally { - if (!messageSent) { - // Some error happened. If the message has been sent, we can wait for the observer - // which will finish the service. - if (tempFile != null) { - tempFile.delete(); - } - finishService(lock, startId); - } - } - } - - // TODO: This was left using the old PackageManager API due to the fact that this code is being - // deprecated and that we need the bare minimum to continue working moving forward - // If this code is used as reference, this logic should be reworked to use the new - // PackageInstaller APIs similar to how installPackage was reworked - private void uninstallPackage(Bundle argsBundle) { - int startId = WearPackageArgs.getStartId(argsBundle); - final String packageName = WearPackageArgs.getPackageName(argsBundle); - - PowerManager.WakeLock lock = getLock(this.getApplicationContext()); - - UninstallParams params = new UninstallParams(packageName, lock); - mServiceIdToParams.put(startId, params); - - final PackageManager pm = getPackageManager(); - try { - PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0); - getLabelAndUpdateNotification(packageName, - getString(R.string.uninstalling_app, pkgInfo.applicationInfo.loadLabel(pm))); - - int uninstallId = UninstallEventReceiver.addObserver(this, - EventResultPersister.GENERATE_NEW_ID, this); - - Intent broadcastIntent = new Intent(BROADCAST_ACTION); - broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); - broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId); - broadcastIntent.putExtra(EventResultPersister.EXTRA_SERVICE_ID, startId); - broadcastIntent.setPackage(getPackageName()); - - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId, - broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT - | PendingIntent.FLAG_MUTABLE); - - // Found package, send uninstall request. - pm.getPackageInstaller().uninstall( - new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), - PackageManager.DELETE_ALL_USERS, - pendingIntent.getIntentSender()); - - Log.i(TAG, "Sent delete request for " + packageName); - } catch (IllegalArgumentException | PackageManager.NameNotFoundException e) { - // Couldn't find the package, no need to call uninstall. - Log.w(TAG, "Could not find package, not deleting " + packageName, e); - finishService(lock, startId); - } catch (EventResultPersister.OutOfIdsException e) { - Log.e(TAG, "Fails to start uninstall", e); - finishService(lock, startId); - } - } - - @Override - public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) { - if (mServiceIdToParams.containsKey(serviceId)) { - UninstallParams params = mServiceIdToParams.get(serviceId); - try { - if (status == PackageInstaller.STATUS_SUCCESS) { - Log.i(TAG, "Package " + params.mPackageName + " was uninstalled."); - } else { - Log.e(TAG, "Package uninstall failed " + params.mPackageName - + ", returnCode " + legacyStatus); - } - } finally { - finishService(params.mLock, serviceId); - } - } - } - - private boolean checkPermissions(PackageInfo pkgInfo, int companionSdkVersion, - int companionDeviceVersion, Uri permUri, List<String> wearablePermissions, - File apkFile) { - // Assumption: We are running on Android O. - // If the Phone App is targeting M, all permissions may not have been granted to the phone - // app. If the Wear App is then not targeting M, there may be permissions that are not - // granted on the Phone app (by the user) right now and we cannot just grant it for the Wear - // app. - if (pkgInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M) { - // Install the app if Wear App is ready for the new perms model. - return true; - } - - if (!doesWearHaveUngrantedPerms(pkgInfo.packageName, permUri, wearablePermissions)) { - // All permissions requested by the watch are already granted on the phone, no need - // to do anything. - return true; - } - - // Log an error if Wear is targeting < 23 and phone is targeting >= 23. - if (companionSdkVersion == 0 || companionSdkVersion >= Build.VERSION_CODES.M) { - Log.e(TAG, "MNC: Wear app's targetSdkVersion should be at least 23, if " - + "phone app is targeting at least 23, will continue."); - } - - return false; - } - - /** - * Given a {@string packageName} corresponding to a phone app, query the provider for all the - * perms that are granted. - * - * @return true if the Wear App has any perms that have not been granted yet on the phone side. - * @return true if there is any error cases. - */ - private boolean doesWearHaveUngrantedPerms(String packageName, Uri permUri, - List<String> wearablePermissions) { - if (permUri == null) { - Log.e(TAG, "Permission URI is null"); - // Pretend there is an ungranted permission to avoid installing for error cases. - return true; - } - Cursor permCursor = getContentResolver().query(permUri, null, null, null, null); - if (permCursor == null) { - Log.e(TAG, "Could not get the cursor for the permissions"); - // Pretend there is an ungranted permission to avoid installing for error cases. - return true; - } - - Set<String> grantedPerms = new HashSet<>(); - Set<String> ungrantedPerms = new HashSet<>(); - while(permCursor.moveToNext()) { - // Make sure that the MatrixCursor returned by the ContentProvider has 2 columns and - // verify their types. - if (permCursor.getColumnCount() == 2 - && Cursor.FIELD_TYPE_STRING == permCursor.getType(0) - && Cursor.FIELD_TYPE_INTEGER == permCursor.getType(1)) { - String perm = permCursor.getString(0); - Integer granted = permCursor.getInt(1); - if (granted == 1) { - grantedPerms.add(perm); - } else { - ungrantedPerms.add(perm); - } - } - } - permCursor.close(); - - boolean hasUngrantedPerm = false; - for (String wearablePerm : wearablePermissions) { - if (!grantedPerms.contains(wearablePerm)) { - hasUngrantedPerm = true; - if (!ungrantedPerms.contains(wearablePerm)) { - // This is an error condition. This means that the wearable has permissions that - // are not even declared in its host app. This is a developer error. - Log.e(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm - + "\" that is not defined in the host application's manifest."); - } else { - Log.w(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm + - "\" that is not granted in the host application."); - } - } - } - return hasUngrantedPerm; - } - - /** Finishes the service after fulfilling obligation to call startForeground. */ - private void finishServiceEarly(int startId) { - Pair<Integer, Notification> notifPair = buildNotification( - getApplicationContext().getPackageName(), ""); - startForeground(notifPair.first, notifPair.second); - finishService(null, startId); - } - - private void finishService(PowerManager.WakeLock lock, int startId) { - if (lock != null && lock.isHeld()) { - lock.release(); - } - stopSelf(startId); - } - - private synchronized PowerManager.WakeLock getLock(Context context) { - if (lockStatic == null) { - PowerManager mgr = - (PowerManager) context.getSystemService(Context.POWER_SERVICE); - lockStatic = mgr.newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, context.getClass().getSimpleName()); - lockStatic.setReferenceCounted(true); - } - return lockStatic; - } - - private class PackageInstallListener implements PackageInstallerImpl.InstallListener { - private Context mContext; - private PowerManager.WakeLock mWakeLock; - private int mStartId; - private String mApplicationPackageName; - private PackageInstallListener(Context context, PowerManager.WakeLock wakeLock, - int startId, String applicationPackageName) { - mContext = context; - mWakeLock = wakeLock; - mStartId = startId; - mApplicationPackageName = applicationPackageName; - } - - @Override - public void installBeginning() { - Log.i(TAG, "Package " + mApplicationPackageName + " is being installed."); - } - - @Override - public void installSucceeded() { - try { - Log.i(TAG, "Package " + mApplicationPackageName + " was installed."); - - // Delete tempFile from the file system. - File tempFile = WearPackageUtil.getTemporaryFile(mContext, mApplicationPackageName); - if (tempFile != null) { - tempFile.delete(); - } - } finally { - finishService(mWakeLock, mStartId); - } - } - - @Override - public void installFailed(int errorCode, String errorDesc) { - Log.e(TAG, "Package install failed " + mApplicationPackageName - + ", errorCode " + errorCode); - finishService(mWakeLock, mStartId); - } - } - - private synchronized Pair<Integer, Notification> buildNotification(final String packageName, - final String title) { - int notifId; - if (mNotifIdMap.containsKey(packageName)) { - notifId = mNotifIdMap.get(packageName); - } else { - notifId = mInstallNotificationId++; - mNotifIdMap.put(packageName, notifId); - } - - if (mNotificationChannel == null) { - mNotificationChannel = new NotificationChannel(WEAR_APPS_CHANNEL, - getString(R.string.wear_app_channel), NotificationManager.IMPORTANCE_MIN); - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(mNotificationChannel); - } - return new Pair<>(notifId, new Notification.Builder(this, WEAR_APPS_CHANNEL) - .setSmallIcon(R.drawable.ic_file_download) - .setContentTitle(title) - .build()); - } - - private void getLabelAndUpdateNotification(String packageName, String title) { - // Update notification since we have a label now. - NotificationManager notificationManager = getSystemService(NotificationManager.class); - Pair<Integer, Notification> notifPair = buildNotification(packageName, title); - notificationManager.notify(notifPair.first, notifPair.second); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java deleted file mode 100644 index 6a9145db9a06..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.packageinstaller.wear; - -import android.content.Context; -import android.net.Uri; -import android.os.ParcelFileDescriptor; -import android.system.ErrnoException; -import android.system.Os; -import android.text.TextUtils; -import android.util.Log; - -import org.tukaani.xz.LZMAInputStream; -import org.tukaani.xz.XZInputStream; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -public class WearPackageUtil { - private static final String TAG = "WearablePkgInstaller"; - - private static final String COMPRESSION_LZMA = "lzma"; - private static final String COMPRESSION_XZ = "xz"; - - public static File getTemporaryFile(Context context, String packageName) { - try { - File newFileDir = new File(context.getFilesDir(), "tmp"); - newFileDir.mkdirs(); - Os.chmod(newFileDir.getAbsolutePath(), 0771); - File newFile = new File(newFileDir, packageName + ".apk"); - return newFile; - } catch (ErrnoException e) { - Log.e(TAG, "Failed to open.", e); - return null; - } - } - - public static File getIconFile(final Context context, final String packageName) { - try { - File newFileDir = new File(context.getFilesDir(), "images/icons"); - newFileDir.mkdirs(); - Os.chmod(newFileDir.getAbsolutePath(), 0771); - return new File(newFileDir, packageName + ".icon"); - } catch (ErrnoException e) { - Log.e(TAG, "Failed to open.", e); - return null; - } - } - - /** - * In order to make sure that the Wearable Asset Manager has a reasonable apk that can be used - * by the PackageManager, we will parse it before sending it to the PackageManager. - * Unfortunately, ParsingPackageUtils needs a file to parse. So, we have to temporarily convert - * the fd to a File. - * - * @param context - * @param fd FileDescriptor to convert to File - * @param packageName Name of package, will define the name of the file - * @param compressionAlg Can be null. For ALT mode the APK will be compressed. We will - * decompress it here - */ - public static File getFileFromFd(Context context, ParcelFileDescriptor fd, - String packageName, String compressionAlg) { - File newFile = getTemporaryFile(context, packageName); - if (fd == null || fd.getFileDescriptor() == null) { - return null; - } - InputStream fr = new ParcelFileDescriptor.AutoCloseInputStream(fd); - try { - if (TextUtils.equals(compressionAlg, COMPRESSION_XZ)) { - fr = new XZInputStream(fr); - } else if (TextUtils.equals(compressionAlg, COMPRESSION_LZMA)) { - fr = new LZMAInputStream(fr); - } - } catch (IOException e) { - Log.e(TAG, "Compression was set to " + compressionAlg + ", but could not decode ", e); - return null; - } - - int nRead; - byte[] data = new byte[1024]; - try { - final FileOutputStream fo = new FileOutputStream(newFile); - while ((nRead = fr.read(data, 0, data.length)) != -1) { - fo.write(data, 0, nRead); - } - fo.flush(); - fo.close(); - Os.chmod(newFile.getAbsolutePath(), 0644); - return newFile; - } catch (IOException e) { - Log.e(TAG, "Reading from Asset FD or writing to temp file failed ", e); - return null; - } catch (ErrnoException e) { - Log.e(TAG, "Could not set permissions on file ", e); - return null; - } finally { - try { - fr.close(); - } catch (IOException e) { - Log.e(TAG, "Failed to close the file from FD ", e); - } - } - } - - /** - * @return com.google.com from expected formats like - * Uri: package:com.google.com, package:/com.google.com, package://com.google.com - */ - public static String getSanitizedPackageName(Uri packageUri) { - String packageName = packageUri.getEncodedSchemeSpecificPart(); - if (packageName != null) { - return packageName.replaceAll("^/+", ""); - } - return packageName; - } -} diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 0b36757868c7..d6cbf2a0f581 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -88,6 +88,7 @@ java_defaults { aconfig_declarations { name: "settingslib_media_flags", package: "com.android.settingslib.media.flags", + container: "system", srcs: [ "aconfig/settingslib_media_flag_declarations.aconfig", ], @@ -101,6 +102,7 @@ java_aconfig_library { aconfig_declarations { name: "settingslib_flags", package: "com.android.settingslib.flags", + container: "system", srcs: [ "aconfig/settingslib.aconfig", ], diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt index 8b546b4de069..791893b3c056 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt @@ -23,7 +23,6 @@ import com.android.settingslib.spa.widget.ui.SettingsSwitch @Composable fun TwoTargetSwitchPreference( model: SwitchPreferenceModel, - icon: @Composable (() -> Unit)? = null, primaryEnabled: () -> Boolean = { true }, primaryOnClick: (() -> Unit)?, ) { @@ -33,7 +32,7 @@ fun TwoTargetSwitchPreference( summary = model.summary, primaryEnabled = primaryEnabled, primaryOnClick = primaryOnClick, - icon = icon, + icon = model.icon, ) { SettingsSwitch( checked = model.checked(), diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItem.kt index 5c2d7701fd6f..1f7122e82c30 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItem.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItem.kt @@ -33,11 +33,13 @@ fun <T : AppRecord> AppListItemModel<T>.AppListTwoTargetSwitchItem( model = object : SwitchPreferenceModel { override val title = label override val summary = this@AppListTwoTargetSwitchItem.summary + override val icon = @Composable { + AppIcon(record.app, SettingsDimension.appIconItemSize) + } override val checked = checked override val changeable = changeable override val onCheckedChange = onCheckedChange }, - icon = { AppIcon(record.app, SettingsDimension.appIconItemSize) }, primaryOnClick = onClick, ) } 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 87cd2b844a2b..c9934adfad22 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 @@ -52,6 +52,8 @@ internal class RestrictedSwitchPreferenceModel( checked = model.checked, ) + override val icon = model.icon + override val checked = when (restrictedMode) { null -> ({ null }) is NoRestricted -> model.checked diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedTwoTargetSwitchPreference.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedTwoTargetSwitchPreference.kt index e100773b2358..1bed73365e80 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedTwoTargetSwitchPreference.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedTwoTargetSwitchPreference.kt @@ -29,14 +29,12 @@ import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitc @Composable fun RestrictedTwoTargetSwitchPreference( model: SwitchPreferenceModel, - icon: @Composable (() -> Unit)? = null, restrictions: Restrictions, primaryEnabled: () -> Boolean = { true }, primaryOnClick: (() -> Unit)?, ) { RestrictedTwoTargetSwitchPreference( model = model, - icon = icon, primaryEnabled = primaryEnabled, primaryOnClick = primaryOnClick, restrictions = restrictions, @@ -48,21 +46,19 @@ fun RestrictedTwoTargetSwitchPreference( @Composable internal fun RestrictedTwoTargetSwitchPreference( model: SwitchPreferenceModel, - icon: @Composable (() -> Unit)? = null, primaryEnabled: () -> Boolean = { true }, primaryOnClick: (() -> Unit)?, restrictions: Restrictions, restrictionsProviderFactory: RestrictionsProviderFactory, ) { if (restrictions.isEmpty()) { - TwoTargetSwitchPreference(model, icon, primaryEnabled, primaryOnClick) + TwoTargetSwitchPreference(model, primaryEnabled, primaryOnClick) return } val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value RestrictedSwitchWrapper(model, restrictedMode) { restrictedModel -> TwoTargetSwitchPreference( model = restrictedModel, - icon = icon, primaryEnabled = restrictedMode.restrictEnabled(primaryEnabled), primaryOnClick = restrictedMode.restrictOnClick(primaryOnClick), ) diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 54c5a14702f6..e09ab0086451 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -1,4 +1,5 @@ package: "com.android.settingslib.flags" +container: "system" flag { name: "new_status_bar_icons" diff --git a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig index f3e537b33230..4d70aec9fa5c 100644 --- a/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig +++ b/packages/SettingsLib/aconfig/settingslib_media_flag_declarations.aconfig @@ -1,4 +1,5 @@ package: "com.android.settingslib.media.flags" +container: "system" flag { name: "use_media_router2_for_info_media_manager" diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java b/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java index 6b833cc68b93..0282f03e0b98 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java @@ -16,6 +16,7 @@ package com.android.settingslib.applications; +import android.annotation.NonNull; import android.app.usage.StorageStats; import android.app.usage.StorageStatsManager; import android.content.Context; @@ -25,6 +26,7 @@ import android.os.UserHandle; import androidx.annotation.VisibleForTesting; import java.io.IOException; +import java.util.UUID; /** * StorageStatsSource wraps the StorageStatsManager for testability purposes. @@ -59,6 +61,10 @@ public class StorageStatsSource { return mStorageStatsManager.getCacheQuotaBytes(volumeUuid, uid); } + public long getTotalBytes(@NonNull UUID storageUuid) throws IOException { + return mStorageStatsManager.getTotalBytes(storageUuid); + } + /** * Static class that provides methods for querying the amount of external storage available as * well as breaking it up into several media types. diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 57fcc7462a65..a906875e11ef 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -3,10 +3,13 @@ package com.android.settingslib.bluetooth; import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED; import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -30,6 +33,7 @@ import androidx.annotation.WorkerThread; import androidx.core.graphics.drawable.IconCompat; import com.android.settingslib.R; +import com.android.settingslib.flags.Flags; import com.android.settingslib.widget.AdaptiveIcon; import com.android.settingslib.widget.AdaptiveOutlineDrawable; @@ -46,14 +50,14 @@ public class BluetoothUtils { private static final String TAG = "BluetoothUtils"; public static final boolean V = false; // verbose logging - public static final boolean D = true; // regular logging + public static final boolean D = true; // regular logging public static final int META_INT_ERROR = -1; public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled"; private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH"; - private static final Set<String> EXCLUSIVE_MANAGERS = ImmutableSet.of( - "com.google.android.gms.dck"); + private static final Set<String> EXCLUSIVE_MANAGERS = + ImmutableSet.of("com.google.android.gms.dck"); private static ErrorListener sErrorListener; @@ -89,23 +93,23 @@ public class BluetoothUtils { /** * @param context to access resources from * @param cachedDevice to get class from - * @return pair containing the drawable and the description of the Bluetooth class - * of the device. + * @return pair containing the drawable and the description of the Bluetooth class of the + * device. */ - public static Pair<Drawable, String> getBtClassDrawableWithDescription(Context context, - CachedBluetoothDevice cachedDevice) { + public static Pair<Drawable, String> getBtClassDrawableWithDescription( + Context context, CachedBluetoothDevice cachedDevice) { BluetoothClass btClass = cachedDevice.getBtClass(); if (btClass != null) { switch (btClass.getMajorDeviceClass()) { case BluetoothClass.Device.Major.COMPUTER: - return new Pair<>(getBluetoothDrawable(context, - com.android.internal.R.drawable.ic_bt_laptop), + return new Pair<>( + getBluetoothDrawable( + context, com.android.internal.R.drawable.ic_bt_laptop), context.getString(R.string.bluetooth_talkback_computer)); case BluetoothClass.Device.Major.PHONE: return new Pair<>( - getBluetoothDrawable(context, - com.android.internal.R.drawable.ic_phone), + getBluetoothDrawable(context, com.android.internal.R.drawable.ic_phone), context.getString(R.string.bluetooth_talkback_phone)); case BluetoothClass.Device.Major.PERIPHERAL: @@ -115,8 +119,8 @@ public class BluetoothUtils { case BluetoothClass.Device.Major.IMAGING: return new Pair<>( - getBluetoothDrawable(context, - com.android.internal.R.drawable.ic_settings_print), + getBluetoothDrawable( + context, com.android.internal.R.drawable.ic_settings_print), context.getString(R.string.bluetooth_talkback_imaging)); default: @@ -125,8 +129,9 @@ public class BluetoothUtils { } if (cachedDevice.isHearingAidDevice()) { - return new Pair<>(getBluetoothDrawable(context, - com.android.internal.R.drawable.ic_bt_hearing_aid), + return new Pair<>( + getBluetoothDrawable( + context, com.android.internal.R.drawable.ic_bt_hearing_aid), context.getString(R.string.bluetooth_talkback_hearing_aids)); } @@ -138,7 +143,8 @@ public class BluetoothUtils { // The device should show hearing aid icon if it contains any hearing aid related // profiles if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) { - return new Pair<>(getBluetoothDrawable(context, profileResId), + return new Pair<>( + getBluetoothDrawable(context, profileResId), context.getString(R.string.bluetooth_talkback_hearing_aids)); } if (resId == 0) { @@ -153,42 +159,40 @@ public class BluetoothUtils { if (btClass != null) { if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) { return new Pair<>( - getBluetoothDrawable(context, - com.android.internal.R.drawable.ic_bt_headset_hfp), + getBluetoothDrawable( + context, com.android.internal.R.drawable.ic_bt_headset_hfp), context.getString(R.string.bluetooth_talkback_headset)); } if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) { return new Pair<>( - getBluetoothDrawable(context, - com.android.internal.R.drawable.ic_bt_headphones_a2dp), + getBluetoothDrawable( + context, com.android.internal.R.drawable.ic_bt_headphones_a2dp), context.getString(R.string.bluetooth_talkback_headphone)); } } return new Pair<>( - getBluetoothDrawable(context, - com.android.internal.R.drawable.ic_settings_bluetooth).mutate(), + getBluetoothDrawable(context, com.android.internal.R.drawable.ic_settings_bluetooth) + .mutate(), context.getString(R.string.bluetooth_talkback_bluetooth)); } - /** - * Get bluetooth drawable by {@code resId} - */ + /** Get bluetooth drawable by {@code resId} */ public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) { return context.getDrawable(resId); } - /** - * Get colorful bluetooth icon with description - */ - public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(Context context, - CachedBluetoothDevice cachedDevice) { + /** Get colorful bluetooth icon with description */ + public static Pair<Drawable, String> getBtRainbowDrawableWithDescription( + Context context, CachedBluetoothDevice cachedDevice) { final Resources resources = context.getResources(); - final Pair<Drawable, String> pair = BluetoothUtils.getBtDrawableWithDescription(context, - cachedDevice); + final Pair<Drawable, String> pair = + BluetoothUtils.getBtDrawableWithDescription(context, cachedDevice); if (pair.first instanceof BitmapDrawable) { - return new Pair<>(new AdaptiveOutlineDrawable( - resources, ((BitmapDrawable) pair.first).getBitmap()), pair.second); + return new Pair<>( + new AdaptiveOutlineDrawable( + resources, ((BitmapDrawable) pair.first).getBitmap()), + pair.second); } int hashCode; @@ -198,15 +202,12 @@ public class BluetoothUtils { hashCode = cachedDevice.getAddress().hashCode(); } - return new Pair<>(buildBtRainbowDrawable(context, - pair.first, hashCode), pair.second); + return new Pair<>(buildBtRainbowDrawable(context, pair.first, hashCode), pair.second); } - /** - * Build Bluetooth device icon with rainbow - */ - private static Drawable buildBtRainbowDrawable(Context context, Drawable drawable, - int hashCode) { + /** Build Bluetooth device icon with rainbow */ + private static Drawable buildBtRainbowDrawable( + Context context, Drawable drawable, int hashCode) { final Resources resources = context.getResources(); // Deal with normal headset @@ -222,38 +223,37 @@ public class BluetoothUtils { return adaptiveIcon; } - /** - * Get bluetooth icon with description - */ - public static Pair<Drawable, String> getBtDrawableWithDescription(Context context, - CachedBluetoothDevice cachedDevice) { - final Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription( - context, cachedDevice); + /** Get bluetooth icon with description */ + public static Pair<Drawable, String> getBtDrawableWithDescription( + Context context, CachedBluetoothDevice cachedDevice) { + final Pair<Drawable, String> pair = + BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice); final BluetoothDevice bluetoothDevice = cachedDevice.getDevice(); - final int iconSize = context.getResources().getDimensionPixelSize( - R.dimen.bt_nearby_icon_size); + final int iconSize = + context.getResources().getDimensionPixelSize(R.dimen.bt_nearby_icon_size); final Resources resources = context.getResources(); // Deal with advanced device icon if (isAdvancedDetailsHeader(bluetoothDevice)) { - final Uri iconUri = getUriMetaData(bluetoothDevice, - BluetoothDevice.METADATA_MAIN_ICON); + final Uri iconUri = getUriMetaData(bluetoothDevice, BluetoothDevice.METADATA_MAIN_ICON); if (iconUri != null) { try { - context.getContentResolver().takePersistableUriPermission(iconUri, - Intent.FLAG_GRANT_READ_URI_PERMISSION); + context.getContentResolver() + .takePersistableUriPermission( + iconUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } catch (SecurityException e) { Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e); } try { - final Bitmap bitmap = MediaStore.Images.Media.getBitmap( - context.getContentResolver(), iconUri); + final Bitmap bitmap = + MediaStore.Images.Media.getBitmap( + context.getContentResolver(), iconUri); if (bitmap != null) { - final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, - iconSize, false); + final Bitmap resizedBitmap = + Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false); bitmap.recycle(); - return new Pair<>(new BitmapDrawable(resources, - resizedBitmap), pair.second); + return new Pair<>( + new BitmapDrawable(resources, resizedBitmap), pair.second); } } catch (IOException e) { Log.e(TAG, "Failed to get drawable for: " + iconUri, e); @@ -280,8 +280,8 @@ public class BluetoothUtils { return true; } // The metadata is for Android S - String deviceType = getStringMetaData(bluetoothDevice, - BluetoothDevice.METADATA_DEVICE_TYPE); + String deviceType = + getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET) || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH) || TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT) @@ -306,8 +306,8 @@ public class BluetoothUtils { return true; } // The metadata is for Android S - String deviceType = getStringMetaData(bluetoothDevice, - BluetoothDevice.METADATA_DEVICE_TYPE); + String deviceType = + getStringMetaData(bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE); if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) { Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device "); return true; @@ -321,15 +321,15 @@ public class BluetoothUtils { * @param device Must be one of the public constants in {@link BluetoothClass.Device} * @return true if device class matches, false otherwise. */ - public static boolean isDeviceClassMatched(@NonNull BluetoothDevice bluetoothDevice, - int device) { + public static boolean isDeviceClassMatched( + @NonNull BluetoothDevice bluetoothDevice, int device) { final BluetoothClass bluetoothClass = bluetoothDevice.getBluetoothClass(); return bluetoothClass != null && bluetoothClass.getDeviceClass() == device; } private static boolean isAdvancedHeaderEnabled() { - if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, - true)) { + if (!DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, true)) { Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false"); return false; } @@ -345,9 +345,7 @@ public class BluetoothUtils { return false; } - /** - * Create an Icon pointing to a drawable. - */ + /** Create an Icon pointing to a drawable. */ public static IconCompat createIconWithDrawable(Drawable drawable) { Bitmap bitmap; if (drawable instanceof BitmapDrawable) { @@ -355,19 +353,15 @@ public class BluetoothUtils { } else { final int width = drawable.getIntrinsicWidth(); final int height = drawable.getIntrinsicHeight(); - bitmap = createBitmap(drawable, - width > 0 ? width : 1, - height > 0 ? height : 1); + bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1); } return IconCompat.createWithBitmap(bitmap); } - /** - * Build device icon with advanced outline - */ + /** Build device icon with advanced outline */ public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) { - final int iconSize = context.getResources().getDimensionPixelSize( - R.dimen.advanced_icon_size); + final int iconSize = + context.getResources().getDimensionPixelSize(R.dimen.advanced_icon_size); final Resources resources = context.getResources(); Bitmap bitmap = null; @@ -376,14 +370,12 @@ public class BluetoothUtils { } else { final int width = drawable.getIntrinsicWidth(); final int height = drawable.getIntrinsicHeight(); - bitmap = createBitmap(drawable, - width > 0 ? width : 1, - height > 0 ? height : 1); + bitmap = createBitmap(drawable, width > 0 ? width : 1, height > 0 ? height : 1); } if (bitmap != null) { - final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, - iconSize, false); + final Bitmap resizedBitmap = + Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false); bitmap.recycle(); return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED); } @@ -391,9 +383,7 @@ public class BluetoothUtils { return drawable; } - /** - * Creates a drawable with specified width and height. - */ + /** Creates a drawable with specified width and height. */ public static Bitmap createBitmap(Drawable drawable, int width, int height) { final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); @@ -487,11 +477,8 @@ public class BluetoothUtils { } /** - * Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means: - * 1) currently connected - * 2) is Hearing Aid or LE Audio - * OR - * 3) connected profile matches currentAudioProfile + * Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means: 1) currently + * connected 2) is Hearing Aid or LE Audio OR 3) connected profile matches currentAudioProfile * * @param cachedDevice the CachedBluetoothDevice * @param audioManager audio manager to get the current audio profile @@ -519,8 +506,11 @@ public class BluetoothUtils { // It would show in Available Devices group. if (cachedDevice.isConnectedAshaHearingAidDevice() || cachedDevice.isConnectedLeAudioDevice()) { - Log.d(TAG, "isFilterMatched() device : " - + cachedDevice.getName() + ", the profile is connected."); + Log.d( + TAG, + "isFilterMatched() device : " + + cachedDevice.getName() + + ", the profile is connected."); return true; } // According to the current audio profile type, @@ -541,11 +531,79 @@ public class BluetoothUtils { return isFilterMatched; } + /** Returns if the le audio sharing is enabled. */ + public static boolean isAudioSharingEnabled() { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + return Flags.enableLeAudioSharing() + && adapter.isLeAudioBroadcastSourceSupported() + == BluetoothStatusCodes.FEATURE_SUPPORTED + && adapter.isLeAudioBroadcastAssistantSupported() + == BluetoothStatusCodes.FEATURE_SUPPORTED; + } + + /** Returns if the broadcast is on-going. */ + @WorkerThread + public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) { + if (manager == null) return false; + LocalBluetoothLeBroadcast broadcast = + manager.getProfileManager().getLeAudioBroadcastProfile(); + return broadcast != null && broadcast.isEnabled(null); + } + + /** + * Check if {@link CachedBluetoothDevice} has connected to a broadcast source. + * + * @param cachedDevice The cached bluetooth device to check. + * @param localBtManager The BT manager to provide BT functions. + * @return Whether the device has connected to a broadcast source. + */ + @WorkerThread + public static boolean hasConnectedBroadcastSource( + CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) { + if (localBtManager == null) { + Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null"); + return false; + } + LocalBluetoothLeBroadcastAssistant assistant = + localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + if (assistant == null) { + Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null"); + return false; + } + List<BluetoothLeBroadcastReceiveState> sourceList = + assistant.getAllSources(cachedDevice.getDevice()); + if (!sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected)) { + Log.d( + TAG, + "Lead device has connected broadcast source, device = " + + cachedDevice.getDevice().getAnonymizedAddress()); + return true; + } + // Return true if member device is in broadcast. + for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) { + List<BluetoothLeBroadcastReceiveState> list = + assistant.getAllSources(device.getDevice()); + if (!list.isEmpty() && list.stream().anyMatch(BluetoothUtils::isConnected)) { + Log.d( + TAG, + "Member device has connected broadcast source, device = " + + device.getDevice().getAnonymizedAddress()); + return true; + } + } + return false; + } + + /** Checks the connectivity status based on the provided broadcast receive state. */ + @WorkerThread + public static boolean isConnected(BluetoothLeBroadcastReceiveState state) { + return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0); + } + /** - * Checks if the Bluetooth device is an available hearing device, which means: - * 1) currently connected - * 2) is Hearing Aid - * 3) connected profile match hearing aid related profiles (e.g. ASHA, HAP) + * Checks if the Bluetooth device is an available hearing device, which means: 1) currently + * connected 2) is Hearing Aid 3) connected profile match hearing aid related profiles (e.g. + * ASHA, HAP) * * @param cachedDevice the CachedBluetoothDevice * @return if the device is Available hearing device @@ -553,19 +611,20 @@ public class BluetoothUtils { @WorkerThread public static boolean isAvailableHearingDevice(CachedBluetoothDevice cachedDevice) { if (isDeviceConnected(cachedDevice) && cachedDevice.isConnectedHearingAidDevice()) { - Log.d(TAG, "isFilterMatched() device : " - + cachedDevice.getName() + ", the profile is connected."); + Log.d( + TAG, + "isFilterMatched() device : " + + cachedDevice.getName() + + ", the profile is connected."); return true; } return false; } /** - * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means: - * 1) currently connected - * 2) is not Hearing Aid or LE Audio - * AND - * 3) connected profile does not match currentAudioProfile + * Check if the Bluetooth device is a ConnectedBluetoothDevice, which means: 1) currently + * connected 2) is not Hearing Aid or LE Audio AND 3) connected profile does not match + * currentAudioProfile * * @param cachedDevice the CachedBluetoothDevice * @param audioManager audio manager to get the current audio profile @@ -675,29 +734,28 @@ public class BluetoothUtils { } /** - * Returns the BluetoothDevice's exclusive manager - * ({@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the - * given set, otherwise null. + * Returns the BluetoothDevice's exclusive manager ({@link + * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the given + * set, otherwise null. */ @Nullable private static String getAllowedExclusiveManager(BluetoothDevice bluetoothDevice) { - byte[] exclusiveManagerNameBytes = bluetoothDevice.getMetadata( - BluetoothDevice.METADATA_EXCLUSIVE_MANAGER); + byte[] exclusiveManagerNameBytes = + bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER); if (exclusiveManagerNameBytes == null) { - Log.d(TAG, "Bluetooth device " + bluetoothDevice.getName() - + " doesn't have exclusive manager"); + Log.d( + TAG, + "Bluetooth device " + + bluetoothDevice.getName() + + " doesn't have exclusive manager"); return null; } String exclusiveManagerName = new String(exclusiveManagerNameBytes); - return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName - : null; + return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName : null; } - /** - * Checks if given package is installed - */ - private static boolean isPackageInstalled(Context context, - String packageName) { + /** Checks if given package is installed */ + private static boolean isPackageInstalled(Context context, String packageName) { PackageManager packageManager = context.getPackageManager(); try { packageManager.getPackageInfo(packageName, 0); @@ -709,13 +767,12 @@ public class BluetoothUtils { } /** - * A BluetoothDevice is exclusively managed if - * 1) it has field {@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. - * 2) the exclusive manager app name is in the allowlist. - * 3) the exclusive manager app is installed. + * A BluetoothDevice is exclusively managed if 1) it has field {@link + * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app name is + * in the allowlist. 3) the exclusive manager app is installed. */ - public static boolean isExclusivelyManagedBluetoothDevice(@NonNull Context context, - @NonNull BluetoothDevice bluetoothDevice) { + public static boolean isExclusivelyManagedBluetoothDevice( + @NonNull Context context, @NonNull BluetoothDevice bluetoothDevice) { String exclusiveManagerName = getAllowedExclusiveManager(bluetoothDevice); if (exclusiveManagerName == null) { return false; @@ -728,9 +785,7 @@ public class BluetoothUtils { } } - /** - * Return the allowlist for exclusive manager names. - */ + /** Return the allowlist for exclusive manager names. */ @NonNull public static Set<String> getExclusiveManagers() { return EXCLUSIVE_MANAGERS; diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index e34c50eb5ce6..9b1e4b7a633b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -174,6 +174,7 @@ public abstract class InfoMediaManager { public void startScan() { mMediaDevices.clear(); + registerRouter(); startScanOnRouter(); updateRouteListingPreference(); refreshDevices(); @@ -188,10 +189,19 @@ public abstract class InfoMediaManager { } } - public abstract void stopScan(); + public final void stopScan() { + stopScanOnRouter(); + unregisterRouter(); + } + + protected abstract void stopScanOnRouter(); protected abstract void startScanOnRouter(); + protected abstract void registerRouter(); + + protected abstract void unregisterRouter(); + protected abstract void transferToRoute(@NonNull MediaRoute2Info route); protected abstract void selectRoute( @@ -244,8 +254,6 @@ public abstract class InfoMediaManager { protected abstract List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName); protected final void rebuildDeviceList() { - mMediaDevices.clear(); - mCurrentConnectedDevice = null; buildAvailableRoutes(); } @@ -514,17 +522,27 @@ public abstract class InfoMediaManager { // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") private synchronized void buildAvailableRoutes() { - for (MediaRoute2Info route : getAvailableRoutes()) { + mMediaDevices.clear(); + RoutingSessionInfo activeSession = getActiveRoutingSession(); + + for (MediaRoute2Info route : getAvailableRoutes(activeSession)) { if (DEBUG) { Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : " + route.getVolume() + ", type : " + route.getType()); } - addMediaDevice(route); + addMediaDevice(route, activeSession); + } + + // In practice, mMediaDevices should always have at least one route. + if (!mMediaDevices.isEmpty()) { + // First device on the list is always the first selected route. + mCurrentConnectedDevice = mMediaDevices.get(0); } } - private synchronized List<MediaRoute2Info> getAvailableRoutes() { + + private synchronized List<MediaRoute2Info> getAvailableRoutes( + RoutingSessionInfo activeSession) { List<MediaRoute2Info> availableRoutes = new ArrayList<>(); - RoutingSessionInfo activeSession = getActiveRoutingSession(); List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(activeSession); availableRoutes.addAll(selectedRoutes); @@ -562,7 +580,7 @@ public abstract class InfoMediaManager { // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") @VisibleForTesting - void addMediaDevice(MediaRoute2Info route) { + void addMediaDevice(MediaRoute2Info route, RoutingSessionInfo activeSession) { final int deviceType = route.getType(); MediaDevice mediaDevice = null; switch (deviceType) { @@ -627,14 +645,10 @@ public abstract class InfoMediaManager { break; } - if (mediaDevice != null - && getActiveRoutingSession().getSelectedRoutes().contains(route.getId())) { - mediaDevice.setState(STATE_SELECTED); - if (mCurrentConnectedDevice == null) { - mCurrentConnectedDevice = mediaDevice; - } - } if (mediaDevice != null) { + if (activeSession.getSelectedRoutes().contains(route.getId())) { + mediaDevice.setState(STATE_SELECTED); + } mMediaDevices.add(mediaDevice); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java index c4fac358c01a..23063da747af 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java @@ -62,22 +62,30 @@ public class ManagerInfoMediaManager extends InfoMediaManager { @Override protected void startScanOnRouter() { if (!mIsScanning) { - mRouterManager.registerCallback(mExecutor, mMediaRouterCallback); mRouterManager.registerScanRequest(); mIsScanning = true; } } @Override - public void stopScan() { + protected void registerRouter() { + mRouterManager.registerCallback(mExecutor, mMediaRouterCallback); + } + + @Override + protected void stopScanOnRouter() { if (mIsScanning) { - mRouterManager.unregisterCallback(mMediaRouterCallback); mRouterManager.unregisterScanRequest(); mIsScanning = false; } } @Override + protected void unregisterRouter() { + mRouterManager.unregisterCallback(mMediaRouterCallback); + } + + @Override protected void transferToRoute(@NonNull MediaRoute2Info route) { // TODO: b/279555229 - provide real user handle of a caller. mRouterManager.transfer(mPackageName, route, android.os.Process.myUserHandle()); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java index 2b8c2dd0d0e3..cf11c6da737f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java @@ -63,12 +63,22 @@ import java.util.List; } @Override - public void stopScan() { + protected void startScanOnRouter() { // Do nothing. } @Override - protected void startScanOnRouter() { + protected void registerRouter() { + // Do nothing. + } + + @Override + protected void stopScanOnRouter() { + // Do nothing. + } + + @Override + protected void unregisterRouter() { // Do nothing. } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java index 9c82cb1ef57d..0dceebab13f8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java @@ -97,11 +97,6 @@ public final class RouterInfoMediaManager extends InfoMediaManager { @Override protected void startScanOnRouter() { - mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY); - mRouter.registerRouteListingPreferenceUpdatedCallback( - mExecutor, mRouteListingPreferenceCallback); - mRouter.registerTransferCallback(mExecutor, mTransferCallback); - mRouter.registerControllerCallback(mExecutor, mControllerCallback); if (Flags.enableScreenOffScanning()) { MediaRouter2.ScanRequest request = new MediaRouter2.ScanRequest.Builder().build(); mScanToken.compareAndSet(null, mRouter.requestScan(request)); @@ -111,7 +106,16 @@ public final class RouterInfoMediaManager extends InfoMediaManager { } @Override - public void stopScan() { + protected void registerRouter() { + mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY); + mRouter.registerRouteListingPreferenceUpdatedCallback( + mExecutor, mRouteListingPreferenceCallback); + mRouter.registerTransferCallback(mExecutor, mTransferCallback); + mRouter.registerControllerCallback(mExecutor, mControllerCallback); + } + + @Override + protected void stopScanOnRouter() { if (Flags.enableScreenOffScanning()) { MediaRouter2.ScanToken token = mScanToken.getAndSet(null); if (token != null) { @@ -120,6 +124,10 @@ public final class RouterInfoMediaManager extends InfoMediaManager { } else { mRouter.stopScan(); } + } + + @Override + protected void unregisterRouter() { mRouter.unregisterControllerCallback(mControllerCallback); mRouter.unregisterTransferCallback(mTransferCallback); mRouter.unregisterRouteListingPreferenceUpdatedCallback(mRouteListingPreferenceCallback); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 1246fd85ee16..f197f9ee0baf 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -17,6 +17,7 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.spy; @@ -25,6 +26,7 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -44,6 +46,9 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import java.util.ArrayList; +import java.util.List; + @RunWith(RobolectricTestRunner.class) public class BluetoothUtilsTest { @@ -55,6 +60,16 @@ public class BluetoothUtilsTest { private AudioManager mAudioManager; @Mock private PackageManager mPackageManager; + @Mock + private LocalBluetoothLeBroadcast mBroadcast; + @Mock + private LocalBluetoothProfileManager mProfileManager; + @Mock + private LocalBluetoothManager mLocalBluetoothManager; + @Mock + private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock + private BluetoothLeBroadcastReceiveState mLeBroadcastReceiveState; private Context mContext; private static final String STRING_METADATA = "string_metadata"; @@ -72,6 +87,9 @@ public class BluetoothUtilsTest { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); + when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); } @Test @@ -432,6 +450,30 @@ public class BluetoothUtilsTest { } @Test + public void testIsBroadcasting_broadcastEnabled_returnTrue() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + assertThat(BluetoothUtils.isBroadcasting(mLocalBluetoothManager)).isEqualTo(true); + } + + @Test + public void testHasConnectedBroadcastSource_deviceConnectedToBroadcastSource() { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + + List<Long> bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); + + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(any())).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasConnectedBroadcastSource( + mCachedBluetoothDevice, mLocalBluetoothManager)) + .isEqualTo(true); + } + + @Test public void isAvailableHearingDevice_isConnectedHearingAid_returnTure() { when(mCachedBluetoothDevice.isConnectedHearingAidDevice()).thenReturn(true); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index d85d2534f856..d7938670c598 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -86,6 +86,41 @@ public class InfoMediaManagerTest { private static final String TEST_DUPLICATED_ID_2 = "test_duplicated_id_2"; private static final String TEST_DUPLICATED_ID_3 = "test_duplicated_id_3"; + private static final String TEST_SYSTEM_ROUTE_ID = "TEST_SYSTEM_ROUTE_ID"; + private static final String TEST_BLUETOOTH_ROUTE_ID = "TEST_BT_ROUTE_ID"; + + private static final RoutingSessionInfo TEST_SYSTEM_ROUTING_SESSION = + new RoutingSessionInfo.Builder("FAKE_SYSTEM_ROUTING_SESSION_ID", TEST_PACKAGE_NAME) + .addSelectedRoute(TEST_SYSTEM_ROUTE_ID) + .addTransferableRoute(TEST_BLUETOOTH_ROUTE_ID) + .setSystemSession(true) + .build(); + + private static final MediaRoute2Info TEST_SELECTED_SYSTEM_ROUTE = + new MediaRoute2Info.Builder(TEST_SYSTEM_ROUTE_ID, "SELECTED_SYSTEM_ROUTE") + .setSystemRoute(true) + .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) + .build(); + + private static final MediaRoute2Info TEST_BLUETOOTH_ROUTE = + new MediaRoute2Info.Builder(TEST_BLUETOOTH_ROUTE_ID, "BLUETOOTH_ROUTE") + .setSystemRoute(true) + .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) + .setType(TYPE_BLUETOOTH_A2DP) + .setAddress("00:00:00:00:00:00") + .build(); + + private static final RoutingSessionInfo TEST_REMOTE_ROUTING_SESSION = + new RoutingSessionInfo.Builder("FAKE_REMOTE_ROUTING_SESSION_ID", TEST_PACKAGE_NAME) + .addSelectedRoute(TEST_ID_1) + .build(); + + private static final MediaRoute2Info TEST_REMOTE_ROUTE = + new MediaRoute2Info.Builder(TEST_ID_1, "REMOTE_ROUTE") + .setSystemRoute(true) + .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) + .build(); + @Mock private MediaRouter2Manager mRouterManager; @Mock @@ -127,7 +162,10 @@ public class InfoMediaManagerTest { RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class); mInfoMediaManager.mRouterManager = mRouterManager; // Since test is running in Robolectric, return a fake session to avoid NPE. - when(mRouterManager.getRoutingSessions(anyString())).thenReturn(List.of(sessionInfo)); + when(mRouterManager.getRoutingSessions(anyString())) + .thenReturn(List.of(TEST_SYSTEM_ROUTING_SESSION)); + when(mRouterManager.getSelectedRoutes(any())) + .thenReturn(List.of(TEST_SELECTED_SYSTEM_ROUTE)); mInfoMediaManager.startScan(); mInfoMediaManager.stopScan(); @@ -167,52 +205,27 @@ public class InfoMediaManagerTest { @Test public void onSessionReleased_shouldUpdateConnectedDevice() { - final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); - final RoutingSessionInfo sessionInfo1 = mock(RoutingSessionInfo.class); - routingSessionInfos.add(sessionInfo1); - final RoutingSessionInfo sessionInfo2 = mock(RoutingSessionInfo.class); - routingSessionInfos.add(sessionInfo2); - - final List<String> selectedRoutesSession1 = new ArrayList<>(); - selectedRoutesSession1.add(TEST_ID_1); - when(sessionInfo1.getSelectedRoutes()).thenReturn(selectedRoutesSession1); - - final List<String> selectedRoutesSession2 = new ArrayList<>(); - selectedRoutesSession2.add(TEST_ID_2); - when(sessionInfo2.getSelectedRoutes()).thenReturn(selectedRoutesSession2); - - mShadowRouter2Manager.setRoutingSessions(routingSessionInfos); - - final MediaRoute2Info info1 = mock(MediaRoute2Info.class); - when(info1.getId()).thenReturn(TEST_ID_1); - when(info1.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); - - final MediaRoute2Info info2 = mock(MediaRoute2Info.class); - when(info2.getId()).thenReturn(TEST_ID_2); - when(info2.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); - - final List<MediaRoute2Info> routes = new ArrayList<>(); - routes.add(info1); - routes.add(info2); - mShadowRouter2Manager.setAllRoutes(routes); - mShadowRouter2Manager.setTransferableRoutes(routes); + mInfoMediaManager.mRouterManager = mRouterManager; - final MediaDevice mediaDevice1 = mInfoMediaManager.findMediaDevice(TEST_ID_1); - assertThat(mediaDevice1).isNull(); - final MediaDevice mediaDevice2 = mInfoMediaManager.findMediaDevice(TEST_ID_2); - assertThat(mediaDevice2).isNull(); + // Active routing session is last one in list. + when(mRouterManager.getRoutingSessions(anyString())) + .thenReturn(List.of(TEST_SYSTEM_ROUTING_SESSION, TEST_REMOTE_ROUTING_SESSION)); + when(mRouterManager.getSelectedRoutes(TEST_SYSTEM_ROUTING_SESSION)) + .thenReturn(List.of(TEST_SELECTED_SYSTEM_ROUTE)); + when(mRouterManager.getSelectedRoutes(TEST_REMOTE_ROUTING_SESSION)) + .thenReturn(List.of(TEST_REMOTE_ROUTE)); mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); - final MediaDevice infoDevice1 = mInfoMediaManager.mMediaDevices.get(0); - assertThat(infoDevice1.getId()).isEqualTo(TEST_ID_1); - final MediaDevice infoDevice2 = mInfoMediaManager.mMediaDevices.get(1); - assertThat(infoDevice2.getId()).isEqualTo(TEST_ID_2); - // The active routing session is the last one in the list, which maps to infoDevice2. - assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(infoDevice2); + MediaDevice remoteDevice = mInfoMediaManager.findMediaDevice(TEST_REMOTE_ROUTE.getId()); + assertThat(remoteDevice).isNotNull(); + assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(remoteDevice); - routingSessionInfos.remove(sessionInfo2); - mInfoMediaManager.mMediaRouterCallback.onSessionReleased(sessionInfo2); - assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(infoDevice1); + when(mRouterManager.getRoutingSessions(anyString())) + .thenReturn(List.of(TEST_SYSTEM_ROUTING_SESSION)); + mInfoMediaManager.mMediaRouterCallback.onSessionReleased(TEST_REMOTE_ROUTING_SESSION); + MediaDevice systemRoute = mInfoMediaManager.findMediaDevice(TEST_SYSTEM_ROUTE_ID); + assertThat(systemRoute).isNotNull(); + assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(systemRoute); } @Test @@ -770,18 +783,16 @@ public class InfoMediaManagerTest { @Test public void onSessionUpdated_shouldDispatchDeviceListAdded() { - final MediaRoute2Info info = mock(MediaRoute2Info.class); - when(info.getId()).thenReturn(TEST_ID); - when(info.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); - when(info.isSystemRoute()).thenReturn(true); - - final List<MediaRoute2Info> routes = new ArrayList<>(); - routes.add(info); - mShadowRouter2Manager.setAllRoutes(routes); + mInfoMediaManager.mRouterManager = mRouterManager; + // Since test is running in Robolectric, return a fake session to avoid NPE. + when(mRouterManager.getRoutingSessions(anyString())) + .thenReturn(List.of(TEST_SYSTEM_ROUTING_SESSION)); + when(mRouterManager.getSelectedRoutes(any())) + .thenReturn(List.of(TEST_SELECTED_SYSTEM_ROUTE)); mInfoMediaManager.registerCallback(mCallback); - mInfoMediaManager.mMediaRouterCallback.onSessionUpdated(mock(RoutingSessionInfo.class)); + mInfoMediaManager.mMediaRouterCallback.onSessionUpdated(TEST_SYSTEM_ROUTING_SESSION); verify(mCallback).onDeviceListAdded(any()); } @@ -795,19 +806,19 @@ public class InfoMediaManagerTest { when(route2Info.getType()).thenReturn(TYPE_REMOTE_SPEAKER); when(route2Info.getId()).thenReturn(TEST_ID); - mInfoMediaManager.addMediaDevice(route2Info); + mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION); assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof InfoMediaDevice).isTrue(); when(route2Info.getType()).thenReturn(TYPE_USB_DEVICE); when(route2Info.getId()).thenReturn(TEST_ID); mInfoMediaManager.mMediaDevices.clear(); - mInfoMediaManager.addMediaDevice(route2Info); + mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION); assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof PhoneMediaDevice).isTrue(); when(route2Info.getType()).thenReturn(TYPE_WIRED_HEADSET); when(route2Info.getId()).thenReturn(TEST_ID); mInfoMediaManager.mMediaDevices.clear(); - mInfoMediaManager.addMediaDevice(route2Info); + mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION); assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof PhoneMediaDevice).isTrue(); when(route2Info.getType()).thenReturn(TYPE_BLUETOOTH_A2DP); @@ -818,12 +829,12 @@ public class InfoMediaManagerTest { when(cachedBluetoothDeviceManager.findDevice(any(BluetoothDevice.class))) .thenReturn(cachedDevice); mInfoMediaManager.mMediaDevices.clear(); - mInfoMediaManager.addMediaDevice(route2Info); + mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION); assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof BluetoothMediaDevice).isTrue(); when(route2Info.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); mInfoMediaManager.mMediaDevices.clear(); - mInfoMediaManager.addMediaDevice(route2Info); + mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION); assertThat(mInfoMediaManager.mMediaDevices.get(0) instanceof PhoneMediaDevice).isTrue(); } @@ -841,34 +852,35 @@ public class InfoMediaManagerTest { .thenReturn(null); mInfoMediaManager.mMediaDevices.clear(); - mInfoMediaManager.addMediaDevice(route2Info); + mInfoMediaManager.addMediaDevice(route2Info, TEST_SYSTEM_ROUTING_SESSION); assertThat(mInfoMediaManager.mMediaDevices.size()).isEqualTo(0); } @Test - public void addMediaDevice_deviceIncludedInSelectedDevices_shouldSetAsCurrentConnected() { - final MediaRoute2Info route2Info = mock(MediaRoute2Info.class); + public void onRoutesUpdated_setsFirstSelectedRouteAsCurrentConnectedDevice() { final CachedBluetoothDeviceManager cachedBluetoothDeviceManager = mock(CachedBluetoothDeviceManager.class); + final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); - final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>(); - final RoutingSessionInfo sessionInfo = mock(RoutingSessionInfo.class); - routingSessionInfos.add(sessionInfo); + RoutingSessionInfo selectedBtSession = + new RoutingSessionInfo.Builder(TEST_SYSTEM_ROUTING_SESSION) + .clearSelectedRoutes() + .clearTransferableRoutes() + .addSelectedRoute(TEST_BLUETOOTH_ROUTE_ID) + .addTransferableRoute(TEST_SYSTEM_ROUTE_ID) + .build(); - when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)).thenReturn(routingSessionInfos); - when(sessionInfo.getSelectedRoutes()).thenReturn(ImmutableList.of(TEST_ID)); - when(route2Info.getType()).thenReturn(TYPE_BLUETOOTH_A2DP); - when(route2Info.getAddress()).thenReturn("00:00:00:00:00:00"); - when(route2Info.getId()).thenReturn(TEST_ID); + when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME)) + .thenReturn(List.of(selectedBtSession)); + when(mRouterManager.getSelectedRoutes(any())).thenReturn(List.of(TEST_BLUETOOTH_ROUTE)); when(mLocalBluetoothManager.getCachedDeviceManager()) .thenReturn(cachedBluetoothDeviceManager); when(cachedBluetoothDeviceManager.findDevice(any(BluetoothDevice.class))) .thenReturn(cachedDevice); mInfoMediaManager.mRouterManager = mRouterManager; - mInfoMediaManager.mMediaDevices.clear(); - mInfoMediaManager.addMediaDevice(route2Info); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); MediaDevice device = mInfoMediaManager.mMediaDevices.get(0); diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp index bf4f60d84e4d..e9c267284e91 100644 --- a/packages/SettingsProvider/Android.bp +++ b/packages/SettingsProvider/Android.bp @@ -32,6 +32,7 @@ android_library { "unsupportedappusage", ], static_libs: [ + "aconfig_demo_flags_java_lib", "device_config_service_flags_java", "libaconfig_java_proto_lite", "SettingsLibDeviceStateRotationLock", @@ -87,6 +88,7 @@ android_test { aconfig_declarations { name: "device_config_service_flags", package: "com.android.providers.settings", + container: "system", srcs: [ "src/com/android/providers/settings/device_config_service.aconfig", ], diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 4e4c22fadcfd..68167e1598e0 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -60,16 +60,17 @@ import libcore.io.IoUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -165,8 +166,8 @@ final class SettingsState { private static final String STORAGE_MIGRATION_FLAG = "core_experiments_team_internal/com.android.providers.settings.storage_test_mission_1"; - private static final String STORAGE_MIGRATION_LOG = - "/metadata/aconfig/flags/storage_migration.log"; + private static final String STORAGE_MIGRATION_MARKER_FILE = + "/metadata/aconfig/storage_test_mission_1"; /** * This tag is applied to all aconfig default value-loaded flags. @@ -1126,7 +1127,7 @@ final class SettingsState { Slog.i(LOG_TAG, "[PERSIST END]"); } } catch (Throwable t) { - Slog.wtf(LOG_TAG, "Failed to write settings, restoring old file", t); + Slog.e(LOG_TAG, "Failed to write settings, restoring old file", t); if (t instanceof IOException) { if (t.getMessage().contains("Couldn't create directory")) { if (DEBUG) { @@ -1467,16 +1468,29 @@ final class SettingsState { } } - if (name != null && name.equals(STORAGE_MIGRATION_FLAG) && value.equals("true")) { - File file = new File(STORAGE_MIGRATION_LOG); - if (!file.exists()) { - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(STORAGE_MIGRATION_LOG))) { - final long timestamp = System.currentTimeMillis(); - String entry = String.format("%d | Log init", timestamp); - writer.write(entry); - } catch (IOException e) { - Slog.e(LOG_TAG, "failed to write storage migration file", e); + if (isConfigSettingsKey(mKey) && name != null + && name.equals(STORAGE_MIGRATION_FLAG)) { + if (value.equals("true")) { + Path path = Paths.get(STORAGE_MIGRATION_MARKER_FILE); + if (!Files.exists(path)) { + Files.createFile(path); + } + + Set<PosixFilePermission> perms = + Files.readAttributes(path, PosixFileAttributes.class).permissions(); + perms.add(PosixFilePermission.OWNER_WRITE); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.GROUP_READ); + perms.add(PosixFilePermission.OTHERS_READ); + try { + Files.setPosixFilePermissions(path, perms); + } catch (Exception e) { + Slog.e(LOG_TAG, "failed to set permissions on migration marker", e); + } + } else { + java.nio.file.Path path = Paths.get(STORAGE_MIGRATION_MARKER_FILE); + if (Files.exists(path)) { + Files.delete(path); } } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig index c572bdb57c6a..d20fbf591a25 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig +++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig @@ -1,4 +1,5 @@ package: "com.android.providers.settings" +container: "system" flag { name: "support_overrides" diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 58040716db3e..b94e224850aa 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -896,7 +896,6 @@ <!-- Permissions required for CTS test - CtsVoiceInteractionTestCases --> <uses-permission android:name="android.permission.RESET_HOTWORD_TRAINING_DATA_EGRESS_COUNT" /> - <uses-permission android:name="android.permission.RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA" /> <uses-permission android:name="android.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO" /> <uses-permission android:name="android.permission.GET_BINDING_UID_IMPORTANCE" /> diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index 42952de1b2b9..5ac0e449b8e1 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -50,7 +50,6 @@ import android.os.AsyncTask; import android.os.Binder; import android.os.BugreportManager; import android.os.BugreportManager.BugreportCallback; -import android.os.BugreportManager.BugreportCallback.BugreportErrorCode; import android.os.BugreportParams; import android.os.Bundle; import android.os.FileUtils; @@ -169,6 +168,8 @@ public class BugreportProgressService extends Service { static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT"; static final String EXTRA_INFO = "android.intent.extra.INFO"; + static final String EXTRA_EXTRA_ATTACHMENT_URI = + "android.intent.extra.EXTRA_ATTACHMENT_URI"; private static final int MSG_SERVICE_COMMAND = 1; private static final int MSG_DELAYED_SCREENSHOT = 2; @@ -634,9 +635,10 @@ public class BugreportProgressService extends Service { long nonce = intent.getLongExtra(EXTRA_BUGREPORT_NONCE, 0); String baseName = getBugreportBaseName(bugreportType); String name = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); + Uri extraAttachment = intent.getParcelableExtra(EXTRA_EXTRA_ATTACHMENT_URI, Uri.class); - BugreportInfo info = new BugreportInfo(mContext, baseName, name, - shareTitle, shareDescription, bugreportType, mBugreportsDir, nonce); + BugreportInfo info = new BugreportInfo(mContext, baseName, name, shareTitle, + shareDescription, bugreportType, mBugreportsDir, nonce, extraAttachment); synchronized (mLock) { if (info.bugreportFile.exists()) { Log.e(TAG, "Failed to start bugreport generation, the requested bugreport file " @@ -1184,6 +1186,10 @@ public class BugreportProgressService extends Service { clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); attachments.add(screenshotUri); } + if (info.extraAttachment != null) { + clipData.addItem(new ClipData.Item(null, null, null, info.extraAttachment)); + attachments.add(info.extraAttachment); + } intent.setClipData(clipData); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); @@ -2042,6 +2048,9 @@ public class BugreportProgressService extends Service { */ final long nonce; + @Nullable + public Uri extraAttachment = null; + private final Object mLock = new Object(); /** @@ -2049,7 +2058,8 @@ public class BugreportProgressService extends Service { */ BugreportInfo(Context context, String baseName, String name, @Nullable String shareTitle, @Nullable String shareDescription, - @BugreportParams.BugreportMode int type, File bugreportsDir, long nonce) { + @BugreportParams.BugreportMode int type, File bugreportsDir, long nonce, + @Nullable Uri extraAttachment) { this.context = context; this.name = this.initialName = name; this.shareTitle = shareTitle == null ? "" : shareTitle; @@ -2058,6 +2068,7 @@ public class BugreportProgressService extends Service { this.nonce = nonce; this.baseName = baseName; this.bugreportFile = new File(bugreportsDir, getFileName(this, ".zip")); + this.extraAttachment = extraAttachment; } void createBugreportFile() { diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp index f74e59abeca5..0ff856e0b91e 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp @@ -5,6 +5,7 @@ package { aconfig_declarations { name: "com_android_a11y_menu_flags", package: "com.android.systemui.accessibility.accessibilitymenu", + container: "system", srcs: [ "accessibility.aconfig", ], diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig index f5db6a4c4573..d868d5c7c4c4 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig +++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig @@ -1,4 +1,5 @@ package: "com.android.systemui.accessibility.accessibilitymenu" +container: "system" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. diff --git a/packages/SystemUI/aconfig/Android.bp b/packages/SystemUI/aconfig/Android.bp index 15c2c17fd7f0..2a32b5854425 100644 --- a/packages/SystemUI/aconfig/Android.bp +++ b/packages/SystemUI/aconfig/Android.bp @@ -36,6 +36,7 @@ package { aconfig_declarations { name: "com_android_systemui_flags", package: "com.android.systemui", + container: "system", srcs: [ "*.aconfig", ], diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index 866aa8945525..8137e408ba39 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -1,4 +1,5 @@ package: "com.android.systemui" +container: "system" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig index 7cc0c83abfea..bd1a442b874f 100644 --- a/packages/SystemUI/aconfig/biometrics_framework.aconfig +++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig @@ -1,4 +1,5 @@ package: "com.android.systemui" +container: "system" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. @@ -14,4 +15,4 @@ flag { namespace: "biometrics_framework" description: "Refactors Biometric Prompt to use a ConstraintLayout" bug: "288175072" -}
\ No newline at end of file +} diff --git a/packages/SystemUI/aconfig/communal.aconfig b/packages/SystemUI/aconfig/communal.aconfig index 2c6ff979cc7f..2e9af7e3a763 100644 --- a/packages/SystemUI/aconfig/communal.aconfig +++ b/packages/SystemUI/aconfig/communal.aconfig @@ -1,4 +1,5 @@ package: "com.android.systemui" +container: "system" flag { name: "communal_hub" diff --git a/packages/SystemUI/aconfig/cross_device_control.aconfig b/packages/SystemUI/aconfig/cross_device_control.aconfig index d3f14c129a0b..5f9a4f42b546 100644 --- a/packages/SystemUI/aconfig/cross_device_control.aconfig +++ b/packages/SystemUI/aconfig/cross_device_control.aconfig @@ -1,4 +1,5 @@ package: "com.android.systemui" +container: "system" flag { name: "legacy_le_audio_sharing" diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig index 7bbe82c212e7..46eb9e1c2330 100644 --- a/packages/SystemUI/aconfig/predictive_back.aconfig +++ b/packages/SystemUI/aconfig/predictive_back.aconfig @@ -1,4 +1,5 @@ package: "com.android.systemui" +container: "system" flag { name: "predictive_back_sysui" @@ -26,4 +27,4 @@ flag { namespace: "systemui" description: "Enable Predictive Back Animation for SysUI dialogs" bug: "327721544" -}
\ No newline at end of file +} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 4bfc6296b8a3..6810aac92925 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1,4 +1,5 @@ package: "com.android.systemui" +container: "system" flag { name: "example_flag" @@ -25,6 +26,14 @@ flag { } flag { + name: "refactor_keyguard_dismiss_intent" + namespace: "systemui" + description: "Update how keyguard dismiss intents are stored." + bug: "275069969" +} + +flag { + name: "notification_heads_up_cycling" namespace: "systemui" description: "Heads-up notification cycling animation for the Notification Avalanche feature." @@ -715,3 +724,10 @@ flag { " Compose for the UI." bug: "325099249" } + +flag { + name: "keyboard_docking_indicator" + namespace: "systemui" + description: "Glow bar indicator reveals upon keyboard docking." + bug: "324600132" +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index ed8027756f47..32c03136bf8e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -17,9 +17,11 @@ package com.android.systemui.communal.ui.compose import android.appwidget.AppWidgetHostView +import android.content.res.Configuration import android.graphics.drawable.Icon import android.os.Bundle import android.util.SizeF +import android.view.ViewGroup import android.widget.FrameLayout import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState @@ -92,6 +94,7 @@ import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag @@ -110,8 +113,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup import androidx.core.view.setPadding import androidx.window.layout.WindowMetricsCalculator -import com.android.compose.modifiers.height -import com.android.compose.modifiers.padding import com.android.compose.modifiers.thenIf import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.ui.graphics.painter.rememberDrawablePainter @@ -364,7 +365,7 @@ private fun ScrollOnUpdatedLiveContentEffect( liveContentKeys.indexOfFirst { !prevLiveContentKeys.contains(it) } // Scroll if current position is behind the first updated content - if (indexOfFirstUpdatedContent in 0..<gridState.firstVisibleItemIndex) { + if (indexOfFirstUpdatedContent in 0 until gridState.firstVisibleItemIndex) { // Launching with a scope to prevent the job from being canceled in the case of a // recomposition during scrolling coroutineScope.launch { gridState.animateScrollToItem(indexOfFirstUpdatedContent) } @@ -834,6 +835,13 @@ private fun WidgetContent( widgetConfigurator: WidgetConfigurator?, modifier: Modifier = Modifier, ) { + var widgetId: Int by remember { mutableStateOf(-1) } + var configuration: Configuration? by remember { mutableStateOf(null) } + + // In addition to returning the current configuration, this also causes a recompose on + // configuration change. + val currentConfiguration = LocalConfiguration.current + Box( modifier = modifier.thenIf(!viewModel.isEditMode && model.inQuietMode) { @@ -849,18 +857,48 @@ private fun WidgetContent( AndroidView( modifier = Modifier.fillMaxSize().allowGestures(allowed = !viewModel.isEditMode), factory = { context -> - model.appWidgetHost - .createViewForCommunal(context, model.appWidgetId, model.providerInfo) - .apply { - updateAppWidgetSize(Bundle.EMPTY, listOf(size)) - // Remove the extra padding applied to AppWidgetHostView to allow widgets to - // occupy the entire box. - setPadding(0) - } + // This FrameLayout becomes the container view for the AppWidgetHostView we add as a + // child in update below. Having a container view allows for updating the contained + // host view as needed (e.g. on configuration changes). + FrameLayout(context).apply { + layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + setPadding(0) + } + }, + update = { widgetContainer: ViewGroup -> + if (widgetId == model.appWidgetId && currentConfiguration.equals(configuration)) { + return@AndroidView + } + + // Add the widget's host view to the FrameLayout parent (after removing any + // previously added host view). + widgetContainer.removeAllViews() + widgetContainer.addView( + model.appWidgetHost + .createViewForCommunal( + widgetContainer.context, + model.appWidgetId, + model.providerInfo + ) + .apply { + updateAppWidgetSize(Bundle.EMPTY, listOf(size)) + // Remove the extra padding applied to AppWidgetHostView to allow + // widgets to occupy the entire box. + setPadding(0) + } + ) + + widgetId = model.appWidgetId + configuration = currentConfiguration }, // For reusing composition in lazy lists. onReset = {}, ) + if ( viewModel is CommunalEditModeViewModel && model.reconfigurable && diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt index 7a73c58ba193..8129e41b4977 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt @@ -71,7 +71,6 @@ private fun rememberBurnInParameters( return remember(clock, topInset, topmostTop) { BurnInParameters( - clockControllerProvider = { clock }, topInset = topInset, minViewY = topmostTop, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index c008a1a4d192..28e92aad914a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -86,16 +86,21 @@ constructor( if (shouldUseSplitNotificationShade) { with(notificationSection) { Notifications( - Modifier.fillMaxWidth(0.5f) - .fillMaxHeight() - .align(alignment = Alignment.TopEnd) + burnInParams = null, + modifier = + Modifier.fillMaxWidth(0.5f) + .fillMaxHeight() + .align(alignment = Alignment.TopEnd) ) } } } if (!shouldUseSplitNotificationShade) { with(notificationSection) { - Notifications(Modifier.weight(weight = 1f)) + Notifications( + burnInParams = null, + modifier = Modifier.weight(weight = 1f) + ) } } if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt index 091a43923a6e..b8f00dce53df 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt @@ -86,16 +86,21 @@ constructor( if (shouldUseSplitNotificationShade) { with(notificationSection) { Notifications( - Modifier.fillMaxWidth(0.5f) - .fillMaxHeight() - .align(alignment = Alignment.TopEnd) + burnInParams = null, + modifier = + Modifier.fillMaxWidth(0.5f) + .fillMaxHeight() + .align(alignment = Alignment.TopEnd) ) } } } if (!shouldUseSplitNotificationShade) { with(notificationSection) { - Notifications(Modifier.weight(weight = 1f)) + Notifications( + burnInParams = null, + modifier = Modifier.weight(weight = 1f) + ) } } if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { 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 09d76a341442..cba54531713b 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 @@ -149,6 +149,7 @@ constructor( if (areNotificationsVisible) { with(notificationSection) { Notifications( + burnInParams = burnIn.parameters, modifier = Modifier.fillMaxWidth().weight(weight = 1f) ) } @@ -375,6 +376,7 @@ constructor( ) } Notifications( + burnInParams = burnIn.parameters, modifier = Modifier.fillMaxHeight() .weight(weight = 1f) 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 51a7e8ed8b40..eb389e609cc5 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 @@ -74,6 +74,7 @@ constructor( modifier = modifier .height(dimensionResource(R.dimen.small_clock_height)) + .padding(horizontal = dimensionResource(R.dimen.clock_padding_start)) .padding(top = { viewModel.getSmallClockTopMargin(context) }) .onTopPlacementChanged(onTopChanged) .burnInAware( 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 fa0a1c4663b1..f48fa88b9722 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 @@ -32,8 +32,11 @@ import com.android.compose.modifiers.thenIf import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.MigrateClocksToBlueprint +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.LockscreenContentViewModel -import com.android.systemui.notifications.ui.composable.NotificationStack +import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout @@ -48,6 +51,7 @@ class NotificationSection @Inject constructor( private val viewModel: NotificationsPlaceholderViewModel, + private val aodBurnInViewModel: AodBurnInViewModel, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, stackScrollLayout: NotificationStackScrollLayout, @@ -77,8 +81,12 @@ constructor( ) } + /** + * @param burnInParams params to make this view adaptive to burn-in, `null` to disable burn-in + * adjustment + */ @Composable - fun SceneScope.Notifications(modifier: Modifier = Modifier) { + fun SceneScope.Notifications(burnInParams: BurnInParameters?, modifier: Modifier = Modifier) { val shouldUseSplitNotificationShade by lockscreenContentViewModel.shouldUseSplitNotificationShade.collectAsState() val areNotificationsVisible by @@ -94,12 +102,24 @@ constructor( return } - NotificationStack( + ConstrainedNotificationStack( viewModel = viewModel, modifier = - modifier.fillMaxWidth().thenIf(shouldUseSplitNotificationShade) { - Modifier.padding(top = splitShadeTopMargin) - }, + modifier + .fillMaxWidth() + .thenIf(shouldUseSplitNotificationShade) { + Modifier.padding(top = splitShadeTopMargin) + } + .let { + if (burnInParams == null) { + it + } else { + it.burnInAware( + viewModel = aodBurnInViewModel, + params = burnInParams, + ) + } + }, ) } } 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 cda43476610e..579e837a62d0 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 @@ -36,12 +36,14 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind @@ -60,7 +62,6 @@ 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 import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import com.android.compose.animation.scene.ElementKey @@ -68,12 +69,12 @@ import com.android.compose.animation.scene.NestedScrollBehavior import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.height import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius -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.shared.model.ShadeScrimBounds import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt @@ -81,7 +82,8 @@ import kotlin.math.roundToInt object Notifications { object Elements { val NotificationScrim = ElementKey("NotificationScrim") - val NotificationPlaceholder = ElementKey("NotificationPlaceholder") + val NotificationStackPlaceholder = ElementKey("NotificationStackPlaceholder") + val HeadsUpNotificationPlaceholder = ElementKey("HeadsUpNotificationPlaceholder") val ShelfSpace = ElementKey("ShelfSpace") } @@ -91,12 +93,6 @@ object Notifications { const val EXPANSION_FOR_MAX_CORNER_RADIUS = 0.1f const val EXPANSION_FOR_MAX_SCRIM_ALPHA = 0.3f } - - enum class Form { - HunFromTop, - Stack, - HunFromBottom, - } } /** @@ -109,24 +105,48 @@ fun SceneScope.HeadsUpNotificationSpace( modifier: Modifier = Modifier, isPeekFromBottom: Boolean = false, ) { - NotificationPlaceholder( - viewModel = viewModel, - form = if (isPeekFromBottom) Form.HunFromBottom else Form.HunFromTop, - modifier = modifier, - ) + val headsUpHeight = viewModel.headsUpHeight.collectAsState() + + Element( + Notifications.Elements.HeadsUpNotificationPlaceholder, + modifier = + modifier + .height { headsUpHeight.value.roundToInt() } + .fillMaxWidth() + .debugBackground(viewModel, DEBUG_HUN_COLOR) + .onGloballyPositioned { coordinates: LayoutCoordinates -> + val boundsInWindow = coordinates.boundsInWindow() + debugLog(viewModel) { + "HUNS onGloballyPositioned:" + + " size=${coordinates.size}" + + " bounds=$boundsInWindow" + } + viewModel.onHeadsUpTopChanged(boundsInWindow.top) + } + ) { + content {} + } } /** Adds the space where notification stack should appear in the scene. */ @Composable -fun SceneScope.NotificationStack( +fun SceneScope.ConstrainedNotificationStack( viewModel: NotificationsPlaceholderViewModel, modifier: Modifier = Modifier, ) { - NotificationPlaceholder( - viewModel = viewModel, - form = Form.Stack, - modifier = modifier, - ) + Box( + modifier = + modifier.onSizeChanged { viewModel.onConstrainedAvailableSpaceChanged(it.height) } + ) { + NotificationPlaceholder( + viewModel = viewModel, + modifier = Modifier.fillMaxSize(), + ) + HeadsUpNotificationSpace( + viewModel = viewModel, + modifier = Modifier.align(Alignment.TopCenter), + ) + } } /** @@ -169,6 +189,9 @@ fun SceneScope.NotificationScrollingStack( // entire height of the scrim is visible on screen. val scrimOffset = remember { mutableStateOf(0f) } + // set the bounds to null when the scrim disappears + DisposableEffect(Unit) { onDispose { viewModel.onScrimBoundsChanged(null) } } + val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() } // The minimum offset for the scrim. The scrim is considered fully expanded when it @@ -235,6 +258,22 @@ fun SceneScope.NotificationScrollingStack( .let { scrimRounding.value.toRoundedCornerShape(it) } clip = true } + .onGloballyPositioned { coordinates -> + val boundsInWindow = coordinates.boundsInWindow() + debugLog(viewModel) { + "SCRIM onGloballyPositioned:" + + " size=${coordinates.size}" + + " bounds=$boundsInWindow" + } + viewModel.onScrimBoundsChanged( + ShadeScrimBounds( + left = boundsInWindow.left, + top = boundsInWindow.top, + right = boundsInWindow.right, + bottom = boundsInWindow.bottom, + ) + ) + } ) { // Creates a cutout in the background scrim in the shape of the notifications scrim. // Only visible when notif scrim alpha < 1, during shade expansion. @@ -254,11 +293,10 @@ fun SceneScope.NotificationScrollingStack( } else 1f } .background(MaterialTheme.colorScheme.surface) - .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f)) + .debugBackground(viewModel, DEBUG_BOX_COLOR) ) { NotificationPlaceholder( viewModel = viewModel, - form = Form.Stack, modifier = Modifier.verticalNestedScrollToScene( topBehavior = NestedScrollBehavior.EdgeWithPreview, @@ -284,6 +322,7 @@ fun SceneScope.NotificationScrollingStack( .height { (stackHeight.value + navBarHeight).roundToInt() }, ) } + HeadsUpNotificationSpace(viewModel = viewModel) } } @@ -304,14 +343,10 @@ fun SceneScope.NotificationShelfSpace( modifier .element(key = Notifications.Elements.ShelfSpace) .fillMaxWidth() - .onSizeChanged { size: IntSize -> - debugLog(viewModel) { "SHELF onSizeChanged: size=$size" } - } .onPlaced { coordinates: LayoutCoordinates -> debugLog(viewModel) { ("SHELF onPlaced:" + " size=${coordinates.size}" + - " position=${coordinates.positionInWindow()}" + " bounds=${coordinates.boundsInWindow()}") } } @@ -326,32 +361,26 @@ fun SceneScope.NotificationShelfSpace( @Composable private fun SceneScope.NotificationPlaceholder( viewModel: NotificationsPlaceholderViewModel, - form: Form, modifier: Modifier = Modifier, ) { - val elementKey = Notifications.Elements.NotificationPlaceholder Element( - elementKey, + Notifications.Elements.NotificationStackPlaceholder, modifier = modifier - .debugBackground(viewModel) - .onSizeChanged { size: IntSize -> - debugLog(viewModel) { "STACK onSizeChanged: size=$size" } - } + .debugBackground(viewModel, DEBUG_STACK_COLOR) + .onSizeChanged { size -> debugLog(viewModel) { "STACK onSizeChanged: size=$size" } } .onGloballyPositioned { coordinates: LayoutCoordinates -> - viewModel.onContentTopChanged(coordinates.positionInWindow().y) + val positionInWindow = coordinates.positionInWindow() debugLog(viewModel) { "STACK onGloballyPositioned:" + " size=${coordinates.size}" + - " position=${coordinates.positionInWindow()}" + + " position=$positionInWindow" + " bounds=${coordinates.boundsInWindow()}" } - val boundsInWindow = coordinates.boundsInWindow() - viewModel.onBoundsChanged( - left = boundsInWindow.left, - top = boundsInWindow.top, - right = boundsInWindow.right, - bottom = boundsInWindow.bottom, + // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top will not + viewModel.onStackBoundsChanged( + top = positionInWindow.y, + bottom = positionInWindow.y + coordinates.size.height, ) } ) { @@ -388,7 +417,7 @@ private inline fun debugLog( private fun Modifier.debugBackground( viewModel: NotificationsPlaceholderViewModel, - color: Color = DEBUG_COLOR, + color: Color, ): Modifier = if (viewModel.isVisualDebuggingEnabled) { background(color) @@ -397,8 +426,8 @@ private fun Modifier.debugBackground( } fun ShadeScrimRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape { - val topRadius = if (roundTop) radius else 0.dp - val bottomRadius = if (roundBottom) radius else 0.dp + val topRadius = if (isTopRounded) radius else 0.dp + val bottomRadius = if (isBottomRounded) radius else 0.dp return RoundedCornerShape( topStart = topRadius, topEnd = topRadius, @@ -408,4 +437,6 @@ fun ShadeScrimRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape { } private const val TAG = "FlexiNotifs" -private val DEBUG_COLOR = Color(1f, 0f, 0f, 0.2f) +private val DEBUG_STACK_COLOR = Color(1f, 0f, 0f, 0.2f) +private val DEBUG_HUN_COLOR = Color(0f, 0f, 1f, 0.2f) +private val DEBUG_BOX_COLOR = Color(0f, 1f, 0f, 0.2f) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/BrightnessMirror.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/BrightnessMirror.kt new file mode 100644 index 000000000000..ca6b3434d90e --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/BrightnessMirror.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.ui.composable + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +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.graphics.graphicsLayer +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.viewinterop.AndroidView +import com.android.compose.modifiers.height +import com.android.compose.modifiers.width +import com.android.systemui.qs.ui.adapter.QSSceneAdapter +import com.android.systemui.settings.brightness.ui.binder.BrightnessMirrorInflater +import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel + +@Composable +fun BrightnessMirror( + viewModel: BrightnessMirrorViewModel, + qsSceneAdapter: QSSceneAdapter, + modifier: Modifier = Modifier, +) { + val isShowing by viewModel.isShowing.collectAsState() + val mirrorAlpha by + animateFloatAsState( + targetValue = if (isShowing) 1f else 0f, + label = "alphaAnimationBrightnessMirrorShowing", + ) + val mirrorOffsetAndSize by viewModel.locationAndSize.collectAsState() + val offset = IntOffset(0, mirrorOffsetAndSize.yOffset) + + Box(modifier = modifier.fillMaxSize().graphicsLayer { alpha = mirrorAlpha }) { + QuickSettingsTheme { + // The assumption for using this AndroidView is that there will be only one in view at + // a given time (which is a reasonable assumption). Because `QSSceneAdapter` (actually + // `BrightnessSliderController` only supports a single mirror). + // The benefit of doing it like this is that if the configuration changes or QSImpl is + // re-inflated, it's not relevant to the composable, as we'll always get a new one. + AndroidView( + modifier = + Modifier.align(Alignment.TopCenter) + .offset { offset } + .width { mirrorOffsetAndSize.width } + .height { mirrorOffsetAndSize.height }, + factory = { context -> + val (view, controller) = + BrightnessMirrorInflater.inflate(context, viewModel.sliderControllerFactory) + viewModel.setToggleSlider(controller) + view + }, + update = { qsSceneAdapter.setBrightnessMirrorController(viewModel) }, + onRelease = { qsSceneAdapter.setBrightnessMirrorController(null) } + ) + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 5b9213a8f23c..a3768346e7c6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -17,7 +17,9 @@ package com.android.systemui.qs.ui.composable import android.view.ViewGroup +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn @@ -48,6 +50,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.colorResource @@ -119,9 +122,28 @@ private fun SceneScope.QuickSettingsScene( statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { + val brightnessMirrorShowing by viewModel.brightnessMirrorViewModel.isShowing.collectAsState() + val contentAlpha by + animateFloatAsState( + targetValue = if (brightnessMirrorShowing) 0f else 1f, + label = "alphaAnimationBrightnessMirrorContentHiding", + ) + + BrightnessMirror( + viewModel = viewModel.brightnessMirrorViewModel, + qsSceneAdapter = viewModel.qsSceneAdapter + ) + // TODO(b/280887232): implement the real UI. - Box(modifier = modifier.fillMaxSize()) { + Box(modifier = modifier.fillMaxSize().graphicsLayer { alpha = contentAlpha }) { val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState() + + BackHandler( + enabled = isCustomizing, + ) { + viewModel.qsSceneAdapter.requestCloseCustomizer() + } + val collapsedHeaderHeight = with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() } val lifecycleOwner = LocalLifecycleOwner.current diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index fcd77609768e..02a12e4e0814 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -54,9 +54,9 @@ import androidx.compose.ui.viewinterop.AndroidView 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.ValueKey import com.android.compose.animation.scene.animateElementFloatAsState -import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.settingslib.Utils import com.android.systemui.battery.BatteryMeterView @@ -87,10 +87,6 @@ object ShadeHeader { val ShadeCarrierGroup = ElementKey("ShadeCarrierGroup") } - object Keys { - val transitionProgress = ValueKey("ShadeHeaderTransitionProgress") - } - object Values { val ClockScale = ValueKey("ShadeHeaderClockScale") } @@ -119,19 +115,17 @@ fun SceneScope.CollapsedShadeHeader( return } - val formatProgress = - animateSceneFloatAsState(0f, ShadeHeader.Keys.transitionProgress) - .unsafeCompositionState(initialValue = 0f) - val cutoutWidth = LocalDisplayCutout.current.width() val cutoutLocation = LocalDisplayCutout.current.location val useExpandedFormat by - remember(formatProgress) { + remember(cutoutLocation) { derivedStateOf { - cutoutLocation != CutoutLocation.CENTER || formatProgress.value > 0.5f + cutoutLocation != CutoutLocation.CENTER || + shouldUseExpandedFormat(layoutState.transitionState) } } + val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsState() // This layout assumes it is globally positioned at (0, 0) and is the @@ -207,7 +201,7 @@ fun SceneScope.CollapsedShadeHeader( val screenWidth = constraints.maxWidth val cutoutWidthPx = cutoutWidth.roundToPx() - val height = ShadeHeader.Dimensions.CollapsedHeight.roundToPx() + val height = CollapsedHeight.roundToPx() val childConstraints = Constraints.fixed((screenWidth - cutoutWidthPx) / 2, height) val startMeasurable = measurables[0][0] @@ -261,11 +255,10 @@ fun SceneScope.ExpandedShadeHeader( return } - val formatProgress = - animateSceneFloatAsState(1f, ShadeHeader.Keys.transitionProgress) - .unsafeCompositionState(initialValue = 1f) - val useExpandedFormat by - remember(formatProgress) { derivedStateOf { formatProgress.value > 0.5f } } + val useExpandedFormat by remember { + derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) } + } + val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsState() Box(modifier = modifier) { @@ -530,3 +523,15 @@ private fun SceneScope.PrivacyChip( modifier = modifier.element(ShadeHeader.Elements.PrivacyChip), ) } + +private fun shouldUseExpandedFormat(state: TransitionState): Boolean { + return when (state) { + is TransitionState.Idle -> { + state.currentScene == Scenes.QuickSettings + } + is TransitionState.Transition -> { + (state.isTransitioning(Scenes.Shade, Scenes.QuickSettings) && state.progress >= 0.5) || + (state.isTransitioning(Scenes.QuickSettings, Scenes.Shade) && state.progress < 0.5) + } + } +} 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 85798acd0dcd..9bd6f817cff3 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 @@ -17,6 +17,7 @@ package com.android.systemui.shade.ui.composable import android.view.ViewGroup +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.clipScrollableContainer @@ -33,6 +34,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -45,6 +47,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity @@ -70,6 +73,7 @@ import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.notifications.ui.composable.NotificationScrollingStack import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility +import com.android.systemui.qs.ui.composable.BrightnessMirror import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -302,12 +306,25 @@ private fun SceneScope.SplitShade( } } + val brightnessMirrorShowing by viewModel.brightnessMirrorViewModel.isShowing.collectAsState() + val contentAlpha by + animateFloatAsState( + targetValue = if (brightnessMirrorShowing) 0f else 1f, + label = "alphaAnimationBrightnessMirrorContentHiding", + ) + + val brightnessMirrorShowingModifier = Modifier.graphicsLayer { alpha = contentAlpha } + Box( modifier = modifier .fillMaxSize() .element(Shade.Elements.BackgroundScrim) - .background(colorResource(R.color.shade_scrim_background_dark)) + // Cannot set the alpha of the whole element to 0, because the mirror should be + // in the QS column. + .background( + colorResource(R.color.shade_scrim_background_dark).copy(alpha = contentAlpha) + ) ) { Column( modifier = Modifier.fillMaxSize(), @@ -317,61 +334,80 @@ private fun SceneScope.SplitShade( createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, - modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) + modifier = + Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) + .then(brightnessMirrorShowingModifier) ) Row(modifier = Modifier.fillMaxWidth().weight(1f)) { - Column( - verticalArrangement = Arrangement.Top, - modifier = - Modifier.weight(1f).fillMaxSize().thenIf(!isCustomizing) { - Modifier.padding(bottom = navBarBottomHeight) - }, - ) { + Box(modifier = Modifier.weight(1f)) { + BrightnessMirror( + viewModel = viewModel.brightnessMirrorViewModel, + qsSceneAdapter = viewModel.qsSceneAdapter, + // Need to remove the offset of the header height, as the mirror uses + // the position of the Brightness slider in the window + modifier = Modifier.offset(y = -ShadeHeader.Dimensions.CollapsedHeight) + ) Column( + verticalArrangement = Arrangement.Top, modifier = - Modifier.fillMaxSize().weight(1f).thenIf(!isCustomizing) { - Modifier.verticalNestedScrollToScene() - .verticalScroll( - quickSettingsScrollState, - enabled = isScrollable - ) - .clipScrollableContainer(Orientation.Horizontal) - } + Modifier.fillMaxSize().thenIf(!isCustomizing) { + Modifier.padding(bottom = navBarBottomHeight) + }, ) { - Box( + Column( modifier = - Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings) + Modifier.fillMaxSize() + .weight(1f) + .thenIf(!isCustomizing) { + Modifier.verticalNestedScrollToScene() + .verticalScroll( + quickSettingsScrollState, + enabled = isScrollable + ) + .clipScrollableContainer(Orientation.Horizontal) + } + .then(brightnessMirrorShowingModifier) ) { - QuickSettings( - qsSceneAdapter = viewModel.qsSceneAdapter, - heightProvider = { viewModel.qsSceneAdapter.qsHeight }, - isSplitShade = true, + 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(), - squishiness = tileSquishiness, ) } - - MediaIfVisible( - viewModel = viewModel, - mediaCarouselController = mediaCarouselController, - mediaHost = mediaHost, - modifier = Modifier.fillMaxWidth(), + FooterActionsWithAnimatedVisibility( + viewModel = footerActionsViewModel, + isCustomizing = isCustomizing, + lifecycleOwner = lifecycleOwner, + modifier = + Modifier.align(Alignment.CenterHorizontally) + .then(brightnessMirrorShowingModifier), ) } - FooterActionsWithAnimatedVisibility( - viewModel = footerActionsViewModel, - isCustomizing = isCustomizing, - lifecycleOwner = lifecycleOwner, - modifier = Modifier.align(Alignment.CenterHorizontally), - ) } NotificationScrollingStack( viewModel = viewModel.notifications, maxScrimTop = { 0f }, modifier = - Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight), + Modifier.weight(1f) + .fillMaxHeight() + .padding(bottom = navBarBottomHeight) + .then(brightnessMirrorShowingModifier), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt index b1fbe35eccd8..9f0da004730d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.component.anc.ui.composable import android.content.Context import android.view.ContextThemeWrapper import android.view.View +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -55,6 +56,7 @@ constructor( @Composable private fun Title() { Text( + modifier = Modifier.basicMarquee(), text = stringResource(R.string.volume_panel_noise_control_title), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/BottomComponentButtonSurface.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/BottomComponentButtonSurface.kt new file mode 100644 index 000000000000..167eb65da7ee --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/BottomComponentButtonSurface.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.volume.panel.component.button.ui.composable + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * Container to create a rim around the button. Both `Expandable` and `OutlinedIconToggleButton` + * have antialiasing problem when used with [androidx.compose.foundation.BorderStroke] and some + * contrast container color. + */ +// TODO(b/331584069): Remove this once antialiasing bug is fixed +@Composable +fun BottomComponentButtonSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Surface( + modifier = modifier.height(64.dp), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surface, + content = content, + ) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt index b721e41eda27..fc511e12ec54 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt @@ -16,13 +16,12 @@ package com.android.systemui.volume.panel.component.button.ui.composable -import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -37,7 +36,6 @@ import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.android.compose.animation.Expandable import com.android.systemui.animation.Expandable @@ -64,28 +62,28 @@ class ButtonComponent( verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Expandable( - modifier = - Modifier.height(64.dp).fillMaxWidth().semantics { - role = Role.Button - contentDescription = label - }, - color = MaterialTheme.colorScheme.primaryContainer, - shape = RoundedCornerShape(28.dp), - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - borderStroke = BorderStroke(8.dp, MaterialTheme.colorScheme.surface), - onClick = onClick, - ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Icon(modifier = Modifier.size(24.dp), icon = viewModel.icon) + BottomComponentButtonSurface { + Expandable( + modifier = + Modifier.fillMaxSize().padding(8.dp).semantics { + role = Role.Button + contentDescription = label + }, + color = MaterialTheme.colorScheme.primaryContainer, + shape = RoundedCornerShape(28.dp), + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + onClick = onClick, + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Icon(modifier = Modifier.size(24.dp), icon = viewModel.icon) + } } } Text( - modifier = Modifier.clearAndSetSemantics {}, + modifier = Modifier.clearAndSetSemantics {}.basicMarquee(), text = label, style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + maxLines = 2, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt index f6f07a5e3825..780e3f2de4c8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt @@ -16,28 +16,28 @@ package com.android.systemui.volume.panel.component.button.ui.composable -import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedIconToggleButton import androidx.compose.material3.Text 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.graphics.Color import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.android.systemui.common.ui.compose.Icon import com.android.systemui.volume.panel.component.button.ui.viewmodel.ToggleButtonViewModel @@ -62,32 +62,38 @@ class ToggleButtonComponent( verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - OutlinedIconToggleButton( - modifier = - Modifier.height(64.dp).fillMaxWidth().semantics { - role = Role.Switch - contentDescription = label - }, - checked = viewModel.isChecked, - onCheckedChange = onCheckedChange, - shape = RoundedCornerShape(28.dp), - colors = - IconButtonDefaults.outlinedIconToggleButtonColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - checkedContainerColor = MaterialTheme.colorScheme.primaryContainer, - checkedContentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ), - border = BorderStroke(8.dp, MaterialTheme.colorScheme.surface), - ) { - Icon(modifier = Modifier.size(24.dp), icon = viewModel.icon) + BottomComponentButtonSurface { + val colors = + if (viewModel.isChecked) { + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } else { + ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Button( + modifier = + Modifier.fillMaxSize().padding(8.dp).semantics { + role = Role.Switch + contentDescription = label + }, + onClick = { onCheckedChange(!viewModel.isChecked) }, + shape = RoundedCornerShape(28.dp), + colors = colors + ) { + Icon(modifier = Modifier.size(24.dp), icon = viewModel.icon) + } } + Text( - modifier = Modifier.clearAndSetSemantics {}, + modifier = Modifier.clearAndSetSemantics {}.basicMarquee(), text = label, style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + maxLines = 2, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt index 26086d1a9d0a..9f9bc623a6b3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/popup/ui/composable/VolumePanelPopup.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable @@ -82,7 +84,8 @@ constructor( title: @Composable (SystemUIDialog) -> Unit, content: @Composable (SystemUIDialog) -> Unit, ) { - Box(Modifier.fillMaxWidth()) { + val paneTitle = stringResource(R.string.accessibility_volume_settings) + Box(Modifier.fillMaxWidth().semantics(properties = { this.paneTitle = paneTitle })) { Column( modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp), verticalArrangement = Arrangement.spacedBy(20.dp), 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 b0b5a7d4d995..c74331477229 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 @@ -21,6 +21,7 @@ import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer @@ -45,6 +46,12 @@ 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.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset @@ -111,10 +118,16 @@ fun VolumePanelRadioButtonBar( horizontalArrangement = Arrangement.spacedBy(spacing) ) { for (itemIndex in items.indices) { + val item = items[itemIndex] Row( modifier = Modifier.height(48.dp) .weight(1f) + .semantics { + item.contentDescription?.let { contentDescription = it } + role = Role.Switch + selected = itemIndex == scope.selectedIndex + } .clickable( interactionSource = null, indication = null, @@ -123,7 +136,6 @@ fun VolumePanelRadioButtonBar( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - val item = items[itemIndex] if (item.icon !== Empty) { with(items[itemIndex]) { icon() } } @@ -137,13 +149,17 @@ fun VolumePanelRadioButtonBar( start = indicatorBackgroundPadding, top = labelIndicatorBackgroundSpacing, end = indicatorBackgroundPadding - ), + ) + .clearAndSetSemantics {}, horizontalArrangement = Arrangement.spacedBy(spacing), ) { for (itemIndex in items.indices) { + val cornersRadius = 4.dp TextButton( modifier = Modifier.weight(1f), onClick = { items[itemIndex].onItemSelected() }, + shape = RoundedCornerShape(cornersRadius), + contentPadding = PaddingValues(cornersRadius) ) { val item = items[itemIndex] if (item.icon !== Empty) { @@ -292,6 +308,7 @@ interface VolumePanelRadioButtonBarScope { onItemSelected: () -> Unit, icon: @Composable RowScope.() -> Unit = Empty, label: @Composable RowScope.() -> Unit = Empty, + contentDescription: String? = null, ) } @@ -313,6 +330,7 @@ private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScop onItemSelected: () -> Unit, icon: @Composable RowScope.() -> Unit, label: @Composable RowScope.() -> Unit, + contentDescription: String?, ) { require(!isSelected || !hasSelectedItem) { "Only one item should be selected at a time" } if (isSelected) { @@ -323,6 +341,7 @@ private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScop onItemSelected = onItemSelected, icon = icon, label = label, + contentDescription = contentDescription, ) ) } @@ -336,6 +355,7 @@ private class Item( val onItemSelected: () -> Unit, val icon: @Composable RowScope.() -> Unit, val label: @Composable RowScope.() -> Unit, + val contentDescription: String?, ) private const val UNSET_OFFSET = -1 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 71b3e8a6a102..eed54dab6faf 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 @@ -16,12 +16,14 @@ package com.android.systemui.volume.panel.component.spatialaudio.ui.composable +import androidx.compose.foundation.basicMarquee import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.android.systemui.animation.Expandable @@ -49,6 +51,7 @@ constructor( @Composable private fun Title() { Text( + modifier = Modifier.basicMarquee(), text = stringResource(R.string.volume_panel_spatial_audio_title), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, @@ -71,9 +74,11 @@ constructor( } VolumePanelRadioButtonBar { for (buttonViewModel in enabledModelStates) { + val label = buttonViewModel.button.label.toString() item( isSelected = buttonViewModel.button.isChecked, onItemSelected = { viewModel.setEnabled(buttonViewModel.model) }, + contentDescription = label, icon = { Icon( icon = buttonViewModel.button.icon, @@ -82,9 +87,12 @@ constructor( }, label = { Text( - text = buttonViewModel.button.label.toString(), + modifier = Modifier.basicMarquee(), + text = label, style = MaterialTheme.typography.labelMedium, color = buttonViewModel.labelColor.toColor(), + textAlign = TextAlign.Center, + maxLines = 2 ) } ) 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 d31064ae23b3..19d3f599ef31 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,10 +17,10 @@ package com.android.systemui.volume.panel.component.volume.ui.composable import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize 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.Text import androidx.compose.runtime.Composable @@ -32,7 +32,6 @@ 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 import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription @@ -130,24 +129,20 @@ private fun SliderIcon( 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) }, - ) - } + val boxModifier = + if (isTappable) { + modifier.clickable( + onClick = onIconTapped, + interactionSource = null, + indication = null + ) + } else { + modifier + } + .fillMaxSize() + Box( + modifier = boxModifier, + 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/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt index dd767817a5ae..9ea20b9da4b6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt @@ -20,6 +20,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState @@ -27,6 +28,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastSumBy import com.android.systemui.volume.panel.ui.layout.ComponentsLayout @Composable @@ -53,6 +55,14 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( modifier = Modifier.fillMaxWidth().wrapContentHeight(), horizontalArrangement = Arrangement.spacedBy(if (isLargeScreen) 28.dp else 20.dp), ) { + val visibleComponentsCount = + layout.footerComponents.fastSumBy { if (it.isVisible) 1 else 0 } + + // Center footer component if there is only one present + if (visibleComponentsCount == 1) { + Spacer(modifier = Modifier.weight(0.5f)) + } + for (component in layout.footerComponents) { AnimatedVisibility( visible = component.isVisible, @@ -63,6 +73,10 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( } } } + + if (visibleComponentsCount == 1) { + Spacer(modifier = Modifier.weight(0.5f)) + } } } } 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 e7cb5a45906d..a8a1d881b907 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 @@ -261,16 +261,19 @@ private fun shouldDrawElement( scene: Scene, element: Element, ): Boolean { - val transition = layoutImpl.state.currentTransition - - // Always draw the element if there is no ongoing transition or if the element is not shared or - // if the current scene is the one that is currently over scrolling with [OverscrollSpec]. - if ( - transition == null || - transition.fromScene !in element.sceneStates || - transition.toScene !in element.sceneStates || - transition.currentOverscrollSpec?.scene == scene.key - ) { + val transition = layoutImpl.state.currentTransition ?: return true + + val inFromScene = transition.fromScene in element.sceneStates + val inToScene = transition.toScene in element.sceneStates + + // If an element is not present in any scene, it should not be drawn. + if (!inFromScene && !inToScene) { + return false + } + + // Always draw if the element is not shared or if the current scene is the one that is currently + // over scrolling with [OverscrollSpec]. + if (!inFromScene || !inToScene || transition.currentOverscrollSpec?.scene == scene.key) { return true } 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 2453e251b5a4..458a2b99f9ce 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 @@ -45,6 +45,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.intermediateLayout import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -175,6 +176,60 @@ class ElementTest { } @Test + fun elementsNotInTransition_shouldNotBeDrawn() { + val nFrames = 20 + val frameDuration = 16L + val tween = tween<Float>(nFrames * frameDuration.toInt()) + val layoutSize = 100.dp + val elementSize = 50.dp + val elementOffset = 20.dp + + lateinit var changeScene: (SceneKey) -> Unit + + rule.testTransition( + from = TestScenes.SceneA, + to = TestScenes.SceneB, + transitionLayout = { currentScene, onChangeScene -> + changeScene = onChangeScene + + SceneTransitionLayout( + currentScene, + onChangeScene, + transitions { + from(TestScenes.SceneA, to = TestScenes.SceneB) { spec = tween } + from(TestScenes.SceneB, to = TestScenes.SceneC) { spec = tween } + }, + ) { + scene(TestScenes.SceneA) { + Box(Modifier.size(layoutSize)) { + // Transformed element + Element( + TestElements.Bar, + elementSize, + elementOffset, + ) + } + } + scene(TestScenes.SceneB) { Box(Modifier.size(layoutSize)) } + scene(TestScenes.SceneC) { Box(Modifier.size(layoutSize)) } + } + }, + ) { + // Start transition from SceneA to SceneB + at(1 * frameDuration) { + onElement(TestElements.Bar).assertExists() + + // Start transition from SceneB to SceneC + changeScene(TestScenes.SceneC) + } + + at(2 * frameDuration) { onElement(TestElements.Bar).assertIsNotDisplayed() } + + at(3 * frameDuration) { onElement(TestElements.Bar).assertDoesNotExist() } + } + } + + @Test fun onlyMovingElements_noLayout_onlyPlacement() { val nFrames = 20 val layoutSize = 100.dp diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index cb8cebf80767..81878aaf4a18 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -179,10 +179,14 @@ class AuthenticationInteractorTest : SysuiTestCase() { setAutoConfirmFeatureEnabled(true) } assertThat(isAutoConfirmEnabled).isTrue() - val shorterPin = - FakeAuthenticationRepository.DEFAULT_PIN.toMutableList().apply { removeLast() } + val defaultPin = FakeAuthenticationRepository.DEFAULT_PIN.toMutableList() - assertSkipped(underTest.authenticate(shorterPin, tryAutoConfirm = true)) + assertSkipped( + underTest.authenticate( + defaultPin.subList(0, defaultPin.size - 1), + tryAutoConfirm = true + ) + ) assertThat(underTest.lockoutEndTimestamp).isNull() assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(0) } @@ -201,7 +205,6 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertFailed( underTest.authenticate(wrongPin, tryAutoConfirm = true), - assertNoResultEvents = true, ) } @@ -219,7 +222,6 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertFailed( underTest.authenticate(longerPin, tryAutoConfirm = true), - assertNoResultEvents = true, ) } @@ -547,14 +549,9 @@ class AuthenticationInteractorTest : SysuiTestCase() { private fun assertFailed( authenticationResult: AuthenticationResult, - assertNoResultEvents: Boolean = false, ) { assertThat(authenticationResult).isEqualTo(AuthenticationResult.FAILED) - if (assertNoResultEvents) { - assertThat(onAuthenticationResult).isNull() - } else { - assertThat(onAuthenticationResult).isFalse() - } + assertThat(onAuthenticationResult).isFalse() } private fun assertSkipped( 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 a0db482cc360..5bb36a0acbdf 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 @@ -252,7 +252,14 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAutoConfirm_whenCorrect() = testScope.runTest { + // TODO(b/332768183) remove this after the bug if fixed. + // Collect the flow so that it is hot, in the real application this is done by using a + // refreshingFlow that relies on the UI to make this flow hot. + val autoConfirmEnabled by + collectLastValue(authenticationInteractor.isAutoConfirmEnabled) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) + + assertThat(autoConfirmEnabled).isTrue() val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) lockDeviceAndOpenPinBouncer() @@ -264,9 +271,17 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAutoConfirm_whenWrong() = testScope.runTest { + // TODO(b/332768183) remove this after the bug if fixed. + // Collect the flow so that it is hot, in the real application this is done by using a + // refreshingFlow that relies on the UI to make this flow hot. + val autoConfirmEnabled by + collectLastValue(authenticationInteractor.isAutoConfirmEnabled) + val currentScene by collectLastValue(sceneInteractor.currentScene) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) + + assertThat(autoConfirmEnabled).isTrue() lockDeviceAndOpenPinBouncer() FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit -> diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 36919d0c74a4..9a13bb261aa7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -827,7 +827,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test - fun isAuthenticatedIsResetToFalseWhenKeyguardDoneAnimationsFinished() = + fun isAuthenticatedIsResetToFalseWhenTransitioningToGone() = testScope.runTest { initCollectors() allPreconditionsToRunFaceAuthAreTrue() @@ -840,7 +840,13 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { assertThat(authenticated()).isTrue() - keyguardRepository.keyguardDoneAnimationsFinished() + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) assertThat(authenticated()).isFalse() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt index f517cec040a0..31337a635bfb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.burnInInteractor @@ -60,10 +61,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private lateinit var underTest: AodBurnInViewModel - private var burnInParameters = - BurnInParameters( - clockControllerProvider = { clockController }, - ) + private var burnInParameters = BurnInParameters() private val burnInFlow = MutableStateFlow(BurnInModel()) @Before @@ -76,6 +74,7 @@ class AodBurnInViewModelTest : SysuiTestCase() { whenever(goneToAodTransitionViewModel.enterFromTopTranslationY(anyInt())) .thenReturn(emptyFlow()) kosmos.goneToAodTransitionViewModel = goneToAodTransitionViewModel + kosmos.fakeKeyguardClockRepository.setCurrentClock(clockController) underTest = kosmos.aodBurnInViewModel } 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 27c4ec125b59..f2eb7f44600e 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 @@ -35,6 +35,7 @@ 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.settings.brightness.MirrorController import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shared.model.ShadeMode @@ -496,4 +497,33 @@ class QSSceneAdapterImplTest : SysuiTestCase() { runCurrent() verify(qsImpl!!).setInSplitShade(true) } + + @Test + fun requestCloseCustomizer() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + underTest.requestCloseCustomizer() + verify(qsImpl!!).closeCustomizer() + } + + @Test + fun setBrightnessMirrorController() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val mirrorController = mock<MirrorController>() + underTest.setBrightnessMirrorController(mirrorController) + + verify(qsImpl!!).setBrightnessMirrorController(mirrorController) + + underTest.setBrightnessMirrorController(null) + verify(qsImpl!!).setBrightnessMirrorController(null) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index ef385673c950..426f94d3e96a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor @@ -108,6 +109,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { underTest = QuickSettingsSceneViewModel( + brightnessMirrorViewModel = kosmos.brightnessMirrorViewModel, shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, notifications = kosmos.notificationsPlaceholderViewModel, @@ -133,18 +135,13 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { } @Test - fun destinationsCustomizing() = + fun destinationsCustomizing_noDestinations() = testScope.runTest { overrideResource(R.bool.config_use_split_notification_shade, false) val destinations by collectLastValue(underTest.destinationScenes) qsFlexiglassAdapter.setCustomizing(true) - assertThat(destinations) - .isEqualTo( - mapOf( - Back to UserActionResult(Scenes.QuickSettings), - ) - ) + assertThat(destinations).isEmpty() } @Test @@ -164,18 +161,13 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { } @Test - fun destinations_whenCustomizing_inSplitShade() = + fun destinations_whenCustomizing_inSplitShade_noDestinations() = testScope.runTest { overrideResource(R.bool.config_use_split_notification_shade, true) val destinations by collectLastValue(underTest.destinationScenes) qsFlexiglassAdapter.setCustomizing(true) - assertThat(destinations) - .isEqualTo( - mapOf( - Back to UserActionResult(Scenes.QuickSettings), - ) - ) + assertThat(destinations).isEmpty() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 98cbda2efd2b..9856f9050c4b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -74,6 +74,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.settings.FakeDisplayTracker +import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor @@ -259,6 +260,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, notifications = kosmos.notificationsPlaceholderViewModel, + brightnessMirrorViewModel = kosmos.brightnessMirrorViewModel, mediaDataManager = mediaDataManager, shadeInteractor = kosmos.shadeInteractor, footerActionsController = kosmos.footerActionsController, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index 3d6619272dbe..ae31058e7a54 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -140,4 +140,23 @@ class SceneContainerRepositoryTest : SysuiTestCase() { ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } + + @Test + fun previousScene() = + testScope.runTest { + val underTest = kosmos.sceneContainerRepository + val currentScene by collectLastValue(underTest.currentScene) + val previousScene by collectLastValue(underTest.previousScene) + + assertThat(previousScene).isNull() + + val firstScene = currentScene + underTest.changeScene(Scenes.Shade) + assertThat(previousScene).isEqualTo(firstScene) + assertThat(currentScene).isEqualTo(Scenes.Shade) + + underTest.changeScene(Scenes.QuickSettings) + assertThat(previousScene).isEqualTo(Scenes.Shade) + assertThat(currentScene).isEqualTo(Scenes.QuickSettings) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 143c0f277209..b179c30e60b9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -291,4 +291,19 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(isVisible).isFalse() } + + @Test + fun previousScene() = + testScope.runTest { + val currentScene by collectLastValue(underTest.currentScene) + val previousScene by collectLastValue(underTest.previousScene) + assertThat(previousScene).isNull() + + val firstScene = currentScene + underTest.changeScene(toScene = Scenes.Shade, "reason") + assertThat(previousScene).isEqualTo(firstScene) + + underTest.changeScene(toScene = Scenes.QuickSettings, "reason") + assertThat(previousScene).isEqualTo(Scenes.Shade) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/data/repository/BrightnessMirrorShowingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/data/repository/BrightnessMirrorShowingRepositoryTest.kt new file mode 100644 index 000000000000..a1af70b316ee --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/data/repository/BrightnessMirrorShowingRepositoryTest.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.settings.brightness.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.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 BrightnessMirrorShowingRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val underTest = BrightnessMirrorShowingRepository() + + @Test + fun isShowing_setAndFlow() = + kosmos.testScope.runTest { + val isShowing by collectLastValue(underTest.isShowing) + + assertThat(isShowing).isFalse() + + underTest.setMirrorShowing(true) + assertThat(isShowing).isTrue() + + underTest.setMirrorShowing(false) + assertThat(isShowing).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/domain/interactor/BrightnessMirrorShowingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/domain/interactor/BrightnessMirrorShowingInteractorTest.kt new file mode 100644 index 000000000000..31d6df250af3 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/domain/interactor/BrightnessMirrorShowingInteractorTest.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.settings.brightness.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.kosmos.testScope +import com.android.systemui.settings.brightness.data.repository.brightnessMirrorShowingRepository +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 BrightnessMirrorShowingInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val underTest = + BrightnessMirrorShowingInteractor(kosmos.brightnessMirrorShowingRepository) + + @Test + fun isShowing_setAndFlow() = + kosmos.testScope.runTest { + val isShowing by collectLastValue(underTest.isShowing) + + assertThat(isShowing).isFalse() + + underTest.setMirrorShowing(true) + assertThat(isShowing).isTrue() + + underTest.setMirrorShowing(false) + assertThat(isShowing).isFalse() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflaterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflaterTest.kt new file mode 100644 index 000000000000..6de7f403a745 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflaterTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings.brightness.ui.binder + +import android.content.applicationContext +import android.view.ContextThemeWrapper +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import com.android.systemui.settings.brightnessSliderControllerFactory +import com.android.systemui.testKosmos +import com.android.systemui.util.Assert +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BrightnessMirrorInflaterTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private val themedContext = + ContextThemeWrapper(kosmos.applicationContext, R.style.Theme_SystemUI_QuickSettings) + + @Test + fun inflate_sliderViewAddedToFrame() { + Assert.setTestThread(Thread.currentThread()) + + val (frame, sliderController) = + BrightnessMirrorInflater.inflate( + themedContext, + kosmos.brightnessSliderControllerFactory + ) + + assertThat(sliderController.rootView.parent).isSameInstanceAs(frame) + + Assert.setTestThread(null) + } + + @Test + fun inflate_frameAndSliderViewVisible() { + Assert.setTestThread(Thread.currentThread()) + + val (frame, sliderController) = + BrightnessMirrorInflater.inflate( + themedContext, + kosmos.brightnessSliderControllerFactory, + ) + + assertThat(frame.visibility).isEqualTo(View.VISIBLE) + assertThat(sliderController.rootView.visibility).isEqualTo(View.VISIBLE) + + Assert.setTestThread(null) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelTest.kt new file mode 100644 index 000000000000..09fc6f9ad96d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings.brightness.ui.viewmodel + +import android.content.applicationContext +import android.content.res.mainResources +import android.view.ContextThemeWrapper +import android.view.View +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.res.R +import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor +import com.android.systemui.settings.brightness.ui.binder.BrightnessMirrorInflater +import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel +import com.android.systemui.settings.brightness.ui.viewModel.LocationAndSize +import com.android.systemui.settings.brightnessSliderControllerFactory +import com.android.systemui.testKosmos +import com.android.systemui.util.Assert +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +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 BrightnessMirrorViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val themedContext = + ContextThemeWrapper(kosmos.applicationContext, R.style.Theme_SystemUI_QuickSettings) + + private val underTest = + with(kosmos) { + BrightnessMirrorViewModel( + brightnessMirrorShowingInteractor, + mainResources, + brightnessSliderControllerFactory, + ) + } + + @Test + fun showHideMirror_isShowing() = + with(kosmos) { + testScope.runTest { + val showing by collectLastValue(underTest.isShowing) + + assertThat(showing).isFalse() + + underTest.showMirror() + assertThat(showing).isTrue() + + underTest.hideMirror() + assertThat(showing).isFalse() + } + } + + @Test + fun setLocationInWindow_correctLocationAndSize() = + with(kosmos) { + testScope.runTest { + val locationAndSize by collectLastValue(underTest.locationAndSize) + + val x = 20 + val y = 100 + val height = 50 + val width = 200 + val padding = + mainResources.getDimensionPixelSize(R.dimen.rounded_slider_background_padding) + + val mockView = + mock<View> { + whenever(getLocationInWindow(any())).then { + it.getArgument<IntArray>(0).apply { + this[0] = x + this[1] = y + } + Unit + } + + whenever(measuredHeight).thenReturn(height) + whenever(measuredWidth).thenReturn(width) + } + + underTest.setLocationAndSize(mockView) + + assertThat(locationAndSize) + .isEqualTo( + // Adjust for padding around the view + LocationAndSize( + yOffset = y - padding, + width = width + 2 * padding, + height = height + 2 * padding, + ) + ) + } + } + + @Test + fun setLocationInWindow_paddingSetToRootView() = + with(kosmos) { + Assert.setTestThread(Thread.currentThread()) + val padding = + mainResources.getDimensionPixelSize(R.dimen.rounded_slider_background_padding) + + val view = mock<View>() + + val (_, sliderController) = + BrightnessMirrorInflater.inflate( + themedContext, + brightnessSliderControllerFactory, + ) + underTest.setToggleSlider(sliderController) + + underTest.setLocationAndSize(view) + + with(sliderController.rootView) { + assertThat(paddingBottom).isEqualTo(padding) + assertThat(paddingTop).isEqualTo(padding) + assertThat(paddingLeft).isEqualTo(padding) + assertThat(paddingRight).isEqualTo(padding) + } + + Assert.setTestThread(null) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt index 05f4f3c93917..cbbcce96873b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt @@ -189,6 +189,7 @@ class ShadeControllerSceneImplTest : SysuiTestCase() { private fun TestScope.setDeviceEntered(isEntered: Boolean) { if (isEntered) { + // Unlock the device marking the device has entered. kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( SuccessFingerprintAuthenticationStatus(0, true) ) 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 77109d65fadc..7a681b383aad 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 @@ -39,6 +39,7 @@ import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.privacyChipInteractor import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor @@ -127,6 +128,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsSceneAdapter, notifications = kosmos.notificationsPlaceholderViewModel, + brightnessMirrorViewModel = kosmos.brightnessMirrorViewModel, mediaDataManager = mediaDataManager, shadeInteractor = kosmos.shadeInteractor, footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory, 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 53522e276112..a3cf92986bd6 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 @@ -31,7 +31,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.notification.stack.shared.model.ShadeScrimBounds -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationScrollViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -57,27 +58,44 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { } private val testScope = kosmos.testScope private val placeholderViewModel by lazy { kosmos.notificationsPlaceholderViewModel } - private val appearanceViewModel by lazy { kosmos.notificationStackAppearanceViewModel } + private val appearanceViewModel by lazy { kosmos.notificationScrollViewModel } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } @Test fun updateBounds() = testScope.runTest { - val clipping by collectLastValue(appearanceViewModel.shadeScrimClipping) - - val top = 200f - val left = 0f - val bottom = 550f - val right = 100f - placeholderViewModel.onBoundsChanged( - left = left, - top = top, - right = right, - bottom = bottom + val radius = MutableStateFlow(32) + val leftOffset = MutableStateFlow(0) + val shape by collectLastValue(appearanceViewModel.shadeScrimShape(radius, leftOffset)) + + placeholderViewModel.onScrimBoundsChanged( + ShadeScrimBounds(left = 0f, top = 200f, right = 100f, bottom = 550f) ) - assertThat(clipping?.bounds) - .isEqualTo(ShadeScrimBounds(left = left, top = top, right = right, bottom = bottom)) + assertThat(shape) + .isEqualTo( + ShadeScrimShape( + bounds = + ShadeScrimBounds(left = 0f, top = 200f, right = 100f, bottom = 550f), + topRadius = 32, + bottomRadius = 0 + ) + ) + + leftOffset.value = 200 + radius.value = 24 + placeholderViewModel.onScrimBoundsChanged( + ShadeScrimBounds(left = 210f, top = 200f, right = 300f, bottom = 550f) + ) + assertThat(shape) + .isEqualTo( + ShadeScrimShape( + bounds = + ShadeScrimBounds(left = 10f, top = 200f, right = 100f, bottom = 550f), + topRadius = 24, + bottomRadius = 0 + ) + ) } @Test 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 dc928c49fbd3..50b77dcf9468 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 @@ -68,11 +68,11 @@ class NotificationStackAppearanceInteractorTest : SysuiTestCase() { kosmos.shadeRepository.setShadeMode(ShadeMode.Single) assertThat(stackRounding) - .isEqualTo(ShadeScrimRounding(roundTop = true, roundBottom = false)) + .isEqualTo(ShadeScrimRounding(isTopRounded = true, isBottomRounded = false)) kosmos.shadeRepository.setShadeMode(ShadeMode.Split) assertThat(stackRounding) - .isEqualTo(ShadeScrimRounding(roundTop = true, roundBottom = true)) + .isEqualTo(ShadeScrimRounding(isTopRounded = true, isBottomRounded = true)) } @Test(expected = IllegalStateException::class) 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 d4a704919a49..1f0812da9fe9 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 @@ -16,14 +16,10 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel -import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds @@ -40,23 +36,21 @@ class NotificationsPlaceholderViewModelTest : SysuiTestCase() { private val underTest = kosmos.notificationsPlaceholderViewModel @Test - @DisableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun onBoundsChanged_setsNotificationContainerBounds() = + fun onBoundsChanged() = kosmos.testScope.runTest { - underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f) - val containerBounds by - collectLastValue(kosmos.keyguardInteractor.notificationContainerBounds) + val bounds = ShadeScrimBounds(left = 5f, top = 15f, right = 25f, bottom = 35f) + underTest.onScrimBoundsChanged(bounds) val stackBounds by collectLastValue(kosmos.notificationStackAppearanceInteractor.shadeScrimBounds) - assertThat(containerBounds) - .isEqualTo(NotificationContainerBounds(top = 5f, bottom = 5f)) - assertThat(stackBounds) - .isEqualTo(ShadeScrimBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) + assertThat(stackBounds).isEqualTo(bounds) } @Test - fun onContentTopChanged_setsContentTop() { - underTest.onContentTopChanged(padding = 5f) - assertThat(kosmos.notificationStackAppearanceInteractor.stackTop.value).isEqualTo(5f) - } + fun onStackBoundsChanged() = + kosmos.testScope.runTest { + underTest.onStackBoundsChanged(top = 5f, bottom = 500f) + assertThat(kosmos.notificationStackAppearanceInteractor.stackTop.value).isEqualTo(5f) + assertThat(kosmos.notificationStackAppearanceInteractor.stackBottom.value) + .isEqualTo(500f) + } } 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 509a82da8eb9..ac8387f28e6c 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 @@ -19,14 +19,17 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -136,12 +139,12 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) - val dimens by collectLastValue(underTest.configurationBasedDimensions) + val paddingTop by collectLastValue(underTest.paddingTopDimen) configurationRepository.onAnyConfigurationChange() // Should directly use the header height (flagged off value) - assertThat(dimens!!.paddingTop).isEqualTo(10) + assertThat(paddingTop).isEqualTo(10) } @Test @@ -154,11 +157,11 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) - val dimens by collectLastValue(underTest.configurationBasedDimensions) + val paddingTop by collectLastValue(underTest.paddingTopDimen) configurationRepository.onAnyConfigurationChange() // Should directly use the header height (flagged on value) - assertThat(dimens!!.paddingTop).isEqualTo(5) + assertThat(paddingTop).isEqualTo(5) } @Test @@ -168,11 +171,11 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) - val dimens by collectLastValue(underTest.configurationBasedDimensions) + val paddingTop by collectLastValue(underTest.paddingTopDimen) configurationRepository.onAnyConfigurationChange() - assertThat(dimens!!.paddingTop).isEqualTo(0) + assertThat(paddingTop).isEqualTo(0) } @Test @@ -200,9 +203,9 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX, FLAG_SCENE_CONTAINER) fun validateMarginTopWithLargeScreenHeader_refactorFlagOff_usesResource() = testScope.runTest { - mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) val headerResourceHeight = 50 val headerHelperHeight = 100 whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) @@ -219,6 +222,27 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) + @EnableSceneContainer + fun validateMarginTopWithLargeScreenHeader_refactorFlagOff_sceneContainerFlagOn_stillZero() = + testScope.runTest { + val headerResourceHeight = 50 + val headerHelperHeight = 100 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(headerHelperHeight) + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) + overrideResource(R.dimen.notification_panel_margin_top, 0) + + val dimens by collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + + assertThat(dimens!!.marginTop).isEqualTo(0) + } + + @Test + @DisableFlags(FLAG_SCENE_CONTAINER) fun validateMarginTopWithLargeScreenHeader_refactorFlagOn_usesHelper() = testScope.runTest { mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) @@ -238,6 +262,26 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer + fun validateMarginTopWithLargeScreenHeader_sceneContainerFlagOn_stillZero() = + testScope.runTest { + mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_HEIGHT_FIX) + val headerResourceHeight = 50 + val headerHelperHeight = 100 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(headerHelperHeight) + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) + overrideResource(R.dimen.notification_panel_margin_top, 0) + + val dimens by collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + + assertThat(dimens!!.marginTop).isEqualTo(0) + } + + @Test fun glanceableHubAlpha_lockscreenToHub() = testScope.runTest { val alpha by collectLastValue(underTest.glanceableHubAlpha) @@ -661,6 +705,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_SCENE_CONTAINER) fun translationYUpdatesOnKeyguard() = testScope.runTest { val translationY by collectLastValue(underTest.translationY(BurnInParameters())) diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml index 173d57b335f0..3b6b5a0db393 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml @@ -91,6 +91,7 @@ android:layout_height="wrap_content" android:contentDescription="@string/keyguard_accessibility_password" android:gravity="center_horizontal" + android:layout_gravity="center" android:imeOptions="flagForceAscii|actionDone" android:inputType="textPassword" android:maxLength="500" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml index 909d4fca5018..5aac65396532 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -55,6 +55,7 @@ android:layout_height="wrap_content" android:contentDescription="@string/keyguard_accessibility_password" android:gravity="center" + android:layout_gravity="center" android:singleLine="true" android:textStyle="normal" android:inputType="textPassword" diff --git a/packages/SystemUI/res/anim/slide_in_up.xml b/packages/SystemUI/res/anim/slide_in_up.xml new file mode 100644 index 000000000000..6089a2809de3 --- /dev/null +++ b/packages/SystemUI/res/anim/slide_in_up.xml @@ -0,0 +1,21 @@ +<?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. +--> + +<translate + xmlns:android="http://schemas.android.com/apk/res/android" + android:fromYDelta="100%p" + android:toYDelta="0" + android:duration="@android:integer/config_shortAnimTime" /> diff --git a/packages/SystemUI/res/anim/slide_out_down.xml b/packages/SystemUI/res/anim/slide_out_down.xml new file mode 100644 index 000000000000..5a7b591fce9e --- /dev/null +++ b/packages/SystemUI/res/anim/slide_out_down.xml @@ -0,0 +1,21 @@ +<?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. +--> + +<translate + xmlns:android="http://schemas.android.com/apk/res/android" + android:fromYDelta="0" + android:toYDelta="100%p" + android:duration="@android:integer/config_shortAnimTime" /> diff --git a/packages/SystemUI/res/drawable/ic_bugreport.xml b/packages/SystemUI/res/drawable/ic_bugreport.xml new file mode 100644 index 000000000000..ed1c6c723543 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_bugreport.xml @@ -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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="#FF000000" + android:pathData="M20,10V8h-2.81c-0.45,-0.78 -1.07,-1.46 -1.82,-1.96L17,4.41L15.59,3l-2.17,2.17c-0.03,-0.01 -0.05,-0.01 -0.08,-0.01c-0.16,-0.04 -0.32,-0.06 -0.49,-0.09c-0.06,-0.01 -0.11,-0.02 -0.17,-0.03C12.46,5.02 12.23,5 12,5h0c-0.49,0 -0.97,0.07 -1.42,0.18l0.02,-0.01L8.41,3L7,4.41l1.62,1.63l0.01,0C7.88,6.54 7.26,7.22 6.81,8H4v2h2.09C6.03,10.33 6,10.66 6,11v1H4v2h2v1c0,0.34 0.04,0.67 0.09,1H4v2h2.81c1.04,1.79 2.97,3 5.19,3h0c2.22,0 4.15,-1.21 5.19,-3H20v-2h-2.09l0,0c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1l0,0H20zM16,15c0,2.21 -1.79,4 -4,4c-2.21,0 -4,-1.79 -4,-4v-4c0,-2.21 1.79,-4 4,-4h0c2.21,0 4,1.79 4,4V15z"/> + <path + android:fillColor="#FF000000" + android:pathData="M10,14h4v2h-4z"/> + <path + android:fillColor="#FF000000" + android:pathData="M10,10h4v2h-4z"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml b/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml index 1b12e74141a7..0406f0e4304e 100644 --- a/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml +++ b/packages/SystemUI/res/drawable/ic_shortcutlist_search.xml @@ -14,12 +14,15 @@ limitations under the License --> <vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="48" android:viewportHeight="48" - android:tint="?android:attr/textColorSecondary"> + android:tint="?androidprv:attr/materialColorOnSurfaceVariant"> <path android:fillColor="@android:color/white" + android:strokeColor="@android:color/white" + android:strokeWidth="2" android:pathData="M39.8,41.95 L26.65,28.8Q25.15,30.1 23.15,30.825Q21.15,31.55 18.9,31.55Q13.5,31.55 9.75,27.8Q6,24.05 6,18.75Q6,13.45 9.75,9.7Q13.5,5.95 18.85,5.95Q24.15,5.95 27.875,9.7Q31.6,13.45 31.6,18.75Q31.6,20.9 30.9,22.9Q30.2,24.9 28.8,26.65L42,39.75ZM18.85,28.55Q22.9,28.55 25.75,25.675Q28.6,22.8 28.6,18.75Q28.6,14.7 25.75,11.825Q22.9,8.95 18.85,8.95Q14.75,8.95 11.875,11.825Q9,14.7 9,18.75Q9,22.8 11.875,25.675Q14.75,28.55 18.85,28.55Z"/> -</vector>
\ No newline at end of file +</vector> diff --git a/packages/SystemUI/res/drawable/shortcut_button_colored.xml b/packages/SystemUI/res/drawable/shortcut_button_colored.xml index bf908532ac17..2e2d9b9a3e35 100644 --- a/packages/SystemUI/res/drawable/shortcut_button_colored.xml +++ b/packages/SystemUI/res/drawable/shortcut_button_colored.xml @@ -21,7 +21,7 @@ android:color="?android:attr/colorControlHighlight"> <item> <shape android:shape="rectangle"> - <corners android:radius="16dp"/> + <corners android:radius="@dimen/ksh_button_corner_radius"/> <solid android:color="?androidprv:attr/materialColorSurfaceBright"/> </shape> </item> diff --git a/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml b/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml index f692ed975319..5b88bb922a9e 100644 --- a/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml +++ b/packages/SystemUI/res/drawable/shortcut_button_focus_colored.xml @@ -21,7 +21,7 @@ android:color="?android:attr/colorControlHighlight"> <item> <shape android:shape="rectangle"> - <corners android:radius="16dp"/> + <corners android:radius="@dimen/ksh_button_corner_radius"/> <solid android:color="?androidprv:attr/materialColorPrimary"/> </shape> </item> diff --git a/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml b/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml index 6ce3eaecc147..aa0b2689f5bf 100644 --- a/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml +++ b/packages/SystemUI/res/drawable/shortcut_dialog_bg.xml @@ -17,8 +17,8 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="?android:attr/colorBackground"/> - <corners android:topLeftRadius="16dp" - android:topRightRadius="16dp" + <corners android:topLeftRadius="@dimen/ksh_dialog_top_corner_radius" + android:topRightRadius="@dimen/ksh_dialog_top_corner_radius" android:bottomLeftRadius="0dp" android:bottomRightRadius="0dp"/> -</shape>
\ No newline at end of file +</shape> diff --git a/packages/SystemUI/res/drawable/shortcut_search_background.xml b/packages/SystemUI/res/drawable/shortcut_search_background.xml index 66fc191cb736..d6847f0abb8d 100644 --- a/packages/SystemUI/res/drawable/shortcut_search_background.xml +++ b/packages/SystemUI/res/drawable/shortcut_search_background.xml @@ -19,8 +19,8 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> <item> <shape android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorSurface" /> - <corners android:radius="24dp" /> + <solid android:color="?androidprv:attr/materialColorSurfaceBright" /> + <corners android:radius="@dimen/ksh_search_box_corner_radius" /> </shape> </item> -</layer-list>
\ No newline at end of file +</layer-list> diff --git a/packages/SystemUI/res/drawable/shortcut_search_cancel_button.xml b/packages/SystemUI/res/drawable/shortcut_search_cancel_button.xml index 6c4d4fbcc1fe..2675906580f1 100644 --- a/packages/SystemUI/res/drawable/shortcut_search_cancel_button.xml +++ b/packages/SystemUI/res/drawable/shortcut_search_cancel_button.xml @@ -20,7 +20,7 @@ <shape android:shape="oval"> <size android:width="24dp" android:height="24dp" /> - <solid android:color="?androidprv:attr/colorSurface"/> + <solid android:color="?androidprv:attr/materialColorSurfaceBright"/> </shape> </item> </ripple> 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 1777bdf92786..dabfe9df6d2e 100644 --- a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml @@ -39,7 +39,6 @@ android:layout_height="match_parent"> android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="0.8" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> @@ -60,11 +59,12 @@ android:layout_height="match_parent"> android:id="@+id/scrollView" android:layout_width="0dp" android:layout_height="0dp" - android:fillViewport="true" - android:padding="24dp" - app:layout_constrainedHeight="true" - app:layout_constrainedWidth="true" - app:layout_constraintBottom_toTopOf="@+id/buttonBarrier" + android:paddingBottom="16dp" + android:paddingLeft="24dp" + android:paddingRight="12dp" + android:paddingTop="24dp" + android:fadeScrollbars="false" + app:layout_constraintBottom_toTopOf="@+id/button_bar" app:layout_constraintEnd_toStartOf="@+id/midGuideline" app:layout_constraintStart_toStartOf="@id/leftGuideline" app:layout_constraintTop_toTopOf="@+id/topGuideline"> @@ -91,7 +91,7 @@ android:layout_height="match_parent"> android:layout_width="0dp" android:layout_height="wrap_content" android:textAlignment="viewStart" - android:paddingLeft="8dp" + android:paddingLeft="16dp" app:layout_constraintBottom_toBottomOf="@+id/logo" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/logo" @@ -119,7 +119,7 @@ android:layout_height="match_parent"> style="@style/TextAppearance.AuthCredential.Subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="12dp" + android:layout_marginTop="16dp" android:gravity="@integer/biometric_dialog_text_gravity" android:paddingHorizontal="0dp" android:textAlignment="viewStart" @@ -133,6 +133,7 @@ android:layout_height="match_parent"> android:id="@+id/customized_view_container" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginTop="24dp" android:gravity="center_vertical" android:orientation="vertical" android:visibility="gone" @@ -148,6 +149,7 @@ android:layout_height="match_parent"> style="@style/TextAppearance.AuthCredential.Description" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="24dp" android:gravity="@integer/biometric_dialog_text_gravity" android:paddingHorizontal="0dp" android:textAlignment="viewStart" @@ -179,7 +181,7 @@ android:layout_height="match_parent"> android:fadingEdge="horizontal" android:gravity="center_horizontal" android:scrollHorizontally="true" - app:layout_constraintBottom_toTopOf="@+id/buttonBarrier" + app:layout_constraintBottom_toTopOf="@+id/button_bar" app:layout_constraintEnd_toEndOf="@+id/biometric_icon" app:layout_constraintStart_toStartOf="@+id/biometric_icon" app:layout_constraintTop_toBottomOf="@+id/biometric_icon" @@ -189,7 +191,7 @@ android:layout_height="match_parent"> android:id="@+id/button_bar" layout="@layout/biometric_prompt_button_bar" android:layout_width="0dp" - android:layout_height="0dp" + android:layout_height="wrap_content" app:layout_constraintBottom_toTopOf="@id/bottomGuideline" app:layout_constraintEnd_toEndOf="@id/scrollView" app:layout_constraintStart_toStartOf="@id/scrollView" @@ -204,14 +206,6 @@ android:layout_height="match_parent"> app:barrierDirection="top" app:constraint_referenced_ids="scrollView" /> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/buttonBarrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierAllowsGoneWidgets="false" - app:barrierDirection="top" - app:constraint_referenced_ids="button_bar" /> - <androidx.constraintlayout.widget.Guideline android:id="@+id/leftGuideline" android:layout_width="wrap_content" @@ -238,13 +232,13 @@ android:layout_height="match_parent"> android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/biometric_dialog_border_padding" /> + app:layout_constraintGuide_end="40dp" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/topGuideline" android:layout_width="0dp" android:layout_height="0dp" android:orientation="horizontal" - app:layout_constraintGuide_begin="@dimen/biometric_dialog_border_padding" /> + app:layout_constraintGuide_begin="0dp" /> </androidx.constraintlayout.widget.ConstraintLayout> 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 8b886a7fdffb..240ababc7da4 100644 --- a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml @@ -29,28 +29,31 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/rightGuideline" app:layout_constraintStart_toStartOf="@+id/leftGuideline" - app:layout_constraintTop_toTopOf="@+id/topBarrier" /> + app:layout_constraintTop_toTopOf="@+id/topBarrier" + app:layout_constraintWidth_max="640dp" /> <include - layout="@layout/biometric_prompt_button_bar" android:id="@+id/button_bar" + layout="@layout/biometric_prompt_button_bar" android:layout_width="0dp" - android:layout_height="match_parent" - app:layout_constraintBottom_toTopOf="@id/bottomGuideline" + android:layout_height="wrap_content" + android:layout_marginBottom="40dp" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/panel" - app:layout_constraintStart_toStartOf="@id/panel"/> + app:layout_constraintStart_toStartOf="@id/panel" /> <ScrollView android:id="@+id/scrollView" android:layout_width="0dp" android:layout_height="wrap_content" + android:fadeScrollbars="false" android:fillViewport="true" - android:paddingBottom="36dp" - android:paddingHorizontal="24dp" + android:paddingBottom="32dp" + android:paddingHorizontal="32dp" android:paddingTop="24dp" app:layout_constrainedHeight="true" app:layout_constrainedWidth="true" - app:layout_constraintBottom_toTopOf="@+id/biometric_icon" + app:layout_constraintBottom_toTopOf="@+id/scrollBarrier" app:layout_constraintEnd_toEndOf="@id/panel" app:layout_constraintStart_toStartOf="@id/panel" app:layout_constraintTop_toTopOf="@+id/topGuideline" @@ -63,10 +66,10 @@ <ImageView android:id="@+id/logo" - 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:contentDescription="@string/biometric_dialog_logo" android:scaleType="fitXY" android:visibility="visible" app:layout_constraintBottom_toTopOf="@+id/logo_description" @@ -79,6 +82,7 @@ style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingTop="16dp" app:layout_constraintBottom_toTopOf="@+id/title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -114,6 +118,7 @@ android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="vertical" + android:paddingTop="24dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -126,6 +131,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="@integer/biometric_dialog_text_gravity" + android:paddingTop="16dp" + android:textAlignment="viewStart" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -153,7 +160,7 @@ android:fadingEdge="horizontal" android:gravity="center_horizontal" android:scrollHorizontally="true" - app:layout_constraintBottom_toTopOf="@+id/buttonBarrier" + app:layout_constraintBottom_toTopOf="@+id/button_bar" app:layout_constraintEnd_toEndOf="@+id/panel" app:layout_constraintStart_toStartOf="@+id/panel" app:layout_constraintTop_toBottomOf="@+id/biometric_icon" @@ -172,12 +179,12 @@ <!-- Try Again Button --> <androidx.constraintlayout.widget.Barrier - android:id="@+id/buttonBarrier" + android:id="@+id/scrollBarrier" android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierAllowsGoneWidgets="false" app:barrierDirection="top" - app:constraint_referenced_ids="button_bar" /> + app:constraint_referenced_ids="biometric_icon, button_bar" /> <!-- Guidelines for setting panel border --> <androidx.constraintlayout.widget.Guideline @@ -199,14 +206,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/biometric_dialog_border_padding" /> + app:layout_constraintGuide_end="40dp" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/topGuideline" android:layout_width="0dp" android:layout_height="0dp" android:orientation="horizontal" - app:layout_constraintGuide_percent="0.25171" /> + app:layout_constraintGuide_begin="56dp" /> <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper android:id="@+id/biometric_icon" @@ -216,7 +223,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.8" + app:layout_constraintVertical_bias="1.0" tools:srcCompat="@tools:sample/avatars" /> <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper diff --git a/packages/SystemUI/res/layout/biometric_prompt_button_bar.xml b/packages/SystemUI/res/layout/biometric_prompt_button_bar.xml index 810c7433e4ad..4d2310a2a6ca 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_button_bar.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_button_bar.xml @@ -17,12 +17,13 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:theme="@style/Theme.SystemUI.Dialog" xmlns:app="http://schemas.android.com/apk/res-auto"> <!-- Negative Button, reserved for app --> <Button android:id="@+id/button_negative" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" + style="@style/Widget.Dialog.Button.BorderButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" @@ -36,7 +37,7 @@ <!-- Cancel Button, replaces negative button when biometric is accepted --> <Button android:id="@+id/button_cancel" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" + style="@style/Widget.Dialog.Button.BorderButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" @@ -49,7 +50,7 @@ <!-- "Use Credential" Button, replaces if device credential is allowed --> <Button android:id="@+id/button_use_credential" - style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" + style="@style/Widget.Dialog.Button.BorderButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" @@ -61,7 +62,7 @@ <!-- Positive Button --> <Button android:id="@+id/button_confirm" - style="@*android:style/Widget.DeviceDefault.Button.Colored" + style="@style/Widget.Dialog.Button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" @@ -76,7 +77,7 @@ <!-- Try Again Button --> <Button android:id="@+id/button_try_again" - style="@*android:style/Widget.DeviceDefault.Button.Colored" + style="@style/Widget.Dialog.Button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml index 74bf318465b6..8e3cf4d9b446 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml @@ -35,24 +35,26 @@ android:id="@+id/button_bar" layout="@layout/biometric_prompt_button_bar" android:layout_width="0dp" - android:layout_height="match_parent" - app:layout_constraintBottom_toTopOf="@id/bottomGuideline" + android:layout_height="wrap_content" + android:layout_marginBottom="40dp" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/panel" app:layout_constraintStart_toStartOf="@id/panel" /> <ScrollView android:id="@+id/scrollView" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:fillViewport="true" + android:fadeScrollbars="false" android:paddingBottom="36dp" android:paddingHorizontal="24dp" android:paddingTop="24dp" app:layout_constrainedHeight="true" app:layout_constrainedWidth="true" - app:layout_constraintBottom_toTopOf="@+id/biometric_icon" - app:layout_constraintEnd_toEndOf="@id/rightGuideline" - app:layout_constraintStart_toStartOf="@id/leftGuideline" + app:layout_constraintBottom_toTopOf="@+id/scrollBarrier" + app:layout_constraintEnd_toEndOf="@id/panel" + app:layout_constraintStart_toStartOf="@id/panel" app:layout_constraintTop_toTopOf="@+id/topGuideline" app:layout_constraintVertical_bias="1.0"> @@ -68,6 +70,7 @@ android:layout_height="@dimen/biometric_prompt_logo_size" android:layout_gravity="center" android:scaleType="fitXY" + android:importantForAccessibility="no" app:layout_constraintBottom_toTopOf="@+id/logo_description" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -79,6 +82,7 @@ style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingTop="8dp" app:layout_constraintBottom_toTopOf="@+id/title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -114,8 +118,8 @@ android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="vertical" - android:visibility="gone" android:paddingTop="24dp" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -127,7 +131,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="@integer/biometric_dialog_text_gravity" - android:paddingTop="24dp" + android:textAlignment="viewStart" + android:paddingTop="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -154,7 +159,7 @@ android:fadingEdge="horizontal" android:gravity="center_horizontal" android:scrollHorizontally="true" - app:layout_constraintBottom_toTopOf="@+id/buttonBarrier" + app:layout_constraintBottom_toTopOf="@+id/button_bar" app:layout_constraintEnd_toEndOf="@+id/panel" app:layout_constraintStart_toStartOf="@+id/panel" app:layout_constraintTop_toBottomOf="@+id/biometric_icon" @@ -169,12 +174,12 @@ app:constraint_referenced_ids="scrollView" /> <androidx.constraintlayout.widget.Barrier - android:id="@+id/buttonBarrier" + android:id="@+id/scrollBarrier" android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierAllowsGoneWidgets="false" app:barrierDirection="top" - app:constraint_referenced_ids="button_bar" /> + app:constraint_referenced_ids="biometric_icon, button_bar" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/leftGuideline" @@ -195,14 +200,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/biometric_dialog_border_padding" /> + app:layout_constraintGuide_end="40dp" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/topGuideline" android:layout_width="0dp" android:layout_height="0dp" android:orientation="horizontal" - app:layout_constraintGuide_percent="0.25" /> + app:layout_constraintGuide_begin="119dp" /> <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper android:id="@+id/biometric_icon" @@ -212,7 +217,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.8" + app:layout_constraintVertical_bias="1.0" tools:srcCompat="@tools:sample/avatars" /> <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper diff --git a/packages/SystemUI/res/layout/keyboard_shortcut_app_item.xml b/packages/SystemUI/res/layout/keyboard_shortcut_app_item.xml index a0051008ddd6..5ab23271922c 100644 --- a/packages/SystemUI/res/layout/keyboard_shortcut_app_item.xml +++ b/packages/SystemUI/res/layout/keyboard_shortcut_app_item.xml @@ -15,13 +15,14 @@ ~ limitations under the License --> <com.android.systemui.statusbar.KeyboardShortcutAppItemLayout + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:background="@drawable/list_item_background" android:focusable="true" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="48dp" + android:minHeight="@dimen/ksh_app_item_minimum_height" android:paddingBottom="8dp"> <ImageView android:id="@+id/keyboard_shortcuts_icon" @@ -39,7 +40,8 @@ android:layout_height="wrap_content" android:paddingEnd="12dp" android:paddingBottom="4dp" - android:textColor="?android:attr/textColorPrimary" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:textAppearance="?android:attr/textAppearanceMedium" android:textSize="16sp" android:maxLines="5" android:singleLine="false" diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml index 530e46eb1c40..76e5b12bdcd6 100644 --- a/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml +++ b/packages/SystemUI/res/layout/keyboard_shortcuts_category_short_separator.xml @@ -15,6 +15,6 @@ limitations under the License --> <View xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_marginTop="8dp" - android:layout_marginBottom="0dp" + android:layout_marginTop="@dimen/ksh_category_separator_margin" + android:layout_marginBottom="@dimen/ksh_category_separator_margin" style="@style/ShortcutHorizontalDivider" /> diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_category_title.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_category_title.xml index 4f100f6b94d1..6e7fde68ca04 100644 --- a/packages/SystemUI/res/layout/keyboard_shortcuts_category_title.xml +++ b/packages/SystemUI/res/layout/keyboard_shortcuts_category_title.xml @@ -16,10 +16,12 @@ ~ limitations under the License --> <TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:textAppearance="?android:attr/textAppearanceMedium" android:textSize="14sp" - android:fontFamily="sans-serif-medium" + android:textColor="?androidprv:attr/materialColorPrimary" android:importantForAccessibility="yes" android:paddingTop="20dp" android:paddingBottom="10dp"/> diff --git a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml index f6042e467987..2cfd644e9d62 100644 --- a/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml +++ b/packages/SystemUI/res/layout/keyboard_shortcuts_search_view.xml @@ -16,15 +16,21 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:background="@drawable/shortcut_dialog_bg" android:layout_width="@dimen/ksh_layout_width" android:layout_height="wrap_content" android:orientation="vertical"> + + <com.google.android.material.bottomsheet.BottomSheetDragHandleView + android:id="@+id/drag_handle" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + <TextView android:id="@+id/shortcut_title" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="40dp" android:layout_gravity="center_horizontal" android:textAppearance="?android:attr/textAppearanceLarge" android:textColor="?android:attr/textColorPrimary" @@ -39,44 +45,47 @@ android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginBottom="24dp" - android:layout_marginStart="49dp" - android:layout_marginEnd="49dp" + android:layout_marginStart="@dimen/ksh_container_horizontal_margin" + android:layout_marginEnd="@dimen/ksh_container_horizontal_margin" android:padding="16dp" android:background="@drawable/shortcut_search_background" android:drawableStart="@drawable/ic_shortcutlist_search" android:drawablePadding="15dp" android:singleLine="true" - android:textColor="?android:attr/textColorPrimary" + android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" android:inputType="text" android:textDirection="locale" android:textAlignment="viewStart" android:hint="@string/keyboard_shortcut_search_list_hint" - android:textColorHint="?android:attr/textColorTertiary" /> + android:textAppearance="@android:style/TextAppearance.Material" + android:textSize="16sp" + android:textColorHint="?androidprv:attr/materialColorOutline" /> <ImageButton android:id="@+id/keyboard_shortcuts_search_cancel" android:layout_gravity="center_vertical|end" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="49dp" + android:layout_marginEnd="@dimen/ksh_container_horizontal_margin" android:padding="16dp" android:contentDescription="@string/keyboard_shortcut_clear_text" android:src="@drawable/ic_shortcutlist_search_button_cancel" android:background="@drawable/shortcut_search_cancel_button" style="@android:style/Widget.Material.Button.Borderless.Small" - android:pointerIcon="arrow" /> + android:pointerIcon="arrow" + android:visibility="gone" /> </FrameLayout> <HorizontalScrollView android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="49dp" + android:layout_marginStart="@dimen/ksh_container_horizontal_margin" android:layout_marginEnd="0dp" android:scrollbars="none"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center_vertical" + android:layout_gravity="center_vertical" android:orientation="horizontal"> <Button android:id="@+id/shortcut_system" @@ -113,29 +122,29 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="50dp" - android:layout_marginStart="49dp" - android:layout_marginEnd="49dp" + android:layout_marginStart="@dimen/ksh_container_horizontal_margin" + android:layout_marginEnd="@dimen/ksh_container_horizontal_margin" android:layout_gravity="center_horizontal" android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="?android:attr/textColorPrimary" android:text="@string/keyboard_shortcut_search_list_no_result"/> - <ScrollView + <androidx.core.widget.NestedScrollView android:id="@+id/keyboard_shortcuts_scroll_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:layout_marginStart="49dp" - android:layout_marginEnd="49dp" + android:layout_marginStart="@dimen/ksh_container_horizontal_margin" + android:layout_marginEnd="@dimen/ksh_container_horizontal_margin" android:overScrollMode="never" - android:layout_marginBottom="16dp" + android:clipToPadding="false" android:scrollbars="none"> <LinearLayout android:id="@+id/keyboard_shortcuts_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"/> - </ScrollView> + </androidx.core.widget.NestedScrollView> <!-- Required for stretching to full available height when the items in the scroll view occupy less space then the full height --> <View diff --git a/packages/SystemUI/res/layout/record_issue_dialog.xml b/packages/SystemUI/res/layout/record_issue_dialog.xml index 53ad9f157a2e..30d7b0ae739a 100644 --- a/packages/SystemUI/res/layout/record_issue_dialog.xml +++ b/packages/SystemUI/res/layout/record_issue_dialog.xml @@ -54,6 +54,7 @@ android:layout_weight="0" android:src="@drawable/ic_screenrecord" app:tint="?androidprv:attr/materialColorOnSurface" + android:importantForAccessibility="no" android:layout_gravity="center" android:layout_marginEnd="@dimen/screenrecord_option_padding" /> @@ -78,4 +79,44 @@ android:layout_weight="0" android:contentDescription="@string/quick_settings_screen_record_label" /> </LinearLayout> + + <!-- Bug Report Switch --> + <LinearLayout + android:id="@+id/bugreport_switch_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/qqs_layout_margin_top" + android:orientation="horizontal"> + + <ImageView + android:layout_width="@dimen/screenrecord_option_icon_size" + android:layout_height="@dimen/screenrecord_option_icon_size" + android:layout_weight="0" + android:src="@drawable/ic_bugreport" + app:tint="?androidprv:attr/materialColorOnSurface" + android:importantForAccessibility="no" + android:layout_gravity="center" + android:layout_marginEnd="@dimen/screenrecord_option_padding" /> + + <TextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="@dimen/screenrecord_option_icon_size" + android:layout_weight="1" + android:layout_gravity="fill_vertical" + android:gravity="center" + android:text="@string/qs_record_issue_bug_report" + android:textAppearance="@style/TextAppearance.Dialog.Body.Message" + android:importantForAccessibility="no"/> + + <Switch + android:id="@+id/bugreport_switch" + android:layout_width="wrap_content" + android:minHeight="@dimen/screenrecord_option_icon_size" + android:layout_height="wrap_content" + android:gravity="center" + android:layout_gravity="fill_vertical" + android:layout_weight="0" + android:contentDescription="@string/qs_record_issue_bug_report" /> + </LinearLayout> </LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml index c988b4afcf74..eeb64bd8460e 100644 --- a/packages/SystemUI/res/layout/screenshot_shelf.xml +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -51,15 +51,7 @@ <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> + android:layout_height="wrap_content" /> </HorizontalScrollView> <View android:id="@+id/screenshot_preview_border" diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 55606aa0bc82..56ebc0668097 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -94,4 +94,7 @@ <dimen name="keyguard_indication_margin_bottom">8dp</dimen> <dimen name="lock_icon_margin_bottom">24dp</dimen> + + <!-- Keyboard shortcuts helper --> + <dimen name="ksh_container_horizontal_margin">48dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index f2288a425a0f..b0b548295b71 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -978,6 +978,7 @@ <dimen name="assist_disclosure_shadow_thickness">1.5dp</dimen> <!-- Keyboard shortcuts helper --> + <dimen name="ksh_container_horizontal_margin">32dp</dimen> <dimen name="ksh_layout_width">@dimen/match_parent</dimen> <dimen name="ksh_item_text_size">14sp</dimen> <dimen name="ksh_item_padding">0dp</dimen> @@ -985,6 +986,11 @@ <dimen name="ksh_icon_scaled_size">18dp</dimen> <dimen name="ksh_key_view_padding_vertical">4dp</dimen> <dimen name="ksh_key_view_padding_horizontal">12dp</dimen> + <dimen name="ksh_button_corner_radius">12dp</dimen> + <dimen name="ksh_dialog_top_corner_radius">28dp</dimen> + <dimen name="ksh_search_box_corner_radius">100dp</dimen> + <dimen name="ksh_app_item_minimum_height">64dp</dimen> + <dimen name="ksh_category_separator_margin">16dp</dimen> <!-- The size of corner radius of the arrow in the onboarding toast. --> <dimen name="recents_onboarding_toast_arrow_corner_radius">2dp</dimen> @@ -1085,7 +1091,7 @@ <dimen name="remote_input_history_extra_height">60dp</dimen> <!-- Biometric Dialog values --> - <dimen name="biometric_dialog_face_icon_size">64dp</dimen> + <dimen name="biometric_dialog_face_icon_size">54dp</dimen> <dimen name="biometric_dialog_fingerprint_icon_width">80dp</dimen> <dimen name="biometric_dialog_fingerprint_icon_height">80dp</dimen> <dimen name="biometric_dialog_button_negative_max_width">160dp</dimen> @@ -1103,6 +1109,22 @@ <dimen name="biometric_dialog_width">240dp</dimen> <dimen name="biometric_dialog_height">240dp</dimen> + <!-- Dimensions for biometric prompt panel padding --> + <dimen name="biometric_prompt_small_horizontal_guideline_padding">344dp</dimen> + <dimen name="biometric_prompt_udfps_horizontal_guideline_padding">114dp</dimen> + <dimen name="biometric_prompt_udfps_mid_guideline_padding">409dp</dimen> + <dimen name="biometric_prompt_medium_horizontal_guideline_padding">640dp</dimen> + <dimen name="biometric_prompt_medium_mid_guideline_padding">330dp</dimen> + + <!-- Dimensions for biometric prompt icon padding --> + <dimen name="biometric_prompt_portrait_small_bottom_padding">60dp</dimen> + <dimen name="biometric_prompt_portrait_medium_bottom_padding">160dp</dimen> + <dimen name="biometric_prompt_portrait_large_screen_bottom_padding">176dp</dimen> + <dimen name="biometric_prompt_landscape_small_bottom_padding">192dp</dimen> + <dimen name="biometric_prompt_landscape_small_horizontal_padding">145dp</dimen> + <dimen name="biometric_prompt_landscape_medium_bottom_padding">148dp</dimen> + <dimen name="biometric_prompt_landscape_medium_horizontal_padding">125dp</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> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 71353b6774af..af661aa172c7 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -872,6 +872,8 @@ <string name="qs_record_issue_start">Start</string> <!-- QuickSettings: Text to prompt the user to stop an ongoing recording [CHAR LIMIT=20] --> <string name="qs_record_issue_stop">Stop</string> + <!-- QuickSettings: Should user take a bugreport or only share trace files [CHAR LIMIT=20] --> + <string name="qs_record_issue_bug_report">Bug Report</string> <!-- QuickSettings: Issue Type Drop down options in Record Issue Start Dialog [CHAR LIMIT=50] --> <string name="qs_record_issue_dropdown_header">What part of your device experience was affected?</string> @@ -1971,7 +1973,7 @@ <!-- Content description for the clear search button in shortcut search list. [CHAR LIMIT=NONE] --> <string name="keyboard_shortcut_clear_text">Clear search query</string> <!-- The title for keyboard shortcut search list [CHAR LIMIT=25] --> - <string name="keyboard_shortcut_search_list_title">Shortcuts</string> + <string name="keyboard_shortcut_search_list_title">Keyboard Shortcuts</string> <!-- The hint for keyboard shortcut search list [CHAR LIMIT=25] --> <string name="keyboard_shortcut_search_list_hint">Search shortcuts</string> <!-- The description for no shortcuts results [CHAR LIMIT=25] --> @@ -2023,12 +2025,12 @@ <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] --> <string name="group_system_quick_memo">Take a note</string> - <!-- User visible title for the system multitasking keyboard shortcuts list. [CHAR LIMIT=70] --> - <string name="keyboard_shortcut_group_system_multitasking">System multitasking</string> - <!-- User visible title for the keyboard shortcut that enters split screen with current app to RHS [CHAR LIMIT=70] --> - <string name="system_multitasking_rhs">Enter split screen with current app to RHS</string> - <!-- User visible title for the keyboard shortcut that enters split screen with current app to LHS [CHAR LIMIT=70] --> - <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string> + <!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] --> + <string name="keyboard_shortcut_group_system_multitasking">Multitasking</string> + <!-- User visible title for the keyboard shortcut that enters split screen with current app on the right [CHAR LIMIT=70] --> + <string name="system_multitasking_rhs">Use split screen with current app on the right</string> + <!-- User visible title for the keyboard shortcut that enters split screen with current app on the left [CHAR LIMIT=70] --> + <string name="system_multitasking_lhs">Use split screen with current app on the left</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] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 6462d02de481..2c9006e50f92 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -224,11 +224,14 @@ <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> + <item name="android:breakStrategy">high_quality</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> + <item name="android:singleLine">true</item> + <item name="android:ellipsize">marquee</item> </style> <style name="TextAppearance.AuthCredential.Error"> @@ -365,6 +368,21 @@ <item name="android:layout_height">wrap_content</item> </style> + <style name="KeyboardShortcutHelper" parent="@android:style/Theme.DeviceDefault.Settings"> + <!-- Needed to be able to use BottomSheetDragHandleView --> + <item name="android:windowActionBar">false</item> + <item name="bottomSheetDragHandleStyle">@style/KeyboardShortcutHelper.BottomSheet.DragHandle</item> + </style> + + <style name="KeyboardShortcutHelper.BottomSheet.DragHandle" parent="Widget.Material3.BottomSheet.DragHandle"> + <item name="tint">?androidprv:attr/materialColorOutlineVariant</item> + </style> + + <style name="KeyboardShortcutHelper.BottomSheetDialogAnimation"> + <item name="android:windowEnterAnimation">@anim/slide_in_up</item> + <item name="android:windowExitAnimation">@anim/slide_out_down</item> + </style> + <style name="BrightnessDialogContainer" parent="@style/BaseBrightnessDialogContainer" /> <style name="Animation" /> @@ -1597,14 +1615,15 @@ <item name="android:layout_marginEnd">12dp</item> <item name="android:paddingLeft">24dp</item> <item name="android:paddingRight">24dp</item> - <item name="android:minHeight">40dp</item> + <item name="android:minHeight">36dp</item> + <item name="android:minWidth">120dp</item> <item name="android:stateListAnimator">@*android:anim/flat_button_state_list_anim_material</item> <item name="android:pointerIcon">arrow</item> </style> <style name="ShortcutHorizontalDivider"> - <item name="android:layout_width">120dp</item> - <item name="android:layout_height">1dp</item> + <item name="android:layout_width">132dp</item> + <item name="android:layout_height">2dp</item> <item name="android:layout_gravity">center_horizontal</item> <item name="android:background">?android:attr/dividerHorizontal</item> </style> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl index 46329148a659..dcc14409f046 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl @@ -155,5 +155,10 @@ interface ISystemUiProxy { */ oneway void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) = 54; - // Next id = 55 + /** + * Set the override value for home button long press duration in ms and slop multiplier. + */ + oneway void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) = 55; + + // Next id = 56 } diff --git a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt index 7af991743457..d0d5caf57ffc 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/unfold/system/SystemUnfoldSharedModule.kt @@ -32,6 +32,8 @@ import dagger.Module import dagger.Provides import java.util.concurrent.Executor import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.android.asCoroutineDispatcher /** * Dagger module with system-only dependencies for the unfold animation. The code that is used to @@ -78,6 +80,13 @@ abstract class SystemUnfoldSharedModule { @Provides @UnfoldBg @Singleton + fun unfoldBgDispatcher(@UnfoldBg handler: Handler): CoroutineDispatcher { + return handler.asCoroutineDispatcher("@UnfoldBg Dispatcher") + } + + @Provides + @UnfoldBg + @Singleton fun provideBgLooper(): Looper { return HandlerThread("UnfoldBg", Process.THREAD_PRIORITY_FOREGROUND) .apply { start() } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 8a18efc721d8..70182c16a093 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -544,7 +544,9 @@ constructor( step.copy(value = 1f - step.value) }, keyguardTransitionInteractor.lockscreenToAodTransition, - ) + ).filter { + it.transitionState != TransitionState.FINISHED + } .collect { handleDoze(it.value) } } } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index ea8fe59d3c89..fb88f0e4e093 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -300,7 +300,7 @@ public class SystemUIApplication extends Application implements Class<?> cls = entry.getKey(); Dependencies dep = cls.getAnnotation(Dependencies.class); - Class<? extends CoreStartable>[] deps = (dep == null ? null : dep.value()); + Class<?>[] deps = (dep == null ? null : dep.value()); if (deps == null || startedStartables.containsAll(Arrays.asList(deps))) { String clsName = cls.getName(); int i = serviceIndex; // Copied to make lambda happy. @@ -324,7 +324,7 @@ public class SystemUIApplication extends Application implements Map.Entry<Class<?>, Provider<CoreStartable>> entry = nextQueue.removeFirst(); Class<?> cls = entry.getKey(); Dependencies dep = cls.getAnnotation(Dependencies.class); - Class<? extends CoreStartable>[] deps = (dep == null ? null : dep.value()); + Class<?>[] deps = (dep == null ? null : dep.value()); StringJoiner stringJoiner = new StringJoiner(", "); for (int i = 0; deps != null && i < deps.length; i++) { if (!startedStartables.contains(deps[i])) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 96eb4b3a4c3f..475bb2c83b0e 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -43,14 +43,14 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.HearingDeviceItemCallback; import com.android.systemui.animation.DialogTransitionAnimator; +import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory; +import com.android.systemui.bluetooth.qsdialog.AvailableHearingDeviceItemFactory; +import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory; +import com.android.systemui.bluetooth.qsdialog.DeviceItem; +import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory; +import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.qs.tiles.dialog.bluetooth.ActiveHearingDeviceItemFactory; -import com.android.systemui.qs.tiles.dialog.bluetooth.AvailableHearingDeviceItemFactory; -import com.android.systemui.qs.tiles.dialog.bluetooth.ConnectedDeviceItemFactory; -import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItem; -import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItemFactory; -import com.android.systemui.qs.tiles.dialog.bluetooth.SavedHearingDeviceItemFactory; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java index 695d04f5df9d..737805b4d62f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java @@ -27,7 +27,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItem; +import com.android.systemui.bluetooth.qsdialog.DeviceItem; import com.android.systemui.res.R; import kotlin.Pair; diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 8ca083fa1e35..5df7fc9865ff 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -243,12 +243,6 @@ constructor( } // Authentication failed. - - if (tryAutoConfirm) { - // Auto-confirm is active, the failed attempt should have no side-effects. - return AuthenticationResult.FAILED - } - repository.reportAuthenticationAttempt(isSuccessful = false) if (authenticationResult.lockoutDurationMs > 0) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 9d79e8713625..b25c3daa9407 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -403,6 +403,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (DeviceEntryUdfpsRefactor.isEnabled()) { if (mOverlay != null && mOverlay.getRequestReason() == REASON_AUTH_KEYGUARD) { mOverlay.updateOverlayParams(mOverlayParams); + } else { + redrawOverlay(); } } else { final boolean wasShowingAlternateBouncer = 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 88aef5675240..7ccac03bcac6 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 @@ -31,18 +31,19 @@ import android.text.style.BulletSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewTreeObserver import android.widget.Button import android.widget.LinearLayout import android.widget.Space import android.widget.TextView -import androidx.lifecycle.lifecycleScope import com.android.settingslib.Utils import com.android.systemui.biometrics.ui.BiometricPromptLayout import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import kotlin.math.ceil -import kotlinx.coroutines.launch + +private const val TAG = "BiometricCustomizedViewBinder" /** Sub-binder for [BiometricPromptLayout.customized_view_container]. */ object BiometricCustomizedViewBinder { @@ -52,21 +53,20 @@ object BiometricCustomizedViewBinder { legacyCallback: Spaghetti.Callback ) { customizedViewContainer.repeatWhenAttached { containerView -> - lifecycleScope.launch { - if (contentView == null) { - containerView.visibility = View.GONE - return@launch - } + if (contentView == null) { + containerView.visibility = View.GONE + return@repeatWhenAttached + } - containerView.width { containerWidth -> - if (containerWidth == 0) { - return@width - } - (containerView as LinearLayout).addView( - contentView.toView(containerView.context, containerWidth, legacyCallback) - ) - containerView.visibility = View.VISIBLE + containerView.width { containerWidth -> + if (containerWidth == 0) { + return@width } + (containerView as LinearLayout).addView( + contentView.toView(containerView.context, containerWidth, legacyCallback), + LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + ) + containerView.visibility = View.VISIBLE } } } @@ -118,51 +118,42 @@ private fun PromptVerticalListContentView.initLayout( containerViewWidth: Int ): View { val inflater = LayoutInflater.from(context) - val resources = context.resources + context.resources val contentView = inflater.inflateContentView( R.layout.biometric_prompt_vertical_list_content_layout, description ) + val listItemsToShow = ArrayList<PromptContentItem>(listItems) // 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) { + listItemsToShow.all { !it.doesExceedMaxLinesIfTwoColumn(context, containerViewWidth) } + // If should show two columns and there are more than one items, make listItems always have odd + // number items. + if (showTwoColumn && listItemsToShow.size > 1 && listItemsToShow.size % 2 == 1) { + listItemsToShow.add(dummyItem()) + } + var currRow = createNewRowLayout(inflater) + for (i in 0 until listItemsToShow.size) { + val item = listItemsToShow[i] 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) + contentView.removeTopPaddingForFirstRow(description, itemView) - // 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 - ) + // If there should be two column, and there is already one item in the current row, add + // space between two items. + if (showTwoColumn && currRow.childCount == 1) { + currRow.addSpaceViewBetweenListItem() } + currRow.addView(itemView) - // 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 there should be one column, or there are already two items (plus the space view) in + // the current row, or it's already the last item, start a new row + if (!showTwoColumn || currRow.childCount == 3 || i == listItemsToShow.size - 1) { + contentView.addView(currRow) + currRow = createNewRowLayout(inflater) } } - if (currRowView.childCount > 0) { - contentView.addView(currRowView) - } return contentView } @@ -170,10 +161,6 @@ 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( context: Context, containerViewWidth: Int, @@ -194,7 +181,10 @@ private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( val contentViewPadding = resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_padding_horizontal) val listItemPadding = getListItemPadding(resources) - val maxWidth = containerViewWidth / 2 - contentViewPadding - listItemPadding + var maxWidth = containerViewWidth / 2 - contentViewPadding - listItemPadding + // Reduce maxWidth a bit since paint#measureText is not accurate. See b/330909104 for + // more context. + maxWidth -= contentViewPadding / 2 val paint = TextPaint() val attributes = @@ -224,6 +214,7 @@ private fun PromptContentItem.toView( inflater: LayoutInflater, ): TextView { val resources = context.resources + // Somehow xml layout params settings doesn't work, set it again here. 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) @@ -251,6 +242,29 @@ private fun PromptContentItem.toView( return textView } +/* [contentView] function */ +private fun LinearLayout.addSpaceViewBetweenListItem() = + addView( + Space(context), + LinearLayout.LayoutParams( + resources.getDimensionPixelSize( + R.dimen.biometric_prompt_content_space_width_between_items + ), + MATCH_PARENT + ) + ) + +/* [contentView] function*/ +private fun LinearLayout.removeTopPaddingForFirstRow(description: String?, itemView: TextView) { + // 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 (description.isNullOrEmpty() && childCount == 1) { + itemView.setPadding(itemView.paddingLeft, 0, itemView.paddingRight, itemView.paddingBottom) + } +} + +private fun dummyItem(): PromptContentItem = PromptContentItemPlainText("") + private fun PromptContentItem.getListItemPadding(resources: Resources): Int { var listItemPadding = resources.getDimensionPixelSize( 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 b2ade4fa1e8a..76d46ed9889f 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 @@ -288,12 +288,14 @@ object BiometricViewBinder { // set padding launch { viewModel.promptPadding.collect { promptPadding -> - view.setPadding( - promptPadding.left, - promptPadding.top, - promptPadding.right, - promptPadding.bottom - ) + if (!constraintBp()) { + view.setPadding( + promptPadding.left, + promptPadding.top, + promptPadding.right, + promptPadding.bottom + ) + } } } 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 e3c0cba42e2d..f380746105ed 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 @@ -20,7 +20,6 @@ import android.animation.Animator import android.animation.AnimatorSet import android.animation.ValueAnimator import android.graphics.Outline -import android.graphics.Rect import android.transition.AutoTransition import android.transition.TransitionManager import android.util.TypedValue @@ -47,17 +46,14 @@ import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.ui.viewmodel.PromptPosition import com.android.systemui.biometrics.ui.viewmodel.PromptSize import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel -import com.android.systemui.biometrics.ui.viewmodel.isBottom import com.android.systemui.biometrics.ui.viewmodel.isLarge import com.android.systemui.biometrics.ui.viewmodel.isLeft import com.android.systemui.biometrics.ui.viewmodel.isMedium import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall -import com.android.systemui.biometrics.ui.viewmodel.isRight import com.android.systemui.biometrics.ui.viewmodel.isSmall -import com.android.systemui.biometrics.ui.viewmodel.isTop import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R -import kotlin.math.min +import kotlin.math.abs import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -97,8 +93,6 @@ object BiometricViewSizeBinder { if (constraintBp()) { val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline) val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline) - val bottomGuideline = view.requireViewById<Guideline>(R.id.bottomGuideline) - val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline) val midGuideline = view.findViewById<Guideline>(R.id.midGuideline) val iconHolderView = view.requireViewById<View>(R.id.biometric_icon) @@ -121,165 +115,12 @@ object BiometricViewSizeBinder { val largeConstraintSet = ConstraintSet() largeConstraintSet.clone(mediumConstraintSet) + largeConstraintSet.constrainMaxWidth(R.id.panel, view.width) // TODO: Investigate better way to handle 180 rotations val flipConstraintSet = ConstraintSet() - flipConstraintSet.clone(mediumConstraintSet) - flipConstraintSet.connect( - R.id.scrollView, - ConstraintSet.START, - R.id.midGuideline, - ConstraintSet.START - ) - flipConstraintSet.connect( - R.id.scrollView, - ConstraintSet.END, - R.id.rightGuideline, - ConstraintSet.END - ) - flipConstraintSet.setHorizontalBias(R.id.biometric_icon, .2f) - - // Round the panel outline - panelView.outlineProvider = - object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - 0, - 0, - view.width, - view.height + cornerRadiusPx, - cornerRadiusPx.toFloat() - ) - } - } view.doOnLayout { - val windowBounds = windowManager.maximumWindowMetrics.bounds - val bottomInset = - windowManager.maximumWindowMetrics.windowInsets - .getInsets(WindowInsets.Type.navigationBars()) - .bottom - - // TODO: Move to viewmodel - fun measureBounds(position: PromptPosition) { - val density = windowManager.currentWindowMetrics.density - val width = min((640 * density).toInt(), windowBounds.width()) - - var left = -1 - var right = -1 - var bottom = -1 - var mid = -1 - - when { - position.isTop -> { - // Round bottom corners - panelView.outlineProvider = - object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - 0, - -cornerRadiusPx, - view.width, - view.height, - cornerRadiusPx.toFloat() - ) - } - } - left = windowBounds.centerX() - width / 2 + viewModel.promptMargin - right = windowBounds.centerX() - width / 2 + viewModel.promptMargin - bottom = iconHolderView.centerY() * 2 - iconHolderView.centerY() / 4 - } - position.isBottom -> { - // Round top corners - panelView.outlineProvider = - object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - 0, - 0, - view.width, - view.height + cornerRadiusPx, - cornerRadiusPx.toFloat() - ) - } - } - - left = windowBounds.centerX() - width / 2 - right = windowBounds.centerX() - width / 2 - bottom = if (view.isLandscape()) bottomInset else 0 - } - position.isLeft -> { - // Round right corners - panelView.outlineProvider = - object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - -cornerRadiusPx, - 0, - view.width, - view.height, - cornerRadiusPx.toFloat() - ) - } - } - - left = 0 - mid = (windowBounds.width() * .85).toInt() / 2 - right = windowBounds.width() - (windowBounds.width() * .85).toInt() - bottom = if (view.isLandscape()) bottomInset else 0 - } - position.isRight -> { - // Round left corners - panelView.outlineProvider = - object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - 0, - 0, - view.width + cornerRadiusPx, - view.height, - cornerRadiusPx.toFloat() - ) - } - } - - left = windowBounds.width() - (windowBounds.width() * .85).toInt() - right = 0 - bottom = if (view.isLandscape()) bottomInset else 0 - mid = windowBounds.width() - (windowBounds.width() * .85).toInt() / 2 - } - } - - val bounds = Rect(left, mid, right, bottom) - if (bounds.shouldAdjustLeftGuideline()) { - leftGuideline.setGuidelineBegin(bounds.left) - smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) - mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) - } - if (bounds.shouldAdjustRightGuideline()) { - rightGuideline.setGuidelineEnd(bounds.right) - smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) - mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) - } - if (bounds.shouldAdjustBottomGuideline()) { - bottomGuideline.setGuidelineEnd(bounds.bottom) - smallConstraintSet.setGuidelineEnd(bottomGuideline.id, bounds.bottom) - mediumConstraintSet.setGuidelineEnd(bottomGuideline.id, bounds.bottom) - } - - if (position.isBottom) { - topGuideline.setGuidelinePercent(.25f) - mediumConstraintSet.setGuidelinePercent(topGuideline.id, .25f) - } else { - topGuideline.setGuidelinePercent(0f) - mediumConstraintSet.setGuidelinePercent(topGuideline.id, 0f) - } - - if (mid != -1 && midGuideline != null) { - midGuideline.setGuidelineBegin(mid) - } - } - fun setVisibilities(size: PromptSize) { viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) } largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) @@ -297,36 +138,151 @@ object BiometricViewSizeBinder { } } + fun roundCorners(size: PromptSize, position: PromptPosition) { + var left = 0 + var top = 0 + var right = 0 + var bottom = 0 + when (size) { + PromptSize.SMALL, + PromptSize.MEDIUM -> + when (position) { + PromptPosition.Right -> { + left = 0 + top = 0 + right = view.width + cornerRadiusPx + bottom = view.height + } + PromptPosition.Left -> { + left = -cornerRadiusPx + top = 0 + right = view.width + bottom = view.height + } + PromptPosition.Top -> { + left = 0 + top = -cornerRadiusPx + right = panelView.width + bottom = view.height + } + PromptPosition.Bottom -> { + left = 0 + top = 0 + right = panelView.width + bottom = view.height + cornerRadiusPx + } + } + PromptSize.LARGE -> { + left = 0 + top = 0 + right = view.width + bottom = view.height + } + } + + // Round the panel outline + panelView.outlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + left, + top, + right, + bottom, + cornerRadiusPx.toFloat() + ) + } + } + } + view.repeatWhenAttached { var currentSize: PromptSize? = null lifecycleScope.launch { + viewModel.guidelineBounds.collect { bounds -> + if (bounds.left >= 0) { + mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) + smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left) + } else if (bounds.left < 0) { + mediumConstraintSet.setGuidelineEnd( + leftGuideline.id, + abs(bounds.left) + ) + smallConstraintSet.setGuidelineEnd( + leftGuideline.id, + abs(bounds.left) + ) + } + + if (bounds.right >= 0) { + mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) + smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right) + } else if (bounds.right < 0) { + mediumConstraintSet.setGuidelineBegin( + rightGuideline.id, + abs(bounds.right) + ) + smallConstraintSet.setGuidelineBegin( + rightGuideline.id, + abs(bounds.right) + ) + } + + if (midGuideline != null) { + if (bounds.bottom >= 0) { + midGuideline.setGuidelineEnd(bounds.bottom) + mediumConstraintSet.setGuidelineEnd( + midGuideline.id, + bounds.bottom + ) + } else if (bounds.bottom < 0) { + midGuideline.setGuidelineBegin(abs(bounds.bottom)) + mediumConstraintSet.setGuidelineBegin( + midGuideline.id, + abs(bounds.bottom) + ) + } + } + } + } + + lifecycleScope.launch { combine(viewModel.position, viewModel.size, ::Pair).collect { (position, size) -> view.doOnAttach { + setVisibilities(size) + if (position.isLeft) { - flipConstraintSet.applyTo(view) - } else if (position.isRight) { - mediumConstraintSet.applyTo(view) + if (size.isSmall) { + flipConstraintSet.clone(smallConstraintSet) + } else { + flipConstraintSet.clone(mediumConstraintSet) + } + + // Move all content to other panel + flipConstraintSet.connect( + R.id.scrollView, + ConstraintSet.START, + R.id.midGuideline, + ConstraintSet.START + ) + flipConstraintSet.connect( + R.id.scrollView, + ConstraintSet.END, + R.id.rightGuideline, + ConstraintSet.END + ) } - measureBounds(position) - setVisibilities(size) + roundCorners(size, position) + when { size.isSmall -> { - val ratio = - if (view.isLandscape()) { - (windowBounds.height() - - bottomInset - - viewModel.promptMargin) - .toFloat() / windowBounds.height() - } else { - (windowBounds.height() - viewModel.promptMargin) - .toFloat() / windowBounds.height() - } - smallConstraintSet.setVerticalBias(iconHolderView.id, ratio) - - smallConstraintSet.applyTo(view as ConstraintLayout?) + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + smallConstraintSet.applyTo(view) + } } size.isMedium && currentSize.isSmall -> { val autoTransition = AutoTransition() @@ -338,7 +294,19 @@ object BiometricViewSizeBinder { view, autoTransition ) - mediumConstraintSet.applyTo(view) + + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + mediumConstraintSet.applyTo(view) + } + } + size.isMedium -> { + if (position.isLeft) { + flipConstraintSet.applyTo(view) + } else { + mediumConstraintSet.applyTo(view) + } } size.isLarge -> { val autoTransition = AutoTransition() @@ -551,20 +519,6 @@ private fun View.showContentOrHide(forceHide: Boolean = false) { } } -private fun View.centerX(): Int { - return (x + width / 2).toInt() -} - -private fun View.centerY(): Int { - return (y + height / 2).toInt() -} - -private fun Rect.shouldAdjustLeftGuideline(): Boolean = left != -1 - -private fun Rect.shouldAdjustRightGuideline(): Boolean = right != -1 - -private fun Rect.shouldAdjustBottomGuideline(): Boolean = bottom != -1 - private fun View.asVerticalAnimator( duration: Long, toY: Float, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt index d8265c7d40e8..66b7d7afb8c4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -37,7 +37,6 @@ import com.android.systemui.util.kotlin.Utils.Companion.toQuint import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** Sub-binder for [BiometricPromptLayout.iconView]. */ @@ -127,14 +126,22 @@ object PromptIconViewBinder { if (constraintBp() && position != Rect()) { val iconParams = iconView.layoutParams as ConstraintLayout.LayoutParams - if (position.left != -1) { + if (position.left != 0) { iconParams.endToEnd = ConstraintSet.UNSET iconParams.leftMargin = position.left } - if (position.top != -1) { + if (position.top != 0) { iconParams.bottomToBottom = ConstraintSet.UNSET iconParams.topMargin = position.top } + if (position.right != 0) { + iconParams.startToStart = ConstraintSet.UNSET + iconParams.rightMargin = position.right + } + if (position.bottom != 0) { + iconParams.topToTop = ConstraintSet.UNSET + iconParams.bottomMargin = position.bottom + } iconView.layoutParams = iconParams } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt index d0c140b353e2..8dbed5f2e323 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt @@ -94,25 +94,76 @@ constructor( params.naturalDisplayHeight, rotation.ordinal ) - rotatedBounds + Rect( + rotatedBounds.left, + rotatedBounds.top, + params.logicalDisplayWidth - rotatedBounds.right, + params.logicalDisplayHeight - rotatedBounds.bottom + ) } .distinctUntilChanged() val iconPosition: Flow<Rect> = - combine(udfpsSensorBounds, promptViewModel.size, promptViewModel.modalities) { - sensorBounds, - size, - modalities -> - // If not Udfps, icon does not change from default layout position - if (!modalities.hasUdfps) { - Rect() // Empty rect, don't offset from default position - } else if (size.isSmall) { - // When small with Udfps, only set horizontal position - Rect(sensorBounds.left, -1, sensorBounds.right, -1) - } else { - sensorBounds + combine( + udfpsSensorBounds, + promptViewModel.size, + promptViewModel.position, + promptViewModel.modalities + ) { sensorBounds, size, position, modalities -> + when (position) { + PromptPosition.Bottom -> + if (size.isSmall) { + Rect(0, 0, 0, promptViewModel.portraitSmallBottomPadding) + } else if (size.isMedium && modalities.hasUdfps) { + Rect(0, 0, 0, sensorBounds.bottom) + } else if (size.isMedium) { + Rect(0, 0, 0, promptViewModel.portraitMediumBottomPadding) + } else { + // Large screen + Rect(0, 0, 0, promptViewModel.portraitLargeScreenBottomPadding) + } + PromptPosition.Right -> + if (size.isSmall || modalities.hasFaceOnly) { + Rect( + 0, + 0, + promptViewModel.landscapeSmallHorizontalPadding, + promptViewModel.landscapeSmallBottomPadding + ) + } else if (size.isMedium && modalities.hasUdfps) { + Rect(0, 0, sensorBounds.right, sensorBounds.bottom) + } else { + // SFPS + Rect( + 0, + 0, + promptViewModel.landscapeMediumHorizontalPadding, + promptViewModel.landscapeMediumBottomPadding + ) + } + PromptPosition.Left -> + if (size.isSmall || modalities.hasFaceOnly) { + Rect( + promptViewModel.landscapeSmallHorizontalPadding, + 0, + 0, + promptViewModel.landscapeSmallBottomPadding + ) + } else if (size.isMedium && modalities.hasUdfps) { + Rect(sensorBounds.left, 0, 0, sensorBounds.bottom) + } else { + // SFPS + Rect( + promptViewModel.landscapeMediumHorizontalPadding, + 0, + 0, + promptViewModel.landscapeMediumBottomPadding + ) + } + PromptPosition.Top -> Rect() + } } - } + .distinctUntilChanged() /** Whether an error message is currently being shown. */ val showingError = promptViewModel.showingError @@ -162,10 +213,11 @@ constructor( val iconSize: Flow<Pair<Int, Int>> = combine( + promptViewModel.position, activeAuthType, promptViewModel.fingerprintSensorWidth, promptViewModel.fingerprintSensorHeight, - ) { activeAuthType, fingerprintSensorWidth, fingerprintSensorHeight -> + ) { _, activeAuthType, fingerprintSensorWidth, fingerprintSensorHeight -> if (activeAuthType == AuthType.Face) { Pair(promptViewModel.faceIconWidth, promptViewModel.faceIconHeight) } else { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index fbd87fd3a9f5..21ebff4d0b71 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -86,6 +86,36 @@ constructor( val faceIconHeight: Int = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) + /** Padding for placing icons */ + val portraitSmallBottomPadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_portrait_small_bottom_padding + ) + val portraitMediumBottomPadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_portrait_medium_bottom_padding + ) + val portraitLargeScreenBottomPadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_portrait_large_screen_bottom_padding + ) + val landscapeSmallBottomPadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_landscape_small_bottom_padding + ) + val landscapeSmallHorizontalPadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_landscape_small_horizontal_padding + ) + val landscapeMediumBottomPadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_landscape_medium_bottom_padding + ) + val landscapeMediumHorizontalPadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_landscape_medium_horizontal_padding + ) + val fingerprintSensorWidth: Flow<Int> = combine(modalities, udfpsOverlayInteractor.udfpsOverlayParams) { modalities, overlayParams -> @@ -111,9 +141,6 @@ constructor( /** Hint for talkback directional guidance */ val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow() - val promptMargin: Int = - context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding) - private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) /** If the user is currently authenticating (i.e. at least one biometric is scanning). */ @@ -205,6 +232,66 @@ constructor( } .distinctUntilChanged() + /** Prompt panel size padding */ + private val smallHorizontalGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_small_horizontal_guideline_padding + ) + private val udfpsHorizontalGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_udfps_horizontal_guideline_padding + ) + private val udfpsMidGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_udfps_mid_guideline_padding + ) + private val mediumHorizontalGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_medium_horizontal_guideline_padding + ) + private val mediumMidGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_medium_mid_guideline_padding + ) + + /** + * Rect for positioning prompt guidelines (left, top, right, mid) + * + * Negative values are used to signify that guideline measuring should be flipped, measuring + * from opposite side of the screen + */ + val guidelineBounds: Flow<Rect> = + combine(size, position, modalities) { size, position, modalities -> + if (position.isBottom) { + Rect(0, 0, 0, 0) + } else if (position.isRight) { + if (size.isSmall) { + Rect(-smallHorizontalGuidelinePadding, 0, 0, 0) + } else if (modalities.hasUdfps) { + Rect(udfpsHorizontalGuidelinePadding, 0, 0, udfpsMidGuidelinePadding) + } else if (modalities.isEmpty) { + // TODO: Temporary fix until no biometric landscape layout is added + Rect(-mediumHorizontalGuidelinePadding, 0, 0, 6) + } else { + Rect(-mediumHorizontalGuidelinePadding, 0, 0, mediumMidGuidelinePadding) + } + } else if (position.isLeft) { + if (size.isSmall) { + Rect(0, 0, -smallHorizontalGuidelinePadding, 0) + } else if (modalities.hasUdfps) { + Rect(0, 0, udfpsHorizontalGuidelinePadding, -udfpsMidGuidelinePadding) + } else if (modalities.isEmpty) { + // TODO: Temporary fix until no biometric landscape layout is added + Rect(0, 0, -mediumHorizontalGuidelinePadding, -6) + } else { + Rect(0, 0, -mediumHorizontalGuidelinePadding, -mediumMidGuidelinePadding) + } + } else { + Rect() + } + } + .distinctUntilChanged() + /** * If the API caller or the user's personal preferences require explicit confirmation after * successful authentication. Confirmation always required when in explicit flow. @@ -424,7 +511,7 @@ constructor( isAuthenticated, promptSelectorInteractor.isCredentialAllowed, ) { size, _, authState, credentialAllowed -> - size.isNotSmall && authState.isNotAuthenticated && credentialAllowed + size.isMedium && authState.isNotAuthenticated && credentialAllowed } private val history = PromptHistoryImpl() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt index cfda75c7851a..b72b1f35595a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt @@ -28,6 +28,7 @@ import android.view.WindowManager import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY import com.airbnb.lottie.model.KeyPath +import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor @@ -155,17 +156,19 @@ constructor( -> val topLeft = Point(sensorLocation.left, sensorLocation.top) - if (sensorLocation.isSensorVerticalInDefaultOrientation) { - if (displayRotation == DisplayRotation.ROTATION_0) { - topLeft.x -= bounds!!.width() - } else if (displayRotation == DisplayRotation.ROTATION_270) { - topLeft.y -= bounds!!.height() - } - } else { - if (displayRotation == DisplayRotation.ROTATION_180) { - topLeft.y -= bounds!!.height() - } else if (displayRotation == DisplayRotation.ROTATION_270) { - topLeft.x -= bounds!!.width() + if (!constraintBp()) { + if (sensorLocation.isSensorVerticalInDefaultOrientation) { + if (displayRotation == DisplayRotation.ROTATION_0) { + topLeft.x -= bounds!!.width() + } else if (displayRotation == DisplayRotation.ROTATION_270) { + topLeft.y -= bounds!!.height() + } + } else { + if (displayRotation == DisplayRotation.ROTATION_180) { + topLeft.y -= bounds!!.height() + } else if (displayRotation == DisplayRotation.ROTATION_270) { + topLeft.x -= bounds!!.width() + } } } defaultOverlayViewParams.apply { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnInteractor.kt index 59fc81c82df0..f86cad5b9c59 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnInteractor.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.util.Log import com.android.systemui.dagger.SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt index 9ee582a77862..81fe2a5a5f0d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import android.util.Log diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt index 9c63a30dfc1c..94d7af74f1dd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter.STATE_OFF diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index a8d9e781228b..c7d171d5b804 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.os.Bundle import android.view.LayoutInflater diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt index 5d18dc1d453a..c30aea07e959 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel.DEBUG diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepository.kt index ea51beecc2c1..6e51915797cc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepository.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import com.android.settingslib.bluetooth.CachedBluetoothDevice diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt index cd52e0dcca4a..add1647143d8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index fd624d2f1ba1..e65b65710f94 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.content.Intent import android.content.SharedPreferences @@ -31,15 +31,15 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.Prefs import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS +import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE +import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE +import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.MAX_DEVICE_ITEM_ENTRY 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.plugins.ActivityStarter -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogDelegate.Companion.MAX_DEVICE_ITEM_ENTRY import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt index 1c621b87533d..dc5efefdfb16 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt @@ -30,7 +30,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.graphics.drawable.Drawable import com.android.settingslib.bluetooth.CachedBluetoothDevice diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index 56ba07941e4d..f04ba75ca3ef 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothDevice import android.content.Context diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt index fce25ec68190..4e28cafb5004 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt index f1a0e5e3539c..78811a96a026 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt @@ -12,14 +12,15 @@ import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.Flags.COMPOSE_BOUNCER_ENABLED import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.log.BouncerLogger import com.android.systemui.user.domain.interactor.SelectedUserInteractor import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi /** Helper data class that allows to lazy load all the dependencies of the legacy bouncer. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton data class LegacyBouncerDependencies @Inject @@ -59,7 +60,7 @@ constructor( private val composeBouncerDependencies: Lazy<ComposeBouncerDependencies>, ) { fun bind(view: ViewGroup) { - if (COMPOSE_BOUNCER_ENABLED && composeBouncerFlags.isOnlyComposeBouncerEnabled()) { + if (composeBouncerFlags.isOnlyComposeBouncerEnabled()) { val deps = composeBouncerDependencies.get() ComposeBouncerViewBinder.bind( view, 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 62da5c0e5675..12cac9251b25 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 @@ -99,7 +99,7 @@ class PinBouncerViewModel( .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown } .stateIn( scope = viewModelScope, - started = SharingStarted.Eagerly, + started = SharingStarted.WhileSubscribed(), initialValue = ActionButtonAppearance.Hidden, ) diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index 357eca37ba37..d2caefd3b552 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -398,6 +398,7 @@ public class BrightLineFalsingManager implements FalsingManager { || mAccessibilityManager.isTouchExplorationEnabled() || mDataProvider.isA11yAction() || mDataProvider.isFromTrackpad() + || mDataProvider.isFromKeyboard() || (mFeatureFlags.isEnabled(Flags.FALSING_OFF_FOR_UNFOLDED) && mDataProvider.isUnfolded()); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java index a79a654aedc2..76b228d4726a 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java @@ -16,6 +16,7 @@ package com.android.systemui.classifier; +import android.view.KeyEvent; import android.view.MotionEvent; /** @@ -50,6 +51,14 @@ public interface FalsingCollector { void onBouncerHidden(); /** + * Call this to record a KeyEvent in the {@link com.android.systemui.plugins.FalsingManager}. + * + * This may decide to only collect certain KeyEvents and ignore others. Do not assume all + * KeyEvents are collected. + */ + void onKeyEvent(KeyEvent ev); + + /** * Call this to record a MotionEvent in the {@link com.android.systemui.plugins.FalsingManager}. * * Be sure to call {@link #onMotionEventComplete()} after the rest of SystemUI is done with the diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java index d6b9a119e31c..dcd419512498 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java @@ -16,6 +16,7 @@ package com.android.systemui.classifier; +import android.view.KeyEvent; import android.view.MotionEvent; import javax.inject.Inject; @@ -23,6 +24,8 @@ import javax.inject.Inject; /** */ public class FalsingCollectorFake implements FalsingCollector { + public KeyEvent lastKeyEvent = null; + @Override public void init() { } @@ -70,6 +73,11 @@ public class FalsingCollectorFake implements FalsingCollector { } @Override + public void onKeyEvent(KeyEvent ev) { + lastKeyEvent = ev; + } + + @Override public void onTouchEvent(MotionEvent ev) { } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index 4f4f3d0324b3..beaa170943fd 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -21,6 +21,7 @@ import static com.android.systemui.dock.DockManager.DockEventListener; import android.hardware.SensorManager; import android.hardware.biometrics.BiometricSourceType; import android.util.Log; +import android.view.KeyEvent; import android.view.MotionEvent; import androidx.annotation.VisibleForTesting; @@ -49,7 +50,10 @@ import com.android.systemui.util.time.SystemClock; import dagger.Lazy; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import javax.inject.Inject; @@ -61,6 +65,14 @@ class FalsingCollectorImpl implements FalsingCollector { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final long GESTURE_PROCESSING_DELAY_MS = 100; + private final Set<Integer> mAcceptedKeycodes = new HashSet<>(Arrays.asList( + KeyEvent.KEYCODE_ENTER, + KeyEvent.KEYCODE_ESCAPE, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_SPACE + )); + private final FalsingDataProvider mFalsingDataProvider; private final FalsingManager mFalsingManager; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @@ -279,6 +291,14 @@ class FalsingCollectorImpl implements FalsingCollector { } @Override + public void onKeyEvent(KeyEvent ev) { + // Only collect if it is an ACTION_UP action and is allow-listed + if (ev.getAction() == KeyEvent.ACTION_UP && mAcceptedKeycodes.contains(ev.getKeyCode())) { + mFalsingDataProvider.onKeyEvent(ev); + } + } + + @Override public void onTouchEvent(MotionEvent ev) { logDebug("REAL: onTouchEvent(" + ev.getActionMasked() + ")"); if (!mKeyguardStateController.isShowing()) { diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt index c5d8c795853d..b289fa49d06c 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt @@ -16,6 +16,7 @@ package com.android.systemui.classifier +import android.view.KeyEvent import android.view.MotionEvent import com.android.systemui.classifier.FalsingCollectorImpl.logDebug import com.android.systemui.dagger.SysUISingleton @@ -59,6 +60,10 @@ class FalsingCollectorNoOp @Inject constructor() : FalsingCollector { logDebug("NOOP: onBouncerHidden") } + override fun onKeyEvent(ev: KeyEvent) { + logDebug("NOOP: onKeyEvent(${ev.action}") + } + override fun onTouchEvent(ev: MotionEvent) { logDebug("NOOP: onTouchEvent(${ev.actionMasked})") } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index 809d5b289cab..15017011134b 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -20,6 +20,7 @@ import static com.android.systemui.classifier.FalsingModule.IS_FOLDABLE_DEVICE; import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.util.DisplayMetrics; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; @@ -41,6 +42,7 @@ import javax.inject.Named; public class FalsingDataProvider { private static final long MOTION_EVENT_AGE_MS = 1000; + private static final long KEY_EVENT_AGE_MS = 500; private static final long DROP_EVENT_THRESHOLD_MS = 50; private static final float THREE_HUNDRED_SIXTY_DEG = (float) (2 * Math.PI); @@ -56,8 +58,10 @@ public class FalsingDataProvider { private final List<MotionEventListener> mMotionEventListeners = new ArrayList<>(); private final List<GestureFinalizedListener> mGestureFinalizedListeners = new ArrayList<>(); - private TimeLimitedMotionEventBuffer mRecentMotionEvents = - new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); + private TimeLimitedInputEventBuffer<MotionEvent> mRecentMotionEvents = + new TimeLimitedInputEventBuffer<>(MOTION_EVENT_AGE_MS); + private final TimeLimitedInputEventBuffer<KeyEvent> mRecentKeyEvents = + new TimeLimitedInputEventBuffer<>(KEY_EVENT_AGE_MS); private List<MotionEvent> mPriorMotionEvents = new ArrayList<>(); private boolean mDirty = true; @@ -89,6 +93,10 @@ public class FalsingDataProvider { FalsingClassifier.logInfo("width, height: " + getWidthPixels() + ", " + getHeightPixels()); } + void onKeyEvent(KeyEvent keyEvent) { + mRecentKeyEvents.add(keyEvent); + } + void onMotionEvent(MotionEvent motionEvent) { List<MotionEvent> motionEvents = unpackMotionEvent(motionEvent); FalsingClassifier.logVerbose("Unpacked into: " + motionEvents.size()); @@ -109,6 +117,10 @@ public class FalsingDataProvider { // previous ACTION_MOVE event and when it happens, it makes some classifiers fail. mDropLastEvent = shouldDropEvent(motionEvent); + if (!motionEvents.isEmpty() && !mRecentKeyEvents.isEmpty()) { + recycleAndClearRecentKeyEvents(); + } + mRecentMotionEvents.addAll(motionEvents); FalsingClassifier.logVerbose("Size: " + mRecentMotionEvents.size()); @@ -141,7 +153,7 @@ public class FalsingDataProvider { mRecentMotionEvents.get(mRecentMotionEvents.size() - 1).getEventTime())); mPriorMotionEvents = mRecentMotionEvents; - mRecentMotionEvents = new TimeLimitedMotionEventBuffer(MOTION_EVENT_AGE_MS); + mRecentMotionEvents = new TimeLimitedInputEventBuffer<>(MOTION_EVENT_AGE_MS); } mDropLastEvent = false; mA11YAction = false; @@ -261,6 +273,13 @@ public class FalsingDataProvider { return mLastMotionEvent.getY() < mFirstRecentMotionEvent.getY(); } + /** + * If it's a specific set of keys as collected from {@link FalsingCollector} + */ + public boolean isFromKeyboard() { + return !mRecentKeyEvents.isEmpty(); + } + public boolean isFromTrackpad() { if (mRecentMotionEvents.isEmpty()) { return false; @@ -318,6 +337,14 @@ public class FalsingDataProvider { } } + private void recycleAndClearRecentKeyEvents() { + for (KeyEvent ev : mRecentKeyEvents) { + ev.recycle(); + } + + mRecentKeyEvents.clear(); + } + private List<MotionEvent> unpackMotionEvent(MotionEvent motionEvent) { List<MotionEvent> motionEvents = new ArrayList<>(); List<PointerProperties> pointerPropertiesList = new ArrayList<>(); @@ -416,6 +443,8 @@ public class FalsingDataProvider { mRecentMotionEvents.clear(); + recycleAndClearRecentKeyEvents(); + mDirty = true; mSessionListeners.forEach(SessionListener::onSessionEnded); diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java b/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedInputEventBuffer.java index 51aede79b581..7627ad19f650 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedMotionEventBuffer.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/TimeLimitedInputEventBuffer.java @@ -16,7 +16,7 @@ package com.android.systemui.classifier; -import android.view.MotionEvent; +import android.view.InputEvent; import java.util.ArrayList; import java.util.Collection; @@ -25,31 +25,31 @@ import java.util.List; import java.util.ListIterator; /** - * Maintains an ordered list of the last N milliseconds of MotionEvents. + * Maintains an ordered list of the last N milliseconds of InputEvents. * * This class is simply a convenience class designed to look like a simple list, but that - * automatically discards old MotionEvents. It functions much like a queue - first in first out - + * automatically discards old InputEvents. It functions much like a queue - first in first out - * but does not have a fixed size like a circular buffer. */ -public class TimeLimitedMotionEventBuffer implements List<MotionEvent> { +public class TimeLimitedInputEventBuffer<T extends InputEvent> implements List<T> { - private final List<MotionEvent> mMotionEvents; + private final List<T> mInputEvents; private final long mMaxAgeMs; - public TimeLimitedMotionEventBuffer(long maxAgeMs) { + public TimeLimitedInputEventBuffer(long maxAgeMs) { super(); mMaxAgeMs = maxAgeMs; - mMotionEvents = new ArrayList<>(); + mInputEvents = new ArrayList<>(); } private void ejectOldEvents() { - if (mMotionEvents.isEmpty()) { + if (mInputEvents.isEmpty()) { return; } - Iterator<MotionEvent> iter = listIterator(); - long mostRecentMs = mMotionEvents.get(mMotionEvents.size() - 1).getEventTime(); + Iterator<T> iter = listIterator(); + long mostRecentMs = mInputEvents.get(mInputEvents.size() - 1).getEventTime(); while (iter.hasNext()) { - MotionEvent ev = iter.next(); + T ev = iter.next(); if (mostRecentMs - ev.getEventTime() > mMaxAgeMs) { iter.remove(); ev.recycle(); @@ -58,140 +58,140 @@ public class TimeLimitedMotionEventBuffer implements List<MotionEvent> { } @Override - public void add(int index, MotionEvent element) { + public void add(int index, T element) { throw new UnsupportedOperationException(); } @Override - public MotionEvent remove(int index) { - return mMotionEvents.remove(index); + public T remove(int index) { + return mInputEvents.remove(index); } @Override public int indexOf(Object o) { - return mMotionEvents.indexOf(o); + return mInputEvents.indexOf(o); } @Override public int lastIndexOf(Object o) { - return mMotionEvents.lastIndexOf(o); + return mInputEvents.lastIndexOf(o); } @Override public int size() { - return mMotionEvents.size(); + return mInputEvents.size(); } @Override public boolean isEmpty() { - return mMotionEvents.isEmpty(); + return mInputEvents.isEmpty(); } @Override public boolean contains(Object o) { - return mMotionEvents.contains(o); + return mInputEvents.contains(o); } @Override - public Iterator<MotionEvent> iterator() { - return mMotionEvents.iterator(); + public Iterator<T> iterator() { + return mInputEvents.iterator(); } @Override public Object[] toArray() { - return mMotionEvents.toArray(); + return mInputEvents.toArray(); } @Override - public <T> T[] toArray(T[] a) { - return mMotionEvents.toArray(a); + public <T2> T2[] toArray(T2[] a) { + return mInputEvents.toArray(a); } @Override - public boolean add(MotionEvent element) { - boolean result = mMotionEvents.add(element); + public boolean add(T element) { + boolean result = mInputEvents.add(element); ejectOldEvents(); return result; } @Override public boolean remove(Object o) { - return mMotionEvents.remove(o); + return mInputEvents.remove(o); } @Override public boolean containsAll(Collection<?> c) { - return mMotionEvents.containsAll(c); + return mInputEvents.containsAll(c); } @Override - public boolean addAll(Collection<? extends MotionEvent> collection) { - boolean result = mMotionEvents.addAll(collection); + public boolean addAll(Collection<? extends T> collection) { + boolean result = mInputEvents.addAll(collection); ejectOldEvents(); return result; } @Override - public boolean addAll(int index, Collection<? extends MotionEvent> elements) { + public boolean addAll(int index, Collection<? extends T> elements) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(Collection<?> c) { - return mMotionEvents.removeAll(c); + return mInputEvents.removeAll(c); } @Override public boolean retainAll(Collection<?> c) { - return mMotionEvents.retainAll(c); + return mInputEvents.retainAll(c); } @Override public void clear() { - mMotionEvents.clear(); + mInputEvents.clear(); } @Override public boolean equals(Object o) { - return mMotionEvents.equals(o); + return mInputEvents.equals(o); } @Override public int hashCode() { - return mMotionEvents.hashCode(); + return mInputEvents.hashCode(); } @Override - public MotionEvent get(int index) { - return mMotionEvents.get(index); + public T get(int index) { + return mInputEvents.get(index); } @Override - public MotionEvent set(int index, MotionEvent element) { + public T set(int index, T element) { throw new UnsupportedOperationException(); } @Override - public ListIterator<MotionEvent> listIterator() { + public ListIterator<T> listIterator() { return new Iter(0); } @Override - public ListIterator<MotionEvent> listIterator(int index) { + public ListIterator<T> listIterator(int index) { return new Iter(index); } @Override - public List<MotionEvent> subList(int fromIndex, int toIndex) { - return mMotionEvents.subList(fromIndex, toIndex); + public List<T> subList(int fromIndex, int toIndex) { + return mInputEvents.subList(fromIndex, toIndex); } - class Iter implements ListIterator<MotionEvent> { + class Iter implements ListIterator<T> { - private final ListIterator<MotionEvent> mIterator; + private final ListIterator<T> mIterator; Iter(int index) { - this.mIterator = mMotionEvents.listIterator(index); + this.mIterator = mInputEvents.listIterator(index); } @Override @@ -200,7 +200,7 @@ public class TimeLimitedMotionEventBuffer implements List<MotionEvent> { } @Override - public MotionEvent next() { + public T next() { return mIterator.next(); } @@ -210,7 +210,7 @@ public class TimeLimitedMotionEventBuffer implements List<MotionEvent> { } @Override - public MotionEvent previous() { + public T previous() { return mIterator.previous(); } @@ -230,12 +230,12 @@ public class TimeLimitedMotionEventBuffer implements List<MotionEvent> { } @Override - public void set(MotionEvent motionEvent) { + public void set(T inputEvent) { throw new UnsupportedOperationException(); } @Override - public void add(MotionEvent element) { + public void add(T element) { throw new UnsupportedOperationException(); } } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt index ce798ba3912f..f7ea25c8c0ca 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt @@ -17,6 +17,7 @@ package com.android.systemui.common.ui.view import android.view.View +import kotlinx.coroutines.DisposableHandle /** * Set this view's [View#importantForAccessibility] to [View#IMPORTANT_FOR_ACCESSIBILITY_YES] or @@ -43,3 +44,27 @@ inline fun <reified T : View> View.getNearestParent(): T? { } return null } + +/** Adds a [View.OnLayoutChangeListener] and provides a [DisposableHandle] for teardown. */ +fun View.onLayoutChanged(onLayoutChanged: (v: View) -> Unit): DisposableHandle = + onLayoutChanged { v, _, _, _, _, _, _, _, _ -> + onLayoutChanged(v) + } + +/** Adds the [View.OnLayoutChangeListener] and provides a [DisposableHandle] for teardown. */ +fun View.onLayoutChanged(listener: View.OnLayoutChangeListener): DisposableHandle { + addOnLayoutChangeListener(listener) + return DisposableHandle { removeOnLayoutChangeListener(listener) } +} + +/** Adds a [View.OnApplyWindowInsetsListener] and provides a [DisposableHandle] for teardown. */ +fun View.onApplyWindowInsets(listener: View.OnApplyWindowInsetsListener): DisposableHandle { + setOnApplyWindowInsetsListener(listener) + return DisposableHandle { setOnApplyWindowInsetsListener(null) } +} + +/** Adds a [View.OnTouchListener] and provides a [DisposableHandle] for teardown. */ +fun View.onTouchListener(listener: View.OnTouchListener): DisposableHandle { + setOnTouchListener(listener) + return DisposableHandle { setOnTouchListener(null) } +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index baae986c494d..0e04d15d8680 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -40,7 +40,6 @@ import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationSta import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.BiometricType import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository @@ -313,11 +312,7 @@ constructor( // or device starts going to sleep. merge( powerInteractor.isAsleep, - if (KeyguardWmStateRefactor.isEnabled) { - keyguardTransitionInteractor.isInTransitionToState(KeyguardState.GONE) - } else { - keyguardRepository.keyguardDoneAnimationsFinished.map { true } - }, + keyguardTransitionInteractor.isInTransitionToState(KeyguardState.GONE), userRepository.selectedUser.map { it.selectionStatus == SelectionStatus.SELECTION_IN_PROGRESS }, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 73878b6780d9..640534cc9d34 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -410,13 +410,6 @@ object Flags { val CLIPBOARD_SHARED_TRANSITIONS = unreleasedFlag("clipboard_shared_transitions", teamfood = true) - /** - * Whether the compose bouncer is enabled. This ensures ProGuard can - * remove unused code from our APK at compile time. - */ - // TODO(b/280877228): Tracking Bug - @JvmField val COMPOSE_BOUNCER_ENABLED = false - // 1900 @JvmField val NOTE_TASKS = releasedFlag("keycode_flag") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 9f7e0d43c3b5..e6e6ff6dcadb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -208,7 +208,7 @@ constructor( chipbarCoordinator, screenOffAnimationController, shadeInteractor, - { keyguardStatusViewController!!.getClockController() }, + clockInteractor, interactionJankMonitor, deviceEntryHapticsInteractor, vibratorHelper, 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 99b691ebd6ae..d551c9b9a4de 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 @@ -17,7 +17,9 @@ package com.android.systemui.keyguard.domain.interactor +import android.util.Log import com.android.keyguard.ClockEventController +import com.android.keyguard.KeyguardClockSwitch import com.android.keyguard.KeyguardClockSwitch.ClockSize import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardClockRepository @@ -54,6 +56,15 @@ constructor( keyguardClockRepository.setClockSize(size) } + val renderedClockId: ClockId + get() { + return clock?.let { clock -> clock.config.id } + ?: run { + Log.e(TAG, "No clock is available") + KeyguardClockSwitch.MISSING_CLOCK_ID + } + } + fun animateFoldToAod(foldFraction: Float) { clock?.let { clock -> clock.smallClock.animations.fold(foldFraction) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index 68ea5d047e1b..141cca329419 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.core.LogLevel.VERBOSE import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject @@ -69,9 +70,11 @@ constructor( } } - scope.launch { - sharedNotificationContainerViewModel.bounds.collect { - logger.log(TAG, VERBOSE, "Notif: bounds", it) + if (!SceneContainerFlag.isEnabled) { + scope.launch { + sharedNotificationContainerViewModel.bounds.collect { + logger.log(TAG, VERBOSE, "Notif: bounds", it) + } } } 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 1382468ce7d3..486320af45a2 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 @@ -34,6 +34,7 @@ import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launch +import com.android.internal.policy.SystemBarUtils import com.android.systemui.customization.R as customizationR import com.android.systemui.keyguard.shared.model.SettingsClockSize import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer @@ -116,7 +117,7 @@ object KeyguardPreviewClockViewBinder { constrainWidth(R.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT) constrainHeight(R.id.lockscreen_clock_view_large, ConstraintSet.MATCH_CONSTRAINT) val largeClockTopMargin = - context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + + SystemBarUtils.getStatusBarHeight(context) + context.resources.getDimensionPixelSize( customizationR.dimen.small_clock_padding_top ) + 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 5f50f7ec46e3..44fd58267250 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,17 +36,20 @@ import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launch 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.newAodTransition import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.common.ui.view.onApplyWindowInsets +import com.android.systemui.common.ui.view.onLayoutChanged +import com.android.systemui.common.ui.view.onTouchListener 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.KeyguardViewMediator import com.android.systemui.keyguard.MigrateClocksToBlueprint +import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters @@ -55,7 +58,6 @@ import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessage import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager -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.statusbar.CrossFadeHelper @@ -65,11 +67,11 @@ import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo +import com.android.systemui.util.kotlin.DisposableHandles import com.android.systemui.util.ui.AnimatedValue import com.android.systemui.util.ui.isAnimating import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value -import javax.inject.Provider import kotlin.math.min import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle @@ -93,7 +95,7 @@ object KeyguardRootViewBinder { chipbarCoordinator: ChipbarCoordinator, screenOffAnimationController: ScreenOffAnimationController, shadeInteractor: ShadeInteractor, - clockControllerProvider: Provider<ClockController>?, + clockInteractor: KeyguardClockInteractor, interactionJankMonitor: InteractionJankMonitor?, deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, vibratorHelper: VibratorHelper?, @@ -101,16 +103,19 @@ object KeyguardRootViewBinder { keyguardViewMediator: KeyguardViewMediator?, mainImmediateDispatcher: CoroutineDispatcher, ): DisposableHandle { - var onLayoutChangeListener: OnLayoutChange? = null + val disposables = DisposableHandles() val childViews = mutableMapOf<Int, View>() if (KeyguardBottomAreaRefactor.isEnabled) { - view.setOnTouchListener { _, event -> - if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) { - viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt())) + disposables += + view.onTouchListener { _, event -> + if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) { + viewModel.setRootViewLastTapPosition( + Point(event.x.toInt(), event.y.toInt()) + ) + } + false } - false - } } val burnInParams = MutableStateFlow(BurnInParameters()) @@ -119,7 +124,7 @@ object KeyguardRootViewBinder { alpha = { view.alpha }, ) - val disposableHandle = + disposables += view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { launch("$TAG#occludingAppDeviceEntryMessageViewModel.message") { @@ -281,14 +286,11 @@ object KeyguardRootViewBinder { viewModel.goneToAodTransition.collect { when (it.transitionState) { TransitionState.STARTED -> { - val clockId = - clockControllerProvider?.get()?.config?.id - ?: MISSING_CLOCK_ID + val clockId = clockInteractor.renderedClockId val builder = InteractionJankMonitor.Configuration.Builder .withView(CUJ_SCREEN_OFF_SHOW_AOD, view) .setTag(clockId) - jankMonitor.begin(builder) } TransitionState.CANCELED -> @@ -345,20 +347,13 @@ object KeyguardRootViewBinder { } } - if (!MigrateClocksToBlueprint.isEnabled) { - burnInParams.update { current -> - current.copy(clockControllerProvider = clockControllerProvider) - } - } - if (MigrateClocksToBlueprint.isEnabled) { burnInParams.update { current -> current.copy(translationY = { childViews[burnInLayerId]?.translationY }) } } - onLayoutChangeListener = OnLayoutChange(viewModel, childViews, burnInParams) - view.addOnLayoutChangeListener(onLayoutChangeListener) + disposables += view.onLayoutChanged(OnLayoutChange(viewModel, childViews, burnInParams)) // Views will be added or removed after the call to bind(). This is needed to avoid many // calls to findViewById @@ -373,24 +368,21 @@ object KeyguardRootViewBinder { } } ) - - view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> - val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() - burnInParams.update { current -> - current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top) - } - insets + disposables += DisposableHandle { + view.setOnHierarchyChangeListener(null) + childViews.clear() } - return object : DisposableHandle { - override fun dispose() { - disposableHandle.dispose() - view.removeOnLayoutChangeListener(onLayoutChangeListener) - view.setOnHierarchyChangeListener(null) - view.setOnApplyWindowInsetsListener(null) - childViews.clear() + disposables += + view.onApplyWindowInsets { _: View, insets: WindowInsets -> + val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() + burnInParams.update { current -> + current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top) + } + insets } - } + + return disposables } /** 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 eab4522271f4..ce1aed08ab49 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 @@ -47,6 +47,7 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.core.view.isInvisible +import com.android.internal.policy.SystemBarUtils import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch import com.android.systemui.animation.view.LaunchableImageView @@ -60,6 +61,7 @@ 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.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder @@ -140,6 +142,7 @@ constructor( private val secureSettings: SecureSettings, private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel, private val defaultShortcutsSection: DefaultShortcutsSection, + private val keyguardClockInteractor: KeyguardClockInteractor, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) @@ -364,6 +367,7 @@ constructor( ), ) } + @OptIn(ExperimentalCoroutinesApi::class) private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) @@ -377,7 +381,7 @@ constructor( chipbarCoordinator, screenOffAnimationController, shadeInteractor, - null, // clock provider only needed for burn in + keyguardClockInteractor, null, // jank monitor not required for preview mode null, // device entry haptics not required preview mode null, // device entry haptics not required for preview mode @@ -536,7 +540,7 @@ constructor( ) ) layoutParams.topMargin = - KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) + + SystemBarUtils.getStatusBarHeight(previewContext) + resources.getDimensionPixelSize( com.android.systemui.customization.R.dimen.small_clock_padding_top ) 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 20549328838f..4ddd57110b38 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 @@ -33,10 +33,8 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.ui.StateToValue -import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import javax.inject.Inject -import javax.inject.Provider import kotlin.math.max import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -128,12 +126,12 @@ constructor( yDimenResourceId = R.dimen.burn_in_prevention_offset_y ), ) { interpolated, burnIn -> + val useAltAod = + keyguardClockViewModel.currentClock.value?.let { clock -> + clock.config.useAlternateSmartspaceAODTransition + } == true val useScaleOnly = - (clockController(params.clockControllerProvider) - ?.get() - ?.config - ?.useAlternateSmartspaceAODTransition - ?: false) && keyguardClockViewModel.clockSize.value == KeyguardClockSwitch.LARGE + useAltAod && keyguardClockViewModel.clockSize.value == KeyguardClockSwitch.LARGE if (useScaleOnly) { BurnInModel( @@ -164,21 +162,10 @@ constructor( } } } - - private fun clockController( - provider: Provider<ClockController>?, - ): Provider<ClockController>? { - return if (MigrateClocksToBlueprint.isEnabled) { - Provider { keyguardClockViewModel.currentClock.value } - } else { - provider - } - } } /** UI-sourced parameters to pass into the various methods of [AodBurnInViewModel]. */ data class BurnInParameters( - val clockControllerProvider: Provider<ClockController>? = null, /** System insets that keyguard needs to stay out of */ val topInset: Int = 0, /** The min y-value of the visible elements on lockscreen */ 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 f6f3bb1431d6..c9251c7c5473 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 @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context import androidx.annotation.VisibleForTesting import androidx.constraintlayout.helper.widget.Layer +import com.android.internal.policy.SystemBarUtils import com.android.keyguard.KeyguardClockSwitch.LARGE import com.android.keyguard.KeyguardClockSwitch.SMALL import com.android.systemui.customization.R as customizationR @@ -165,7 +166,7 @@ constructor( companion object { fun getLargeClockTopMargin(context: Context): Int { - return context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + + return SystemBarUtils.getStatusBarHeight(context) + context.resources.getDimensionPixelSize( customizationR.dimen.small_clock_padding_top ) + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt index a90634e24580..b57e3ecbe05b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewSmartspaceViewModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context -import android.content.res.Resources import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.SettingsClockSize @@ -89,14 +88,4 @@ constructor( } } } - companion object { - fun getStatusBarHeight(resource: Resources): Int { - var result = 0 - val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android") - if (resourceId > 0) { - result = resource.getDimensionPixelSize(resourceId) - } - return result - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt index 3a19780c7017..a09d58ac381b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt @@ -21,15 +21,21 @@ import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.res.R +import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge /** * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to @@ -43,6 +49,8 @@ constructor( deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, configurationInteractor: ConfigurationInteractor, animationFlow: KeyguardTransitionAnimationFlow, + keyguardInteractor: KeyguardInteractor, + keyguardTransitionInteractor: KeyguardTransitionInteractor, ) : DeviceEntryIconTransition { private val transitionAnimation = @@ -74,11 +82,28 @@ constructor( /** Lockscreen views alpha */ val lockscreenAlpha: Flow<Float> = - transitionAnimation.sharedFlow( - startTime = 233.milliseconds, - duration = 250.milliseconds, - onStep = { it }, - name = "OCCLUDED->LOCKSCREEN: lockscreenAlpha", + merge( + transitionAnimation.sharedFlow( + startTime = 233.milliseconds, + duration = 250.milliseconds, + onStep = { it }, + name = "OCCLUDED->LOCKSCREEN: lockscreenAlpha", + ), + // Required to fix a bug where the shade expands while lockscreenAlpha=1f, due to a call + // to setOccluded(false) triggering a reset() call in KeyguardViewMediator. The + // permanent solution is to only expand the shade once the keyguard transition from + // OCCLUDED starts, but that requires more refactoring of expansion amounts. For now, + // emit alpha = 0f for OCCLUDED -> LOCKSCREEN whenever isOccluded flips from true to + // false while currentState == OCCLUDED, so that alpha = 0f when that expansion occurs. + // TODO(b/332946323): Remove this once it's no longer needed. + keyguardInteractor.isKeyguardOccluded + .pairwise() + .filter { (wasOccluded, isOccluded) -> + wasOccluded && + !isOccluded && + keyguardTransitionInteractor.getCurrentState() == KeyguardState.OCCLUDED + } + .map { 0f } ) val deviceEntryBackgroundViewAlpha: Flow<Float> = diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt index 1c11178b5b35..5dafd94f05e9 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -26,7 +26,9 @@ 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.Flags.coroutineTracing import com.android.systemui.util.Assert +import com.android.systemui.util.Compile import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.Dispatchers @@ -69,6 +71,12 @@ fun View.repeatWhenAttached( // presumably want to call view methods that require being called from said UI thread. val lifecycleCoroutineContext = Dispatchers.Main + createCoroutineTracingContext() + coroutineContext + val traceName = + if (Compile.IS_DEBUG && coroutineTracing()) { + traceSectionName() + } else { + DEFAULT_TRACE_NAME + } var lifecycleOwner: ViewLifecycleOwner? = null val onAttachListener = object : View.OnAttachStateChangeListener { @@ -77,6 +85,7 @@ fun View.repeatWhenAttached( lifecycleOwner?.onDestroy() lifecycleOwner = createLifecycleOwnerAndRun( + traceName, view, lifecycleCoroutineContext, block, @@ -93,6 +102,7 @@ fun View.repeatWhenAttached( if (view.isAttachedToWindow) { lifecycleOwner = createLifecycleOwnerAndRun( + traceName, view, lifecycleCoroutineContext, block, @@ -109,18 +119,14 @@ fun View.repeatWhenAttached( } private fun createLifecycleOwnerAndRun( + nameForTrace: String, view: View, coroutineContext: CoroutineContext, block: suspend LifecycleOwner.(View) -> Unit, ): ViewLifecycleOwner { return ViewLifecycleOwner(view).apply { onCreate() - lifecycleScope.launch( - "ViewLifecycleOwner(${view::class.java.simpleName})", - coroutineContext - ) { - block(view) - } + lifecycleScope.launch(nameForTrace, coroutineContext) { block(view) } } } @@ -186,3 +192,24 @@ class ViewLifecycleOwner( } } } + +private fun isFrameInteresting(frame: StackWalker.StackFrame): Boolean = + frame.className != CURRENT_CLASS_NAME && frame.className != JAVA_ADAPTER_CLASS_NAME + +/** Get a name for the trace section include the name of the call site. */ +private fun traceSectionName(): String { + val interestingFrame = + StackWalker.getInstance().walk { stream -> + stream.filter(::isFrameInteresting).limit(5).findFirst() + } + if (interestingFrame.isPresent) { + val frame = interestingFrame.get() + return "${frame.className}#${frame.methodName}:${frame.lineNumber} [$DEFAULT_TRACE_NAME]" + } else { + return DEFAULT_TRACE_NAME + } +} + +private const val DEFAULT_TRACE_NAME = "repeatWhenAttached" +private const val CURRENT_CLASS_NAME = "com.android.systemui.lifecycle.RepeatWhenAttachedKt" +private const val JAVA_ADAPTER_CLASS_NAME = "com.android.systemui.util.kotlin.JavaAdapterKt" diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt index d2471225a093..f08bc17c4f23 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt @@ -25,8 +25,8 @@ import com.android.launcher3.icons.IconFactory import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerLabelLoader import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader -import com.android.systemui.mediaprojection.appselector.data.AppIconLoader -import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader +import com.android.systemui.mediaprojection.appselector.data.BasicAppIconLoader +import com.android.systemui.mediaprojection.appselector.data.BasicPackageManagerAppIconLoader import com.android.systemui.mediaprojection.appselector.data.RecentTaskLabelLoader import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader @@ -102,7 +102,7 @@ interface MediaProjectionAppSelectorModule { @Binds @MediaProjectionAppSelectorScope - fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader + fun bindAppIconLoader(impl: BasicPackageManagerAppIconLoader): BasicAppIconLoader @Binds @IntoSet diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/BadgedAppIconLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/BadgedAppIconLoader.kt new file mode 100644 index 000000000000..ca5b5f842b25 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/BadgedAppIconLoader.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.mediaprojection.appselector.data + +import android.content.ComponentName +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.UserHandle +import com.android.launcher3.icons.BaseIconFactory +import com.android.launcher3.icons.IconFactory +import com.android.launcher3.util.UserIconInfo +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject +import javax.inject.Provider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +class BadgedAppIconLoader +@Inject +constructor( + private val basicAppIconLoader: BasicAppIconLoader, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val context: Context, + private val iconFactoryProvider: Provider<IconFactory>, +) { + + suspend fun loadIcon( + userId: Int, + userType: RecentTask.UserType, + componentName: ComponentName + ): Drawable? = + withContext(backgroundDispatcher) { + iconFactoryProvider.get().use<IconFactory, Drawable?> { iconFactory -> + val icon = + basicAppIconLoader.loadIcon(userId, componentName) ?: return@withContext null + val userHandler = UserHandle.of(userId) + val iconType = getIconType(userType) + val options = + BaseIconFactory.IconOptions().apply { + setUser(UserIconInfo(userHandler, iconType)) + } + val badgedIcon = iconFactory.createBadgedIconBitmap(icon, options) + badgedIcon.newIcon(context) + } + } + + private fun getIconType(userType: RecentTask.UserType): Int = + when (userType) { + RecentTask.UserType.CLONED -> UserIconInfo.TYPE_CLONED + RecentTask.UserType.WORK -> UserIconInfo.TYPE_WORK + RecentTask.UserType.PRIVATE -> UserIconInfo.TYPE_PRIVATE + RecentTask.UserType.STANDARD -> UserIconInfo.TYPE_MAIN + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/BasicAppIconLoader.kt index b85d6285c35b..03f6f015300f 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/BasicAppIconLoader.kt @@ -17,45 +17,29 @@ package com.android.systemui.mediaprojection.appselector.data import android.content.ComponentName -import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.Drawable -import android.os.UserHandle -import com.android.launcher3.icons.BaseIconFactory.IconOptions -import com.android.launcher3.icons.IconFactory import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.shared.system.PackageManagerWrapper import javax.inject.Inject -import javax.inject.Provider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -interface AppIconLoader { +interface BasicAppIconLoader { suspend fun loadIcon(userId: Int, component: ComponentName): Drawable? } -class IconLoaderLibAppIconLoader +class BasicPackageManagerAppIconLoader @Inject constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, - private val context: Context, // Use wrapper to access hidden API that allows to get ActivityInfo for any user id private val packageManagerWrapper: PackageManagerWrapper, private val packageManager: PackageManager, - private val iconFactoryProvider: Provider<IconFactory> -) : AppIconLoader { +) : BasicAppIconLoader { override suspend fun loadIcon(userId: Int, component: ComponentName): Drawable? = withContext(backgroundDispatcher) { - iconFactoryProvider.get().use<IconFactory, Drawable?> { iconFactory -> - val activityInfo = - packageManagerWrapper.getActivityInfo(component, userId) - ?: return@withContext null - val icon = activityInfo.loadIcon(packageManager) ?: return@withContext null - val userHandler = UserHandle.of(userId) - val options = IconOptions().apply { setUser(userHandler) } - val badgedIcon = iconFactory.createBadgedIconBitmap(icon, options) - badgedIcon.newIcon(context) - } + packageManagerWrapper.getActivityInfo(component, userId)?.loadIcon(packageManager) } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt index e9b458271ef7..3e9b546d58c9 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt @@ -28,4 +28,12 @@ data class RecentTask( val baseIntentComponent: ComponentName?, @ColorInt val colorBackground: Int?, val isForegroundTask: Boolean, -) + val userType: UserType, +) { + enum class UserType { + STANDARD, + WORK, + PRIVATE, + CLONED + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt index 5dde14bf0867..a6049c8b556d 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt @@ -17,6 +17,8 @@ package com.android.systemui.mediaprojection.appselector.data import android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE +import android.content.pm.UserInfo +import android.os.UserManager import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.settings.UserTracker import com.android.systemui.util.kotlin.getOrNull @@ -41,7 +43,8 @@ constructor( @Background private val coroutineDispatcher: CoroutineDispatcher, @Background private val backgroundExecutor: Executor, private val recentTasks: Optional<RecentTasks>, - private val userTracker: UserTracker + private val userTracker: UserTracker, + private val userManager: UserManager, ) : RecentTaskListProvider { private val recents by lazy { recentTasks.getOrNull() } @@ -65,7 +68,8 @@ constructor( it.topActivity, it.baseIntent?.component, it.taskDescription?.backgroundColor, - isForegroundTask = it.taskId in foregroundTaskIds && it.isVisible + isForegroundTask = it.taskId in foregroundTaskIds && it.isVisible, + userType = userManager.getUserInfo(it.userId).toUserType(), ) } } @@ -81,4 +85,15 @@ constructor( continuation.resume(tasks) } } + + private fun UserInfo.toUserType(): RecentTask.UserType = + if (isCloneProfile) { + RecentTask.UserType.CLONED + } else if (isManagedProfile) { + RecentTask.UserType.WORK + } else if (isPrivateProfile) { + RecentTask.UserType.PRIVATE + } else { + RecentTask.UserType.STANDARD + } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt index 3fe040a0d715..3b84d2c53a2b 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt @@ -21,12 +21,12 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView.ViewHolder -import com.android.systemui.res.R import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelector -import com.android.systemui.mediaprojection.appselector.data.AppIconLoader +import com.android.systemui.mediaprojection.appselector.data.BadgedAppIconLoader import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.data.RecentTaskLabelLoader import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -39,7 +39,7 @@ class RecentTaskViewHolder @AssistedInject constructor( @Assisted private val root: ViewGroup, - private val iconLoader: AppIconLoader, + private val iconLoader: BadgedAppIconLoader, private val thumbnailLoader: RecentTaskThumbnailLoader, private val labelLoader: RecentTaskLabelLoader, private val taskViewSizeProvider: TaskPreviewSizeProvider, @@ -63,7 +63,7 @@ constructor( scope.launch { task.baseIntentComponent?.let { component -> launch { - val icon = iconLoader.loadIcon(task.userId, component) + val icon = iconLoader.loadIcon(task.userId, task.userType, component) iconView.setImageDrawable(icon) } launch { diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index e861ddf69aa6..da9e00ddb6c2 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -16,11 +16,8 @@ package com.android.systemui.mediaprojection.permission; -import static android.Manifest.permission.LOG_COMPAT_CHANGE; -import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG; import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT; import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT; -import static android.media.projection.MediaProjectionManager.OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION; import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL; import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; @@ -29,13 +26,11 @@ import static com.android.systemui.mediaprojection.permission.ScreenShareOptionK import static com.android.systemui.mediaprojection.permission.ScreenShareOptionKt.SINGLE_APP; import android.annotation.Nullable; -import android.annotation.RequiresPermission; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions.LaunchCookie; import android.app.AlertDialog; import android.app.StatusBarManager; -import android.app.compat.CompatChanges; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -113,7 +108,6 @@ public class MediaProjectionPermissionActivity extends Activity } @Override - @RequiresPermission(allOf = {READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE}) public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -241,10 +235,6 @@ public class MediaProjectionPermissionActivity extends Activity // the correct screen width when in split screen. Context dialogContext = getApplicationContext(); if (isPartialScreenSharingEnabled()) { - final boolean overrideDisableSingleAppOption = - CompatChanges.isChangeEnabled( - OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION, - mPackageName, getHostUserHandle()); MediaProjectionPermissionDialogDelegate delegate = new MediaProjectionPermissionDialogDelegate( dialogContext, @@ -256,7 +246,6 @@ public class MediaProjectionPermissionActivity extends Activity }, () -> finish(RECORD_CANCEL, /* projection= */ null), appName, - overrideDisableSingleAppOption, mUid, mMediaProjectionMetricsLogger); mDialog = diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt index 8858041ae529..0f54e934f3cf 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt @@ -30,12 +30,11 @@ class MediaProjectionPermissionDialogDelegate( private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>, private val onCancelClicked: Runnable, private val appName: String?, - private val forceShowPartialScreenshare: Boolean, hostUid: Int, mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, ) : BaseMediaProjectionPermissionDialogDelegate<AlertDialog>( - createOptionList(context, appName, mediaProjectionConfig, forceShowPartialScreenshare), + createOptionList(context, appName, mediaProjectionConfig), appName, hostUid, mediaProjectionMetricsLogger @@ -66,8 +65,7 @@ class MediaProjectionPermissionDialogDelegate( private fun createOptionList( context: Context, appName: String?, - mediaProjectionConfig: MediaProjectionConfig?, - overrideDisableSingleAppOption: Boolean = false, + mediaProjectionConfig: MediaProjectionConfig? ): List<ScreenShareOption> { val singleAppWarningText = if (appName == null) { @@ -82,13 +80,8 @@ class MediaProjectionPermissionDialogDelegate( R.string.media_projection_entry_app_permission_dialog_warning_entire_screen } - // The single app option should only be disabled if there is an app name provided, - // the client has setup a MediaProjection with - // MediaProjectionConfig#createConfigForDefaultDisplay, AND it hasn't been overridden by - // the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override. val singleAppOptionDisabled = appName != null && - !overrideDisableSingleAppOption && mediaProjectionConfig?.regionToCapture == MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 4fe3a11078db..ade56c435421 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -85,6 +85,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.InternalInsetsInfo; import android.view.ViewTreeObserver.OnComputeInternalInsetsListener; @@ -186,6 +187,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements /** Allow some time inbetween the long press for back and recents. */ private static final int LOCK_TO_APP_GESTURE_TOLERANCE = 200; private static final long AUTODIM_TIMEOUT_MS = 2250; + private static final float QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON = 3f; private final Context mContext; private final Bundle mSavedState; @@ -223,6 +225,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final int mNavColorSampleMargin; private EdgeBackGestureHandler mEdgeBackGestureHandler; private NavigationBarFrame mFrame; + private MotionEvent mCurrentDownEvent; private @WindowVisibleState int mNavigationBarWindowState = WINDOW_STATE_SHOWING; @@ -238,6 +241,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private int mLayoutDirection; private Optional<Long> mHomeButtonLongPressDurationMs; + private Optional<Long> mOverrideHomeButtonLongPressDurationMs = Optional.empty(); + private Optional<Float> mOverrideHomeButtonLongPressSlopMultiplier = Optional.empty(); /** @see android.view.WindowInsetsController#setSystemBarsAppearance(int, int) */ private @Appearance int mAppearance; @@ -405,6 +410,25 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } @Override + public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) { + mOverrideHomeButtonLongPressDurationMs = Optional.of(duration) + .filter(value -> value > 0); + mOverrideHomeButtonLongPressSlopMultiplier = Optional.of(slopMultiplier) + .filter(value -> value > 0); + if (mOverrideHomeButtonLongPressDurationMs.isPresent()) { + Log.d(TAG, "Receive duration override: " + + mOverrideHomeButtonLongPressDurationMs.get()); + } + if (mOverrideHomeButtonLongPressSlopMultiplier.isPresent()) { + Log.d(TAG, "Receive slop multiplier override: " + + mOverrideHomeButtonLongPressSlopMultiplier.get()); + } + if (mView != null) { + reconfigureHomeLongClick(); + } + } + + @Override public void onHomeRotationEnabled(boolean enabled) { mView.getRotationButtonController().setHomeRotationEnabled(enabled); } @@ -1016,7 +1040,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements if (mView.getHomeButton().getCurrentView() == null) { return; } - if (mHomeButtonLongPressDurationMs.isPresent() || !mLongPressHomeEnabled) { + if (mHomeButtonLongPressDurationMs.isPresent() + || mOverrideHomeButtonLongPressDurationMs.isPresent() + || mOverrideHomeButtonLongPressSlopMultiplier.isPresent() + || !mLongPressHomeEnabled) { mView.getHomeButton().getCurrentView().setLongClickable(false); mView.getHomeButton().getCurrentView().setHapticFeedbackEnabled(false); mView.getHomeButton().setOnLongClickListener(null); @@ -1038,6 +1065,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements pw.println(" mStartingQuickSwitchRotation=" + mStartingQuickSwitchRotation); pw.println(" mCurrentRotation=" + mCurrentRotation); pw.println(" mHomeButtonLongPressDurationMs=" + mHomeButtonLongPressDurationMs); + pw.println(" mOverrideHomeButtonLongPressDurationMs=" + + mOverrideHomeButtonLongPressDurationMs); + pw.println(" mOverrideHomeButtonLongPressSlopMultiplier=" + + mOverrideHomeButtonLongPressSlopMultiplier); pw.println(" mLongPressHomeEnabled=" + mLongPressHomeEnabled); pw.println(" mNavigationBarWindowState=" + windowStateToString(mNavigationBarWindowState)); @@ -1331,6 +1362,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements final Optional<CentralSurfaces> centralSurfacesOptional = mCentralSurfacesOptionalLazy.get(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: + if (mCurrentDownEvent != null) { + mCurrentDownEvent.recycle(); + } + mCurrentDownEvent = MotionEvent.obtain(event); mHomeBlockedThisTouch = false; if (mTelecomManagerOptional.isPresent() && mTelecomManagerOptional.get().isRinging()) { @@ -1342,9 +1377,45 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } } if (mLongPressHomeEnabled) { - mHomeButtonLongPressDurationMs.ifPresent(longPressDuration -> { - mHandler.postDelayed(mOnVariableDurationHomeLongClick, longPressDuration); - }); + if (mOverrideHomeButtonLongPressDurationMs.isPresent()) { + Log.d(TAG, "ACTION_DOWN Launcher override duration: " + + mOverrideHomeButtonLongPressDurationMs.get()); + mHandler.postDelayed(mOnVariableDurationHomeLongClick, + mOverrideHomeButtonLongPressDurationMs.get()); + } else if (mOverrideHomeButtonLongPressSlopMultiplier.isPresent()) { + // If override timeout doesn't exist but override touch slop exists, we use + // system default long press duration + Log.d(TAG, "ACTION_DOWN default duration: " + + ViewConfiguration.getLongPressTimeout()); + mHandler.postDelayed(mOnVariableDurationHomeLongClick, + ViewConfiguration.getLongPressTimeout()); + } else { + mHomeButtonLongPressDurationMs.ifPresent(longPressDuration -> { + Log.d(TAG, "ACTION_DOWN original duration: " + longPressDuration); + mHandler.postDelayed(mOnVariableDurationHomeLongClick, + longPressDuration); + }); + } + } + break; + case MotionEvent.ACTION_MOVE: + if (!mHandler.hasCallbacks(mOnVariableDurationHomeLongClick)) { + Log.w(TAG, "No callback. Don't handle touch slop."); + break; + } + float customSlopMultiplier = mOverrideHomeButtonLongPressSlopMultiplier.orElse(1f); + float touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + float calculatedTouchSlop = + customSlopMultiplier * QUICKSTEP_TOUCH_SLOP_RATIO_TWO_BUTTON * touchSlop; + float touchSlopSquared = calculatedTouchSlop * calculatedTouchSlop; + + float dx = event.getX() - mCurrentDownEvent.getX(); + float dy = event.getY() - mCurrentDownEvent.getY(); + double distanceSquared = (dx * dx) + (dy * dy); + if (distanceSquared > touchSlopSquared) { + Log.i(TAG, "Touch slop passed. Abort."); + mView.abortCurrentGesture(); + mHandler.removeCallbacks(mOnVariableDurationHomeLongClick); } break; case MotionEvent.ACTION_UP: diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index 3f8834af3ea4..9380d44dc27f 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -281,5 +281,10 @@ constructor( powerButtonLaunchGestureTriggeredDuringSleep = false, ) } + + /** Helper method for tests to simulate the device screen state change event. */ + fun PowerInteractor.setScreenPowerState(screenPowerState: ScreenPowerState) { + this.onScreenPowerStateUpdated(screenPowerState) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java index 3b3844a4be7b..e4249757d737 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java @@ -31,7 +31,7 @@ import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.qs.QSContainerController; import com.android.systemui.qs.dagger.QSFragmentComponent; import com.android.systemui.res.R; -import com.android.systemui.statusbar.policy.BrightnessMirrorController; +import com.android.systemui.settings.brightness.MirrorController; import com.android.systemui.util.LifecycleFragment; import java.util.function.Consumer; @@ -182,7 +182,7 @@ public class QSFragmentLegacy extends LifecycleFragment implements QS { } public void setBrightnessMirrorController( - BrightnessMirrorController brightnessMirrorController) { + MirrorController brightnessMirrorController) { if (mQsImpl != null) { mQsImpl.setBrightnessMirrorController(brightnessMirrorController); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java index a000d63a2ee3..a0607e9f859a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java @@ -61,6 +61,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; import com.android.systemui.scene.shared.flag.SceneContainerFlags; +import com.android.systemui.settings.brightness.MirrorController; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; @@ -68,7 +69,6 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.phone.KeyguardBypassController; -import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.util.Utils; @@ -544,7 +544,7 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl } public void setBrightnessMirrorController( - BrightnessMirrorController brightnessMirrorController) { + @Nullable MirrorController brightnessMirrorController) { mQSPanelController.setBrightnessMirror(brightnessMirrorController); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index cd6511979375..55dc4859cf90 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -24,6 +24,8 @@ import static com.android.systemui.qs.dagger.QSScopeModule.QS_USING_MEDIA_PLAYER import android.view.MotionEvent; import android.view.View; +import androidx.annotation.Nullable; + import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.dump.DumpManager; @@ -38,9 +40,9 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlags; import com.android.systemui.settings.brightness.BrightnessController; import com.android.systemui.settings.brightness.BrightnessMirrorHandler; import com.android.systemui.settings.brightness.BrightnessSliderController; +import com.android.systemui.settings.brightness.MirrorController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.tuner.TunerService; @@ -139,6 +141,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { mBrightnessMirrorHandler.onQsPanelAttached(); PagedTileLayout pagedTileLayout= ((PagedTileLayout) mView.getOrCreateTileLayout()); pagedTileLayout.setOnTouchListener(mTileLayoutTouchListener); + maybeReinflateBrightnessSlider(); } @Override @@ -157,15 +160,18 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { @Override protected void onConfigurationChanged() { mView.updateResources(); + maybeReinflateBrightnessSlider(); + if (mView.isListening()) { + refreshAllTiles(); + } + } + + private void maybeReinflateBrightnessSlider() { int newDensity = mView.getResources().getConfiguration().densityDpi; if (newDensity != mLastDensity) { mLastDensity = newDensity; reinflateBrightnessSlider(); } - - if (mView.isListening()) { - refreshAllTiles(); - } } private void reinflateBrightnessSlider() { @@ -210,7 +216,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { } } - public void setBrightnessMirror(BrightnessMirrorController brightnessMirrorController) { + public void setBrightnessMirror(@Nullable MirrorController brightnessMirrorController) { mBrightnessMirrorHandler.setController(brightnessMirrorController); } 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 b0707db0d02d..6eae32a0ffd6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -39,6 +39,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogViewModel; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; @@ -51,7 +52,6 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogViewModel; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.BluetoothController; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java index 2f8fe42672c2..3eeb2a3303be 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java @@ -25,6 +25,7 @@ import android.os.Looper; import android.provider.Settings; import android.safetycenter.SafetyCenterManager; import android.service.quicksettings.Tile; +import android.text.TextUtils; import android.view.View; import android.widget.Switch; @@ -127,7 +128,7 @@ public abstract class SensorPrivacyToggleTile extends QSTileImpl<QSTile.BooleanS } else { state.secondaryLabel = mContext.getString(R.string.quick_settings_camera_mic_available); } - state.contentDescription = state.label; + state.contentDescription = TextUtils.concat(state.label, ", ", state.secondaryLabel); state.expandedAccessibilityClassName = Switch.class.getName(); } 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 671050477042..3d86e3c084f8 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 @@ -34,6 +34,7 @@ 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.settings.brightness.MirrorController import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.util.kotlin.sample @@ -68,6 +69,9 @@ interface QSSceneAdapter { */ val qsView: Flow<View> + /** Sets the [MirrorController] in [QSImpl]. Set to `null` to remove. */ + fun setBrightnessMirrorController(mirrorController: MirrorController?) + /** * Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in * [qsView]. Re-inflations due to configuration changes will use the last used [context]. @@ -93,6 +97,9 @@ interface QSSceneAdapter { val isQsFullyCollapsed: Boolean get() = true + /** Request that the customizer be closed. Possibly animating it. */ + fun requestCloseCustomizer() + sealed interface State { val isVisible: Boolean @@ -203,7 +210,7 @@ constructor( applicationScope.launch { launch { state.sample(_isCustomizing, ::Pair).collect { (state, customizing) -> - _qsImpl.value?.apply { + qsImpl.value?.apply { if (state != QSSceneAdapter.State.QS && customizing) { this@apply.closeCustomizerImmediately() } @@ -277,6 +284,14 @@ constructor( bottomNavBarSize.emit(padding) } + override fun requestCloseCustomizer() { + qsImpl.value?.closeCustomizer() + } + + override fun setBrightnessMirrorController(mirrorController: MirrorController?) { + qsImpl.value?.setBrightnessMirrorController(mirrorController) + } + private fun QSImpl.applyState(state: QSSceneAdapter.State) { setQsVisible(state.isVisible) setExpanded(state.isVisible && state.expansion > 0f) 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 c695d4c98308..4c8647191cb6 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 @@ -20,13 +20,13 @@ import androidx.lifecycle.LifecycleOwner import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.SwipeDirection -import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import java.util.concurrent.atomic.AtomicBoolean @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.map class QuickSettingsSceneViewModel @Inject constructor( + val brightnessMirrorViewModel: BrightnessMirrorViewModel, val shadeHeaderViewModel: ShadeHeaderViewModel, val qsSceneAdapter: QSSceneAdapter, val notifications: NotificationsPlaceholderViewModel, @@ -47,7 +48,9 @@ constructor( val destinationScenes = qsSceneAdapter.isCustomizing.map { customizing -> if (customizing) { - mapOf<UserAction, UserActionResult>(Back to UserActionResult(Scenes.QuickSettings)) + // TODO(b/332749288) Empty map so there are no back handlers and back can close + // customizer + emptyMap() // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade // while customizing } else { diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 7c1a2c032bea..4ece7b6c8990 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -99,6 +99,7 @@ import com.android.systemui.navigationbar.buttons.KeyButtonView; import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.SceneContainerFlags; +import com.android.systemui.scene.shared.model.Scenes; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeViewController; @@ -239,6 +240,11 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } else { mShadeViewControllerLazy.get().finishInputFocusTransfer(velocity); } + } else if (action == ACTION_UP) { + // Gesture was too short to be picked up by scene container touch + // handling; programmatically start the transition to shade scene. + mSceneInteractor.get().changeScene( + Scenes.Shade, "short launcher swipe"); } } event.recycle(); @@ -259,6 +265,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } @Override + public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) { + verifyCallerAndClearCallingIdentityPostMain("setOverrideHomeButtonLongPress", + () -> notifySetOverrideHomeButtonLongPress(duration, slopMultiplier)); + } + + @Override public void onBackPressed() { verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> { sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); @@ -947,6 +959,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } } + private void notifySetOverrideHomeButtonLongPress(long duration, float slopMultiplier) { + for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) { + mConnectionCallbacks.get(i).setOverrideHomeButtonLongPress(duration, slopMultiplier); + } + } + public void notifyAssistantVisibilityChanged(float visibility) { try { if (mOverviewProxy != null) { @@ -1104,6 +1122,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis default void startAssistant(Bundle bundle) {} default void setAssistantOverridesRequested(int[] invocationTypes) {} default void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) {} + /** Set override of home button long press duration and touch slop multiplier. */ + default void setOverrideHomeButtonLongPress(long override, float slopMultiplier) {} } /** diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index 5e4919d44f23..4d34a869002a 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -16,6 +16,7 @@ package com.android.systemui.recordissue +import android.app.IActivityManager import android.app.NotificationManager import android.content.Context import android.content.Intent @@ -51,7 +52,7 @@ class IssueRecordingService @Inject constructor( controller: RecordingController, - @LongRunning executor: Executor, + @LongRunning private val bgExecutor: Executor, @Main handler: Handler, uiEventLogger: UiEventLogger, notificationManager: NotificationManager, @@ -60,10 +61,12 @@ constructor( private val dialogTransitionAnimator: DialogTransitionAnimator, private val panelInteractor: PanelInteractor, private val issueRecordingState: IssueRecordingState, + private val iActivityManager: IActivityManager, + private val launcherApps: LauncherApps, ) : RecordingService( controller, - executor, + bgExecutor, handler, uiEventLogger, notificationManager, @@ -103,12 +106,26 @@ constructor( // ViewCapture needs to save it's data before it is disabled, or else the data will // be lost. This is expected to change in the near future, and when that happens // this line should be removed. - getSystemService(LauncherApps::class.java)?.saveViewCaptureData() + launcherApps.saveViewCaptureData() TraceUtils.traceStop(contentResolver) issueRecordingState.isRecording = false } ACTION_SHARE -> { - shareRecording(intent) + bgExecutor.execute { + mNotificationManager.cancelAsUser( + null, + mNotificationId, + UserHandle(mUserContextTracker.userContext.userId) + ) + + val screenRecording = intent.getParcelableExtra(EXTRA_PATH, Uri::class.java) + if (issueRecordingState.takeBugReport) { + iActivityManager.requestBugReportWithExtraAttachment(screenRecording) + } else { + shareRecording(screenRecording) + } + } + dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() panelInteractor.collapsePanels() @@ -122,23 +139,17 @@ constructor( return super.onStartCommand(intent, flags, startId) } - private fun shareRecording(intent: Intent) { + private fun shareRecording(screenRecording: Uri?) { val sharableUri: Uri = zipAndPackageRecordings( TraceUtils.traceDump(contentResolver, TRACE_FILE_NAME).get(), - intent.getStringExtra(EXTRA_PATH) + screenRecording ) ?: return val sendIntent = FileSender.buildSendIntent(this, listOf(sharableUri)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - mNotificationManager.cancelAsUser( - null, - mNotificationId, - UserHandle(mUserContextTracker.userContext.userId) - ) - // TODO: Debug why the notification shade isn't closing upon starting the BetterBug activity mKeyguardDismissUtil.executeWhenUnlocked( { @@ -150,7 +161,7 @@ constructor( ) } - private fun zipAndPackageRecordings(traceFiles: List<File>, screenRecordingUri: String?): Uri? { + private fun zipAndPackageRecordings(traceFiles: List<File>, screenRecording: Uri?): Uri? { try { externalCacheDir?.mkdirs() val outZip: File = File.createTempFile(TEMP_FILE_PREFIX, ZIP_SUFFIX, externalCacheDir) @@ -160,8 +171,8 @@ constructor( Files.copy(file.toPath(), os) os.closeEntry() } - if (screenRecordingUri != null) { - contentResolver.openInputStream(Uri.parse(screenRecordingUri))?.use { + if (screenRecording != null) { + contentResolver.openInputStream(screenRecording)?.use { os.putNextEntry(ZipEntry(SCREEN_RECORDING_ZIP_LABEL)) it.transferTo(os) os.closeEntry() @@ -215,7 +226,7 @@ constructor( fun getStartIntent( context: Context, screenRecord: Boolean, - winscopeTracing: Boolean + winscopeTracing: Boolean, ): Intent = Intent(context, IssueRecordingService::class.java) .setAction(ACTION_START) diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt index 394c5c2775a4..12ed06d75ce3 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt @@ -25,6 +25,8 @@ class IssueRecordingState @Inject constructor() { private val listeners = CopyOnWriteArrayList<Runnable>() + var takeBugReport: Boolean = false + var isRecording = false set(value) { field = value diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt index dab61fa54908..68b88362427a 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt @@ -63,6 +63,7 @@ constructor( private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, private val userFileManager: UserFileManager, private val screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate, + private val issueRecordingState: IssueRecordingState, @Assisted private val onStarted: Consumer<IssueRecordingConfig>, ) : SystemUIDialog.Delegate { @@ -74,6 +75,7 @@ constructor( } @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch + @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var bugReportSwitch: Switch private lateinit var issueTypeButton: Button @MainThread @@ -86,6 +88,7 @@ constructor( setPositiveButton( R.string.qs_record_issue_start, { _, _ -> + issueRecordingState.takeBugReport = bugReportSwitch.isChecked onStarted.accept( IssueRecordingConfig( screenRecordSwitch.isChecked, @@ -113,6 +116,7 @@ constructor( bgExecutor.execute { onScreenRecordSwitchClicked() } } } + bugReportSwitch = requireViewById(R.id.bugreport_switch) val startButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) issueTypeButton = requireViewById(R.id.issue_type_button) issueTypeButton.setOnClickListener { diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index 994b01216c22..3082eb904a51 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -24,6 +24,8 @@ import com.android.compose.animation.scene.TransitionKey import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSource +import com.android.systemui.util.kotlin.WithPrev +import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -34,6 +36,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** Source of truth for scene framework application state. */ @@ -44,7 +47,32 @@ constructor( private val config: SceneContainerConfig, private val dataSource: SceneDataSource, ) { - val currentScene: StateFlow<SceneKey> = dataSource.currentScene + private val previousAndCurrentScene: StateFlow<WithPrev<SceneKey?, SceneKey>> = + dataSource.currentScene + .pairwise() + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = WithPrev(null, dataSource.currentScene.value), + ) + + val currentScene: StateFlow<SceneKey> = + previousAndCurrentScene + .map { it.newValue } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = previousAndCurrentScene.value.newValue, + ) + + val previousScene: StateFlow<SceneKey?> = + previousAndCurrentScene + .map { it.previousValue } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = previousAndCurrentScene.value.previousValue, + ) private val _isVisible = MutableStateFlow(true) val isVisible: StateFlow<Boolean> = _isVisible.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 2ccd3b9e9f8a..02394552e8f9 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -140,6 +140,14 @@ constructor( ) /** + * The previous scene. + * + * This is effectively the previous value of [currentScene] which means that all caveats, for + * example regarding when in a transition the current scene changes, apply. + */ + val previousScene: StateFlow<SceneKey?> = repository.previousScene + + /** * Returns the keys of all scenes in the container. * * The scenes will be sorted in z-order such that the last one is the one that should be 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 9e27dad0ea73..c9291966bc15 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 @@ -28,6 +28,8 @@ 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 com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor +import com.android.systemui.statusbar.phone.PredictiveBackSysUiFlag import dagger.Module import dagger.Provides @@ -40,11 +42,13 @@ object SceneContainerFlag { inline val isEnabled get() = sceneContainer() && // mainAconfigFlag - KeyguardBottomAreaRefactor.isEnabled && - MigrateClocksToBlueprint.isEnabled && - ComposeLockscreen.isEnabled && + ComposeLockscreen.isEnabled && + KeyguardBottomAreaRefactor.isEnabled && + KeyguardWmStateRefactor.isEnabled && MediaInSceneContainerFlag.isEnabled && - KeyguardWmStateRefactor.isEnabled + MigrateClocksToBlueprint.isEnabled && + NotificationsHeadsUpRefactor.isEnabled && + PredictiveBackSysUiFlag.isEnabled // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer /** The main aconfig flag. */ @@ -53,11 +57,13 @@ object SceneContainerFlag { /** The set of secondary flags which must be enabled for scene container to work properly */ inline fun getSecondaryFlags(): Sequence<FlagToken> = sequenceOf( + ComposeLockscreen.token, KeyguardBottomAreaRefactor.token, - MigrateClocksToBlueprint.token, KeyguardWmStateRefactor.token, - ComposeLockscreen.token, MediaInSceneContainerFlag.token, + MigrateClocksToBlueprint.token, + NotificationsHeadsUpRefactor.token, + PredictiveBackSysUiFlag.token, // NOTE: Changes should also be made in isEnabled and @EnableSceneContainer ) diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index b2c01e180ec4..cbb61b37b7a4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -206,7 +206,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList break; case ACTION_SHARE: - Uri shareUri = Uri.parse(intent.getStringExtra(EXTRA_PATH)); + Uri shareUri = intent.getParcelableExtra(EXTRA_PATH, Uri.class); Intent shareIntent = new Intent(Intent.ACTION_SEND) .setType("video/mp4") @@ -356,7 +356,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList PendingIntent.getService( this, REQUEST_CODE, - getShareIntent(this, uri != null ? uri.toString() : null), + getShareIntent(this, uri), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); @@ -512,7 +512,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList return new Intent(context, this.getClass()).setAction(ACTION_STOP_NOTIF); } - private Intent getShareIntent(Context context, String path) { + private Intent getShareIntent(Context context, Uri path) { return new Intent(context, this.getClass()).setAction(ACTION_SHARE) .putExtra(EXTRA_PATH, path); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt new file mode 100644 index 000000000000..caa67dff086f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionExecutor.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.app.ActivityOptions +import android.app.BroadcastOptions +import android.app.ExitTransitionCoordinator +import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks +import android.app.PendingIntent +import android.content.Intent +import android.os.UserHandle +import android.util.Log +import android.util.Pair +import android.view.View +import android.view.Window +import com.android.app.tracing.coroutines.launch +import com.android.internal.app.ChooserActivity +import com.android.systemui.dagger.qualifiers.Application +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope + +class ActionExecutor +@AssistedInject +constructor( + private val intentExecutor: ActionIntentExecutor, + @Application private val applicationScope: CoroutineScope, + @Assisted val window: Window, + @Assisted val transitionView: View, + @Assisted val onDismiss: (() -> Unit) +) { + + var isPendingSharedTransition = false + private set + + fun startSharedTransition(intent: Intent, user: UserHandle, overrideTransition: Boolean) { + isPendingSharedTransition = true + val windowTransition = createWindowTransition() + applicationScope.launch("$TAG#launchIntentAsync") { + intentExecutor.launchIntent( + intent, + user, + overrideTransition, + windowTransition.first, + windowTransition.second + ) + } + } + + fun sendPendingIntent(pendingIntent: PendingIntent) { + try { + val options = BroadcastOptions.makeBasic() + options.setInteractive(true) + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + pendingIntent.send(options.toBundle()) + onDismiss.invoke() + } catch (e: PendingIntent.CanceledException) { + Log.e(TAG, "Intent cancelled", e) + } + } + + /** + * Supplies the necessary bits for the shared element transition to share sheet. Note that once + * called, the action intent to share must be sent immediately after. + */ + private fun createWindowTransition(): Pair<ActivityOptions, ExitTransitionCoordinator> { + val callbacks: ExitTransitionCallbacks = + object : ExitTransitionCallbacks { + override fun isReturnTransitionAllowed(): Boolean { + return false + } + + override fun hideSharedElements() { + isPendingSharedTransition = false + onDismiss.invoke() + } + + override fun onFinish() {} + } + return ActivityOptions.startSharedElementAnimation( + window, + callbacks, + null, + Pair.create(transitionView, ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME) + ) + } + + @AssistedFactory + interface Factory { + fun create(window: Window, transitionView: View, onDismiss: (() -> Unit)): ActionExecutor + } + + companion object { + private const val TAG = "ActionExecutor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt index 8e9769ab7d0e..a0cef529ecde 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt @@ -23,7 +23,9 @@ import android.content.ContentProvider import android.content.Context import android.content.Intent import android.net.Uri +import android.os.UserHandle import com.android.systemui.res.R +import com.android.systemui.screenshot.scroll.LongScreenshotActivity object ActionIntentCreator { /** @return a chooser intent to share the given URI. */ @@ -89,6 +91,14 @@ object ActionIntentCreator { .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) } + /** @return an Intent to start the LongScreenshotActivity */ + fun createLongScreenshotIntent(owner: UserHandle, context: Context): Intent { + return Intent(context, LongScreenshotActivity::class.java) + .putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE, owner) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + private const val EXTRA_EDIT_SOURCE = "edit_source" private const val EDIT_SOURCE_SCREENSHOT = "screenshot" } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt index 1f9853b17a28..4eca51d47a36 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt @@ -25,7 +25,6 @@ import android.os.Process.myUserHandle import android.os.RemoteException import android.os.UserHandle import android.util.Log -import android.util.Pair import android.view.IRemoteAnimationFinishedCallback import android.view.IRemoteAnimationRunner import android.view.RemoteAnimationAdapter @@ -67,20 +66,22 @@ constructor( */ fun launchIntentAsync( intent: Intent, - transition: Pair<ActivityOptions, ExitTransitionCoordinator>?, user: UserHandle, overrideTransition: Boolean, + options: ActivityOptions?, + transitionCoordinator: ExitTransitionCoordinator?, ) { applicationScope.launch("$TAG#launchIntentAsync") { - launchIntent(intent, transition, user, overrideTransition) + launchIntent(intent, user, overrideTransition, options, transitionCoordinator) } } suspend fun launchIntent( intent: Intent, - transition: Pair<ActivityOptions, ExitTransitionCoordinator>?, user: UserHandle, overrideTransition: Boolean, + options: ActivityOptions?, + transitionCoordinator: ExitTransitionCoordinator?, ) { if (screenshotActionDismissSystemWindows()) { keyguardController.dismiss() @@ -90,14 +91,12 @@ constructor( } else { dismissKeyguard() } - transition?.second?.startExit() + transitionCoordinator?.startExit() if (user == myUserHandle()) { - withContext(mainDispatcher) { - context.startActivity(intent, transition?.first?.toBundle()) - } + withContext(mainDispatcher) { context.startActivity(intent, options?.toBundle()) } } else { - launchCrossProfileIntent(user, intent, transition?.first?.toBundle()) + launchCrossProfileIntent(user, intent, options?.toBundle()) } if (overrideTransition) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt index ca0a539d5ee0..f69021f34ebb 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -16,27 +16,15 @@ package com.android.systemui.screenshot -import android.app.ActivityOptions -import android.app.BroadcastOptions -import android.app.ExitTransitionCoordinator -import android.app.PendingIntent +import android.app.assist.AssistContent import android.content.Context -import android.content.Intent -import android.os.Process -import android.os.UserHandle -import android.provider.DeviceConfig import android.util.Log -import android.util.Pair import androidx.appcompat.content.res.AppCompatResources -import com.android.app.tracing.coroutines.launch -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags import com.android.internal.logging.UiEventLogger -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.res.R import com.android.systemui.screenshot.ActionIntentCreator.createEdit import com.android.systemui.screenshot.ActionIntentCreator.createShareWithSubject -import com.android.systemui.screenshot.ScreenshotController.SavedImageData import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_EDIT_TAPPED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SHARE_TAPPED @@ -46,24 +34,22 @@ import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import java.text.DateFormat -import java.util.Date -import kotlinx.coroutines.CoroutineScope /** * Provides actions for screenshots. This class can be overridden by a vendor-specific SysUI * implementation. */ interface ScreenshotActionsProvider { - fun setCompletedScreenshot(result: SavedImageData) - fun isPendingSharedTransition(): Boolean + fun onScrollChipReady(onClick: Runnable) + fun setCompletedScreenshot(result: ScreenshotSavedResult) + + fun onAssistContentAvailable(assistContent: AssistContent) {} interface Factory { fun create( request: ScreenshotData, requestId: String, - windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>, - requestDismissal: () -> Unit, + actionExecutor: ActionExecutor, ): ScreenshotActionsProvider } } @@ -73,25 +59,25 @@ class DefaultScreenshotActionsProvider constructor( private val context: Context, private val viewModel: ScreenshotViewModel, - private val actionExecutor: ActionIntentExecutor, private val smartActionsProvider: SmartActionsProvider, private val uiEventLogger: UiEventLogger, - @Application private val applicationScope: CoroutineScope, @Assisted val request: ScreenshotData, @Assisted val requestId: String, - @Assisted val windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>, - @Assisted val requestDismissal: () -> Unit, + @Assisted val actionExecutor: ActionExecutor, ) : ScreenshotActionsProvider { - private var pendingAction: ((SavedImageData) -> Unit)? = null - private var result: SavedImageData? = null - private var isPendingSharedTransition = false + private var pendingAction: ((ScreenshotSavedResult) -> Unit)? = null + private var result: ScreenshotSavedResult? = null init { viewModel.setPreviewAction { debugLog(LogConfig.DEBUG_ACTIONS) { "Preview tapped" } uiEventLogger.log(SCREENSHOT_PREVIEW_TAPPED, 0, request.packageNameString) onDeferrableActionTapped { result -> - startSharedTransition(createEdit(result.uri, context), true) + actionExecutor.startSharedTransition( + createEdit(result.uri, context), + result.user, + true + ) } } viewModel.addAction( @@ -103,7 +89,11 @@ constructor( debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" } uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString) onDeferrableActionTapped { result -> - startSharedTransition(createEdit(result.uri, context), true) + actionExecutor.startSharedTransition( + createEdit(result.uri, context), + result.user, + true + ) } } ) @@ -116,125 +106,78 @@ constructor( debugLog(LogConfig.DEBUG_ACTIONS) { "Share tapped" } uiEventLogger.log(SCREENSHOT_SHARE_TAPPED, 0, request.packageNameString) onDeferrableActionTapped { result -> - startSharedTransition(createShareWithSubject(result.uri, result.subject), false) + actionExecutor.startSharedTransition( + createShareWithSubject(result.uri, result.subject), + result.user, + false + ) } } ) - if (smartActionsEnabled(request.userHandle ?: Process.myUserHandle())) { - smartActionsProvider.requestQuickShare(request, requestId) { quickShare -> - if (!quickShare.actionIntent.isImmutable) { - viewModel.addAction( - ActionButtonViewModel( - quickShare.getIcon().loadDrawable(context), - quickShare.title, - quickShare.title - ) { - debugLog(LogConfig.DEBUG_ACTIONS) { "Quickshare tapped" } - onDeferrableActionTapped { result -> - uiEventLogger.log( - SCREENSHOT_SMART_ACTION_TAPPED, - 0, - request.packageNameString - ) - sendPendingIntent( - smartActionsProvider - .wrapIntent( - quickShare, - result.uri, - result.subject, - requestId - ) - .actionIntent + smartActionsProvider.requestQuickShare(request, requestId) { quickShare -> + if (!quickShare.actionIntent.isImmutable) { + viewModel.addAction( + ActionButtonViewModel( + quickShare.getIcon().loadDrawable(context), + quickShare.title, + quickShare.title + ) { + debugLog(LogConfig.DEBUG_ACTIONS) { "Quickshare tapped" } + onDeferrableActionTapped { result -> + uiEventLogger.log( + SCREENSHOT_SMART_ACTION_TAPPED, + 0, + request.packageNameString + ) + val pendingIntentWithUri = + smartActionsProvider.wrapIntent( + quickShare, + result.uri, + result.subject, + requestId ) - } + actionExecutor.sendPendingIntent(pendingIntentWithUri) } - ) - } else { - Log.w(TAG, "Received immutable quick share pending intent; ignoring") - } + } + ) + } else { + Log.w(TAG, "Received immutable quick share pending intent; ignoring") } } } - override fun setCompletedScreenshot(result: SavedImageData) { + override fun onScrollChipReady(onClick: Runnable) { + viewModel.addAction( + ActionButtonViewModel( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_scroll), + context.resources.getString(R.string.screenshot_scroll_label), + context.resources.getString(R.string.screenshot_scroll_label), + ) { + onClick.run() + } + ) + } + + override fun setCompletedScreenshot(result: ScreenshotSavedResult) { if (this.result != null) { Log.e(TAG, "Got a second completed screenshot for existing request!") return } - if (result.uri == null || result.owner == null || result.imageTime == null) { - Log.e(TAG, "Invalid result provided!") - return - } - if (result.subject == null) { - result.subject = getSubjectString(result.imageTime) - } this.result = result pendingAction?.invoke(result) - if (smartActionsEnabled(result.owner)) { - smartActionsProvider.requestSmartActions(request, requestId, result) { smartActions -> - viewModel.addActions( - smartActions.map { - ActionButtonViewModel( - it.getIcon().loadDrawable(context), - it.title, - it.title - ) { - sendPendingIntent(it.actionIntent) - } + smartActionsProvider.requestSmartActions(request, requestId, result) { smartActions -> + viewModel.addActions( + smartActions.map { + ActionButtonViewModel(it.getIcon().loadDrawable(context), it.title, it.title) { + actionExecutor.sendPendingIntent(it.actionIntent) } - ) - } - } - } - - override fun isPendingSharedTransition(): Boolean { - return isPendingSharedTransition - } - - private fun onDeferrableActionTapped(onResult: (SavedImageData) -> Unit) { - result?.let { onResult.invoke(it) } ?: run { pendingAction = onResult } - } - - private fun startSharedTransition(intent: Intent, overrideTransition: Boolean) { - val user = - result?.owner - ?: run { - Log.wtf(TAG, "User handle not provided in screenshot result! Result: $result") - return } - isPendingSharedTransition = true - applicationScope.launch("$TAG#launchIntentAsync") { - actionExecutor.launchIntent(intent, windowTransition.invoke(), user, overrideTransition) - } - } - - private fun sendPendingIntent(pendingIntent: PendingIntent) { - try { - val options = BroadcastOptions.makeBasic() - options.setInteractive(true) - options.setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED ) - pendingIntent.send(options.toBundle()) - requestDismissal.invoke() - } catch (e: PendingIntent.CanceledException) { - Log.e(TAG, "Intent cancelled", e) } } - private fun smartActionsEnabled(user: UserHandle): Boolean { - val savingToOtherUser = user != Process.myUserHandle() - return !savingToOtherUser && - DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, - true - ) - } - - private fun getSubjectString(imageTime: Long): String { - val subjectDate = DateFormat.getDateTimeInstance().format(Date(imageTime)) - return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate) + private fun onDeferrableActionTapped(onResult: (ScreenshotSavedResult) -> Unit) { + result?.let { onResult.invoke(it) } ?: run { pendingAction = onResult } } @AssistedFactory @@ -242,13 +185,11 @@ constructor( override fun create( request: ScreenshotData, requestId: String, - windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>, - requestDismissal: () -> Unit, + actionExecutor: ActionExecutor, ): DefaultScreenshotActionsProvider } companion object { private const val TAG = "ScreenshotActionsProvider" - private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)" } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 70d1129a2b40..13dd229d8f62 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -34,7 +34,6 @@ import android.animation.AnimatorListenerAdapter; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ExitTransitionCoordinator; import android.app.ICompatCameraControlCallback; @@ -51,7 +50,6 @@ import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.net.Uri; import android.os.Process; -import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -59,17 +57,12 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; import android.view.Display; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; import android.view.ScrollCaptureResponse; import android.view.View; import android.view.ViewRootImpl; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowManager; -import android.view.WindowManagerGlobal; import android.widget.Toast; import android.window.WindowContext; @@ -84,10 +77,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback; -import com.android.systemui.screenshot.scroll.LongScreenshotActivity; -import com.android.systemui.screenshot.scroll.LongScreenshotData; -import com.android.systemui.screenshot.scroll.ScrollCaptureClient; -import com.android.systemui.screenshot.scroll.ScrollCaptureController; +import com.android.systemui.screenshot.scroll.ScrollCaptureExecutor; import com.android.systemui.util.Assert; import com.google.common.util.concurrent.ListenableFuture; @@ -100,12 +90,9 @@ import kotlin.Unit; import java.util.List; import java.util.UUID; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.function.Consumer; import javax.inject.Provider; @@ -117,34 +104,6 @@ import javax.inject.Provider; public class ScreenshotController { private static final String TAG = logTag(ScreenshotController.class); - private ScrollCaptureResponse mLastScrollCaptureResponse; - private ListenableFuture<ScrollCaptureResponse> mLastScrollCaptureRequest; - - /** - * This is effectively a no-op, but we need something non-null to pass in, in order to - * successfully override the pending activity entrance animation. - */ - static final IRemoteAnimationRunner.Stub SCREENSHOT_REMOTE_RUNNER = - new IRemoteAnimationRunner.Stub() { - @Override - public void onAnimationStart( - @WindowManager.TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - final IRemoteAnimationFinishedCallback finishedCallback) { - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - Log.e(TAG, "Error finishing screenshot remote animation", e); - } - } - - @Override - public void onAnimationCancelled() { - } - }; - /** * POD used in the AsyncTask which saves an image in the background. */ @@ -242,22 +201,20 @@ public class ScreenshotController { private final ExecutorService mBgExecutor; private final BroadcastSender mBroadcastSender; private final BroadcastDispatcher mBroadcastDispatcher; + private final ActionExecutor mActionExecutor; private final WindowManager mWindowManager; private final WindowManager.LayoutParams mWindowLayoutParams; @Nullable private final ScreenshotSoundController mScreenshotSoundController; - private final ScrollCaptureClient mScrollCaptureClient; private final PhoneWindow mWindow; private final DisplayManager mDisplayManager; private final int mDisplayId; - private final ScrollCaptureController mScrollCaptureController; - private final LongScreenshotData mLongScreenshotHolder; - private final boolean mIsLowRamDevice; + private final ScrollCaptureExecutor mScrollCaptureExecutor; private final ScreenshotNotificationSmartActionsProvider mScreenshotNotificationSmartActionsProvider; private final TimeoutHandler mScreenshotHandler; - private final ActionIntentExecutor mActionExecutor; + private final ActionIntentExecutor mActionIntentExecutor; private final UserManager mUserManager; private final AssistContentRequester mAssistContentRequester; @@ -299,19 +256,17 @@ public class ScreenshotController { ScreenshotActionsProvider.Factory actionsProviderFactory, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, - ScrollCaptureClient scrollCaptureClient, UiEventLogger uiEventLogger, ImageExporter imageExporter, ImageCapture imageCapture, @Main Executor mainExecutor, - ScrollCaptureController scrollCaptureController, - LongScreenshotData longScreenshotHolder, - ActivityManager activityManager, + ScrollCaptureExecutor scrollCaptureExecutor, TimeoutHandler timeoutHandler, BroadcastSender broadcastSender, BroadcastDispatcher broadcastDispatcher, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, - ActionIntentExecutor actionExecutor, + ActionIntentExecutor actionIntentExecutor, + ActionExecutor.Factory actionExecutorFactory, UserManager userManager, AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, @@ -322,14 +277,11 @@ public class ScreenshotController { mScreenshotSmartActions = screenshotSmartActions; mActionsProviderFactory = actionsProviderFactory; mNotificationsController = screenshotNotificationsControllerFactory.create(displayId); - mScrollCaptureClient = scrollCaptureClient; mUiEventLogger = uiEventLogger; mImageExporter = imageExporter; mImageCapture = imageCapture; mMainExecutor = mainExecutor; - mScrollCaptureController = scrollCaptureController; - mLongScreenshotHolder = longScreenshotHolder; - mIsLowRamDevice = activityManager.isLowRamDevice(); + mScrollCaptureExecutor = scrollCaptureExecutor; mScreenshotNotificationSmartActionsProvider = screenshotNotificationSmartActionsProvider; mBgExecutor = Executors.newSingleThreadExecutor(); mBroadcastSender = broadcastSender; @@ -345,7 +297,7 @@ public class ScreenshotController { final Context displayContext = context.createDisplayContext(getDisplay()); mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); mFlags = flags; - mActionExecutor = actionExecutor; + mActionIntentExecutor = actionIntentExecutor; mUserManager = userManager; mMessageContainerController = messageContainerController; mAssistContentRequester = assistContentRequester; @@ -369,6 +321,12 @@ public class ScreenshotController { mConfigChanges.applyNewConfig(context.getResources()); reloadAssets(); + mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy.getScreenshotPreview(), + () -> { + requestDismissal(null); + return Unit.INSTANCE; + }); + // Sound is only reproduced from the controller of the default display. if (displayId == Display.DEFAULT_DISPLAY) { mScreenshotSoundController = screenshotSoundController.get(); @@ -395,10 +353,10 @@ public class ScreenshotController { Assert.isMainThread(); mCurrentRequestCallback = requestCallback; - if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) { + if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN + && screenshot.getBitmap() == null) { Rect bounds = getFullScreenRect(); - screenshot.setBitmap( - mImageCapture.captureDisplay(mDisplayId, bounds)); + screenshot.setBitmap(mImageCapture.captureDisplay(mDisplayId, bounds)); screenshot.setScreenBounds(bounds); } @@ -447,12 +405,16 @@ public class ScreenshotController { if (screenshotShelfUi()) { final UUID requestId = UUID.randomUUID(); final String screenshotId = String.format("Screenshot_%s", requestId); - mActionsProvider = mActionsProviderFactory.create(screenshot, screenshotId, - this::createWindowTransition, () -> { - mViewProxy.requestDismissal(null); - return Unit.INSTANCE; - }); + mActionsProvider = mActionsProviderFactory.create( + screenshot, screenshotId, mActionExecutor); saveScreenshotInBackground(screenshot, requestId, finisher); + + if (screenshot.getTaskId() >= 0) { + mAssistContentRequester.requestAssistContent(screenshot.getTaskId(), + assistContent -> { + mActionsProvider.onAssistContentAvailable(assistContent); + }); + } } else { saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher, this::showUiOnActionsReady, this::showUiOnQuickShareActionReady); @@ -541,7 +503,7 @@ public class ScreenshotController { boolean isPendingSharedTransition() { if (screenshotShelfUi()) { - return mActionsProvider != null && mActionsProvider.isPendingSharedTransition(); + return mActionExecutor.isPendingSharedTransition(); } else { return mViewProxy.isPendingSharedTransition(); } @@ -592,8 +554,9 @@ public class ScreenshotController { @Override public void onAction(Intent intent, UserHandle owner, boolean overrideTransition) { - mActionExecutor.launchIntentAsync( - intent, createWindowTransition(), owner, overrideTransition); + Pair<ActivityOptions, ExitTransitionCoordinator> exit = createWindowTransition(); + mActionIntentExecutor.launchIntentAsync( + intent, owner, overrideTransition, exit.first, exit.second); } @Override @@ -630,9 +593,8 @@ public class ScreenshotController { mViewProxy.hideScrollChip(); // Delay scroll capture eval a bit to allow the underlying activity // to set up in the new orientation. - mScreenshotHandler.postDelayed(() -> { - requestScrollCapture(owner); - }, 150); + mScreenshotHandler.postDelayed( + () -> requestScrollCapture(owner), 150); mViewProxy.updateInsets( mWindowManager.getCurrentWindowMetrics().getWindowInsets()); // Screenshot animation calculations won't be valid anymore, @@ -655,119 +617,51 @@ public class ScreenshotController { } private void requestScrollCapture(UserHandle owner) { - if (!allowLongScreenshots()) { - Log.d(TAG, "Long screenshots not supported on this device"); - return; - } - mScrollCaptureClient.setHostWindowToken(mWindow.getDecorView().getWindowToken()); - if (mLastScrollCaptureRequest != null) { - mLastScrollCaptureRequest.cancel(true); - } - final ListenableFuture<ScrollCaptureResponse> future = mScrollCaptureClient.request( - mDisplayId); - mLastScrollCaptureRequest = future; - mLastScrollCaptureRequest.addListener(() -> - onScrollCaptureResponseReady(future, owner), mMainExecutor); - } - - private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture, - UserHandle owner) { - try { - if (mLastScrollCaptureResponse != null) { - mLastScrollCaptureResponse.close(); - mLastScrollCaptureResponse = null; - } - if (responseFuture.isCancelled()) { - return; - } - mLastScrollCaptureResponse = responseFuture.get(); - if (!mLastScrollCaptureResponse.isConnected()) { - // No connection means that the target window wasn't found - // or that it cannot support scroll capture. - Log.d(TAG, "ScrollCapture: " + mLastScrollCaptureResponse.getDescription() + " [" - + mLastScrollCaptureResponse.getWindowTitle() + "]"); - return; - } - Log.d(TAG, "ScrollCapture: connected to window [" - + mLastScrollCaptureResponse.getWindowTitle() + "]"); - - final ScrollCaptureResponse response = mLastScrollCaptureResponse; - mViewProxy.showScrollChip(response.getPackageName(), /* onClick */ () -> { - Bitmap newScreenshot = - mImageCapture.captureDisplay(mDisplayId, getFullScreenRect()); - - if (newScreenshot != null) { - // delay starting scroll capture to make sure scrim is up before the app - // moves - mViewProxy.prepareScrollingTransition( - response, mScreenBitmap, newScreenshot, mScreenshotTakenInPortrait, - () -> runBatchScrollCapture(response, owner)); - } else { - Log.wtf(TAG, "failed to capture current screenshot for scroll transition"); + mScrollCaptureExecutor.requestScrollCapture( + mDisplayId, + mWindow.getDecorView().getWindowToken(), + (response) -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, + 0, response.getPackageName()); + if (screenshotShelfUi() && mActionsProvider != null) { + mActionsProvider.onScrollChipReady( + () -> onScrollButtonClicked(owner, response)); + } else { + mViewProxy.showScrollChip(response.getPackageName(), + () -> onScrollButtonClicked(owner, response)); + } + return Unit.INSTANCE; } - }); - } catch (InterruptedException | ExecutionException e) { - Log.e(TAG, "requestScrollCapture failed", e); - } + ); } - ListenableFuture<ScrollCaptureController.LongScreenshot> mLongScreenshotFuture; - - private void runBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) { - // Clear the reference to prevent close() in dismissScreenshot - mLastScrollCaptureResponse = null; - - if (mLongScreenshotFuture != null) { - mLongScreenshotFuture.cancel(true); + private void onScrollButtonClicked(UserHandle owner, ScrollCaptureResponse response) { + if (DEBUG_INPUT) { + Log.d(TAG, "scroll chip tapped"); } - mLongScreenshotFuture = mScrollCaptureController.run(response); - mLongScreenshotFuture.addListener(() -> { - ScrollCaptureController.LongScreenshot longScreenshot; - try { - longScreenshot = mLongScreenshotFuture.get(); - } catch (CancellationException e) { - Log.e(TAG, "Long screenshot cancelled"); - return; - } catch (InterruptedException | ExecutionException e) { - Log.e(TAG, "Exception", e); - mViewProxy.restoreNonScrollingUi(); - return; - } + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0, + response.getPackageName()); + Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId, getFullScreenRect()); + if (newScreenshot == null) { + Log.e(TAG, "Failed to capture current screenshot for scroll transition!"); + return; + } + // delay starting scroll capture to make sure scrim is up before the app moves + mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, + mScreenshotTakenInPortrait, () -> executeBatchScrollCapture(response, owner)); + } - if (longScreenshot.getHeight() == 0) { - mViewProxy.restoreNonScrollingUi(); - return; - } + private void executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) { + mScrollCaptureExecutor.executeBatchScrollCapture(response, + () -> { + final Intent intent = ActionIntentCreator.INSTANCE.createLongScreenshotIntent( + owner, mContext); + mActionIntentExecutor.launchIntentAsync(intent, owner, true, + ActivityOptions.makeCustomAnimation(mContext, 0, 0), null); - mLongScreenshotHolder.setLongScreenshot(longScreenshot); - mLongScreenshotHolder.setTransitionDestinationCallback( - (transitionDestination, onTransitionEnd) -> { - mViewProxy.startLongScreenshotTransition( - transitionDestination, onTransitionEnd, - longScreenshot); - // TODO: Do this via ActionIntentExecutor instead. - mContext.closeSystemDialogs(); - } - ); - - final Intent intent = new Intent(mContext, LongScreenshotActivity.class); - intent.putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE, - owner); - intent.setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - - mContext.startActivity(intent, - ActivityOptions.makeCustomAnimation(mContext, 0, 0).toBundle()); - RemoteAnimationAdapter runner = new RemoteAnimationAdapter( - SCREENSHOT_REMOTE_RUNNER, 0, 0); - try { - WindowManagerGlobal.getWindowManagerService() - .overridePendingAppTransitionRemote(runner, - mDisplayId); - } catch (Exception e) { - Log.e(TAG, "Error overriding screenshot app transition", e); - } - }, mMainExecutor); + }, + mViewProxy::restoreNonScrollingUi, + mViewProxy::startLongScreenshotTransition); } private void withWindowAttached(Runnable action) { @@ -912,17 +806,7 @@ public class ScreenshotController { /** Reset screenshot view and then call onCompleteRunnable */ private void finishDismiss() { Log.d(TAG, "finishDismiss"); - if (mLastScrollCaptureRequest != null) { - mLastScrollCaptureRequest.cancel(true); - mLastScrollCaptureRequest = null; - } - if (mLastScrollCaptureResponse != null) { - mLastScrollCaptureResponse.close(); - mLastScrollCaptureResponse = null; - } - if (mLongScreenshotFuture != null) { - mLongScreenshotFuture.cancel(true); - } + mScrollCaptureExecutor.close(); if (mCurrentRequestCallback != null) { mCurrentRequestCallback.onFinish(); mCurrentRequestCallback = null; @@ -935,7 +819,7 @@ public class ScreenshotController { private void saveScreenshotInBackground( ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) { ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor, - requestId, screenshot.getBitmap(), screenshot.getUserHandle(), mDisplayId); + requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(), mDisplayId); future.addListener(() -> { try { ImageExporter.Result result = future.get(); @@ -943,12 +827,8 @@ public class ScreenshotController { logScreenshotResultStatus(result.uri, screenshot.getUserHandle()); mScreenshotHandler.resetTimeout(); if (result.uri != null) { - final SavedImageData savedImageData = new SavedImageData(); - savedImageData.uri = result.uri; - savedImageData.owner = screenshot.getUserHandle(); - savedImageData.imageTime = result.timestamp; - mActionsProvider.setCompletedScreenshot(savedImageData); - mViewProxy.setChipIntents(savedImageData); + mActionsProvider.setCompletedScreenshot(new ScreenshotSavedResult( + result.uri, screenshot.getUserOrDefault(), result.timestamp)); } if (DEBUG_CALLBACK) { Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) " @@ -1110,10 +990,6 @@ public class ScreenshotController { return mDisplayManager.getDisplay(mDisplayId); } - private boolean allowLongScreenshots() { - return !mIsLowRamDevice; - } - private Rect getFullScreenRect() { DisplayMetrics displayMetrics = new DisplayMetrics(); getDisplay().getRealMetrics(displayMetrics); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt index 92e933a9557b..4fdd90bdcded 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt @@ -5,6 +5,7 @@ import android.graphics.Bitmap import android.graphics.Insets import android.graphics.Rect import android.net.Uri +import android.os.Process import android.os.UserHandle import android.view.Display import android.view.WindowManager.ScreenshotSource @@ -31,6 +32,10 @@ data class ScreenshotData( val packageNameString: String get() = if (topComponent == null) "" else topComponent!!.packageName + fun getUserOrDefault(): UserHandle { + return userHandle ?: Process.myUserHandle() + } + companion object { @JvmStatic fun fromRequest(request: ScreenshotRequest, displayId: Int = Display.DEFAULT_DISPLAY) = diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt index 796457d88cc2..3ad4075a2b89 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt @@ -17,13 +17,14 @@ package com.android.systemui.screenshot /** Processes a screenshot request sent from [ScreenshotHelper]. */ -interface ScreenshotRequestProcessor { +fun interface ScreenshotRequestProcessor { /** * Inspects the incoming ScreenshotData, potentially modifying it based upon policy. * - * @param screenshot the screenshot to process + * @param original the screenshot to process + * @return a potentially modified screenshot data */ - suspend fun process(screenshot: ScreenshotData): ScreenshotData + suspend fun process(original: ScreenshotData): ScreenshotData } /** Exception thrown by [RequestProcessor] if something goes wrong. */ diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSavedResult.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSavedResult.kt new file mode 100644 index 000000000000..5b6e7ac3e4e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSavedResult.kt @@ -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.systemui.screenshot + +import android.net.Uri +import android.os.UserHandle +import java.text.DateFormat +import java.util.Date + +/** + * Represents a saved screenshot, with the uri and user it was saved to as well as the time it was + * saved. + */ +data class ScreenshotSavedResult(val uri: Uri, val user: UserHandle, val imageTime: Long) { + val subject: String + + init { + val subjectDate = DateFormat.getDateTimeInstance().format(Date(imageTime)) + subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate) + } + + companion object { + private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 65e845749f9e..59e38a836258 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -259,16 +259,8 @@ public class ScreenshotView extends FrameLayout implements if (DEBUG_SCROLL) { Log.d(TAG, "Showing Scroll option"); } - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, 0, packageName); mScrollChip.setVisibility(VISIBLE); - mScrollChip.setOnClickListener((v) -> { - if (DEBUG_INPUT) { - Log.d(TAG, "scroll chip tapped"); - } - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0, - packageName); - onClick.run(); - }); + mScrollChip.setOnClickListener((v) -> onClick.run()); } @Override // ViewTreeObserver.OnComputeInternalInsetsListener diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt index 2eaff8654a9e..a895b300b900 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt @@ -65,10 +65,9 @@ constructor( onAction: (Notification.Action) -> Unit ) { val bitmap = data.bitmap ?: return - val user = data.userHandle ?: return val component = data.topComponent ?: ComponentName("", "") - requestQuickShareAction(id, bitmap, component, user) { quickShareAction -> - onAction(quickShareAction) + requestQuickShareAction(id, bitmap, component, data.getUserOrDefault()) { quickShare -> + onAction(quickShare) } } @@ -83,14 +82,19 @@ constructor( fun requestSmartActions( data: ScreenshotData, id: String, - result: ScreenshotController.SavedImageData, + result: ScreenshotSavedResult, onActions: (List<Notification.Action>) -> Unit ) { val bitmap = data.bitmap ?: return - val user = data.userHandle ?: return - val uri = result.uri ?: return val component = data.topComponent ?: ComponentName("", "") - requestSmartActions(id, bitmap, component, user, uri, REGULAR_SMART_ACTIONS) { actions -> + requestSmartActions( + id, + bitmap, + component, + data.getUserOrDefault(), + result.uri, + REGULAR_SMART_ACTIONS + ) { actions -> onActions(actions) } } @@ -102,14 +106,14 @@ constructor( * @param uri the URI of the saved screenshot * @param subject the subject/title for the screenshot * @param id the request ID of the screenshot - * @return the wrapped action + * @return the pending intent with correct URI */ fun wrapIntent( quickShare: Notification.Action, uri: Uri, subject: String, id: String - ): Notification.Action { + ): PendingIntent { val wrappedIntent: Intent = Intent(context, SmartActionsReceiver::class.java) .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, quickShare.actionIntent) @@ -130,17 +134,12 @@ constructor( .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType) .putExtra(ScreenshotController.EXTRA_ID, id) .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, true) - val broadcastIntent = - PendingIntent.getBroadcast( - context, - Random.nextInt(), - wrappedIntent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - return Notification.Action.Builder(quickShare.getIcon(), quickShare.title, broadcastIntent) - .setContextual(true) - .addExtras(extras) - .build() + return PendingIntent.getBroadcast( + context, + Random.nextInt(), + wrappedIntent, + PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) } private fun createFillInIntent(uri: Uri, subject: String): Intent { @@ -197,7 +196,7 @@ constructor( onActions(listOf()) return } - var smartActionsFuture: CompletableFuture<List<Notification.Action>> + val smartActionsFuture: CompletableFuture<List<Notification.Action>> val startTimeMs = SystemClock.uptimeMillis() try { smartActionsFuture = @@ -266,6 +265,7 @@ constructor( Log.e(TAG, "Error in notifyScreenshotOp: ", e) } } + private fun isSmartActionsEnabled(user: UserHandle): Boolean { // Smart actions don't yet work for cross-user saves. val savingToOtherUser = user !== Process.myUserHandle() diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt index 92d3e550dc0f..ec7707c83980 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt @@ -92,14 +92,14 @@ constructor( // Let's wait before logging "screenshot requested", as we should log the processed // ScreenshotData. val screenshotData = - try { - screenshotRequestProcessor.process(rawScreenshotData) - } catch (e: RequestProcessorException) { - Log.e(TAG, "Failed to process screenshot request!", e) - logScreenshotRequested(rawScreenshotData) - onFailedScreenshotRequest(rawScreenshotData, callback) - return - } + runCatching { screenshotRequestProcessor.process(rawScreenshotData) } + .onFailure { + Log.e(TAG, "Failed to process screenshot request!", it) + logScreenshotRequested(rawScreenshotData) + onFailedScreenshotRequest(rawScreenshotData, callback) + } + .getOrNull() + ?: return logScreenshotRequested(screenshotData) Log.d(TAG, "Screenshot request: $screenshotData") diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ChildTaskModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ChildTaskModel.kt new file mode 100644 index 000000000000..c380db0ca3a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ChildTaskModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.data.model + +import android.content.ComponentName +import android.graphics.Rect + +/** A child task within a RootTaskInfo */ +data class ChildTaskModel( + /** The task identifier */ + val id: Int, + /** The task name */ + val name: String, + /** The location and size of the task */ + val bounds: Rect, + /** The user which created the task. */ + val userId: Int, +) { + val componentName: ComponentName? + get() = ComponentName.unflattenFromString(name) +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt index 837a661230cb..2048b7c0c142 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt @@ -24,6 +24,6 @@ data class DisplayContentModel( val displayId: Int, /** Information about the current System UI state which can affect capture. */ val systemUiState: SystemUiState, - /** A list of root tasks on the display, ordered from bottom to top along the z-axis */ + /** A list of root tasks on the display, ordered from top to bottom along the z-axis */ val rootTasks: List<RootTaskInfo>, ) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt index 9c81b322a2b7..48e813d89af7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt @@ -18,7 +18,7 @@ package com.android.systemui.screenshot.data.repository import com.android.systemui.screenshot.data.model.DisplayContentModel /** Provides information about tasks related to a display. */ -interface DisplayContentRepository { +fun interface DisplayContentRepository { /** Provides information about the tasks and content presented on a given display. */ suspend fun getDisplayContent(displayId: Int): DisplayContentModel } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureParameters.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureParameters.kt new file mode 100644 index 000000000000..5e2b57651de7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureParameters.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.screenshot.policy + +import android.content.ComponentName +import android.os.UserHandle + +/** The parameters dictated by a [CapturePolicy], used to adjust alter screenshot request. */ +data class CaptureParameters( + /** How should the content be captured? */ + val type: CaptureType, + /** The focused or top component at the time of the screenshot. */ + val component: ComponentName?, + /** Which user should receive the screenshot file? */ + val owner: UserHandle, +) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/CapturePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CapturePolicy.kt new file mode 100644 index 000000000000..4a88180d8f73 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CapturePolicy.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.systemui.screenshot.policy + +import com.android.systemui.screenshot.data.model.DisplayContentModel + +/** Contains logic to determine when and how an adjust to screenshot behavior applies. */ +fun interface CapturePolicy { + /** + * Test the policy against the current display task state. If the policy applies, Returns a + * non-null [CaptureParameters] describing how the screenshot request should be augmented. + */ + suspend fun apply(content: DisplayContentModel): CaptureParameters? +} diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt index be1f081d5b8f..6ca2e9d6d5e0 100644 --- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/CaptureType.kt @@ -1,13 +1,11 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright (c) 2023, The Android Open Source Project +/* + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,8 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ ---> -<resources> - <!-- If true, attach the navigation bar to the app during app transition --> - <bool name="config_attachNavBarToAppDuringTransition">false</bool> -</resources> + +package com.android.systemui.screenshot.policy + +import android.graphics.Rect + +/** What to capture */ +sealed interface CaptureType { + /** Capture the entire screen contents. */ + class FullScreen(val displayId: Int) : CaptureType + + /** Capture the contents of the task only. */ + class IsolatedTask( + val taskId: Int, + val taskBounds: Rect?, + ) : CaptureType +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt new file mode 100644 index 000000000000..2c0a0dbf8ea9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.policy + +import android.content.ComponentName +import android.graphics.Bitmap +import android.graphics.Rect +import android.os.UserHandle +import android.util.Log +import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN +import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.screenshot.ImageCapture +import com.android.systemui.screenshot.ScreenshotData +import com.android.systemui.screenshot.ScreenshotRequestProcessor +import com.android.systemui.screenshot.data.repository.DisplayContentRepository +import com.android.systemui.screenshot.policy.CaptureType.FullScreen +import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +private const val TAG = "PolicyRequestProcessor" + +/** A [ScreenshotRequestProcessor] which supports general policy rule matching. */ +class PolicyRequestProcessor( + @Background private val background: CoroutineDispatcher, + private val capture: ImageCapture, + private val displayTasks: DisplayContentRepository, + private val policies: List<CapturePolicy>, +) : ScreenshotRequestProcessor { + override suspend fun process(original: ScreenshotData): ScreenshotData { + if (original.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) { + // The request contains an already captured screenshot, accept it as is. + Log.i(TAG, "Screenshot bitmap provided. No modifications applied.") + return original + } + + val tasks = displayTasks.getDisplayContent(original.displayId) + + // If policies yield explicit modifications, apply them and return the result + Log.i(TAG, "Applying policy checks....") + policies + .firstNotNullOfOrNull { policy -> policy.apply(tasks) } + ?.let { + Log.i(TAG, "Modifying screenshot: $it") + return apply(it, original) + } + + // Otherwise capture normally, filling in additional information as needed. + return replaceWithScreenshot( + original = original, + componentName = original.topComponent ?: tasks.rootTasks.firstOrNull()?.topActivity, + owner = original.userHandle, + displayId = original.displayId + ) + } + + /** Produce a new [ScreenshotData] using [CaptureParameters] */ + suspend fun apply(updates: CaptureParameters, original: ScreenshotData): ScreenshotData { + // Update and apply bitmap capture depending on the parameters. + val updated = + when (val type = updates.type) { + is IsolatedTask -> + replaceWithTaskSnapshot( + original, + updates.component, + updates.owner, + type.taskId, + type.taskBounds + ) + is FullScreen -> + replaceWithScreenshot( + original, + updates.component, + updates.owner, + type.displayId + ) + } + return updated + } + + suspend fun replaceWithTaskSnapshot( + original: ScreenshotData, + componentName: ComponentName?, + owner: UserHandle, + taskId: Int, + taskBounds: Rect?, + ): ScreenshotData { + val taskSnapshot = capture.captureTask(taskId) + return original.copy( + type = TAKE_SCREENSHOT_PROVIDED_IMAGE, + bitmap = taskSnapshot, + userHandle = owner, + taskId = taskId, + topComponent = componentName, + screenBounds = taskBounds + ) + } + + suspend fun replaceWithScreenshot( + original: ScreenshotData, + componentName: ComponentName?, + owner: UserHandle?, + displayId: Int, + ): ScreenshotData { + val screenshot = captureDisplay(displayId) + return original.copy( + type = TAKE_SCREENSHOT_FULLSCREEN, + bitmap = screenshot, + userHandle = owner, + topComponent = componentName, + screenBounds = Rect(0, 0, screenshot?.width ?: 0, screenshot?.height ?: 0) + ) + } + + /** TODO: Move to ImageCapture (existing function is non-suspending) */ + private suspend fun captureDisplay(displayId: Int): Bitmap? { + return withContext(background) { capture.captureDisplay(displayId) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt new file mode 100644 index 000000000000..221e64782894 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PrivateProfilePolicy.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.policy + +import android.os.UserHandle +import com.android.systemui.screenshot.data.model.DisplayContentModel +import com.android.systemui.screenshot.data.model.ProfileType +import com.android.systemui.screenshot.data.repository.ProfileTypeRepository +import com.android.systemui.screenshot.policy.CaptureType.FullScreen +import javax.inject.Inject +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.firstOrNull + +/** + * Condition: When any visible task belongs to a private user. + * + * Parameters: Capture the whole screen, owned by the private user. + */ +class PrivateProfilePolicy +@Inject +constructor( + private val profileTypes: ProfileTypeRepository, +) : CapturePolicy { + override suspend fun apply(content: DisplayContentModel): CaptureParameters? { + // Find the first visible rootTaskInfo with a child task owned by a private user + val (rootTask, childTask) = + content.rootTasks + .filter { it.isVisible } + .firstNotNullOfOrNull { root -> + root + .childTasksTopDown() + .firstOrNull { + profileTypes.getProfileType(it.userId) == ProfileType.PRIVATE + } + ?.let { root to it } + } + ?: return null + + // If matched, return parameters needed to modify the request. + return CaptureParameters( + type = FullScreen(content.displayId), + component = childTask.componentName ?: rootTask.topActivity, + owner = UserHandle.of(childTask.userId), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.kt new file mode 100644 index 000000000000..d2f4d9e039f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/RootTaskInfoExt.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.screenshot.policy + +import android.app.ActivityTaskManager.RootTaskInfo +import com.android.systemui.screenshot.data.model.ChildTaskModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.map + +internal fun RootTaskInfo.childTasksTopDown(): Flow<ChildTaskModel> { + return ((numActivities - 1) downTo 0).asFlow().map { index -> + ChildTaskModel( + childTaskIds[index], + childTaskNames[index], + childTaskBounds[index], + childTaskUserIds[index] + ) + } +} + +internal suspend fun RootTaskInfo.firstChildTaskOrNull( + filter: suspend (Int) -> Boolean +): Pair<RootTaskInfo, Int>? { + // Child tasks are provided in bottom-up order + // Filtering is done top-down, so iterate backwards here. + for (index in numActivities - 1 downTo 0) { + if (filter(index)) { + return (this to index) + } + } + return null +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt index bc71ab71b626..63d15087d12c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt @@ -16,7 +16,9 @@ package com.android.systemui.screenshot.policy +import com.android.systemui.Flags.screenshotPrivateProfile import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.screenshot.ImageCapture import com.android.systemui.screenshot.RequestProcessor import com.android.systemui.screenshot.ScreenshotPolicy @@ -29,6 +31,7 @@ import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Provider +import kotlinx.coroutines.CoroutineDispatcher @Module interface ScreenshotPolicyModule { @@ -37,18 +40,42 @@ interface ScreenshotPolicyModule { @SysUISingleton fun bindProfileTypeRepository(impl: ProfileTypeRepositoryImpl): ProfileTypeRepository + @Binds + @SysUISingleton + fun bindDisplayContentRepository(impl: DisplayContentRepositoryImpl): DisplayContentRepository + companion object { + @JvmStatic + @Provides + @SysUISingleton + fun bindCapturePolicyList( + privateProfilePolicy: PrivateProfilePolicy, + workProfilePolicy: WorkProfilePolicy, + ): List<CapturePolicy> { + // In order of priority. The first matching policy applies. + return listOf(workProfilePolicy, privateProfilePolicy) + } + + @JvmStatic @Provides @SysUISingleton fun bindScreenshotRequestProcessor( + @Background background: CoroutineDispatcher, imageCapture: ImageCapture, policyProvider: Provider<ScreenshotPolicy>, + displayContentRepoProvider: Provider<DisplayContentRepository>, + policyListProvider: Provider<List<CapturePolicy>>, ): ScreenshotRequestProcessor { - return RequestProcessor(imageCapture, policyProvider.get()) + return if (screenshotPrivateProfile()) { + PolicyRequestProcessor( + background, + imageCapture, + displayContentRepoProvider.get(), + policyListProvider.get() + ) + } else { + RequestProcessor(imageCapture, policyProvider.get()) + } } } - - @Binds - @SysUISingleton - fun bindDisplayContentRepository(impl: DisplayContentRepositoryImpl): DisplayContentRepository } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt new file mode 100644 index 000000000000..d6b5d6dfda25 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/WorkProfilePolicy.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.policy + +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED +import android.os.UserHandle +import com.android.systemui.screenshot.data.model.DisplayContentModel +import com.android.systemui.screenshot.data.model.ProfileType +import com.android.systemui.screenshot.data.repository.ProfileTypeRepository +import com.android.systemui.screenshot.policy.CaptureType.IsolatedTask +import javax.inject.Inject +import kotlinx.coroutines.flow.first + +/** + * Condition: When the top visible task (excluding PIP mode) belongs to a work user. + * + * Parameters: Capture only the foreground task, owned by the work user. + */ +class WorkProfilePolicy +@Inject +constructor( + private val profileTypes: ProfileTypeRepository, +) : CapturePolicy { + override suspend fun apply(content: DisplayContentModel): CaptureParameters? { + // Find the first non PiP rootTask with a top child task owned by a work user + val (rootTask, childTask) = + content.rootTasks + .filter { it.isVisible && it.windowingMode != WINDOWING_MODE_PINNED } + .map { it to it.childTasksTopDown().first() } + .firstOrNull { (_, child) -> + profileTypes.getProfileType(child.userId) == ProfileType.WORK + } + ?: return null + + // If matched, return parameters needed to modify the request. + return CaptureParameters( + type = IsolatedTask(taskId = childTask.id, taskBounds = childTask.bounds), + component = childTask.componentName ?: rootTask.topActivity, + owner = UserHandle.of(childTask.userId), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java index 1e1a577ebd4a..706ac9c46be1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java @@ -335,8 +335,8 @@ public class LongScreenshotActivity extends Activity { // TODO: Fix transition for work profile. Omitting it in the meantime. mActionExecutor.launchIntentAsync( ActionIntentCreator.INSTANCE.createEdit(uri, this), - null, - mScreenshotUserHandle, false); + mScreenshotUserHandle, false, + /* activityOptions */ null, /* transitionCoordinator */ null); } else { String editorPackage = getString(R.string.config_screenshotEditor); Intent intent = new Intent(Intent.ACTION_EDIT); @@ -363,7 +363,8 @@ public class LongScreenshotActivity extends Activity { private void doShare(Uri uri) { Intent shareIntent = ActionIntentCreator.INSTANCE.createShare(uri); - mActionExecutor.launchIntentAsync(shareIntent, null, mScreenshotUserHandle, false); + mActionExecutor.launchIntentAsync(shareIntent, mScreenshotUserHandle, false, + /* activityOptions */ null, /* transitionCoordinator */ null); } private void onClicked(View v) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureExecutor.kt new file mode 100644 index 000000000000..6c4ee3e9e89b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureExecutor.kt @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.scroll + +import android.app.ActivityManager +import android.graphics.Rect +import android.os.IBinder +import android.util.Log +import android.view.ScrollCaptureResponse +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.screenshot.scroll.ScrollCaptureController.LongScreenshot +import com.google.common.util.concurrent.ListenableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executor +import java.util.concurrent.Future +import javax.inject.Inject + +class ScrollCaptureExecutor +@Inject +constructor( + activityManager: ActivityManager, + private val scrollCaptureClient: ScrollCaptureClient, + private val scrollCaptureController: ScrollCaptureController, + private val longScreenshotHolder: LongScreenshotData, + @Main private val mainExecutor: Executor +) { + private val isLowRamDevice = activityManager.isLowRamDevice + private var lastScrollCaptureRequest: ListenableFuture<ScrollCaptureResponse>? = null + private var lastScrollCaptureResponse: ScrollCaptureResponse? = null + private var longScreenshotFuture: ListenableFuture<LongScreenshot>? = null + + fun requestScrollCapture( + displayId: Int, + token: IBinder, + callback: (ScrollCaptureResponse) -> Unit + ) { + if (!allowLongScreenshots()) { + Log.d(TAG, "Long screenshots not supported on this device") + return + } + scrollCaptureClient.setHostWindowToken(token) + lastScrollCaptureRequest?.cancel(true) + val scrollRequest = + scrollCaptureClient.request(displayId).apply { + addListener( + { onScrollCaptureResponseReady(this)?.let { callback.invoke(it) } }, + mainExecutor + ) + } + lastScrollCaptureRequest = scrollRequest + } + + fun interface ScrollTransitionReady { + fun onTransitionReady( + destRect: Rect, + onTransitionEnd: Runnable, + longScreenshot: LongScreenshot + ) + } + + fun executeBatchScrollCapture( + response: ScrollCaptureResponse, + onCaptureComplete: Runnable, + onFailure: Runnable, + transition: ScrollTransitionReady, + ) { + // Clear the reference to prevent close() on reset + lastScrollCaptureResponse = null + longScreenshotFuture?.cancel(true) + longScreenshotFuture = + scrollCaptureController.run(response).apply { + addListener( + { + getLongScreenshotChecked(this, onFailure)?.let { + longScreenshotHolder.setLongScreenshot(it) + longScreenshotHolder.setTransitionDestinationCallback { + destinationRect: Rect, + onTransitionEnd: Runnable -> + transition.onTransitionReady(destinationRect, onTransitionEnd, it) + } + onCaptureComplete.run() + } + }, + mainExecutor + ) + } + } + + fun close() { + lastScrollCaptureRequest?.cancel(true) + lastScrollCaptureRequest = null + lastScrollCaptureResponse?.close() + lastScrollCaptureResponse = null + longScreenshotFuture?.cancel(true) + } + + private fun getLongScreenshotChecked( + future: ListenableFuture<LongScreenshot>, + onFailure: Runnable + ): LongScreenshot? { + var longScreenshot: LongScreenshot? = null + runCatching { longScreenshot = future.get() } + .onFailure { + Log.e(TAG, "Caught exception", it) + onFailure.run() + return null + } + if (longScreenshot?.height != 0) { + return longScreenshot + } + onFailure.run() + return null + } + + private fun onScrollCaptureResponseReady( + responseFuture: Future<ScrollCaptureResponse> + ): ScrollCaptureResponse? { + try { + lastScrollCaptureResponse?.close() + lastScrollCaptureResponse = null + if (responseFuture.isCancelled) { + return null + } + val captureResponse = responseFuture.get().apply { lastScrollCaptureResponse = this } + if (!captureResponse.isConnected) { + // No connection means that the target window wasn't found + // or that it cannot support scroll capture. + Log.d( + TAG, + "ScrollCapture: ${captureResponse.description} [${captureResponse.windowTitle}]" + ) + return null + } + Log.d(TAG, "ScrollCapture: connected to window [${captureResponse.windowTitle}]") + return captureResponse + } catch (e: InterruptedException) { + Log.e(TAG, "requestScrollCapture interrupted", e) + } catch (e: ExecutionException) { + Log.e(TAG, "requestScrollCapture failed", e) + } + return null + } + + private fun allowLongScreenshots(): Boolean { + return !isLowRamDevice + } + + private companion object { + private const val TAG = "ScrollCaptureExecutor" + } +} 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 index c7fe3f608a2f..a6374ae3304d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt @@ -36,6 +36,7 @@ object ActionButtonViewBinder { } else { view.setOnClickListener(null) } + view.tag = viewModel.id view.contentDescription = viewModel.description view.visibility = View.VISIBLE view.alpha = 1f 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 index b191a1a52616..32e9296107a3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt @@ -16,7 +16,6 @@ package com.android.systemui.screenshot.ui.binder -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -71,21 +70,36 @@ object ScreenshotShelfViewBinder { .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) { + + // Remove any buttons not in the new list, then do another pass to add + // any new actions and update any that are already there. + // This assumes that actions can never change order and that each action + // ID is unique. + val newIds = actions.map { it.id } + + for (view in actionsContainer.children.toList()) { + if (view.tag !in newIds) { + actionsContainer.removeView(view) + } + } + + for ((index, action) in actions.withIndex()) { + val currentView: View? = actionsContainer.getChildAt(index) + if (action.id == currentView?.tag) { + // Same ID, update the display + ActionButtonViewBinder.bind(currentView, action) + } else { + // Different ID. Removals have already happened so this must + // mean that the new action must be inserted here. + val actionButton = layoutInflater.inflate( R.layout.overlay_action_chip, actionsContainer, false ) - } + actionsContainer.addView(actionButton, index) + ActionButtonViewBinder.bind(actionButton, action) } - 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 index 05bfed159527..97b24c1b7df7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt @@ -22,5 +22,13 @@ data class ActionButtonViewModel( val icon: Drawable?, val name: CharSequence?, val description: CharSequence, - val onClicked: (() -> Unit)? -) + val onClicked: (() -> Unit)?, +) { + val id: Int = getId() + + companion object { + private var nextId = 0 + + private fun getId() = nextId.also { nextId += 1 } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java index 92d6ec97ad83..8397d9f1e7b9 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java @@ -52,7 +52,6 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.util.settings.SecureSettings; import dagger.assisted.Assisted; @@ -107,7 +106,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig private ValueAnimator mSliderAnimator; @Override - public void setMirror(BrightnessMirrorController controller) { + public void setMirror(@Nullable MirrorController controller) { mControl.setMirrorControllerAndMirror(controller); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt index 701d814a843b..073279be5af2 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessMirrorHandler.kt @@ -16,12 +16,9 @@ package com.android.systemui.settings.brightness -import com.android.systemui.statusbar.policy.BrightnessMirrorController -import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener - class BrightnessMirrorHandler(brightnessController: MirroredBrightnessController) { - var mirrorController: BrightnessMirrorController? = null + var mirrorController: MirrorController? = null private set var brightnessController: MirroredBrightnessController = brightnessController @@ -30,7 +27,8 @@ class BrightnessMirrorHandler(brightnessController: MirroredBrightnessController updateBrightnessMirror() } - private val brightnessMirrorListener = BrightnessMirrorListener { updateBrightnessMirror() } + private val brightnessMirrorListener = + MirrorController.BrightnessMirrorListener { updateBrightnessMirror() } fun onQsPanelAttached() { mirrorController?.addCallback(brightnessMirrorListener) @@ -40,7 +38,7 @@ class BrightnessMirrorHandler(brightnessController: MirroredBrightnessController mirrorController?.removeCallback(brightnessMirrorListener) } - fun setController(controller: BrightnessMirrorController?) { + fun setController(controller: MirrorController?) { mirrorController?.removeCallback(brightnessMirrorListener) mirrorController = controller mirrorController?.addCallback(brightnessMirrorListener) @@ -48,6 +46,6 @@ class BrightnessMirrorHandler(brightnessController: MirroredBrightnessController } private fun updateBrightnessMirror() { - mirrorController?.let { brightnessController.setMirror(it) } + brightnessController.setMirror(mirrorController) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java index 539b0c2dd599..b425fb997d9e 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java @@ -57,8 +57,10 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV ToggleSlider { private Listener mListener; + @Nullable private ToggleSlider mMirror; - private BrightnessMirrorController mMirrorController; + @Nullable + private MirrorController mMirrorController; private boolean mTracking; private final FalsingManager mFalsingManager; private final UiEventLogger mUiEventLogger; @@ -108,6 +110,9 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV protected void onViewAttached() { mView.setOnSeekBarChangeListener(mSeekListener); mView.setOnInterceptListener(mOnInterceptListener); + if (mMirror != null) { + mView.setOnDispatchTouchEventListener(this::mirrorTouchEvent); + } } @Override @@ -129,7 +134,10 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV private boolean copyEventToMirror(MotionEvent ev) { MotionEvent copy = ev.copy(); - boolean out = mMirror.mirrorTouchEvent(copy); + boolean out = false; + if (mMirror != null) { + out = mMirror.mirrorTouchEvent(copy); + } copy.recycle(); return out; } @@ -166,9 +174,13 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV * @param c */ @Override - public void setMirrorControllerAndMirror(BrightnessMirrorController c) { + public void setMirrorControllerAndMirror(@Nullable MirrorController c) { mMirrorController = c; - setMirror(c.getToggleSlider()); + if (c != null) { + setMirror(c.getToggleSlider()); + } else { + setMirror(null); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/MirrorController.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/MirrorController.kt new file mode 100644 index 000000000000..6a9af26ba4a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/MirrorController.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.settings.brightness + +import android.view.View +import com.android.systemui.settings.brightness.MirrorController.BrightnessMirrorListener +import com.android.systemui.statusbar.policy.CallbackController + +interface MirrorController : CallbackController<BrightnessMirrorListener> { + + /** + * Get the [ToggleSlider] currently associated with this controller, or `null` if none currently + */ + fun getToggleSlider(): ToggleSlider? + + /** + * Indicate to this controller that the user is dragging on the brightness view and the mirror + * should show + */ + fun showMirror() + + /** + * Indicate to this controller that the user has stopped dragging on the brightness view and the + * mirror should hide + */ + fun hideMirror() + + /** + * Set the location and size of the current brightness [view] in QS so it can be properly + * adapted to show the mirror in the same location and with the same size. + */ + fun setLocationAndSize(view: View) + + fun interface BrightnessMirrorListener { + fun onBrightnessMirrorReinflated(brightnessMirror: View?) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/MirroredBrightnessController.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/MirroredBrightnessController.kt index 8d857dec2108..b1a532baa710 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/MirroredBrightnessController.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/MirroredBrightnessController.kt @@ -22,5 +22,5 @@ import com.android.systemui.statusbar.policy.BrightnessMirrorController * Indicates controller that has brightness slider and uses [BrightnessMirrorController] */ interface MirroredBrightnessController { - fun setMirror(controller: BrightnessMirrorController) + fun setMirror(controller: MirrorController?) }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java index 648e33b1d228..24bc67047a47 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java @@ -19,7 +19,6 @@ package com.android.systemui.settings.brightness; import android.view.MotionEvent; import com.android.settingslib.RestrictedLockUtils; -import com.android.systemui.statusbar.policy.BrightnessMirrorController; public interface ToggleSlider { interface Listener { @@ -27,7 +26,7 @@ public interface ToggleSlider { } void setEnforcedAdmin(RestrictedLockUtils.EnforcedAdmin admin); - void setMirrorControllerAndMirror(BrightnessMirrorController c); + void setMirrorControllerAndMirror(MirrorController c); boolean mirrorTouchEvent(MotionEvent ev); void setOnChangedListener(Listener l); diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/data/repository/BrightnessMirrorShowingRepository.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/data/repository/BrightnessMirrorShowingRepository.kt new file mode 100644 index 000000000000..a0c9be46b6e8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/data/repository/BrightnessMirrorShowingRepository.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.settings.brightness.data.repository + +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class BrightnessMirrorShowingRepository @Inject constructor() { + private val _isShowing = MutableStateFlow(false) + val isShowing = _isShowing.asStateFlow() + + fun setMirrorShowing(showing: Boolean) { + _isShowing.value = showing + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/domain/interactor/BrightnessMirrorShowingInteractor.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/domain/interactor/BrightnessMirrorShowingInteractor.kt new file mode 100644 index 000000000000..ef6e72f76ae6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/domain/interactor/BrightnessMirrorShowingInteractor.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings.brightness.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.settings.brightness.data.repository.BrightnessMirrorShowingRepository +import javax.inject.Inject + +@SysUISingleton +class BrightnessMirrorShowingInteractor +@Inject +constructor( + private val brightnessMirrorShowingRepository: BrightnessMirrorShowingRepository, +) { + val isShowing = brightnessMirrorShowingRepository.isShowing + + fun setMirrorShowing(showing: Boolean) { + brightnessMirrorShowingRepository.setMirrorShowing(showing) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflater.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflater.kt new file mode 100644 index 000000000000..468a87325507 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/binder/BrightnessMirrorInflater.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.settings.brightness.ui.binder + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.android.systemui.res.R +import com.android.systemui.settings.brightness.BrightnessSliderController + +object BrightnessMirrorInflater { + + fun inflate( + context: Context, + sliderControllerFactory: BrightnessSliderController.Factory, + ): Pair<View, BrightnessSliderController> { + val frame = + (LayoutInflater.from(context).inflate(R.layout.brightness_mirror_container, null) + as ViewGroup) + .apply { isVisible = true } + val sliderController = sliderControllerFactory.create(context, frame) + sliderController.init() + frame.addView( + sliderController.rootView, + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + return frame to sliderController + } +} diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt new file mode 100644 index 000000000000..2651a994bb55 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.settings.brightness.ui.viewModel + +import android.content.res.Resources +import android.view.View +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.settings.brightness.BrightnessSliderController +import com.android.systemui.settings.brightness.MirrorController +import com.android.systemui.settings.brightness.ToggleSlider +import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class BrightnessMirrorViewModel +@Inject +constructor( + private val brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor, + @Main private val resources: Resources, + val sliderControllerFactory: BrightnessSliderController.Factory, +) : MirrorController { + + private val tempPosition = IntArray(2) + + private var _toggleSlider: BrightnessSliderController? = null + + val isShowing = brightnessMirrorShowingInteractor.isShowing + + private val _locationAndSize: MutableStateFlow<LocationAndSize> = + MutableStateFlow(LocationAndSize()) + val locationAndSize = _locationAndSize.asStateFlow() + + override fun getToggleSlider(): ToggleSlider? { + return _toggleSlider + } + + fun setToggleSlider(toggleSlider: BrightnessSliderController) { + _toggleSlider = toggleSlider + } + + override fun showMirror() { + brightnessMirrorShowingInteractor.setMirrorShowing(true) + } + + override fun hideMirror() { + brightnessMirrorShowingInteractor.setMirrorShowing(false) + } + + override fun setLocationAndSize(view: View) { + view.getLocationInWindow(tempPosition) + val padding = resources.getDimensionPixelSize(R.dimen.rounded_slider_background_padding) + _toggleSlider?.rootView?.setPadding(padding, padding, padding, padding) + // Account for desired padding + _locationAndSize.value = + LocationAndSize( + yOffset = tempPosition[1] - padding, + width = view.measuredWidth + 2 * padding, + height = view.measuredHeight + 2 * padding, + ) + } + + // Callbacks are used for indicating reinflation when the config changes in some ways (like + // density). However, we don't need that as we recompose the view anyway + override fun addCallback(listener: MirrorController.BrightnessMirrorListener) {} + + override fun removeCallback(listener: MirrorController.BrightnessMirrorListener) {} +} + +data class LocationAndSize( + val yOffset: Int = 0, + val width: Int = 0, + val height: Int = 0, +) diff --git a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java index c42fdf8e7b93..b24edd9beece 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/shade/DebugDrawable.java @@ -25,6 +25,7 @@ import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import com.android.keyguard.LockIconViewController; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import java.util.HashSet; @@ -80,12 +81,14 @@ public class DebugDrawable extends Drawable { mNotificationPanelViewController.getClockPositionResult() .stackScrollerPadding), Color.YELLOW, "calculatePanelHeightShade()"); - drawDebugInfo(canvas, - (int) mQsController.calculateNotificationsTopPadding( - mNotificationPanelViewController.isExpandingOrCollapsing(), - mNotificationPanelViewController.getKeyguardNotificationStaticPadding(), - mNotificationPanelViewController.getExpandedFraction()), - Color.MAGENTA, "calculateNotificationsTopPadding()"); + if (!SceneContainerFlag.isEnabled()) { + drawDebugInfo(canvas, + (int) mQsController.calculateNotificationsTopPadding( + mNotificationPanelViewController.isExpandingOrCollapsing(), + mNotificationPanelViewController.getKeyguardNotificationStaticPadding(), + mNotificationPanelViewController.getExpandedFraction()), + Color.MAGENTA, "calculateNotificationsTopPadding()"); + } drawDebugInfo(canvas, mNotificationPanelViewController.getClockPositionResult().clockY, Color.GRAY, "mClockPositionResult.clockY"); drawDebugInfo(canvas, (int) mLockIconViewController.getTop(), Color.GRAY, diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 343f37744ce9..4660831b77af 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1588,7 +1588,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump * @param forceClockUpdate Should the clock be updated even when not on keyguard */ private void positionClockAndNotifications(boolean forceClockUpdate) { - boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); + boolean animate = !SceneContainerFlag.isEnabled() + && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); int stackScrollerPadding; boolean onKeyguard = isKeyguardShowing(); @@ -1675,7 +1676,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mClockPositionResult.clockX, mClockPositionResult.clockY); } - boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); + boolean animate = !SceneContainerFlag.isEnabled() + && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange; if (!MigrateClocksToBlueprint.isEnabled()) { @@ -2483,6 +2485,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump /** Returns the topPadding of notifications when on keyguard not respecting QS expansion. */ int getKeyguardNotificationStaticPadding() { + SceneContainerFlag.assertInLegacyMode(); if (!isKeyguardShowing()) { return 0; } @@ -2524,12 +2527,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } void requestScrollerTopPaddingUpdate(boolean animate) { - float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing, - getKeyguardNotificationStaticPadding(), mExpandedFraction); - if (MigrateClocksToBlueprint.isEnabled()) { - mSharedNotificationContainerInteractor.setTopPosition(padding); - } else { - mNotificationStackScrollLayoutController.updateTopPadding(padding, animate); + if (!SceneContainerFlag.isEnabled()) { + float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing, + getKeyguardNotificationStaticPadding(), mExpandedFraction); + if (MigrateClocksToBlueprint.isEnabled()) { + mSharedNotificationContainerInteractor.setTopPosition(padding); + } else { + mNotificationStackScrollLayoutController.updateTopPadding(padding, animate); + } } if (isKeyguardShowing() @@ -3174,6 +3179,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } notifyExpandingFinished(); } + // TODO(b/332732878): replace this call when scene container is enabled mNotificationStackScrollLayoutController.setAnimationsEnabled(!disabled); } @@ -3963,7 +3969,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mShadeRepository.setLegacyShadeExpansion(mExpandedFraction); mQsController.setShadeExpansion(mExpandedHeight, mExpandedFraction); mExpansionDragDownAmountPx = h; - mAmbientState.setExpansionFraction(mExpandedFraction); + if (!SceneContainerFlag.isEnabled()) { + mAmbientState.setExpansionFraction(mExpandedFraction); + } onHeightUpdated(mExpandedHeight); updateExpansionAndVisibility(); }); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 89e84133c783..fb32b9fce909 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -417,6 +417,12 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } else { mLpChanged.flags &= ~LayoutParams.FLAG_SECURE; } + + if (state.bouncerShowing) { + mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; + } else { + mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; + } } protected boolean isDebuggable() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java index f9b4e671f373..903af61f2202 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java @@ -81,6 +81,8 @@ public class NotificationShadeWindowView extends WindowRootView { @Override public boolean dispatchKeyEvent(KeyEvent event) { + mInteractionEventHandler.collectKeyEvent(event); + if (mInteractionEventHandler.interceptMediaKey(event)) { return true; } @@ -301,6 +303,11 @@ public class NotificationShadeWindowView extends WindowRootView { boolean dispatchKeyEvent(KeyEvent event); boolean dispatchKeyEventPreIme(KeyEvent event); + + /** + * Collects the KeyEvent without intercepting it + */ + void collectKeyEvent(KeyEvent event); } /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 324dfdf32a3f..6ac81d226eee 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -563,6 +563,11 @@ public class NotificationShadeWindowViewController implements Dumpable { public boolean dispatchKeyEvent(KeyEvent event) { return mSysUIKeyEventHandler.dispatchKeyEvent(event); } + + @Override + public void collectKeyEvent(KeyEvent event) { + mFalsingCollector.onKeyEvent(event); + } }); mView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 35b40592a47e..2507507ce22e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -1371,6 +1371,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum @Override public float calculateNotificationsTopPadding(boolean isShadeExpanding, int keyguardNotificationStaticPadding, float expandedFraction) { + SceneContainerFlag.assertInLegacyMode(); float topPadding; boolean keyguardShowing = mBarState == KEYGUARD; if (mSplitShadeEnabled) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt index b8250cc284bd..34629934e467 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt @@ -17,7 +17,6 @@ package com.android.systemui.shade import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.plugins.qs.QSContainerController import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.shade.domain.interactor.ShadeInteractor import javax.inject.Inject @@ -28,7 +27,6 @@ class QuickSettingsControllerSceneImpl constructor( private val shadeInteractor: ShadeInteractor, private val qsSceneAdapter: QSSceneAdapter, - private val qsContainerController: QSContainerController, ) : QuickSettingsController { override val expanded: Boolean @@ -43,7 +41,7 @@ constructor( } override fun closeQsCustomizer() { - qsContainerController.setCustomizerShowing(false) + qsSceneAdapter.requestCloseCustomizer() } @Deprecated("specific to legacy split shade") diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index c5e07e818b6b..ebebbe65d54b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -23,6 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.ShadeTouchLog import com.android.systemui.scene.domain.interactor.SceneInteractor @@ -61,6 +62,7 @@ constructor( private val shadeInteractor: ShadeInteractor, private val sceneInteractor: SceneInteractor, private val deviceEntryInteractor: DeviceEntryInteractor, + private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val notificationStackScrollLayout: NotificationStackScrollLayout, @ShadeTouchLog private val touchLog: LogBuffer, private val vibratorHelper: VibratorHelper, @@ -148,7 +150,11 @@ constructor( } private fun getCollapseDestinationScene(): SceneKey { - return if (deviceEntryInteractor.isDeviceEntered.value) { + // Always check whether device is unlocked before transitioning to gone scene. + return if ( + deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked && + deviceEntryInteractor.isDeviceEntered.value + ) { Scenes.Gone } else { Scenes.Lockscreen diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurfaceImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurfaceImpl.kt index adb29287e40e..ec4018c7d238 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurfaceImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurfaceImpl.kt @@ -46,7 +46,7 @@ class ShadeSurfaceImpl @Inject constructor() : ShadeSurface, ShadeViewController } override fun setTouchAndAnimationDisabled(disabled: Boolean) { - // TODO(b/322197941): determine if still needed + // TODO(b/332732878): determine if still needed } override fun setWillPlayDelayedDozeAmountAnimation(willPlay: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index 2cb9f9acca26..f5dd5e465505 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -22,6 +22,7 @@ import android.os.Handler import android.view.LayoutInflater import android.view.ViewStub import androidx.constraintlayout.motion.widget.MotionLayout +import com.android.compose.animation.scene.SceneKey import com.android.keyguard.logging.ScrimLogger import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.BatteryMeterViewController @@ -43,6 +44,7 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationInsetsController import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.StatusBarLocation @@ -51,6 +53,7 @@ import com.android.systemui.statusbar.phone.TapAgainView import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.tuner.TunerService +import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Named @@ -59,6 +62,14 @@ import javax.inject.Provider /** Module for providing views related to the shade. */ @Module abstract class ShadeViewProviderModule { + + @Binds + @SysUISingleton + // TODO(b/277762009): Only allow this view's binder to inject the view. + abstract fun bindsNotificationScrollView( + notificationStackScrollLayout: NotificationStackScrollLayout + ): NotificationScrollView + companion object { const val SHADE_HEADER = "large_screen_shade_header" @@ -76,6 +87,7 @@ abstract class ShadeViewProviderModule { sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>, ): WindowRootView { return if (sceneContainerFlags.isEnabled()) { + checkNoSceneDuplicates(scenesProvider.get()) val sceneWindowRootView = layoutInflater.inflate(R.layout.scene_window_root, null) as SceneWindowRootView sceneWindowRootView.init( @@ -271,5 +283,21 @@ abstract class ShadeViewProviderModule { ): StatusIconContainer { return header.requireViewById(R.id.statusIcons) } + + private fun checkNoSceneDuplicates(scenes: Set<Scene>) { + val keys = mutableSetOf<SceneKey>() + val duplicates = mutableSetOf<SceneKey>() + scenes + .map { it.key } + .forEach { sceneKey -> + if (keys.contains(sceneKey)) { + duplicates.add(sceneKey) + } else { + keys.add(sceneKey) + } + } + + check(duplicates.isEmpty()) { "Duplicate scenes detected: $duplicates" } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java index 3349345b1391..c4293291de51 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java @@ -345,9 +345,7 @@ public class ShadeCarrierGroupController { } } - if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { - Log.d(TAG, "ignoring old pipeline callback because new mobile icon is enabled"); - } else { + if (!mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { for (int i = 0; i < SIM_SLOTS; i++) { mCarrierGroups[i].updateState(mInfos[i], singleCarrier); } 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 9362cd058929..980f665ae61f 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 @@ -33,6 +33,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel @@ -57,6 +58,7 @@ constructor( val qsSceneAdapter: QSSceneAdapter, val shadeHeaderViewModel: ShadeHeaderViewModel, val notifications: NotificationsPlaceholderViewModel, + val brightnessMirrorViewModel: BrightnessMirrorViewModel, val mediaDataManager: MediaDataManager, shadeInteractor: ShadeInteractor, private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, diff --git a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt index 5e57f1d1a11f..8eed0975579d 100644 --- a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt @@ -27,4 +27,4 @@ import kotlin.reflect.KClass @MustBeDocumented @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -annotation class Dependencies(vararg val value: KClass<out CoreStartable> = []) +annotation class Dependencies(vararg val value: KClass<*> = []) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index d6858cad6d0b..78e108d444d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -40,6 +40,7 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.util.Pair; @@ -57,6 +58,7 @@ import android.view.View; import android.view.View.AccessibilityDelegate; import android.view.ViewGroup; import android.view.Window; +import android.view.WindowInsets; import android.view.WindowManager; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Button; @@ -104,6 +106,7 @@ public final class KeyboardShortcutListSearch { private WindowManager mWindowManager; private EditText mSearchEditText; + private ImageButton mEditTextCancel; private String mQueryString; private int mCurrentCategoryIndex = 0; private Map<Integer, Boolean> mKeySearchResultMap = new HashMap<>(); @@ -143,7 +146,7 @@ public final class KeyboardShortcutListSearch { @VisibleForTesting KeyboardShortcutListSearch(Context context, WindowManager windowManager) { this.mContext = new ContextThemeWrapper( - context, android.R.style.Theme_DeviceDefault_Settings); + context, R.style.KeyboardShortcutHelper); this.mPackageManager = AppGlobals.getPackageManager(); if (windowManager != null) { this.mWindowManager = windowManager; @@ -853,13 +856,14 @@ public final class KeyboardShortcutListSearch { List<List<KeyboardShortcutMultiMappingGroup>> keyboardShortcutMultiMappingGroupList) { mQueryString = null; LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class); - mKeyboardShortcutsBottomSheetDialog = - new BottomSheetDialog(mContext); + mKeyboardShortcutsBottomSheetDialog = new BottomSheetDialog(mContext); final View keyboardShortcutsView = inflater.inflate( R.layout.keyboard_shortcuts_search_view, null); LinearLayout shortcutsContainer = keyboardShortcutsView.findViewById( R.id.keyboard_shortcuts_container); mNoSearchResults = keyboardShortcutsView.findViewById(R.id.shortcut_search_no_result); + Window keyboardShortcutsWindow = mKeyboardShortcutsBottomSheetDialog.getWindow(); + setWindowProperties(keyboardShortcutsWindow); mKeyboardShortcutsBottomSheetDialog.setContentView(keyboardShortcutsView); setButtonsDefaultStatus(keyboardShortcutsView); populateCurrentAppButton(); @@ -874,25 +878,11 @@ public final class KeyboardShortcutListSearch { } BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(bottomSheet); + behavior.setDraggable(true); behavior.setState(BottomSheetBehavior.STATE_EXPANDED); behavior.setSkipCollapsed(true); - behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { - @Override - public void onStateChanged(@NonNull View bottomSheet, int newState) { - if (newState == BottomSheetBehavior.STATE_DRAGGING) { - behavior.setState(BottomSheetBehavior.STATE_EXPANDED); - } - } - @Override - public void onSlide(@NonNull View bottomSheet, float slideOffset) { - // Do nothing. - } - }); - mKeyboardShortcutsBottomSheetDialog.setCanceledOnTouchOutside(true); - Window keyboardShortcutsWindow = mKeyboardShortcutsBottomSheetDialog.getWindow(); - keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG); synchronized (sLock) { // show KeyboardShortcutsBottomSheetDialog only if it has not been dismissed already if (sInstance != null) { @@ -908,6 +898,8 @@ public final class KeyboardShortcutListSearch { } } mSearchEditText = keyboardShortcutsView.findViewById(R.id.keyboard_shortcuts_search); + mEditTextCancel = keyboardShortcutsView.findViewById( + R.id.keyboard_shortcuts_search_cancel); mSearchEditText.addTextChangedListener( new TextWatcher() { @Override @@ -921,6 +913,8 @@ public final class KeyboardShortcutListSearch { shortcutsContainer.setAccessibilityPaneTitle(mContext.getString( R.string.keyboard_shortcut_a11y_show_search_results)); } + mEditTextCancel.setVisibility( + TextUtils.isEmpty(mQueryString) ? View.GONE : View.VISIBLE); } @Override @@ -933,9 +927,28 @@ public final class KeyboardShortcutListSearch { // Do nothing. } }); - ImageButton editTextCancel = keyboardShortcutsView.findViewById( - R.id.keyboard_shortcuts_search_cancel); - editTextCancel.setOnClickListener(v -> mSearchEditText.setText(null)); + + mEditTextCancel.setOnClickListener(v -> mSearchEditText.setText(null)); + } + + private static void setWindowProperties(Window keyboardShortcutsWindow) { + keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG); + WindowManager.LayoutParams params = new WindowManager.LayoutParams(); + params.copyFrom(keyboardShortcutsWindow.getAttributes()); + // Allows the bottom sheet dialog to render all the way to the bottom of the screen, + // behind the gesture navigation bar. + params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + params.setFitInsetsTypes(WindowInsets.Type.statusBars()); + keyboardShortcutsWindow.setAttributes(params); + keyboardShortcutsWindow.getDecorView().setOnApplyWindowInsetsListener((v, insets) -> { + int bottom = insets.getInsets(WindowInsets.Type.navigationBars()).bottom; + View container = v.findViewById(R.id.keyboard_shortcuts_container); + container.setPadding(container.getPaddingLeft(), container.getPaddingTop(), + container.getPaddingRight(), bottom); + return WindowInsets.CONSUMED; + }); + keyboardShortcutsWindow.setWindowAnimations( + R.style.KeyboardShortcutHelper_BottomSheetDialogAnimation); } private void populateKeyboardShortcutSearchList(LinearLayout keyboardShortcutsLayout) { @@ -1256,10 +1269,10 @@ public final class KeyboardShortcutListSearch { if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { lp.width = (int) (display.getWidth() * 0.8); - lp.height = (int) (display.getHeight() * 0.7); + lp.height = (int) (display.getHeight() * 0.8); } else { lp.width = (int) (display.getWidth() * 0.7); - lp.height = (int) (display.getHeight() * 0.8); + lp.height = (int) (display.getHeight() * 0.95); } window.setGravity(Gravity.BOTTOM); window.setAttributes(lp); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index 8ea29dd63487..aa6bec1f06f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -49,7 +49,6 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteract import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus; import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; -import com.android.systemui.plugins.clocks.ClockController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.res.R; import com.android.systemui.scene.domain.interactor.SceneInteractor; @@ -469,13 +468,7 @@ public class StatusBarStateControllerImpl implements /** Returns the id of the currently rendering clock */ public String getClockId() { if (MigrateClocksToBlueprint.isEnabled()) { - ClockController clock = mKeyguardClockInteractorLazy.get() - .getCurrentClock().getValue(); - if (clock == null) { - Log.e(TAG, "No clock is available"); - return KeyguardClockSwitch.MISSING_CLOCK_ID; - } - return clock.getConfig().getId(); + return mKeyguardClockInteractorLazy.get().getRenderedClockId(); } if (mClockSwitchView == null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java index 8104755b5e7b..d2fe20d9c50c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java @@ -23,6 +23,7 @@ import android.view.View; import com.android.systemui.CoreStartable; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.startable.Dependencies; import com.android.systemui.statusbar.phone.CentralSurfaces; import java.lang.annotation.Retention; @@ -30,6 +31,7 @@ import java.lang.annotation.Retention; /** * Sends updates to {@link StateListener}s about changes to the status bar state and dozing state */ +@Dependencies(CentralSurfaces.class) public interface SysuiStatusBarStateController extends StatusBarStateController, CoreStartable { // TODO: b/115739177 (remove this explicit ordering if we can) 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 8531eaa46804..1a223c110ad5 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 @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator +import android.os.SystemProperties import android.os.UserHandle import android.provider.Settings import androidx.annotation.VisibleForTesting @@ -340,12 +341,41 @@ constructor( var hasFilteredAnyNotifs = false + /** + * the [notificationMinimalismPrototype] will now show seen notifications on the locked + * shade by default, but this property read allows that to be quickly disabled for + * testing + */ + private val minimalismShowOnLockedShade + get() = + SystemProperties.getBoolean( + "persist.notification_minimalism_prototype.show_on_locked_shade", + true + ) + + /** + * Encapsulates a definition of "being on the keyguard". Note that these two definitions + * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does + * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing] + * is any state where the keyguard has not been dismissed, including locked shade and + * occluded lock screen. + * + * Returning false for locked shade and occluded states means that this filter will + * allow seen notifications to appear in the locked shade. + */ + private fun isOnKeyguard(): Boolean = + if (notificationMinimalismPrototype() && minimalismShowOnLockedShade) { + statusBarStateController.state == StatusBarState.KEYGUARD + } else { + keyguardRepository.isKeyguardShowing() + } + override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean = when { // Don't apply filter if the setting is disabled !unseenFilterEnabled -> false // Don't apply filter if the keyguard isn't currently showing - !keyguardRepository.isKeyguardShowing() -> false + !isOnKeyguard() -> false // Don't apply the filter if the notification is unseen unseenNotifications.contains(entry) -> false // Don't apply the filter to (non-promoted) group summaries 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 adcbbfbde002..968b591b9a09 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 @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; import com.android.systemui.statusbar.notification.row.FooterViewButton; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; +import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.ViewState; import com.android.systemui.util.DrawableDumpKt; @@ -102,6 +103,11 @@ public class FooterView extends StackScrollerDecorView { setClearAllButtonVisible(visible, animate, /* onAnimationEnded = */ null); } + /** Set the visibility of the "Manage"/"History" button to {@code visible}. */ + public void setManageOrHistoryButtonVisible(boolean visible) { + mManageOrHistoryButton.setVisibility(visible ? View.VISIBLE : View.GONE); + } + /** * Set the visibility of the "Clear all" button to {@code visible}. Animate the change if * {@code animate} is true. @@ -274,14 +280,23 @@ public class FooterView extends StackScrollerDecorView { /** Show a message instead of the footer buttons. */ public void setFooterLabelVisible(boolean isVisible) { - if (isVisible) { - mManageOrHistoryButton.setVisibility(View.GONE); - mClearAllButton.setVisibility(View.GONE); - mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); + // In the refactored code, hiding the buttons is handled in the FooterViewModel + if (FooterViewRefactor.isEnabled()) { + if (isVisible) { + mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); + } else { + mSeenNotifsFooterTextView.setVisibility(View.GONE); + } } else { - mManageOrHistoryButton.setVisibility(View.VISIBLE); - mClearAllButton.setVisibility(View.VISIBLE); - mSeenNotifsFooterTextView.setVisibility(View.GONE); + if (isVisible) { + mManageOrHistoryButton.setVisibility(View.GONE); + mClearAllButton.setVisibility(View.GONE); + mSeenNotifsFooterTextView.setVisibility(View.VISIBLE); + } else { + mManageOrHistoryButton.setVisibility(View.VISIBLE); + mClearAllButton.setVisibility(View.VISIBLE); + mSeenNotifsFooterTextView.setVisibility(View.GONE); + } } } @@ -443,6 +458,12 @@ public class FooterView extends StackScrollerDecorView { */ public boolean hideContent; + /** + * When true, skip animating Y on the next #animateTo. + * Once true, remains true until reset in #animateTo. + */ + public boolean resetY = false; + @Override public void copyFrom(ViewState viewState) { super.copyFrom(viewState); @@ -459,5 +480,17 @@ public class FooterView extends StackScrollerDecorView { footerView.setContentVisibleAnimated(!hideContent); } } + + @Override + public void animateTo(View child, AnimationProperties properties) { + if (child instanceof FooterView) { + // Must set animateY=false before super.animateTo, which checks for animateY + if (resetY) { + properties.getAnimationFilter().animateY = false; + resetY = false; + } + } + super.animateTo(child, properties); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt index 65ab4fdeb77b..637cadde6fc6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt @@ -141,8 +141,12 @@ object FooterViewBinder { } } - // NOTE: The manage/history button is always visible as long as the footer is visible, no - // need to update the visibility here. + launch { + viewModel.manageOrHistoryButton.isVisible.collect { isVisible -> + // NOTE: This visibility change is never animated. + footer.setManageOrHistoryButtonVisible(isVisible.value) + } + } } private suspend fun bindMessage( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt index b23ef356c1c0..90fb7285e939 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt @@ -34,6 +34,7 @@ import java.util.Optional import javax.inject.Provider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -45,12 +46,33 @@ class FooterViewModel( seenNotificationsInteractor: SeenNotificationsInteractor, shadeInteractor: ShadeInteractor, ) { + /** A message to show instead of the footer buttons. */ + val message: FooterMessageViewModel = + FooterMessageViewModel( + messageId = R.string.unlock_to_see_notif_text, + iconId = R.drawable.ic_friction_lock_closed, + isVisible = seenNotificationsInteractor.hasFilteredOutSeenNotifications, + ) + + private val clearAllButtonVisible = + activeNotificationsInteractor.hasClearableNotifications + .combine(message.isVisible) { hasClearableNotifications, isMessageVisible -> + if (isMessageVisible) { + // If the message is visible, the button never is + false + } else { + hasClearableNotifications + } + } + .distinctUntilChanged() + + /** The button for clearing notifications. */ val clearAllButton: FooterButtonViewModel = FooterButtonViewModel( labelId = flowOf(R.string.clear_all_notifications_text), accessibilityDescriptionId = flowOf(R.string.accessibility_clear_all), isVisible = - activeNotificationsInteractor.hasClearableNotifications + clearAllButtonVisible .sample( // TODO(b/322167853): This check is currently duplicated in // NotificationListViewModel, but instead it should be a field in @@ -61,9 +83,9 @@ class FooterViewModel( ::Pair ) .onStart { emit(Pair(false, false)) } - ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) -> + ) { clearAllButtonVisible, (isShadeFullyExpanded, animationsEnabled) -> val shouldAnimate = isShadeFullyExpanded && animationsEnabled - AnimatableEvent(hasClearableNotifications, shouldAnimate) + AnimatableEvent(clearAllButtonVisible, shouldAnimate) } .toAnimatedValueFlow(), ) @@ -77,18 +99,16 @@ class FooterViewModel( else R.string.manage_notifications_text } + /** The button for managing notification settings or opening notification history. */ val manageOrHistoryButton: FooterButtonViewModel = FooterButtonViewModel( labelId = manageOrHistoryButtonText, accessibilityDescriptionId = manageOrHistoryButtonText, - isVisible = flowOf(AnimatedValue.NotAnimating(true)), - ) - - val message: FooterMessageViewModel = - FooterMessageViewModel( - messageId = R.string.unlock_to_see_notif_text, - iconId = R.drawable.ic_friction_lock_closed, - isVisible = seenNotificationsInteractor.hasFilteredOutSeenNotifications, + isVisible = + // Hide the manage button if the message is visible + message.isVisible.map { messageVisible -> + AnimatedValue.NotAnimating(!messageVisible) + }, ) } 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 eb6c7b520037..9e0b16cfb312 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 @@ -1767,7 +1767,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ public ExpandableNotificationRow(Context context, AttributeSet attrs) { this(context, attrs, context); - Log.wtf(TAG, "This constructor shouldn't be called"); + Log.e(TAG, "This constructor shouldn't be called"); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 5b9eb21bf5e9..0bb871b23d27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -742,7 +742,7 @@ public class AmbientState implements Dumpable { pw.println("mHideSensitive=" + mHideSensitive); pw.println("mShadeExpanded=" + mShadeExpanded); pw.println("mClearAllInProgress=" + mClearAllInProgress); - pw.println("mStatusBarState=" + mStatusBarState); + pw.println("mStatusBarState=" + StatusBarState.toString(mStatusBarState)); pw.println("mExpansionChanging=" + mExpansionChanging); pw.println("mPanelFullWidth=" + mIsSmallScreen); pw.println("mPulsing=" + mPulsing); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java index 03a108287087..0c248f5949cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationFilter.java @@ -30,7 +30,7 @@ public class AnimationFilter { public static final int NO_DELAY = -1; boolean animateAlpha; boolean animateX; - boolean animateY; + public boolean animateY; ArraySet<View> animateYViews = new ArraySet<>(); boolean animateZ; boolean animateHeight; 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 5dc37e0525da..92c597cf384e 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 @@ -56,6 +56,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.HybridGroupManager; import com.android.systemui.statusbar.notification.row.HybridNotificationView; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; @@ -1429,7 +1430,7 @@ public class NotificationChildrenContainer extends ViewGroup if (singleLineView != null) { minExpandHeight += singleLineView.getHeight(); } else { - if (AsyncGroupHeaderViewInflation.isEnabled()) { + if (AsyncHybridViewInflation.isEnabled()) { minExpandHeight += mMinSingleLineHeight; } else { Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey() 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 3367dc427f43..82559dec9f86 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 @@ -113,6 +113,9 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds; +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape; +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -135,6 +138,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Callable; import java.util.function.BiConsumer; @@ -143,7 +147,9 @@ import java.util.function.Consumer; /** * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack. */ -public class NotificationStackScrollLayout extends ViewGroup implements Dumpable { +public class NotificationStackScrollLayout + extends ViewGroup + implements Dumpable, NotificationScrollView { public static final float BACKGROUND_ALPHA_DIMMED = 0.7f; private static final String TAG = "StackScroller"; @@ -218,6 +224,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ private final StackScrollAlgorithm mStackScrollAlgorithm; private final AmbientState mAmbientState; + private final ScrollViewFields mScrollViewFields = new ScrollViewFields(); private final GroupMembershipManager mGroupMembershipManager; private final GroupExpansionManager mGroupExpansionManager; @@ -230,7 +237,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private final ArrayList<View> mSwipedOutViews = new ArrayList<>(); private NotificationStackSizeCalculator mNotificationStackSizeCalculator; private final StackStateAnimator mStateAnimator; - private boolean mAnimationsEnabled; + // TODO(b/332732878): call setAnimationsEnabled with scene container enabled, then remove this + private boolean mAnimationsEnabled = SceneContainerFlag.isEnabled(); private boolean mChangePositionInProgress; private boolean mChildTransferInProgress; @@ -589,7 +597,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override public boolean isScrolledToTop() { if (SceneContainerFlag.isEnabled()) { - return mController.isPlaceholderScrolledToTop(); + return mScrollViewFields.isScrolledToTop(); } else { return mOwnScrollY == 0; } @@ -854,7 +862,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "mContentHeight = " + y); - drawDebugInfo(canvas, mRoundedRectClippingBottom, Color.DKGRAY, + y = mRoundedRectClippingBottom; + drawDebugInfo(canvas, y, Color.DKGRAY, /* label= */ "mRoundedRectClippingBottom) = " + y); } @@ -1130,6 +1139,48 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } + @Override + public View asView() { + return this; + } + + @Override + public void setScrolledToTop(boolean scrolledToTop) { + mScrollViewFields.setScrolledToTop(scrolledToTop); + } + + @Override + public void setStackTop(float stackTop) { + mScrollViewFields.setStackTop(stackTop); + // TODO(b/332574413): replace the following with using stackTop + updateTopPadding(stackTop, isAddOrRemoveAnimationPending()); + } + + @Override + public void setStackBottom(float stackBottom) { + mScrollViewFields.setStackBottom(stackBottom); + } + + @Override + public void setHeadsUpTop(float headsUpTop) { + mScrollViewFields.setHeadsUpTop(headsUpTop); + } + + @Override + public void setSyntheticScrollConsumer(@Nullable Consumer<Float> consumer) { + mScrollViewFields.setSyntheticScrollConsumer(consumer); + } + + @Override + public void setStackHeightConsumer(@Nullable Consumer<Float> consumer) { + mScrollViewFields.setStackHeightConsumer(consumer); + } + + @Override + public void setHeadsUpHeightConsumer(@Nullable Consumer<Float> consumer) { + mScrollViewFields.setHeadsUpHeightConsumer(consumer); + } + /** * @param listener to be notified after the location of Notification children might have * changed. @@ -1380,6 +1431,31 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mOnStackYChanged = onStackYChanged; } + @Override + public void setExpandFraction(float expandFraction) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + final float oldFraction = mAmbientState.getExpansionFraction(); + final boolean wasExpanding = oldFraction != 0f && oldFraction != 1f; + final boolean nowExpanding = expandFraction != 0f && expandFraction != 1f; + + // need to enter 'expanding' state before handling the new expand fraction, and then + if (nowExpanding && !wasExpanding) { + onExpansionStarted(); + mController.checkSnoozeLeavebehind(); + } + + // Update the expand progress between started/stopped events + mAmbientState.setExpansionFraction(expandFraction); + // TODO(b/332577544): don't convert to height which then converts to the fraction again + setExpandedHeight(expandFraction * getHeight()); + + // expansion stopped event requires that the expandFraction has already been updated + if (!nowExpanding && wasExpanding) { + setCheckForLeaveBehind(false); + onExpansionStopped(); + } + } + /** * Update the height of the panel. * @@ -1792,6 +1868,13 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private void updateImeInset(WindowInsets windowInsets) { mImeInset = windowInsets.getInsets(WindowInsets.Type.ime()).bottom; + if (mFooterView != null && mFooterView.getViewState() != null) { + // Do not animate footer Y when showing IME so that after IME hides, the footer + // appears at the correct Y. Once resetY is true, it remains true (even when IME + // hides, where mImeInset=0) until reset in FooterViewState#animateTo. + ((FooterView.FooterViewState) mFooterView.getViewState()).resetY |= mImeInset > 0; + } + if (mForcedScroll != null) { updateForcedScroll(); } @@ -2314,7 +2397,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /* notificationStackScrollLayout= */ this, mMaxDisplayedNotifications, shelfIntrinsicHeight); mIntrinsicContentHeight = height; - mController.setIntrinsicContentHeight(mIntrinsicContentHeight); + mScrollViewFields.sendStackHeight(height); // The topPadding can be bigger than the regular padding when qs is expanded, in that // state the maxPanelHeight and the contentHeight should be bigger @@ -2904,6 +2987,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } public void setAnimationsEnabled(boolean animationsEnabled) { + // TODO(b/332732878): remove the initial value of this field once the setter is called mAnimationsEnabled = animationsEnabled; updateNotificationAnimationStates(); if (!animationsEnabled) { @@ -3553,7 +3637,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable protected boolean isInsideQsHeader(MotionEvent ev) { if (SceneContainerFlag.isEnabled()) { - return ev.getY() < mController.getPlaceholderTop(); + return ev.getY() < mScrollViewFields.getScrimClippingShape().getBounds().getTop(); } mQsHeader.getBoundsOnScreen(mQsHeaderBound); @@ -4086,7 +4170,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // to it so that it can scroll the stack and scrim accordingly. if (SceneContainerFlag.isEnabled()) { float diff = endPosition - layoutEnd; - mController.sendSyntheticScrollToSceneFramework(diff); + mScrollViewFields.sendSyntheticScroll(diff); } setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd)); mDisallowScrollingInThisMotion = true; @@ -4969,6 +5053,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable elapsedRealtime - mLastUpdateSidePaddingElapsedRealtime); println(pw, "isSmallLandscapeLockscreenEnabled", mIsSmallLandscapeLockscreenEnabled); mNotificationStackSizeCalculator.dump(pw, args); + mScrollViewFields.dump(pw); }); pw.println(); pw.println("Contents:"); @@ -5509,8 +5594,40 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * Set rounded rect clipping bounds on this view. */ + @Override + public void setScrimClippingShape(@Nullable ShadeScrimShape shape) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + if (Objects.equals(mScrollViewFields.getScrimClippingShape(), shape)) return; + mScrollViewFields.setScrimClippingShape(shape); + mShouldUseRoundedRectClipping = shape != null; + mRoundedClipPath.reset(); + if (shape != null) { + ShadeScrimBounds bounds = shape.getBounds(); + mRoundedRectClippingLeft = (int) bounds.getLeft(); + mRoundedRectClippingTop = (int) bounds.getTop(); + mRoundedRectClippingRight = (int) bounds.getRight(); + mRoundedRectClippingBottom = (int) bounds.getBottom(); + mBgCornerRadii[0] = shape.getTopRadius(); + mBgCornerRadii[1] = shape.getTopRadius(); + mBgCornerRadii[2] = shape.getTopRadius(); + mBgCornerRadii[3] = shape.getTopRadius(); + mBgCornerRadii[4] = shape.getBottomRadius(); + mBgCornerRadii[5] = shape.getBottomRadius(); + mBgCornerRadii[6] = shape.getBottomRadius(); + mBgCornerRadii[7] = shape.getBottomRadius(); + mRoundedClipPath.addRoundRect( + bounds.getLeft(), bounds.getTop(), bounds.getRight(), bounds.getBottom(), + mBgCornerRadii, Path.Direction.CW); + } + invalidate(); + } + + /** + * Set rounded rect clipping bounds on this view. + */ public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius, int bottomRadius) { + SceneContainerFlag.assertInLegacyMode(); if (mRoundedRectClippingLeft == left && mRoundedRectClippingRight == right && mRoundedRectClippingBottom == bottom && mRoundedRectClippingTop == top && mBgCornerRadii[0] == topRadius && mBgCornerRadii[5] == bottomRadius) { @@ -5588,6 +5705,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable * Should we use rounded rect clipping */ private void updateUseRoundedRectClipping() { + if (SceneContainerFlag.isEnabled()) return; // We don't want to clip notifications when QS is expanded, because incoming heads up on // the bottom would be clipped otherwise boolean qsAllowsClipping = mQsExpansionFraction < 0.5f || mShouldUseSplitNotificationShade; 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 59901aca0425..06479e5a8e0e 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 @@ -83,6 +83,7 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.scene.ui.view.WindowRootView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeViewController; @@ -125,7 +126,6 @@ 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; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; @@ -189,7 +189,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; private final ShadeController mShadeController; private final Provider<WindowRootView> mWindowRootView; - private final NotificationStackAppearanceInteractor mStackAppearanceInteractor; private final KeyguardMediaController mKeyguardMediaController; private final SysuiStatusBarStateController mStatusBarStateController; private final KeyguardBypassController mKeyguardBypassController; @@ -742,7 +741,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { NotificationListViewBinder viewBinder, ShadeController shadeController, Provider<WindowRootView> windowRootView, - NotificationStackAppearanceInteractor stackAppearanceInteractor, InteractionJankMonitor jankMonitor, StackStateLogger stackLogger, NotificationStackScrollLogger logger, @@ -795,7 +793,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { mSeenNotificationsInteractor = seenNotificationsInteractor; mShadeController = shadeController; mWindowRootView = windowRootView; - mStackAppearanceInteractor = stackAppearanceInteractor; mFeatureFlags = featureFlags; mNotificationTargetsHelper = notificationTargetsHelper; mSecureSettings = secureSettings; @@ -1124,6 +1121,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public boolean isAddOrRemoveAnimationPending() { + SceneContainerFlag.assertInLegacyMode(); return mView != null && mView.isAddOrRemoveAnimationPending(); } @@ -1163,29 +1161,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { } } - /** Send internal notification expansion to the scene container framework. */ - public void sendSyntheticScrollToSceneFramework(Float delta) { - mStackAppearanceInteractor.setSyntheticScroll(delta); - } - - /** Get the y-coordinate of the top bound of the stack. */ - public float getPlaceholderTop() { - return mStackAppearanceInteractor.getShadeScrimBounds().getValue().getTop(); - } - - /** - * Returns whether the notification stack is scrolled to the top; i.e., it cannot be scrolled - * down any further. - */ - public boolean isPlaceholderScrolledToTop() { - return mStackAppearanceInteractor.getScrolledToTop().getValue(); - } - - /** Set the intrinsic height of the stack content without additional padding. */ - public void setIntrinsicContentHeight(float intrinsicContentHeight) { - mStackAppearanceInteractor.setStackHeight(intrinsicContentHeight); - } - public void setIntrinsicPadding(int intrinsicPadding) { mView.setIntrinsicPadding(intrinsicPadding); } @@ -1264,6 +1239,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void setScrollingEnabled(boolean enabled) { + SceneContainerFlag.assertInLegacyMode(); mView.setScrollingEnabled(enabled); } @@ -1284,6 +1260,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void updateTopPadding(float qsHeight, boolean animate) { + SceneContainerFlag.assertInLegacyMode(); mView.updateTopPadding(qsHeight, animate); } @@ -1386,11 +1363,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void onExpansionStarted() { + SceneContainerFlag.assertInLegacyMode(); mView.onExpansionStarted(); checkSnoozeLeavebehind(); } public void onExpansionStopped() { + SceneContainerFlag.assertInLegacyMode(); mView.setCheckForLeaveBehind(false); mView.onExpansionStopped(); } @@ -1519,6 +1498,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void setExpandedHeight(float expandedHeight) { + SceneContainerFlag.assertInLegacyMode(); mView.setExpandedHeight(expandedHeight); } @@ -1794,6 +1774,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { */ public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius, int bottomRadius) { + SceneContainerFlag.assertInLegacyMode(); mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius); } 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 1b53cbed8354..5bd4c758d678 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 @@ -17,14 +17,15 @@ package com.android.systemui.statusbar.notification.stack import android.content.res.Resources +import android.os.SystemProperties 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 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager +import com.android.systemui.res.R import com.android.systemui.statusbar.LockscreenShadeTransitionController import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -177,8 +178,8 @@ constructor( // TODO: Avoid making this split shade assumption by simply checking the stack for media val isMediaShowing = mediaDataManager.hasActiveMediaOrRecommendation() - val isMediaShowingInStack = isMediaShowing && !splitShadeStateController - .shouldUseSplitNotificationShade(resources) + val isMediaShowingInStack = + isMediaShowing && !splitShadeStateController.shouldUseSplitNotificationShade(resources) log { "\tGet maxNotifWithoutSavingSpace ---" } val maxNotifWithoutSavingSpace = @@ -378,8 +379,17 @@ constructor( } fun updateResources() { - maxKeyguardNotifications = if (notificationMinimalismPrototype()) 1 - else infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count)) + maxKeyguardNotifications = + infiniteIfNegative( + if (notificationMinimalismPrototype()) { + SystemProperties.getInt( + "persist.notification_minimalism_prototype.lock_screen_max_notifs", + 1 + ) + } else { + resources.getInteger(R.integer.keyguard_max_notification_count) + } + ) maxNotificationsExcludesMedia = notificationMinimalismPrototype() dividerHeight = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt new file mode 100644 index 000000000000..edac5ede1e91 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 + +import android.util.IndentingPrintWriter +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape +import com.android.systemui.util.printSection +import com.android.systemui.util.println +import java.util.function.Consumer + +/** + * This is a state holder object used by [NSSL][NotificationStackScrollLayout] to contain states + * provided by the `NotificationScrollViewBinder` to the `NotificationScrollView`. + * + * Unlike AmbientState, no class other than NSSL should ever have access to this class in any way. + * These fields are effectively NSSL's private fields. + */ +class ScrollViewFields { + /** Used to produce the clipping path */ + var scrimClippingShape: ShadeScrimShape? = null + /** Y coordinate in view pixels of the top of the notification stack */ + var stackTop: Float = 0f + /** + * Y coordinate in view pixels above which the bottom of the notification stack / shelf / footer + * must be. + */ + var stackBottom: Float = 0f + /** Y coordinate in view pixels of the top of the HUN */ + var headsUpTop: Float = 0f + /** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */ + var isScrolledToTop: Boolean = true + + /** + * When internal NSSL expansion requires the stack to be scrolled (e.g. to keep an expanding + * notification in view), that scroll amount can be sent here and it will be handled by the + * placeholder + */ + var syntheticScrollConsumer: Consumer<Float>? = null + /** + * Any time the stack height is recalculated, it should be updated here to be used by the + * placeholder + */ + var stackHeightConsumer: Consumer<Float>? = null + /** + * Any time the heads up height is recalculated, it should be updated here to be used by the + * placeholder + */ + var headsUpHeightConsumer: Consumer<Float>? = null + + /** send the [syntheticScroll] to the [syntheticScrollConsumer], if present. */ + fun sendSyntheticScroll(syntheticScroll: Float) = + syntheticScrollConsumer?.accept(syntheticScroll) + /** send the [stackHeight] to the [stackHeightConsumer], if present. */ + fun sendStackHeight(stackHeight: Float) = stackHeightConsumer?.accept(stackHeight) + /** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */ + fun sendHeadsUpHeight(headsUpHeight: Float) = headsUpHeightConsumer?.accept(headsUpHeight) + + fun dump(pw: IndentingPrintWriter) { + pw.printSection("StackViewStates") { + pw.println("scrimClippingShape", scrimClippingShape) + pw.println("stackTop", stackTop) + pw.println("stackBottom", stackBottom) + pw.println("headsUpTop", headsUpTop) + pw.println("isScrolledToTop", isScrolledToTop) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt index 462547ea2385..b04737936166 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt @@ -27,8 +27,12 @@ import kotlinx.coroutines.flow.MutableStateFlow */ @SysUISingleton class NotificationPlaceholderRepository @Inject constructor() { - /** The bounds of the notification shade scrim / container in the current scene. */ - val shadeScrimBounds = MutableStateFlow(ShadeScrimBounds()) + /** + * The bounds of the notification shade scrim / container in the current scene. + * + * When `null`, clipping should not be applied to notifications. + */ + val shadeScrimBounds = MutableStateFlow<ShadeScrimBounds?>(null) /** * The y-coordinate in px of top of the contents of the notification stack. This value can be @@ -44,7 +48,7 @@ class NotificationPlaceholderRepository @Inject constructor() { val headsUpTop = MutableStateFlow(0f) /** height made available to the notifications in the size-constrained mode of lock screen. */ - val constrainedAvailableSpace = MutableStateFlow(0f) + val constrainedAvailableSpace = MutableStateFlow(0) /** * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt index 938e43e4a2d4..8a9da69079d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt @@ -35,7 +35,7 @@ class NotificationViewHeightRepository @Inject constructor() { val stackHeight = MutableStateFlow(0f) /** The height in px of the current heads up notification. */ - val activeHeadsUpRowHeight = MutableStateFlow(0f) + val headsUpHeight = MutableStateFlow(0f) /** * The amount in px that the notification stack should scroll due to internal expansion. This 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 32562f109252..a5b4f5f2bb4f 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 @@ -42,7 +42,7 @@ constructor( shadeInteractor: ShadeInteractor, ) { /** The bounds of the notification stack in the current scene. */ - val shadeScrimBounds: StateFlow<ShadeScrimBounds> = + val shadeScrimBounds: StateFlow<ShadeScrimBounds?> = placeholderRepository.shadeScrimBounds.asStateFlow() /** @@ -59,8 +59,8 @@ constructor( isExpandingFromHeadsUp, ) { shadeMode, isExpandingFromHeadsUp -> ShadeScrimRounding( - roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), - roundBottom = shadeMode != ShadeMode.Single, + isTopRounded = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), + isBottomRounded = shadeMode != ShadeMode.Single, ) } .distinctUntilChanged() @@ -72,15 +72,28 @@ constructor( */ val stackHeight: StateFlow<Float> = viewHeightRepository.stackHeight.asStateFlow() + /** The height in px of the contents of the HUN. */ + val headsUpHeight: StateFlow<Float> = viewHeightRepository.headsUpHeight.asStateFlow() + /** The y-coordinate in px of top of the contents of the notification stack. */ val stackTop: StateFlow<Float> = placeholderRepository.stackTop.asStateFlow() + /** The y-coordinate in px of bottom of the contents of the notification stack. */ + val stackBottom: StateFlow<Float> = placeholderRepository.stackBottom.asStateFlow() + + /** The height of the keyguard's available space bounds */ + val constrainedAvailableSpace: StateFlow<Int> = + placeholderRepository.constrainedAvailableSpace.asStateFlow() + /** * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any * further. */ val scrolledToTop: StateFlow<Boolean> = placeholderRepository.scrolledToTop.asStateFlow() + /** The y-coordinate in px of bottom of the contents of the HUN. */ + val headsUpTop: StateFlow<Float> = placeholderRepository.headsUpTop.asStateFlow() + /** * The amount in px that the notification stack should scroll due to internal expansion. This * should only happen when a notification expansion hits the bottom of the screen, so it is @@ -89,8 +102,8 @@ constructor( val syntheticScroll: Flow<Float> = viewHeightRepository.syntheticScroll.asStateFlow() /** Sets the position of the notification stack in the current scene. */ - fun setShadeScrimBounds(bounds: ShadeScrimBounds) { - check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" } + fun setShadeScrimBounds(bounds: ShadeScrimBounds?) { + check(bounds == null || bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" } placeholderRepository.shadeScrimBounds.value = bounds } @@ -99,9 +112,19 @@ constructor( viewHeightRepository.stackHeight.value = height } + /** Sets the height of heads up notification. */ + fun setHeadsUpHeight(height: Float) { + viewHeightRepository.headsUpHeight.value = height + } + /** Sets the y-coord in px of the top of the contents of the notification stack. */ - fun setStackTop(startY: Float) { - placeholderRepository.stackTop.value = startY + fun setStackTop(stackTop: Float) { + placeholderRepository.stackTop.value = stackTop + } + + /** Sets the y-coord in px of the bottom of the contents of the notification stack. */ + fun setStackBottom(stackBottom: Float) { + placeholderRepository.stackBottom.value = stackBottom } /** Sets whether the notification stack is scrolled to the top. */ @@ -113,4 +136,12 @@ constructor( fun setSyntheticScroll(delta: Float) { viewHeightRepository.syntheticScroll.value = delta } + + fun setConstrainedAvailableSpace(height: Int) { + placeholderRepository.constrainedAvailableSpace.value = height + } + + fun setHeadsUpTop(headsUpTop: Float) { + placeholderRepository.headsUpTop.value = headsUpTop + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt index 448127a55b89..832e69080f3c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt @@ -29,4 +29,16 @@ data class ShadeScrimBounds( ) { /** The current height of the notification container. */ val height: Float = bottom - top + + fun minus(leftOffset: Int = 0, topOffset: Int = 0) = + if (leftOffset == 0 && topOffset == 0) { + this + } else { + ShadeScrimBounds( + left = this.left - leftOffset, + top = this.top - topOffset, + right = this.right - leftOffset, + bottom = this.bottom - topOffset, + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt index 621dd0c49f54..e86fd1c7cdc7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.stack.shared.model -/** Models the clipping rounded rectangle of the notification stack */ +/** Models the clipping rounded rectangle of the notification stack, where the rounding is binary */ data class ShadeScrimClipping( val bounds: ShadeScrimBounds = ShadeScrimBounds(), val rounding: ShadeScrimRounding = ShadeScrimRounding() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt index 2fe265f35604..9d4231c8967b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.stack.shared.model /** Models the corner rounds of the notification stack. */ data class ShadeScrimRounding( /** Whether the top corners of the notification stack should be rounded. */ - val roundTop: Boolean = false, + val isTopRounded: Boolean = false, /** Whether the bottom corners of the notification stack should be rounded. */ - val roundBottom: Boolean = false, + val isBottomRounded: Boolean = false, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.kt new file mode 100644 index 000000000000..e6f0647a059d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.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.statusbar.notification.stack.shared.model + +/** Models the clipping rounded rectangle of the notification stack, with corner radii in px */ +data class ShadeScrimShape( + val bounds: ShadeScrimBounds = ShadeScrimBounds(), + /** radius in px of the top corners */ + val topRadius: Int, + /** radius in px of the bottom corners */ + val bottomRadius: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt new file mode 100644 index 000000000000..ac00d3b27493 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.ui.view + +import android.view.View +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape +import java.util.function.Consumer + +/** + * This view (interface) is the view which scrolls and positions the heads up notification and + * notification stack, but is otherwise agnostic to the content. + */ +interface NotificationScrollView { + /** + * Since this is an interface rather than a literal View, this provides cast-like access to the + * underlying view. + */ + fun asView(): View + + /** Set the clipping bounds used when drawing */ + fun setScrimClippingShape(shape: ShadeScrimShape?) + + /** set the y position in px of the top of the stack in this view's coordinates */ + fun setStackTop(stackTop: Float) + + /** set the y position in px of the bottom of the stack in this view's coordinates */ + fun setStackBottom(stackBottom: Float) + + /** set the y position in px of the top of the HUN in this view's coordinates */ + fun setHeadsUpTop(headsUpTop: Float) + + /** set whether the view has been scrolled all the way to the top */ + fun setScrolledToTop(scrolledToTop: Boolean) + + /** Set a consumer for synthetic scroll events */ + fun setSyntheticScrollConsumer(consumer: Consumer<Float>?) + /** Set a consumer for stack height changed events */ + fun setStackHeightConsumer(consumer: Consumer<Float>?) + /** Set a consumer for heads up height changed events */ + fun setHeadsUpHeightConsumer(consumer: Consumer<Float>?) + + /** sets that scrolling is allowed */ + fun setScrollingEnabled(enabled: Boolean) + + /** sets the current expand fraction */ + fun setExpandFraction(expandFraction: Float) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt new file mode 100644 index 000000000000..047b560afbc7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -0,0 +1,103 @@ +/* + * 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.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.common.ui.view.onLayoutChanged +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationScrollViewModel +import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.util.kotlin.launchAndDispose +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +/** Binds the [NotificationScrollView]. */ +@SysUISingleton +class NotificationScrollViewBinder +@Inject +constructor( + dumpManager: DumpManager, + @Main private val mainImmediateDispatcher: CoroutineDispatcher, + private val view: NotificationScrollView, + private val viewModel: NotificationScrollViewModel, + private val configuration: ConfigurationState, +) : FlowDumperImpl(dumpManager) { + + private val viewLeftOffset = MutableStateFlow(0).dumpValue("viewLeftOffset") + + private fun updateViewPosition() { + val trueView = view.asView() + if (trueView.top != 0) { + Log.w("NSSL", "Expected $trueView to have top==0") + } + viewLeftOffset.value = trueView.left + } + + fun bindWhileAttached(): DisposableHandle { + return view.asView().repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { bind() } + } + } + + suspend fun bind() = coroutineScope { + launchAndDispose { + updateViewPosition() + view.asView().onLayoutChanged { updateViewPosition() } + } + + launch { + viewModel + .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset) + .collect { view.setScrimClippingShape(it) } + } + + launch { viewModel.stackTop.collect { view.setStackTop(it) } } + launch { viewModel.stackBottom.collect { view.setStackBottom(it) } } + launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } } + launch { viewModel.headsUpTop.collect { view.setHeadsUpTop(it) } } + launch { viewModel.expandFraction.collect { view.setExpandFraction(it) } } + launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } } + + launchAndDispose { + view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) + view.setStackHeightConsumer(viewModel.stackHeightConsumer) + view.setHeadsUpHeightConsumer(viewModel.headsUpHeightConsumer) + DisposableHandle { + view.setSyntheticScrollConsumer(null) + view.setStackHeightConsumer(null) + view.setHeadsUpHeightConsumer(null) + } + } + } + + /** flow of the scrim clipping radius */ + private val scrimRadius: 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/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt deleted file mode 100644 index d6d31db86e61..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt +++ /dev/null @@ -1,103 +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 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.shadeScrimClipping, 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.stackTop.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 4a096a89848a..741102bcd574 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 @@ -21,11 +21,14 @@ import android.view.WindowInsets import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags.communalHub +import com.android.systemui.common.ui.view.onApplyWindowInsets +import com.android.systemui.common.ui.view.onLayoutChanged 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 import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -49,7 +52,7 @@ constructor( private val sceneContainerFlags: SceneContainerFlags, private val controller: NotificationStackScrollLayoutController, private val notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val notificationStackViewBinder: NotificationStackViewBinder, + private val notificationScrollViewBinder: NotificationScrollViewBinder, @Main private val mainImmediateDispatcher: CoroutineDispatcher, ) { @@ -137,10 +140,12 @@ constructor( } } - launch { - burnInParams - .flatMapLatest { params -> viewModel.translationY(params) } - .collect { y -> controller.setTranslationY(y) } + if (!SceneContainerFlag.isEnabled) { + launch { + burnInParams + .flatMapLatest { params -> viewModel.translationY(params) } + .collect { y -> controller.setTranslationY(y) } + } } launch { viewModel.translationX.collect { x -> controller.translationX = x } } @@ -162,28 +167,22 @@ constructor( } if (sceneContainerFlags.isEnabled()) { - disposables += notificationStackViewBinder.bindWhileAttached() + disposables += notificationScrollViewBinder.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() - burnInParams.update { current -> - current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top) + disposables += + view.onApplyWindowInsets { _: View, insets: WindowInsets -> + val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() + burnInParams.update { current -> + current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top) + } + insets } - insets - } - disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) } - // 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) } + disposables += view.onLayoutChanged { viewModel.notificationStackChanged() } return disposables } 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/NotificationScrollViewModel.kt index 071127c03202..516ec319ceb9 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/NotificationScrollViewModel.kt @@ -26,17 +26,17 @@ 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.ShadeScrimClipping +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */ @SysUISingleton -class NotificationStackAppearanceViewModel +class NotificationScrollViewModel @Inject constructor( dumpManager: DumpManager, @@ -80,16 +80,48 @@ constructor( .dumpWhileCollecting("expandFraction") /** The bounds of the notification stack in the current scene. */ - val shadeScrimClipping: Flow<ShadeScrimClipping> = + private val shadeScrimClipping: Flow<ShadeScrimClipping?> = combine( stackAppearanceInteractor.shadeScrimBounds, stackAppearanceInteractor.shadeScrimRounding, - ::ShadeScrimClipping - ) + ) { bounds, rounding -> + bounds?.let { ShadeScrimClipping(it, rounding) } + } .dumpWhileCollecting("stackClipping") + fun shadeScrimShape( + cornerRadius: Flow<Int>, + viewLeftOffset: Flow<Int> + ): Flow<ShadeScrimShape?> = + combine(shadeScrimClipping, cornerRadius, viewLeftOffset) { clipping, radius, leftOffset -> + if (clipping == null) return@combine null + ShadeScrimShape( + bounds = clipping.bounds.minus(leftOffset = leftOffset), + topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0, + bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0 + ) + } + .dumpWhileCollecting("shadeScrimShape") + /** The y-coordinate in px of top of the contents of the notification stack. */ - val stackTop: StateFlow<Float> = stackAppearanceInteractor.stackTop.dumpValue("stackTop") + val stackTop: Flow<Float> = stackAppearanceInteractor.stackTop.dumpValue("stackTop") + /** The y-coordinate in px of bottom of the contents of the notification stack. */ + val stackBottom: Flow<Float> = stackAppearanceInteractor.stackBottom.dumpValue("stackBottom") + /** + * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any + * further. + */ + val scrolledToTop: Flow<Boolean> = + stackAppearanceInteractor.scrolledToTop.dumpValue("scrolledToTop") + /** The y-coordinate in px of bottom of the contents of the HUN. */ + val headsUpTop: Flow<Float> = stackAppearanceInteractor.headsUpTop.dumpValue("headsUpTop") + + /** Receives the amount (px) that the stack should scroll due to internal expansion. */ + val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll + /** Receives the height of the contents of the notification stack. */ + val stackHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setStackHeight + /** Receives the height of the heads up notification. */ + val headsUpHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setHeadsUpHeight /** Whether the notification stack is scrollable or not. */ val isScrollable: Flow<Boolean> = 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 477f139bcd7d..b284179d42b7 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 @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.common.shared.model.NotificationContainerBounds 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.domain.interactor.KeyguardInteractor @@ -26,8 +27,10 @@ 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.ShadeScrimBounds import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding +import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** * ViewModel used by the Notification placeholders inside the scene container to update the @@ -37,67 +40,72 @@ import kotlinx.coroutines.flow.Flow class NotificationsPlaceholderViewModel @Inject constructor( + dumpManager: DumpManager, private val interactor: NotificationStackAppearanceInteractor, shadeInteractor: ShadeInteractor, flags: SceneContainerFlags, featureFlags: FeatureFlagsClassic, private val keyguardInteractor: KeyguardInteractor, -) { +) : FlowDumperImpl(dumpManager) { /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */ val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES) /** DEBUG: whether the debug logging should be output. */ val isDebugLoggingEnabled: Boolean = flags.isEnabled() - /** - * Notifies that the bounds of the notification placeholder have changed. - * - * @param top The position of the top of the container in its window coordinate system, in - * pixels. - * @param bottom The position of the bottom of the container in its window coordinate system, in - * pixels. - */ - fun onBoundsChanged( - left: Float, + /** Notifies that the bounds of the notification scrim have changed. */ + fun onScrimBoundsChanged(bounds: ShadeScrimBounds?) { + interactor.setShadeScrimBounds(bounds) + } + + /** Notifies that the bounds of the notification placeholder have changed. */ + fun onStackBoundsChanged( top: Float, - right: Float, bottom: Float, ) { keyguardInteractor.setNotificationContainerBounds( NotificationContainerBounds(top = top, bottom = bottom) ) - interactor.setShadeScrimBounds( - ShadeScrimBounds(top = top, bottom = bottom, left = left, right = right) - ) + interactor.setStackTop(top) + interactor.setStackBottom(bottom) + } + + /** Sets the available space */ + fun onConstrainedAvailableSpaceChanged(height: Int) { + interactor.setConstrainedAvailableSpace(height) + } + + fun onHeadsUpTopChanged(headsUpTop: Float) { + interactor.setHeadsUpTop(headsUpTop) } /** Corner rounding of the stack */ - val shadeScrimRounding: Flow<ShadeScrimRounding> = interactor.shadeScrimRounding + val shadeScrimRounding: Flow<ShadeScrimRounding> = + interactor.shadeScrimRounding.dumpWhileCollecting("shadeScrimRounding") /** * 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 * point the notification stack should become scrollable. */ - val stackHeight = interactor.stackHeight + val stackHeight: StateFlow<Float> = interactor.stackHeight.dumpValue("stackHeight") + + /** The height in px of the contents of the HUN. */ + val headsUpHeight: StateFlow<Float> = interactor.headsUpHeight.dumpValue("headsUpHeight") /** * The amount [0-1] that the shade has been opened. At 0, the shade is closed; at 1, the shade * is open. */ - val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion + val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion.dumpValue("expandFraction") /** * The amount in px that the notification stack should scroll due to internal expansion. This * should only happen when a notification expansion hits the bottom of the screen, so it is * necessary to scroll up to keep expanding the notification. */ - val syntheticScroll: Flow<Float> = interactor.syntheticScroll - - /** Sets the y-coord in px of the top of the contents of the notification stack. */ - fun onContentTopChanged(padding: Float) { - interactor.setStackTop(padding) - } + val syntheticScroll: Flow<Float> = + interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll") /** Sets whether the notification stack is scrolled to the top. */ fun setScrolledToTop(scrolledToTop: Boolean) { 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 9f576066cc98..0486ef5bca51 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 @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import androidx.annotation.VisibleForTesting import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -60,7 +61,9 @@ import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransition import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor +import com.android.systemui.scene.shared.flag.SceneContainerFlag 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.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.util.kotlin.BooleanFlowOperators.and import com.android.systemui.util.kotlin.BooleanFlowOperators.or @@ -97,6 +100,7 @@ constructor( private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val shadeInteractor: ShadeInteractor, + private val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor, private val alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel, private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, @@ -163,23 +167,35 @@ constructor( ) .dumpWhileCollecting("isShadeLocked") + @VisibleForTesting + val paddingTopDimen: Flow<Int> = + interactor.configurationBasedDimensions + .map { + when { + !it.useSplitShade -> 0 + it.useLargeScreenHeader -> it.marginTopLargeScreen + else -> it.marginTop + } + } + .distinctUntilChanged() + .dumpWhileCollecting("paddingTopDimen") + val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = interactor.configurationBasedDimensions .map { val marginTop = - if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop + when { + // y position of the NSSL in the window needs to be 0 under scene container + SceneContainerFlag.isEnabled -> 0 + it.useLargeScreenHeader -> it.marginTopLargeScreen + else -> it.marginTop + } ConfigurationBasedDimensions( marginStart = if (it.useSplitShade) 0 else it.marginHorizontal, marginEnd = it.marginHorizontal, marginBottom = it.marginBottom, marginTop = marginTop, useSplitShade = it.useSplitShade, - paddingTop = - if (it.useSplitShade) { - marginTop - } else { - 0 - }, ) } .distinctUntilChanged() @@ -338,16 +354,16 @@ constructor( combine( isOnLockscreenWithoutShade, keyguardInteractor.notificationContainerBounds, - configurationBasedDimensions, + paddingTopDimen, interactor.topPosition .sampleCombine( keyguardTransitionInteractor.isInTransitionToAnyState, shadeInteractor.qsExpansion, ) .onStart { emit(Triple(0f, false, 0f)) } - ) { onLockscreen, bounds, config, (top, isInTransitionToAnyState, qsExpansion) -> + ) { onLockscreen, bounds, paddingTop, (top, isInTransitionToAnyState, qsExpansion) -> if (onLockscreen) { - bounds.copy(top = bounds.top - config.paddingTop) + bounds.copy(top = bounds.top - paddingTop) } else { // When QS expansion > 0, it should directly set the top padding so do not // animate it @@ -539,6 +555,8 @@ constructor( * translated as the keyguard fades out. */ fun translationY(params: BurnInParameters): Flow<Float> { + // with SceneContainer, x translation is handled by views, y is handled by compose + SceneContainerFlag.assertInLegacyMode() return combine( aodBurnInViewModel .movement(params) @@ -571,8 +589,11 @@ constructor( .dumpWhileCollecting("translationX") private val availableHeight: Flow<Float> = - bounds - .map { it.bottom - it.top } + if (SceneContainerFlag.isEnabled) { + notificationStackAppearanceInteractor.constrainedAvailableSpace.map { it.toFloat() } + } else { + bounds.map { it.bottom - it.top } + } .distinctUntilChanged() .dumpWhileCollecting("availableHeight") @@ -634,6 +655,5 @@ constructor( val marginEnd: Int, val marginBottom: Int, val useSplitShade: Boolean, - val paddingTop: Int, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index a1fae03a2090..2651d2eed404 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -103,8 +103,8 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { /** * Returns an ActivityOptions bundle created using the given parameters. * - * @param displayId The ID of the display to launch the activity in. Typically this would - * be the display the status bar is on. + * @param displayId The ID of the display to launch the activity in. Typically this would + * be the display the status bar is on. * @param animationAdapter The animation adapter used to start this activity, or {@code null} * for the default animation. */ @@ -197,7 +197,7 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { void onKeyguardViewManagerStatesUpdated(); - /** */ + /** */ boolean getCommandQueuePanelsEnabled(); void showWirelessChargingAnimation(int batteryLevel); 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 2798dbfc62e4..1d6b744a7bf0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -28,7 +28,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.newAodTransition; -import static com.android.systemui.Flags.predictiveBackSysui; import static com.android.systemui.Flags.truncatedStatusBarIconsFix; import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF; @@ -169,6 +168,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlags; import com.android.systemui.scrim.ScrimView; import com.android.systemui.settings.UserTracker; import com.android.systemui.settings.brightness.BrightnessSliderController; +import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor; import com.android.systemui.shade.CameraLauncher; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; @@ -595,6 +595,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final SceneContainerFlags mSceneContainerFlags; + private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor; + /** * Public constructor for CentralSurfaces. * @@ -706,7 +708,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { UserTracker userTracker, Provider<FingerprintManager> fingerprintManager, ActivityStarter activityStarter, - SceneContainerFlags sceneContainerFlags + SceneContainerFlags sceneContainerFlags, + BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor ) { mContext = context; mNotificationsController = notificationsController; @@ -802,6 +805,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mFingerprintManager = fingerprintManager; mActivityStarter = activityStarter; mSceneContainerFlags = sceneContainerFlags; + mBrightnessMirrorShowingInteractor = brightnessMirrorShowingInteractor; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; mStartingSurfaceOptional = startingSurfaceOptional; @@ -837,7 +841,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy; mLightRevealScrim = lightRevealScrim; - if (predictiveBackSysui()) { + if (PredictiveBackSysUiFlag.isEnabled()) { mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true); } } @@ -1084,6 +1088,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mJavaAdapter.alwaysCollectFlow( mCommunalInteractor.isIdleOnCommunal(), mIdleOnCommunalConsumer); + if (mSceneContainerFlags.isEnabled()) { + mJavaAdapter.alwaysCollectFlow( + mBrightnessMirrorShowingInteractor.isShowing(), + this::setBrightnessMirrorShowing + ); + } } /** @@ -1285,10 +1295,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mShadeSurface, mNotificationShadeDepthControllerLazy.get(), mBrightnessSliderFactory, - (visible) -> { - mBrightnessMirrorVisible = visible; - updateScrimController(); - }); + this::setBrightnessMirrorShowing); fragmentHostManager.addTagListener(QS.TAG, (tag, f) -> { QS qs = (QS) f; if (qs instanceof QSFragmentLegacy) { @@ -1347,6 +1354,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { ThreadedRenderer.overrideProperty("ambientRatio", String.valueOf(1.5f)); } + private void setBrightnessMirrorShowing(boolean showing) { + mBrightnessMirrorVisible = showing; + updateScrimController(); + } /** * When swiping up to dismiss the lock screen, the panel expansion fraction goes from 1f to 0f. @@ -3061,7 +3072,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { public void onConfigChanged(Configuration newConfig) { updateResources(); updateDisplaySize(); // populates mDisplayMetrics - if (predictiveBackSysui()) { + if (PredictiveBackSysUiFlag.isEnabled()) { mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java index 495b4e1e14cd..4c3c7d56df50 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; @@ -421,9 +422,12 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar public void updateHeader(NotificationEntry entry) { ExpandableNotificationRow row = entry.getRow(); float headerVisibleAmount = 1.0f; - if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild - || row.showingPulsing()) { - headerVisibleAmount = mAppearFraction; + // To fix the invisible HUN group header issue + if (!AsyncGroupHeaderViewInflation.isEnabled()) { + if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild + || row.showingPulsing()) { + headerVisibleAmount = mAppearFraction; + } } row.setHeaderVisibleAmount(headerVisibleAmount); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt new file mode 100644 index 000000000000..74d6ba57a8ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.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.phone + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the predictive back flag state. */ +@Suppress("NOTHING_TO_INLINE") +object PredictiveBackSysUiFlag { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_PREDICTIVE_BACK_SYSUI + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.predictiveBackSysui() + + /** + * 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/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 20a82a403eb7..d99af2ddb95d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -165,6 +165,8 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { public void showNotification(@NonNull NotificationEntry entry) { HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry); + mLogger.logShowNotificationRequest(entry); + Runnable runnable = () -> { // TODO(b/315362456) log outside runnable too mLogger.logShowNotification(entry); @@ -219,6 +221,8 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) { HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); + mLogger.logUpdateNotificationRequest(key, shouldHeadsUpAgain, headsUpEntry != null); + Runnable runnable = () -> { updateNotificationInternal(key, shouldHeadsUpAgain); }; @@ -378,8 +382,11 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ protected final void removeEntry(@NonNull String key) { HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); + mLogger.logRemoveEntryRequest(key); Runnable runnable = () -> { + mLogger.logRemoveEntry(key); + if (headsUpEntry == null) { return; } @@ -566,8 +573,10 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { public void unpinAll(boolean userUnPinned) { for (String key : mHeadsUpEntryMap.keySet()) { HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); - + mLogger.logUnpinEntryRequest(key); Runnable runnable = () -> { + mLogger.logUnpinEntry(key); + setEntryPinned(headsUpEntry, false /* isPinned */); // maybe it got un sticky headsUpEntry.updateEntry(false /* updatePostTime */, "unpinAll"); @@ -886,6 +895,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * Clear any pending removal runnables. */ public void cancelAutoRemovalCallbacks(@Nullable String reason) { + mLogger.logAutoRemoveCancelRequest(this.mEntry, reason); Runnable runnable = () -> { final boolean removed = cancelAutoRemovalCallbackInternal(); @@ -900,6 +910,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { public void scheduleAutoRemovalCallback(FinishTimeUpdater finishTimeCalculator, @NonNull String reason) { + mLogger.logAutoRemoveRequest(this.mEntry, reason); Runnable runnable = () -> { long delayMs = finishTimeCalculator.updateAndGetTimeRemaining(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java index 13f76feca9fd..7d920eab73fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BrightnessMirrorController.java @@ -27,6 +27,7 @@ import android.widget.FrameLayout; import com.android.systemui.res.R; import com.android.systemui.settings.brightness.BrightnessSliderController; +import com.android.systemui.settings.brightness.MirrorController; import com.android.systemui.settings.brightness.ToggleSlider; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeViewController; @@ -38,8 +39,7 @@ import java.util.function.Consumer; /** * Controls showing and hiding of the brightness mirror. */ -public class BrightnessMirrorController - implements CallbackController<BrightnessMirrorController.BrightnessMirrorListener> { +public class BrightnessMirrorController implements MirrorController { private final NotificationShadeWindowView mStatusBarWindow; private final Consumer<Boolean> mVisibilityCallback; @@ -71,6 +71,7 @@ public class BrightnessMirrorController updateResources(); } + @Override public void showMirror() { mBrightnessMirror.setVisibility(View.VISIBLE); mVisibilityCallback.accept(true); @@ -78,16 +79,14 @@ public class BrightnessMirrorController mDepthController.setBrightnessMirrorVisible(true); } + @Override public void hideMirror() { mVisibilityCallback.accept(false); mNotificationPanel.setAlpha(255, true /* animate */); mDepthController.setBrightnessMirrorVisible(false); } - /** - * Set the location and size of the mirror container to match that of the slider in QS - * @param original the original view in QS - */ + @Override public void setLocationAndSize(View original) { original.getLocationInWindow(mInt2Cache); @@ -112,6 +111,7 @@ public class BrightnessMirrorController } } + @Override public ToggleSlider getToggleSlider() { return mToggleSliderController; } @@ -176,8 +176,4 @@ public class BrightnessMirrorController public void onUiModeChanged() { reinflate(); } - - public interface BrightnessMirrorListener { - void onBrightnessMirrorReinflated(View brightnessMirror); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt index f6154afec273..a30660645990 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt @@ -58,6 +58,14 @@ class HeadsUpManagerLogger @Inject constructor( }) } + fun logShowNotificationRequest(entry: NotificationEntry) { + buffer.log(TAG, INFO, { + str1 = entry.logKey + }, { + "request: show notification $str1" + }) + } + fun logShowNotification(entry: NotificationEntry) { buffer.log(TAG, INFO, { str1 = entry.logKey @@ -76,6 +84,15 @@ class HeadsUpManagerLogger @Inject constructor( }) } + fun logAutoRemoveRequest(entry: NotificationEntry, reason: String) { + buffer.log(TAG, INFO, { + str1 = entry.logKey + str2 = reason + }, { + "request: reschedule auto remove of $str1 reason: $str2" + }) + } + fun logAutoRemoveRescheduled(entry: NotificationEntry, delayMillis: Long, reason: String) { buffer.log(TAG, INFO, { str1 = entry.logKey @@ -86,6 +103,15 @@ class HeadsUpManagerLogger @Inject constructor( }) } + fun logAutoRemoveCancelRequest(entry: NotificationEntry, reason: String?) { + buffer.log(TAG, INFO, { + str1 = entry.logKey + str2 = reason ?: "unknown" + }, { + "request: cancel auto remove of $str1 reason: $str2" + }) + } + fun logAutoRemoveCanceled(entry: NotificationEntry, reason: String?) { buffer.log(TAG, INFO, { str1 = entry.logKey @@ -95,6 +121,38 @@ class HeadsUpManagerLogger @Inject constructor( }) } + fun logRemoveEntryRequest(key: String) { + buffer.log(TAG, INFO, { + str1 = logKey(key) + }, { + "request: remove entry $str1" + }) + } + + fun logRemoveEntry(key: String) { + buffer.log(TAG, INFO, { + str1 = logKey(key) + }, { + "remove entry $str1" + }) + } + + fun logUnpinEntryRequest(key: String) { + buffer.log(TAG, INFO, { + str1 = logKey(key) + }, { + "request: unpin entry $str1" + }) + } + + fun logUnpinEntry(key: String) { + buffer.log(TAG, INFO, { + str1 = logKey(key) + }, { + "unpin entry $str1" + }) + } + fun logRemoveNotification(key: String, releaseImmediately: Boolean) { buffer.log(TAG, INFO, { str1 = logKey(key) @@ -112,6 +170,16 @@ class HeadsUpManagerLogger @Inject constructor( }) } + fun logUpdateNotificationRequest(key: String, alert: Boolean, hasEntry: Boolean) { + buffer.log(TAG, INFO, { + str1 = logKey(key) + bool1 = alert + bool2 = hasEntry + }, { + "request: update notification $str1 alert: $bool1 hasEntry: $bool2 reason: $str2" + }) + } + fun logUpdateNotification(key: String, alert: Boolean, hasEntry: Boolean) { buffer.log(TAG, INFO, { str1 = logKey(key) 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 860068c137a3..0d53277e51dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -20,6 +20,7 @@ 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; +import static com.android.systemui.Flags.screenshareNotificationHidingBugFix; import android.annotation.MainThread; import android.app.IActivityManager; @@ -31,6 +32,7 @@ import android.media.projection.MediaProjectionManager; import android.os.Handler; import android.os.RemoteException; import android.os.Trace; +import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; @@ -316,6 +318,10 @@ public class SensitiveNotificationProtectionControllerImpl return false; } + if (screenshareNotificationHidingBugFix() && UserHandle.isCore(sbn.getUid())) { + return false; // do not hide/redact notifications from system uid + } + // Only protect/redact notifications if the developer has not explicitly set notification // visibility as public and users has not adjusted default channel visibility to private boolean notificationRequestsRedaction = entry.isNotificationVisibilityPrivate(); diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt index e977014e00f2..aea739dcb6cc 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt @@ -20,11 +20,10 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.annotation.BinderThread -import android.content.Context -import android.os.Handler import android.os.SystemProperties import android.util.Log import android.view.animation.DecelerateInterpolator +import com.android.app.tracing.TraceUtils.traceAsync import com.android.internal.foldables.FoldLockSettingAvailabilityProvider import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DeviceStateRepository @@ -36,12 +35,13 @@ import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Comp import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Companion.isVerticalRotation import com.android.systemui.unfold.dagger.UnfoldBg import com.android.systemui.util.animation.data.repository.AnimationStatusRepository +import com.android.systemui.util.kotlin.race import javax.inject.Inject import kotlin.coroutines.resume import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged @@ -59,13 +59,13 @@ import kotlinx.coroutines.withTimeout class FoldLightRevealOverlayAnimation @Inject constructor( - private val context: Context, - @UnfoldBg private val bgHandler: Handler, + @UnfoldBg private val bgDispatcher: CoroutineDispatcher, private val deviceStateRepository: DeviceStateRepository, private val powerInteractor: PowerInteractor, @Background private val applicationScope: CoroutineScope, private val animationStatusRepository: AnimationStatusRepository, - private val controllerFactory: FullscreenLightRevealAnimationController.Factory + private val controllerFactory: FullscreenLightRevealAnimationController.Factory, + private val foldLockSettingAvailabilityProvider: FoldLockSettingAvailabilityProvider ) : FullscreenLightRevealAnimation { private val revealProgressValueAnimator: ValueAnimator = @@ -79,7 +79,7 @@ constructor( override fun init() { // This method will be called only on devices where this animation is enabled, // so normally this thread won't be created - if (!FoldLockSettingAvailabilityProvider(context.resources).isFoldLockBehaviorAvailable) { + if (!foldLockSettingAvailabilityProvider.isFoldLockBehaviorAvailable) { return } @@ -91,7 +91,6 @@ constructor( ) controller.init() - val bgDispatcher = bgHandler.asCoroutineDispatcher("@UnfoldBg Handler") applicationScope.launch(bgDispatcher) { powerInteractor.screenPowerState.collect { if (it == ScreenPowerState.SCREEN_ON) { @@ -109,14 +108,21 @@ constructor( if (!areAnimationEnabled.first() || !isFolded) { return@flow } - withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) { - readyCallback = CompletableDeferred() - val onReady = readyCallback?.await() - readyCallback = null - controller.addOverlay(ALPHA_OPAQUE, onReady) - waitForScreenTurnedOn() - } - playFoldLightRevealOverlayAnimation() + race( + { + traceAsync(TAG, "prepareAndPlayFoldAnimation()") { + withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) { + readyCallback = CompletableDeferred() + val onReady = readyCallback?.await() + readyCallback = null + controller.addOverlay(ALPHA_OPAQUE, onReady) + waitForScreenTurnedOn() + } + playFoldLightRevealOverlayAnimation() + } + }, + { waitForGoToSleep() } + ) } .catchTimeoutAndLog() .onCompletion { @@ -135,9 +141,13 @@ constructor( readyCallback?.complete(onOverlayReady) ?: onOverlayReady.run() } - private suspend fun waitForScreenTurnedOn() { - powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() - } + private suspend fun waitForScreenTurnedOn() = + traceAsync(TAG, "waitForScreenTurnedOn()") { + powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() + } + + private suspend fun waitForGoToSleep() = + traceAsync(TAG, "waitForGoToSleep()") { powerInteractor.isAsleep.filter { it }.first() } private suspend fun playFoldLightRevealOverlayAnimation() { revealProgressValueAnimator.duration = ANIMATION_DURATION diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index 9bd0e324a964..35228508f727 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -19,6 +19,7 @@ package com.android.systemui.unfold import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.os.SystemProperties +import com.android.internal.foldables.FoldLockSettingAvailabilityProvider import com.android.systemui.CoreStartable import com.android.systemui.Flags import com.android.systemui.dagger.qualifiers.Application @@ -175,6 +176,12 @@ class UnfoldTransitionModule { fun provideDisplaySwitchLatencyLogger(): DisplaySwitchLatencyLogger = DisplaySwitchLatencyLogger() + @Provides + @Singleton + fun provideFoldLockSettingAvailabilityProvider( + context: Context + ): FoldLockSettingAvailabilityProvider = FoldLockSettingAvailabilityProvider(context.resources) + @Module interface Bindings { @Binds fun bindRepository(impl: UnfoldTransitionRepositoryImpl): UnfoldTransitionRepository diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt index 909a18be4b9e..97d957dde01b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt @@ -17,8 +17,14 @@ package com.android.systemui.util.kotlin import com.android.systemui.lifecycle.repeatWhenAttached +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch /** * Suspends to keep getting updates until cancellation. Once cancelled, mark this as eligible for @@ -42,3 +48,22 @@ suspend fun DisposableHandle.awaitCancellationThenDispose() { dispose() } } + +/** + * This will [launch], run [onLaunch] to get a [DisposableHandle], and finally + * [awaitCancellationThenDispose][DisposableHandle.awaitCancellationThenDispose]. This can be used + * to structure self-disposing code which attaches listeners, for example in ViewBinders: + * ``` + * suspend fun bind(view: MyView, viewModel: MyViewModel) = coroutineScope { + * launchAndDispose { + * view.setOnClickListener { viewModel.handleClick() } + * DisposableHandle { view.setOnClickListener(null) } + * } + * } + * ``` + */ +inline fun CoroutineScope.launchAndDispose( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + crossinline onLaunch: () -> DisposableHandle +): Job = launch(context, start) { onLaunch().awaitCancellationThenDispose() } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 9bfc4ce49cc7..1568e8c011a1 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -20,8 +20,12 @@ import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.FLAG_SYSTEM; import static android.app.WallpaperManager.SetWallpaperFlags; +import static com.android.window.flags.Flags.offloadColorExtraction; + +import android.annotation.Nullable; import android.app.WallpaperColors; import android.app.WallpaperManager; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.RecordingCanvas; @@ -134,6 +138,12 @@ public class ImageWallpaper extends WallpaperService { mLongExecutor, mLock, new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { + + @Override + public void onColorsProcessed() { + CanvasEngine.this.notifyColorsChanged(); + } + @Override public void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { @@ -183,8 +193,11 @@ public class ImageWallpaper extends WallpaperService { @Override public void onDestroy() { - getDisplayContext().getSystemService(DisplayManager.class) - .unregisterDisplayListener(this); + Context context = getDisplayContext(); + if (context != null) { + DisplayManager displayManager = context.getSystemService(DisplayManager.class); + if (displayManager != null) displayManager.unregisterDisplayListener(this); + } mWallpaperLocalColorExtractor.cleanUp(); } @@ -429,6 +442,12 @@ public class ImageWallpaper extends WallpaperService { } @Override + public @Nullable WallpaperColors onComputeColors() { + if (!offloadColorExtraction()) return null; + return mWallpaperLocalColorExtractor.onComputeColors(); + } + + @Override public boolean supportsLocalColorExtraction() { return true; } @@ -465,6 +484,12 @@ public class ImageWallpaper extends WallpaperService { } @Override + public void onDimAmountChanged(float dimAmount) { + if (!offloadColorExtraction()) return; + mWallpaperLocalColorExtractor.onDimAmountChanged(dimAmount); + } + + @Override public void onDisplayAdded(int displayId) { } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java index e2ec8dc056ca..d37dfb49c5e5 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractor.java @@ -17,6 +17,8 @@ package com.android.systemui.wallpapers; +import static com.android.window.flags.Flags.offloadColorExtraction; + import android.app.WallpaperColors; import android.graphics.Bitmap; import android.graphics.Rect; @@ -66,6 +68,12 @@ public class WallpaperLocalColorExtractor { private final List<RectF> mPendingRegions = new ArrayList<>(); private final Set<RectF> mProcessedRegions = new ArraySet<>(); + private float mWallpaperDimAmount = 0f; + private WallpaperColors mWallpaperColors; + + // By default we assume that colors were loaded from disk and don't need to be recomputed + private boolean mRecomputeColors = false; + @LongRunning private final Executor mLongExecutor; @@ -75,6 +83,12 @@ public class WallpaperLocalColorExtractor { * Interface to handle the callbacks after the different steps of the color extraction */ public interface WallpaperLocalColorExtractorCallback { + + /** + * Callback after the wallpaper colors have been computed + */ + void onColorsProcessed(); + /** * Callback after the colors of new regions have been extracted * @param regions the list of new regions that have been processed @@ -129,7 +143,7 @@ public class WallpaperLocalColorExtractor { if (displayWidth == mDisplayWidth && displayHeight == mDisplayHeight) return; mDisplayWidth = displayWidth; mDisplayHeight = displayHeight; - processColorsInternal(); + processLocalColorsInternal(); } } @@ -166,7 +180,8 @@ public class WallpaperLocalColorExtractor { mBitmapHeight = bitmap.getHeight(); mMiniBitmap = createMiniBitmap(bitmap); mWallpaperLocalColorExtractorCallback.onMiniBitmapUpdated(); - recomputeColors(); + if (offloadColorExtraction() && mRecomputeColors) recomputeColorsInternal(); + recomputeLocalColors(); } } @@ -184,16 +199,66 @@ public class WallpaperLocalColorExtractor { if (mPages == pages) return; mPages = pages; if (mMiniBitmap != null && !mMiniBitmap.isRecycled()) { - recomputeColors(); + recomputeLocalColors(); } } } - // helper to recompute colors, to be called in synchronized methods - private void recomputeColors() { + /** + * Should be called when the dim amount of the wallpaper changes, to recompute the colors + */ + public void onDimAmountChanged(float dimAmount) { + mLongExecutor.execute(() -> onDimAmountChangedSynchronized(dimAmount)); + } + + private void onDimAmountChangedSynchronized(float dimAmount) { + synchronized (mLock) { + if (mWallpaperDimAmount == dimAmount) return; + mWallpaperDimAmount = dimAmount; + mRecomputeColors = true; + recomputeColorsInternal(); + } + } + + /** + * To be called by {@link ImageWallpaper.CanvasEngine#onComputeColors}. This will either + * return the current wallpaper colors, or if the bitmap is not yet loaded, return null and call + * {@link WallpaperLocalColorExtractorCallback#onColorsProcessed()} when the colors are ready. + */ + public WallpaperColors onComputeColors() { + mLongExecutor.execute(this::onComputeColorsSynchronized); + return mWallpaperColors; + } + + private void onComputeColorsSynchronized() { + synchronized (mLock) { + if (mRecomputeColors) return; + mRecomputeColors = true; + recomputeColorsInternal(); + } + } + + /** + * helper to recompute main colors, to be called in synchronized methods + */ + private void recomputeColorsInternal() { + if (mMiniBitmap == null) return; + mWallpaperColors = getWallpaperColors(mMiniBitmap, mWallpaperDimAmount); + mWallpaperLocalColorExtractorCallback.onColorsProcessed(); + } + + @VisibleForTesting + WallpaperColors getWallpaperColors(@NonNull Bitmap bitmap, float dimAmount) { + return WallpaperColors.fromBitmap(bitmap, dimAmount); + } + + /** + * helper to recompute local colors, to be called in synchronized methods + */ + private void recomputeLocalColors() { mPendingRegions.addAll(mProcessedRegions); mProcessedRegions.clear(); - processColorsInternal(); + processLocalColorsInternal(); } /** @@ -216,7 +281,7 @@ public class WallpaperLocalColorExtractor { if (!wasActive && isActive()) { mWallpaperLocalColorExtractorCallback.onActivated(); } - processColorsInternal(); + processLocalColorsInternal(); } } @@ -353,7 +418,7 @@ public class WallpaperLocalColorExtractor { * then notify the callback with the resulting colors for these regions * This method should only be called synchronously */ - private void processColorsInternal() { + private void processLocalColorsInternal() { /* * if the miniBitmap is not yet loaded, that means the onBitmapChanged has not yet been * called, and thus the wallpaper is not yet loaded. In that case, exit, the function diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 6a35340dec58..ca55dd8ba5fa 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -330,7 +330,8 @@ class ClockEventControllerTest : SysuiTestCase() { TransitionStep( from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, - value = 0.4f + value = 0.4f, + transitionState = TransitionState.RUNNING, ) yield() diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index b0f0363a3e97..15764571ce02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -43,10 +43,10 @@ import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; +import com.android.systemui.bluetooth.qsdialog.DeviceItem; +import com.android.systemui.bluetooth.qsdialog.DeviceItemType; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItem; -import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItemType; import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.phone.SystemUIDialogManager; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapterTest.java index 95d5597ef645..d16db65334d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapterTest.java @@ -27,7 +27,7 @@ import androidx.test.filters.SmallTest; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.systemui.SysuiTestCase; -import com.android.systemui.qs.tiles.dialog.bluetooth.DeviceItem; +import com.android.systemui.bluetooth.qsdialog.DeviceItem; import org.junit.Before; import org.junit.Rule; diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 1acb20322c14..238a76eb7400 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -33,6 +33,7 @@ import com.airbnb.lottie.model.KeyPath import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor import com.android.settingslib.Utils +import com.android.systemui.Flags.FLAG_CONSTRAINT_BP import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository @@ -351,6 +352,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { @Test fun updatesOverlayViewParams_onDisplayRotationChange_xAlignedSensor() { testScope.runTest { + mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) setupTestConfiguration( DeviceConfig.X_ALIGNED, rotation = DisplayRotation.ROTATION_0, @@ -392,6 +394,7 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { @Test fun updatesOverlayViewParams_onDisplayRotationChange_yAlignedSensor() { testScope.runTest { + mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) setupTestConfiguration( DeviceConfig.Y_ALIGNED, rotation = DisplayRotation.ROTATION_0, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnInteractorTest.kt index 036d3c862ae0..4949716ad129 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnInteractorTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import android.testing.AndroidTestingRunner diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepositoryTest.kt index 31192841ec77..85e2a8d4b48e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepositoryTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import android.testing.AndroidTestingRunner diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt index 479e62d4d724..a8f82eda51c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.testing.AndroidTestingRunner import android.testing.TestableLooper diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt index 17b612714fe2..12dfe97649d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,8 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog -import android.content.Context import android.graphics.drawable.Drawable import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -121,23 +120,18 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { sysuiDialogFactory ) - whenever( - sysuiDialogFactory.create( - any(SystemUIDialog.Delegate::class.java) - ) + whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java))).thenAnswer { + SystemUIDialog( + mContext, + 0, + SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, + dialogManager, + sysuiState, + fakeBroadcastDispatcher, + dialogTransitionAnimator, + it.getArgument(0) ) - .thenAnswer { - SystemUIDialog( - mContext, - 0, - SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, - dialogManager, - sysuiState, - fakeBroadcastDispatcher, - dialogTransitionAnimator, - it.getArgument(0) - ) - } + } icon = Pair(drawable, DEVICE_NAME) deviceItem = @@ -163,6 +157,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { assertThat(recyclerView.visibility).isEqualTo(VISIBLE) assertThat(recyclerView.adapter).isNotNull() assertThat(recyclerView.layoutManager is LinearLayoutManager).isTrue() + dialog.dismiss() } @Test @@ -184,6 +179,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME) assertThat(adapter.getItem(0).connectionSummary).isEqualTo(DEVICE_CONNECTION_SUMMARY) assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon) + dialog.dismiss() } } @@ -259,6 +255,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { assertThat(pairNewButton.visibility).isEqualTo(VISIBLE) assertThat(adapter.itemCount).isEqualTo(1) assertThat(scrollViewContent.layoutParams.height).isEqualTo(WRAP_CONTENT) + dialog.dismiss() } } @@ -283,6 +280,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { dialog.show() assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height) .isEqualTo(cachedHeight) + dialog.dismiss() } } @@ -306,6 +304,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { dialog.show() assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height) .isGreaterThan(MATCH_PARENT) + dialog.dismiss() } } @@ -331,6 +330,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { dialog.requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout).visibility ) .isEqualTo(GONE) + dialog.dismiss() } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepositoryTest.kt index da8f60a9b926..4aa6209fab3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogRepositoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import android.testing.AndroidTestingRunner diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt index c8a2aa64ffa2..6d99c5b62e9b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import android.testing.AndroidTestingRunner diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt index a8cd8c801a95..28cbcb435223 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothDevice import android.content.pm.PackageInfo diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt index ddf0b9a78165..eb735cbfec47 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.tiles.dialog.bluetooth +package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java index 45d20dcd9d11..8e5ddc79976f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.KeyEvent; import android.view.MotionEvent; import androidx.test.filters.SmallTest; @@ -202,6 +203,36 @@ public class FalsingCollectorImplTest extends SysuiTestCase { } @Test + public void testPassThroughEnterKeyEvent() { + KeyEvent enterDown = KeyEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, + 0, 0, 0, 0, 0, 0, 0, ""); + KeyEvent enterUp = KeyEvent.obtain(0, 0, MotionEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, + 0, 0, 0, 0, 0, 0, ""); + + mFalsingCollector.onKeyEvent(enterDown); + verify(mFalsingDataProvider, never()).onKeyEvent(any(KeyEvent.class)); + + mFalsingCollector.onKeyEvent(enterUp); + verify(mFalsingDataProvider, times(1)).onKeyEvent(enterUp); + } + + @Test + public void testAvoidAKeyEvent() { + // Arbitrarily chose the "A" key, as it is not currently allowlisted. If this key is + // allowlisted in the future, please choose another key that will not be collected. + KeyEvent aKeyDown = KeyEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, + 0, 0, 0, 0, 0, 0, 0, ""); + KeyEvent aKeyUp = KeyEvent.obtain(0, 0, MotionEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, + 0, 0, 0, 0, 0, 0, ""); + + mFalsingCollector.onKeyEvent(aKeyDown); + verify(mFalsingDataProvider, never()).onKeyEvent(any(KeyEvent.class)); + + mFalsingCollector.onKeyEvent(aKeyUp); + verify(mFalsingDataProvider, never()).onKeyEvent(any(KeyEvent.class)); + } + + @Test public void testPassThroughGesture() { MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); MotionEvent up = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java index 0353f87ae9b4..057b0a158cab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.when; import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; +import android.view.KeyEvent; import android.view.MotionEvent; import androidx.test.filters.SmallTest; @@ -282,6 +283,22 @@ public class FalsingDataProviderTest extends ClassifierTest { } @Test + public void test_isFromKeyboard_disallowedKey_false() { + KeyEvent eventDown = KeyEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, + 0, 0, 0, 0, 0, 0, ""); + KeyEvent eventUp = KeyEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, + 0, 0, 0, 0, 0, ""); + + //events have not come in yet + assertThat(mDataProvider.isFromKeyboard()).isFalse(); + + mDataProvider.onKeyEvent(eventDown); + mDataProvider.onKeyEvent(eventUp); + assertThat(mDataProvider.isFromKeyboard()).isTrue(); + mDataProvider.onSessionEnd(); + } + + @Test public void test_IsFromTrackpad() { MotionEvent motionEventOrigin = appendTrackpadDownEvent(0, 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/TimeLimitedMotionEventBufferTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/TimeLimitedInputEventBufferTest.java index 901196f8bbba..ad7afa3c593f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/TimeLimitedMotionEventBufferTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/TimeLimitedInputEventBufferTest.java @@ -20,6 +20,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import android.testing.AndroidTestingRunner; +import android.view.InputEvent; +import android.view.KeyEvent; import android.view.MotionEvent; import androidx.test.filters.SmallTest; @@ -34,28 +36,28 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) -public class TimeLimitedMotionEventBufferTest extends SysuiTestCase { +public class TimeLimitedInputEventBufferTest extends SysuiTestCase { private static final long MAX_AGE_MS = 100; - private TimeLimitedMotionEventBuffer mBuffer; + private TimeLimitedInputEventBuffer<InputEvent> mBuffer; @Before public void setup() { MockitoAnnotations.initMocks(this); - mBuffer = new TimeLimitedMotionEventBuffer(MAX_AGE_MS); + mBuffer = new TimeLimitedInputEventBuffer<>(MAX_AGE_MS); } @After public void tearDown() { - for (MotionEvent motionEvent : mBuffer) { - motionEvent.recycle(); + for (InputEvent inputEvent : mBuffer) { + inputEvent.recycle(); } mBuffer.clear(); } @Test - public void testAllEventsRetained() { + public void testMotionEventAllEventsRetained() { MotionEvent eventA = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); MotionEvent eventB = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 0, 0, 0); MotionEvent eventC = MotionEvent.obtain(0, 2, MotionEvent.ACTION_MOVE, 0, 0, 0); @@ -70,7 +72,7 @@ public class TimeLimitedMotionEventBufferTest extends SysuiTestCase { } @Test - public void testOlderEventsRemoved() { + public void testMotionEventOlderEventsRemoved() { MotionEvent eventA = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0); MotionEvent eventB = MotionEvent.obtain(0, 1, MotionEvent.ACTION_MOVE, 0, 0, 0); MotionEvent eventC = MotionEvent.obtain( @@ -89,4 +91,40 @@ public class TimeLimitedMotionEventBufferTest extends SysuiTestCase { assertThat(mBuffer.get(0), is(eventC)); assertThat(mBuffer.get(1), is(eventD)); } + + @Test + public void testKeyEventAllEventsRetained() { + KeyEvent eventA = KeyEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, + 0, 0, 0, 0, 0, ""); + KeyEvent eventB = KeyEvent.obtain(0, 3, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0, 0, 0, + 0, 0, 0, ""); + + mBuffer.add(eventA); + mBuffer.add(eventB); + + assertThat(mBuffer.size(), is(2)); + } + + @Test + public void testKeyEventOlderEventsRemoved() { + KeyEvent eventA = KeyEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_A, 0, 0, + 0, 0, 0, 0, 0, ""); + KeyEvent eventB = KeyEvent.obtain(0, 1, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, 0, 0, 0, 0, + 0, 0, 0, ""); + KeyEvent eventC = KeyEvent.obtain(0, MAX_AGE_MS + 1, MotionEvent.ACTION_DOWN, + KeyEvent.KEYCODE_A, 0, 0, 0, 0, 0, 0, 0, ""); + KeyEvent eventD = KeyEvent.obtain(0, MAX_AGE_MS + 2, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_A, + 0, 0, 0, 0, 0, 0, 0, ""); + + mBuffer.add(eventA); + mBuffer.add(eventB); + assertThat(mBuffer.size(), is(2)); + + mBuffer.add(eventC); + mBuffer.add(eventD); + assertThat(mBuffer.size(), is(2)); + + assertThat(mBuffer.get(0), is(eventC)); + assertThat(mBuffer.get(1), is(eventD)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index 2ca5aeffcc15..c47f0bcb3192 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -23,6 +23,7 @@ import android.view.View.GONE import android.view.View.VISIBLE import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest +import com.android.internal.policy.SystemBarUtils import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor @@ -70,7 +71,7 @@ class ClockSectionTest : SysuiTestCase() { Utils.getStatusBarHeaderHeightKeyguard(context) private val LARGE_CLOCK_TOP_WITHOUT_SMARTSPACE = - context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + + SystemBarUtils.getStatusBarHeight(context) + context.resources.getDimensionPixelSize( com.android.systemui.customization.R.dimen.small_clock_padding_top ) + diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index 695d3b2c6c78..ca403e0addec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -19,6 +19,7 @@ package com.android.systemui.media.dialog; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -47,6 +48,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.media.LocalMediaManager; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.broadcast.BroadcastSender; @@ -127,6 +129,12 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager, mKeyguardManager, mFlags, mUserTracker); + // Using a fake package will cause routing operations to fail, so we intercept + // scanning-related operations. + mMediaOutputController.mLocalMediaManager = mock(LocalMediaManager.class); + doNothing().when(mMediaOutputController.mLocalMediaManager).startScan(); + doNothing().when(mMediaOutputController.mLocalMediaManager).stopScan(); + mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender, mMediaOutputController); mMediaOutputBaseDialogImpl.onCreate(new Bundle()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt index 44798ea99bee..8b79fa45b8ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt @@ -257,6 +257,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { userId = userId, colorBackground = 0, isForegroundTask = isForegroundTask, + userType = RecentTask.UserType.STANDARD, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/IconLoaderLibAppIconLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/BasicPackageManagerAppIconLoaderTest.kt index 9b346d0120ef..fa1c8f8ea2c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/IconLoaderLibAppIconLoaderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/BasicPackageManagerAppIconLoaderTest.kt @@ -20,15 +20,10 @@ import android.content.ComponentName import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.graphics.Bitmap -import android.graphics.drawable.Drawable import androidx.test.filters.SmallTest -import com.android.launcher3.icons.BitmapInfo import com.android.launcher3.icons.FastBitmapDrawable -import com.android.launcher3.icons.IconFactory import com.android.systemui.SysuiTestCase import com.android.systemui.shared.system.PackageManagerWrapper -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 com.google.common.truth.Truth.assertThat @@ -40,20 +35,17 @@ import org.junit.runners.JUnit4 @SmallTest @RunWith(JUnit4::class) -class IconLoaderLibAppIconLoaderTest : SysuiTestCase() { +class BasicPackageManagerAppIconLoaderTest : SysuiTestCase() { - private val iconFactory: IconFactory = mock() private val packageManagerWrapper: PackageManagerWrapper = mock() private val packageManager: PackageManager = mock() private val dispatcher = Dispatchers.Unconfined private val appIconLoader = - IconLoaderLibAppIconLoader( + BasicPackageManagerAppIconLoader( backgroundDispatcher = dispatcher, - context = context, packageManagerWrapper = packageManagerWrapper, packageManager = packageManager, - iconFactoryProvider = { iconFactory } ) @Test @@ -70,12 +62,7 @@ class IconLoaderLibAppIconLoaderTest : SysuiTestCase() { private fun givenIcon(component: ComponentName, userId: Int, icon: FastBitmapDrawable) { val activityInfo = mock<ActivityInfo>() whenever(packageManagerWrapper.getActivityInfo(component, userId)).thenReturn(activityInfo) - val rawIcon = mock<Drawable>() - whenever(activityInfo.loadIcon(packageManager)).thenReturn(rawIcon) - - val bitmapInfo = mock<BitmapInfo>() - whenever(iconFactory.createBadgedIconBitmap(eq(rawIcon), any())).thenReturn(bitmapInfo) - whenever(bitmapInfo.newIcon(context)).thenReturn(icon) + whenever(activityInfo.loadIcon(packageManager)).thenReturn(icon) } private fun createIcon(): FastBitmapDrawable = diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt index b593def283ae..dd621129ad9a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt @@ -1,9 +1,15 @@ package com.android.systemui.mediaprojection.appselector.data import android.app.ActivityManager.RecentTaskInfo +import android.content.pm.UserInfo +import android.os.UserManager import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.mediaprojection.appselector.data.RecentTask.UserType.CLONED +import com.android.systemui.mediaprojection.appselector.data.RecentTask.UserType.PRIVATE +import com.android.systemui.mediaprojection.appselector.data.RecentTask.UserType.STANDARD +import com.android.systemui.mediaprojection.appselector.data.RecentTask.UserType.WORK import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock @@ -17,6 +23,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt @RunWith(AndroidTestingRunner::class) @SmallTest @@ -25,12 +32,16 @@ class ShellRecentTaskListProviderTest : SysuiTestCase() { private val dispatcher = Dispatchers.Unconfined private val recentTasks: RecentTasks = mock() private val userTracker: UserTracker = mock() + private val userManager: UserManager = mock { + whenever(getUserInfo(anyInt())).thenReturn(mock()) + } private val recentTaskListProvider = ShellRecentTaskListProvider( dispatcher, Runnable::run, Optional.of(recentTasks), - userTracker + userTracker, + userManager, ) @Test @@ -147,6 +158,22 @@ class ShellRecentTaskListProviderTest : SysuiTestCase() { .inOrder() } + @Test + fun loadRecentTasks_assignsCorrectUserType() { + givenRecentTasks( + createSingleTask(taskId = 1, userId = 10, userType = STANDARD), + createSingleTask(taskId = 2, userId = 20, userType = WORK), + createSingleTask(taskId = 3, userId = 30, userType = CLONED), + createSingleTask(taskId = 4, userId = 40, userType = PRIVATE), + ) + + val result = runBlocking { recentTaskListProvider.loadRecentTasks() } + + assertThat(result.map { it.userType }) + .containsExactly(STANDARD, WORK, CLONED, PRIVATE) + .inOrder() + } + @Suppress("UNCHECKED_CAST") private fun givenRecentTasks(vararg tasks: GroupedRecentTaskInfo) { whenever(recentTasks.getRecentTasks(any(), any(), any(), any(), any())).thenAnswer { @@ -155,7 +182,10 @@ class ShellRecentTaskListProviderTest : SysuiTestCase() { } } - private fun createRecentTask(taskId: Int): RecentTask = + private fun createRecentTask( + taskId: Int, + userType: RecentTask.UserType = STANDARD + ): RecentTask = RecentTask( taskId = taskId, displayId = 0, @@ -164,25 +194,42 @@ class ShellRecentTaskListProviderTest : SysuiTestCase() { baseIntentComponent = null, colorBackground = null, isForegroundTask = false, + userType = userType, ) - private fun createSingleTask(taskId: Int, isVisible: Boolean = false): GroupedRecentTaskInfo = - GroupedRecentTaskInfo.forSingleTask(createTaskInfo(taskId, isVisible)) + private fun createSingleTask( + taskId: Int, + userId: Int = 0, + isVisible: Boolean = false, + userType: RecentTask.UserType = STANDARD, + ): GroupedRecentTaskInfo { + val userInfo = + mock<UserInfo> { + whenever(isCloneProfile).thenReturn(userType == CLONED) + whenever(isManagedProfile).thenReturn(userType == WORK) + whenever(isPrivateProfile).thenReturn(userType == PRIVATE) + } + whenever(userManager.getUserInfo(userId)).thenReturn(userInfo) + return GroupedRecentTaskInfo.forSingleTask(createTaskInfo(taskId, userId, isVisible)) + } private fun createTaskPair( taskId1: Int, + userId1: Int = 0, taskId2: Int, + userId2: Int = 0, isVisible: Boolean = false ): GroupedRecentTaskInfo = GroupedRecentTaskInfo.forSplitTasks( - createTaskInfo(taskId1, isVisible), - createTaskInfo(taskId2, isVisible), + createTaskInfo(taskId1, userId1, isVisible), + createTaskInfo(taskId2, userId2, isVisible), null ) - private fun createTaskInfo(taskId: Int, isVisible: Boolean = false) = + private fun createTaskInfo(taskId: Int, userId: Int, isVisible: Boolean = false) = RecentTaskInfo().apply { this.taskId = taskId this.isVisible = isVisible + this.userId = userId } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt index ac4107359dbc..a84008b0353c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt @@ -56,7 +56,8 @@ class MediaProjectionRecentsViewControllerTest : SysuiTestCase() { topActivityComponent = null, baseIntentComponent = null, colorBackground = null, - isForegroundTask = false + isForegroundTask = false, + userType = RecentTask.UserType.STANDARD, ) private val taskView = diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt deleted file mode 100644 index e044eeca8303..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt +++ /dev/null @@ -1,141 +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.systemui.mediaprojection.permission - -import android.app.AlertDialog -import android.media.projection.MediaProjectionConfig -import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.view.WindowManager -import android.widget.Spinner -import android.widget.TextView -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FeatureFlagsClassic -import com.android.systemui.flags.Flags -import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger -import com.android.systemui.res.R -import com.android.systemui.statusbar.phone.AlertDialogWithDelegate -import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.util.mockito.mock -import junit.framework.Assert.assertEquals -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.`when` as whenever - -@SmallTest -@RunWith(AndroidTestingRunner::class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() { - - private lateinit var dialog: AlertDialog - - private val flags = mock<FeatureFlagsClassic>() - private val onStartRecordingClicked = mock<Runnable>() - private val mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>() - - private val mediaProjectionConfig: MediaProjectionConfig = - MediaProjectionConfig.createConfigForDefaultDisplay() - private val appName: String = "testApp" - private val hostUid: Int = 12345 - - private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app - private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen - private val resIdSingleAppDisabled = - R.string.media_projection_entry_app_permission_dialog_single_app_disabled - - @Before - fun setUp() { - whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true) - } - - @After - fun teardown() { - if (::dialog.isInitialized) { - dialog.dismiss() - } - } - - @Test - fun showDialog_forceShowPartialScreenShareFalse() { - // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and - // overrideDisableSingleAppOption = false - val overrideDisableSingleAppOption = false - setUpAndShowDialog(overrideDisableSingleAppOption) - - val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner) - val secondOptionText = - spinner.adapter - .getDropDownView(1, null, spinner) - .findViewById<TextView>(android.R.id.text2) - ?.text - - // check that the first option is full screen and enabled - assertEquals(context.getString(resIdFullScreen), spinner.selectedItem) - - // check that the second option is single app and disabled - assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionText) - } - - @Test - fun showDialog_forceShowPartialScreenShareTrue() { - // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and - // overrideDisableSingleAppOption = true - val overrideDisableSingleAppOption = true - setUpAndShowDialog(overrideDisableSingleAppOption) - - val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_spinner) - val secondOptionText = - spinner.adapter - .getDropDownView(1, null, spinner) - .findViewById<TextView>(android.R.id.text1) - ?.text - - // check that the first option is single app and enabled - assertEquals(context.getString(resIdSingleApp), spinner.selectedItem) - - // check that the second option is full screen and enabled - assertEquals(context.getString(resIdFullScreen), secondOptionText) - } - - private fun setUpAndShowDialog(overrideDisableSingleAppOption: Boolean) { - val delegate = - MediaProjectionPermissionDialogDelegate( - context, - mediaProjectionConfig, - {}, - onStartRecordingClicked, - appName, - overrideDisableSingleAppOption, - hostUid, - mediaProjectionMetricsLogger - ) - - dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate) - SystemUIDialog.applyFlags(dialog) - SystemUIDialog.setDialogSize(dialog) - - dialog.window?.addSystemFlags( - WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS - ) - - delegate.onCreate(dialog, savedInstanceState = null) - dialog.show() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt index 82ee99a29427..830f08a0c445 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt @@ -23,7 +23,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.tileimpl.QSTileImpl -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogViewModel +import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogViewModel import com.android.systemui.statusbar.policy.BluetoothController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq 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 9104f8e43892..6846c7227d9c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt @@ -88,6 +88,7 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { private lateinit var dialog: SystemUIDialog private lateinit var factory: SystemUIDialog.Factory private lateinit var latch: CountDownLatch + private var issueRecordingState = IssueRecordingState() @Before fun setup() { @@ -128,6 +129,7 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { mediaProjectionMetricsLogger, userFileManager, screenCaptureDisabledDialogDelegate, + issueRecordingState, ) { latch.countDown() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt new file mode 100644 index 000000000000..5e7d8fb5df02 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.app.PendingIntent +import android.content.Intent +import android.os.Bundle +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import android.view.View +import android.view.Window +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.kotlin.capture +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyBlocking + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ActionExecutorTest : SysuiTestCase() { + private val scheduler = TestCoroutineScheduler() + private val mainDispatcher = StandardTestDispatcher(scheduler) + private val testScope = TestScope(mainDispatcher) + + private val intentExecutor = mock<ActionIntentExecutor>() + private val window = mock<Window>() + private val view = mock<View>() + private val onDismiss = mock<(() -> Unit)>() + private val pendingIntent = mock<PendingIntent>() + + private lateinit var actionExecutor: ActionExecutor + + @Test + fun startSharedTransition_callsLaunchIntent() = runTest { + actionExecutor = createActionExecutor() + + actionExecutor.startSharedTransition(Intent(Intent.ACTION_EDIT), UserHandle.CURRENT, true) + scheduler.advanceUntilIdle() + + val intentCaptor = argumentCaptor<Intent>() + verifyBlocking(intentExecutor) { + launchIntent(capture(intentCaptor), eq(UserHandle.CURRENT), eq(true), any(), any()) + } + assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_EDIT) + } + + @Test + fun sendPendingIntent_dismisses() = runTest { + actionExecutor = createActionExecutor() + + actionExecutor.sendPendingIntent(pendingIntent) + + verify(pendingIntent).send(any(Bundle::class.java)) + verify(onDismiss).invoke() + } + + private fun createActionExecutor(): ActionExecutor { + return ActionExecutor(intentExecutor, testScope, window, view, onDismiss) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt index 0c324706857f..5e53fe16534d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentExecutorTest.kt @@ -64,7 +64,7 @@ class ActionIntentExecutorTest : SysuiTestCase() { val intent = Intent(Intent.ACTION_EDIT).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } val userHandle = myUserHandle() - actionIntentExecutor.launchIntent(intent, null, userHandle, false) + actionIntentExecutor.launchIntent(intent, userHandle, false, null, null) scheduler.advanceUntilIdle() verify(activityManagerWrapper) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt index 4a5cf57e6fa8..bde821b79469 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt @@ -16,12 +16,11 @@ package com.android.systemui.screenshot -import android.app.ActivityOptions -import android.app.ExitTransitionCoordinator import android.app.Notification import android.app.PendingIntent import android.content.Intent import android.net.Uri +import android.os.Process import android.os.UserHandle import android.testing.AndroidTestingRunner import android.view.accessibility.AccessibilityManager @@ -36,11 +35,7 @@ import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat -import kotlin.test.Ignore import kotlin.test.Test -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Assert.assertNotNull import org.junit.Before @@ -49,32 +44,18 @@ import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.kotlin.any import org.mockito.kotlin.never import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyBlocking import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) @SmallTest class DefaultScreenshotActionsProviderTest : SysuiTestCase() { - private val scheduler = TestCoroutineScheduler() - private val mainDispatcher = StandardTestDispatcher(scheduler) - private val testScope = TestScope(mainDispatcher) - - private val actionIntentExecutor = mock<ActionIntentExecutor>() + private val actionExecutor = mock<ActionExecutor>() private val accessibilityManager = mock<AccessibilityManager>() private val uiEventLogger = mock<UiEventLogger>() private val smartActionsProvider = mock<SmartActionsProvider>() - private val transition = mock<android.util.Pair<ActivityOptions, ExitTransitionCoordinator>>() - private val requestDismissal = mock<() -> Unit>() private val request = ScreenshotData.forTesting() - private val invalidResult = ScreenshotController.SavedImageData() - private val validResult = - ScreenshotController.SavedImageData().apply { - uri = Uri.EMPTY - owner = UserHandle.OWNER - subject = "Test" - imageTime = 0 - } + private val validResult = ScreenshotSavedResult(Uri.EMPTY, Process.myUserHandle(), 0) private lateinit var viewModel: ScreenshotViewModel private lateinit var actionsProvider: ScreenshotActionsProvider @@ -91,7 +72,7 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { assertNotNull(viewModel.previewAction.value) viewModel.previewAction.value!!.invoke() - verifyNoMoreInteractions(actionIntentExecutor) + verifyNoMoreInteractions(actionExecutor) } @Test @@ -105,39 +86,24 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { assertThat(secondAction.onClicked).isNotNull() firstAction.onClicked!!.invoke() secondAction.onClicked!!.invoke() - verifyNoMoreInteractions(actionIntentExecutor) - } - - @Test - fun actionAccessed_withInvalidResult_doesNothing() { - actionsProvider = createActionsProvider() - - actionsProvider.setCompletedScreenshot(invalidResult) - viewModel.previewAction.value!!.invoke() - viewModel.actions.value[1].onClicked!!.invoke() - - verifyNoMoreInteractions(actionIntentExecutor) + verifyNoMoreInteractions(actionExecutor) } @Test - @Ignore("b/332526567") fun actionAccessed_withResult_launchesIntent() = runTest { actionsProvider = createActionsProvider() actionsProvider.setCompletedScreenshot(validResult) viewModel.actions.value[0].onClicked!!.invoke() - scheduler.advanceUntilIdle() verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED), eq(0), eq("")) val intentCaptor = argumentCaptor<Intent>() - verifyBlocking(actionIntentExecutor) { - launchIntent(capture(intentCaptor), eq(transition), eq(UserHandle.CURRENT), eq(true)) - } + verify(actionExecutor) + .startSharedTransition(capture(intentCaptor), eq(Process.myUserHandle()), eq(true)) assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_EDIT) } @Test - @Ignore("b/332526567") fun actionAccessed_whilePending_launchesMostRecentAction() = runTest { actionsProvider = createActionsProvider() @@ -145,13 +111,11 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { viewModel.previewAction.value!!.invoke() viewModel.actions.value[1].onClicked!!.invoke() actionsProvider.setCompletedScreenshot(validResult) - scheduler.advanceUntilIdle() verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED), eq(0), eq("")) val intentCaptor = argumentCaptor<Intent>() - verifyBlocking(actionIntentExecutor) { - launchIntent(capture(intentCaptor), eq(transition), eq(UserHandle.CURRENT), eq(false)) - } + verify(actionExecutor) + .startSharedTransition(capture(intentCaptor), eq(Process.myUserHandle()), eq(false)) assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_CHOOSER) } @@ -172,7 +136,7 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { (it.getArgument(2) as ((Notification.Action) -> Unit)).invoke(quickShare) } whenever(smartActionsProvider.wrapIntent(any(), any(), any(), any())).thenAnswer { - it.getArgument(0) + (it.getArgument(0) as Notification.Action).actionIntent } actionsProvider = createActionsProvider() @@ -190,14 +154,11 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { return DefaultScreenshotActionsProvider( context, viewModel, - actionIntentExecutor, smartActionsProvider, uiEventLogger, - testScope, request, "testid", - { transition }, - requestDismissal, + actionExecutor ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt index 25ba09a0ce90..6a22d8648d91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt @@ -84,7 +84,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - whenever(mirrorController.toggleSlider).thenReturn(mirror) + whenever(mirrorController.getToggleSlider()).thenReturn(mirror) whenever(motionEvent.copy()).thenReturn(motionEvent) whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0)) @@ -129,7 +129,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() { @Test fun testNullMirrorNotTrackingTouch() { - whenever(mirrorController.toggleSlider).thenReturn(null) + whenever(mirrorController.getToggleSlider()).thenReturn(null) mController.setMirrorControllerAndMirror(mirrorController) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 02f2e16b9570..cf7c6f4e2174 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; import static com.google.common.truth.Truth.assertThat; @@ -436,6 +437,10 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) != 0).isTrue(); + assertThat( + (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) + != 0) + .isTrue(); } @Test @@ -444,6 +449,10 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue(); + assertThat( + (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) + == 0) + .isTrue(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index dfbb699ed915..2c0a15dd4e5a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -69,7 +69,6 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat -import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow @@ -87,6 +86,8 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import kotlin.test.assertEquals +import java.util.Optional import org.mockito.Mockito.`when` as whenever @OptIn(ExperimentalCoroutinesApi::class) @@ -138,6 +139,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { private val notificationLaunchAnimationInteractor = NotificationLaunchAnimationInteractor(notificationLaunchAnimationRepository) + private lateinit var falsingCollector: FalsingCollectorFake private lateinit var fakeClock: FakeSystemClock private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler> private lateinit var interactionEventHandler: InteractionEventHandler @@ -170,11 +172,12 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES) testScope = TestScope() + falsingCollector = FalsingCollectorFake() fakeClock = FakeSystemClock() underTest = NotificationShadeWindowViewController( lockscreenShadeTransitionController, - FalsingCollectorFake(), + falsingCollector, sysuiStatusBarStateController, dockManager, notificationShadeDepthController, @@ -566,6 +569,13 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { verify(sysUIKeyEventHandler).interceptMediaKey(keyEvent) } + @Test + fun forwardsCollectKeyEvent() { + val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A) + interactionEventHandler.collectKeyEvent(keyEvent) + assertEquals(keyEvent, falsingCollector.lastKeyEvent) + } + companion object { private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java index cac4a8d2d37f..6bda4d4fd48b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java @@ -299,8 +299,6 @@ public class FooterViewTest extends SysuiTestCase { @Test public void testSetFooterLabelVisible() { mView.setFooterLabelVisible(true); - assertThat(mView.findViewById(R.id.manage_text).getVisibility()).isEqualTo(View.GONE); - assertThat(mView.findSecondaryView().getVisibility()).isEqualTo(View.GONE); assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility()) .isEqualTo(View.VISIBLE); } @@ -308,8 +306,6 @@ public class FooterViewTest extends SysuiTestCase { @Test public void testSetFooterLabelInvisible() { mView.setFooterLabelVisible(false); - assertThat(mView.findViewById(R.id.manage_text).getVisibility()).isEqualTo(View.VISIBLE); - assertThat(mView.findSecondaryView().getVisibility()).isEqualTo(View.VISIBLE); assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility()) .isEqualTo(View.GONE); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt index 620d97275949..158f38d48197 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt @@ -66,7 +66,7 @@ class FooterViewModelTest : SysuiTestCase() { val underTest = kosmos.footerViewModel @Test - fun testMessageVisible_whenFilteredNotifications() = + fun messageVisible_whenFilteredNotifications() = testScope.runTest { val visible by collectLastValue(underTest.message.isVisible) @@ -76,7 +76,7 @@ class FooterViewModelTest : SysuiTestCase() { } @Test - fun testMessageVisible_whenNoFilteredNotifications() = + fun messageVisible_whenNoFilteredNotifications() = testScope.runTest { val visible by collectLastValue(underTest.message.isVisible) @@ -86,7 +86,7 @@ class FooterViewModelTest : SysuiTestCase() { } @Test - fun testClearAllButtonVisible_whenHasClearableNotifs() = + fun clearAllButtonVisible_whenHasClearableNotifs() = testScope.runTest { val visible by collectLastValue(underTest.clearAllButton.isVisible) @@ -104,7 +104,7 @@ class FooterViewModelTest : SysuiTestCase() { } @Test - fun testClearAllButtonVisible_whenHasNoClearableNotifs() = + fun clearAllButtonVisible_whenHasNoClearableNotifs() = testScope.runTest { val visible by collectLastValue(underTest.clearAllButton.isVisible) @@ -122,7 +122,26 @@ class FooterViewModelTest : SysuiTestCase() { } @Test - fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() = + fun clearAllButtonVisible_whenMessageVisible() = + testScope.runTest { + val visible by collectLastValue(underTest.clearAllButton.isVisible) + + activeNotificationListRepository.notifStats.value = + NotifStats( + numActiveNotifs = 2, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = true, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = true, + ) + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true + runCurrent() + + assertThat(visible?.value).isFalse() + } + + @Test + fun clearAllButtonAnimating_whenShadeExpandedAndTouchable() = testScope.runTest { val visible by collectLastValue(underTest.clearAllButton.isVisible) runCurrent() @@ -156,7 +175,7 @@ class FooterViewModelTest : SysuiTestCase() { } @Test - fun testClearAllButtonAnimating_whenShadeNotExpanded() = + fun clearAllButtonAnimating_whenShadeNotExpanded() = testScope.runTest { val visible by collectLastValue(underTest.clearAllButton.isVisible) runCurrent() @@ -190,7 +209,7 @@ class FooterViewModelTest : SysuiTestCase() { } @Test - fun testManageButton_whenHistoryDisabled() = + fun manageButton_whenHistoryDisabled() = testScope.runTest { val buttonLabel by collectLastValue(underTest.manageOrHistoryButton.labelId) runCurrent() @@ -203,7 +222,7 @@ class FooterViewModelTest : SysuiTestCase() { } @Test - fun testHistoryButton_whenHistoryEnabled() = + fun historyButton_whenHistoryEnabled() = testScope.runTest { val buttonLabel by collectLastValue(underTest.manageOrHistoryButton.labelId) runCurrent() @@ -214,4 +233,24 @@ class FooterViewModelTest : SysuiTestCase() { // THEN label is "History" assertThat(buttonLabel).isEqualTo(R.string.manage_notifications_history_text) } + + @Test + fun manageButtonVisible_whenMessageVisible() = + testScope.runTest { + val visible by collectLastValue(underTest.manageOrHistoryButton.isVisible) + + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true + + assertThat(visible?.value).isFalse() + } + + @Test + fun manageButtonVisible_whenMessageNotVisible() = + testScope.runTest { + val visible by collectLastValue(underTest.manageOrHistoryButton.isVisible) + + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false + + assertThat(visible?.value).isTrue() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt index 33a838ed5183..4b0b4b89fad4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt @@ -25,6 +25,7 @@ import android.testing.AndroidTestingRunner import android.util.StatsEvent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.log.assertLogsWtf import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder @@ -133,8 +134,10 @@ class NotificationMemoryLoggerTest : SysuiTestCase() { val pipeline: NotifPipeline = mock() whenever(pipeline.allNotifs).thenThrow(RuntimeException("Something broke!")) val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor) - assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf())) - .isEqualTo(StatsManager.PULL_SKIP) + assertLogsWtf { + assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf())) + .isEqualTo(StatsManager.PULL_SKIP) + } } @Test 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 cd8be573a2ac..912ecb340c3c 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 @@ -94,7 +94,6 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback; -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.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -161,7 +160,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; @Mock private ShadeController mShadeController; @Mock private Provider<WindowRootView> mWindowRootView; - @Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor; private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(), logcatLogBuffer()); private final NotificationStackScrollLogger mLogger = new NotificationStackScrollLogger( @@ -1016,7 +1014,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mViewBinder, mShadeController, mWindowRootView, - mNotificationStackAppearanceInteractor, mKosmos.getInteractionJankMonitor(), mStackLogger, mLogger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index ed29665c6e16..2f153d8b7003 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -25,6 +25,8 @@ import static com.android.systemui.Flags.FLAG_LIGHT_REVEAL_MIGRATION; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -119,9 +121,9 @@ import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.res.R; import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor; import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; -import com.android.systemui.scene.shared.flag.SceneContainerFlags; import com.android.systemui.settings.UserTracker; import com.android.systemui.settings.brightness.BrightnessSliderController; +import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor; import com.android.systemui.shade.CameraLauncher; import com.android.systemui.shade.NotificationPanelView; import com.android.systemui.shade.NotificationPanelViewController; @@ -331,7 +333,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { private final DumpManager mDumpManager = new DumpManager(); private final ScreenLifecycle mScreenLifecycle = new ScreenLifecycle(mDumpManager); - private final SceneContainerFlags mSceneContainerFlags = new FakeSceneContainerFlags(); + private final FakeSceneContainerFlags mSceneContainerFlags = new FakeSceneContainerFlags(); + + private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor = + mKosmos.getBrightnessMirrorShowingInteractor(); @Before public void setup() throws Exception { @@ -553,7 +558,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mUserTracker, () -> mFingerprintManager, mActivityStarter, - mSceneContainerFlags + mSceneContainerFlags, + mBrightnessMirrorShowingInteractor ); mScreenLifecycle.addObserver(mCentralSurfaces.mScreenObserver); mCentralSurfaces.initShadeVisibilityListener(); @@ -1084,6 +1090,34 @@ public class CentralSurfacesImplTest extends SysuiTestCase { verify(mStatusBarWindowController).refreshStatusBarHeight(); } + @Test + public void brightnesShowingChanged_flagEnabled_ScrimControllerNotified() { + mSceneContainerFlags.setEnabled(true); + mCentralSurfaces.registerCallbacks(); + + mBrightnessMirrorShowingInteractor.setMirrorShowing(true); + mTestScope.getTestScheduler().runCurrent(); + verify(mScrimController).transitionTo(ScrimState.BRIGHTNESS_MIRROR); + + mBrightnessMirrorShowingInteractor.setMirrorShowing(false); + mTestScope.getTestScheduler().runCurrent(); + ArgumentCaptor<ScrimState> captor = ArgumentCaptor.forClass(ScrimState.class); + // The default is to call the one with the callback argument + verify(mScrimController).transitionTo(captor.capture(), any()); + assertThat(captor.getValue()).isNotEqualTo(ScrimState.BRIGHTNESS_MIRROR); + } + + @Test + public void brightnesShowingChanged_flagDisabled_ScrimControllerNotified() { + mSceneContainerFlags.setEnabled(false); + mCentralSurfaces.registerCallbacks(); + + mBrightnessMirrorShowingInteractor.setMirrorShowing(true); + mTestScope.getTestScheduler().runCurrent(); + verify(mScrimController, never()).transitionTo(ScrimState.BRIGHTNESS_MIRROR); + verify(mScrimController, never()).transitionTo(eq(ScrimState.BRIGHTNESS_MIRROR), any()); + } + /** * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard} * to reconfigure the keyguard to reflect the requested showing/occluded states. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java index c1ef1ad9eef9..3e9006e5268c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.platform.test.annotations.DisableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -46,6 +47,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.policy.Clock; @@ -157,6 +159,7 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { } @Test + @DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void testHeaderUpdated() { mRow.setPinned(true); when(mHeadsUpManager.hasPinnedHeadsUp()).thenReturn(true); 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 581ca3b14822..4ace163164f1 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 @@ -28,8 +28,10 @@ import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE import android.content.pm.PackageManager import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager +import android.os.Process import android.os.UserHandle import android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.annotations.RequiresFlagsEnabled @@ -41,7 +43,8 @@ import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.verify import com.android.internal.util.FrameworkStatsLog -import com.android.server.notification.Flags +import com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING +import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX import com.android.systemui.SysuiTestCase import com.android.systemui.log.logcatLogBuffer import com.android.systemui.statusbar.RankingBuilder @@ -77,7 +80,7 @@ import org.mockito.quality.Strictness @SmallTest @RunWith(AndroidTestingRunner::class) @RunWithLooper -@EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) +@EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() @@ -384,13 +387,33 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX) + fun shouldProtectNotification_projectionActive_isFromCoreApp_fixDisabled_true() { + mediaProjectionCallback.onStart(mediaProjectionInfo) + + val notificationEntry = setupCoreAppNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING_BUG_FIX) + fun shouldProtectNotification_projectionActive_isFromCoreApp_false() { + mediaProjectionCallback.onStart(mediaProjectionInfo) + + val notificationEntry = setupCoreAppNotificationEntry(TEST_PROJECTION_PACKAGE_NAME) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test fun shouldProtectNotification_projectionActive_sysuiExempt_false() { // SystemUi context package name is exempt, but in test scenarios its // com.android.systemui.tests so use that instead of hardcoding setShareFullScreenViaSystemUi() mediaProjectionCallback.onStart(mediaProjectionInfo) - val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME) assertFalse(controller.shouldProtectNotification(notificationEntry)) } @@ -407,7 +430,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { .thenReturn(PackageManager.PERMISSION_GRANTED) mediaProjectionCallback.onStart(mediaProjectionInfo) - val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME) assertTrue(controller.shouldProtectNotification(notificationEntry)) } @@ -424,7 +447,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { .thenReturn(PackageManager.PERMISSION_GRANTED) mediaProjectionCallback.onStart(mediaProjectionInfo) - val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME) assertFalse(controller.shouldProtectNotification(notificationEntry)) } @@ -434,7 +457,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { setShareFullScreenViaBugReportHandler() mediaProjectionCallback.onStart(mediaProjectionInfo) - val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME) assertFalse(controller.shouldProtectNotification(notificationEntry)) } @@ -657,6 +680,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { private fun setupNotificationEntry( packageName: String, isFgs: Boolean = false, + isCoreApp: Boolean = false, overrideVisibility: Boolean = false, overrideChannelVisibility: Boolean = false, ): NotificationEntry { @@ -668,8 +692,14 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { // Developer has marked notification as public notification.visibility = VISIBILITY_PUBLIC } - val notificationEntry = - NotificationEntryBuilder().setNotification(notification).setPkg(packageName).build() + val notificationEntryBuilder = + NotificationEntryBuilder().setNotification(notification).setPkg(packageName) + if (isCoreApp) { + notificationEntryBuilder.setUid(Process.FIRST_APPLICATION_UID - 10) + } else { + notificationEntryBuilder.setUid(Process.FIRST_APPLICATION_UID + 10) + } + val notificationEntry = notificationEntryBuilder.build() val channel = NotificationChannel("1", "1", IMPORTANCE_HIGH) if (overrideChannelVisibility) { // User doesn't allow private notifications at the channel level @@ -688,6 +718,10 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { return setupNotificationEntry(packageName, isFgs = true) } + private fun setupCoreAppNotificationEntry(packageName: String): NotificationEntry { + return setupNotificationEntry(packageName, isCoreApp = true) + } + private fun setupPublicNotificationEntry(packageName: String): NotificationEntry { return setupNotificationEntry(packageName, overrideVisibility = true) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimationTest.kt new file mode 100644 index 000000000000..dddc71216da1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimationTest.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.unfold + +import android.os.PowerManager +import android.os.SystemProperties +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.foldables.FoldLockSettingAvailabilityProvider +import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState +import com.android.systemui.display.data.repository.fakeDeviceStateRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setScreenPowerState +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.power.shared.model.ScreenPowerState +import com.android.systemui.util.animation.data.repository.fakeAnimationStatusRepository +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.atLeast +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) +class FoldLightRevealOverlayAnimationTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val testScope: TestScope = kosmos.testScope + private val fakeDeviceStateRepository = kosmos.fakeDeviceStateRepository + private val powerInteractor = kosmos.powerInteractor + private val fakeAnimationStatusRepository = kosmos.fakeAnimationStatusRepository + private val mockControllerFactory = kosmos.fullscreenLightRevealAnimationControllerFactory + private val mockFullScreenController = kosmos.fullscreenLightRevealAnimationController + private val mockFoldLockSettingAvailabilityProvider = + mock<FoldLockSettingAvailabilityProvider>() + private val onOverlayReady = mock<Runnable>() + private lateinit var foldLightRevealAnimation: FoldLightRevealOverlayAnimation + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + whenever(mockFoldLockSettingAvailabilityProvider.isFoldLockBehaviorAvailable) + .thenReturn(true) + fakeAnimationStatusRepository.onAnimationStatusChanged(true) + + foldLightRevealAnimation = + FoldLightRevealOverlayAnimation( + kosmos.testDispatcher, + fakeDeviceStateRepository, + powerInteractor, + testScope.backgroundScope, + fakeAnimationStatusRepository, + mockControllerFactory, + mockFoldLockSettingAvailabilityProvider + ) + foldLightRevealAnimation.init() + } + + @Test + fun foldToScreenOn_playFoldAnimation() = + testScope.runTest { + foldDeviceToScreenOff() + turnScreenOn() + + verifyFoldAnimationPlayed() + } + + @Test + fun foldToAod_doNotPlayFoldAnimation() = + testScope.runTest { + foldDeviceToScreenOff() + emitLastWakefulnessEventStartingToSleep() + advanceTimeBy(SHORT_DELAY_MS) + turnScreenOn() + advanceTimeBy(ANIMATION_DURATION) + + verifyFoldAnimationDidNotPlay() + } + + @Test + fun foldToScreenOff_doNotPlayFoldAnimation() = + testScope.runTest { + foldDeviceToScreenOff() + emitLastWakefulnessEventStartingToSleep() + advanceTimeBy(SHORT_DELAY_MS) + advanceTimeBy(ANIMATION_DURATION) + + verifyFoldAnimationDidNotPlay() + } + + @Test + fun foldToScreenOnWithDelay_doNotPlayFoldAnimation() = + testScope.runTest { + foldDeviceToScreenOff() + foldLightRevealAnimation.onScreenTurningOn {} + advanceTimeBy(WAIT_FOR_ANIMATION_TIMEOUT_MS) + powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_ON) + advanceTimeBy(SHORT_DELAY_MS) + advanceTimeBy(ANIMATION_DURATION) + + verifyFoldAnimationDidNotPlay() + } + + @Test + fun immediateUnfoldAfterFold_removeOverlayAfterCancellation() = + testScope.runTest { + foldDeviceToScreenOff() + foldLightRevealAnimation.onScreenTurningOn {} + advanceTimeBy(SHORT_DELAY_MS) + advanceTimeBy(ANIMATION_DURATION) + fakeDeviceStateRepository.emit(DeviceState.HALF_FOLDED) + advanceTimeBy(SHORT_DELAY_MS) + powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_ON) + + verifyOverlayWasRemoved() + } + + @Test + fun foldToScreenOn_removeOverlayAfterCompletion() = + testScope.runTest { + foldDeviceToScreenOff() + turnScreenOn() + advanceTimeBy(ANIMATION_DURATION) + + verifyOverlayWasRemoved() + } + + @Test + fun unfold_immediatelyRunRunnable() = + testScope.runTest { + foldLightRevealAnimation.onScreenTurningOn(onOverlayReady) + + verify(onOverlayReady).run() + } + + private suspend fun TestScope.foldDeviceToScreenOff() { + fakeDeviceStateRepository.emit(DeviceState.HALF_FOLDED) + powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_ON) + advanceTimeBy(SHORT_DELAY_MS) + fakeDeviceStateRepository.emit(DeviceState.FOLDED) + advanceTimeBy(SHORT_DELAY_MS) + powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_OFF) + advanceTimeBy(SHORT_DELAY_MS) + } + + private fun TestScope.turnScreenOn() { + foldLightRevealAnimation.onScreenTurningOn {} + advanceTimeBy(SHORT_DELAY_MS) + powerInteractor.setScreenPowerState(ScreenPowerState.SCREEN_ON) + advanceTimeBy(SHORT_DELAY_MS) + } + + private fun emitLastWakefulnessEventStartingToSleep() = + powerInteractor.setAsleepForTest(PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD) + + private fun verifyFoldAnimationPlayed() = + verify(mockFullScreenController, atLeast(1)).updateRevealAmount(any()) + + private fun verifyFoldAnimationDidNotPlay() = + verify(mockFullScreenController, never()).updateRevealAmount(any()) + + private fun verifyOverlayWasRemoved() = + verify(mockFullScreenController, atLeast(1)).ensureOverlayRemoved() + + private companion object { + const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L + val ANIMATION_DURATION: Long + get() = SystemProperties.getLong("persist.fold_animation_duration", 200L) + const val SHORT_DELAY_MS = 50L + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractorTest.java index 1125d41856c6..0eba21ada789 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/WallpaperLocalColorExtractorTest.java @@ -16,9 +16,12 @@ package com.android.systemui.wallpapers; +import static com.android.window.flags.Flags.FLAG_OFFLOAD_COLOR_EXTRACTION; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; @@ -32,6 +35,7 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Rect; import android.graphics.RectF; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -77,6 +81,7 @@ public class WallpaperLocalColorExtractorTest extends SysuiTestCase { private Executor mBackgroundExecutor; private int mColorsProcessed; + private int mLocalColorsProcessed; private int mMiniBitmapUpdatedCount; private int mActivatedCount; private int mDeactivatedCount; @@ -93,6 +98,7 @@ public class WallpaperLocalColorExtractorTest extends SysuiTestCase { private void resetCounters() { mColorsProcessed = 0; + mLocalColorsProcessed = 0; mMiniBitmapUpdatedCount = 0; mActivatedCount = 0; mDeactivatedCount = 0; @@ -112,10 +118,14 @@ public class WallpaperLocalColorExtractorTest extends SysuiTestCase { new Object(), new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { @Override + public void onColorsProcessed() { + mColorsProcessed++; + } + @Override public void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { assertThat(regions.size()).isEqualTo(colors.size()); - mColorsProcessed += regions.size(); + mLocalColorsProcessed += regions.size(); } @Override @@ -148,8 +158,10 @@ public class WallpaperLocalColorExtractorTest extends SysuiTestCase { .when(spyColorExtractor) .createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); - doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0))) - .when(spyColorExtractor).getLocalWallpaperColors(any(Rect.class)); + WallpaperColors colors = new WallpaperColors( + Color.valueOf(0), Color.valueOf(0), Color.valueOf(0)); + doReturn(colors).when(spyColorExtractor).getLocalWallpaperColors(any(Rect.class)); + doReturn(colors).when(spyColorExtractor).getWallpaperColors(any(Bitmap.class), anyFloat()); return spyColorExtractor; } @@ -244,7 +256,7 @@ public class WallpaperLocalColorExtractorTest extends SysuiTestCase { assertThat(mActivatedCount).isEqualTo(1); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); - assertThat(mColorsProcessed).isEqualTo(regions.size()); + assertThat(mLocalColorsProcessed).isEqualTo(regions.size()); spyColorExtractor.removeLocalColorAreas(regions); assertThat(mDeactivatedCount).isEqualTo(1); @@ -329,12 +341,69 @@ public class WallpaperLocalColorExtractorTest extends SysuiTestCase { spyColorExtractor.onBitmapChanged(newBitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); } - assertThat(mColorsProcessed).isEqualTo(regions.size()); + assertThat(mLocalColorsProcessed).isEqualTo(regions.size()); } spyColorExtractor.removeLocalColorAreas(regions); assertThat(mDeactivatedCount).isEqualTo(1); } + /** + * Test that after the bitmap changes, the colors are computed only if asked via onComputeColors + */ + @Test + @EnableFlags(FLAG_OFFLOAD_COLOR_EXTRACTION) + public void testRecomputeColors() { + resetCounters(); + Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); + spyColorExtractor.onBitmapChanged(bitmap); + assertThat(mColorsProcessed).isEqualTo(0); + spyColorExtractor.onComputeColors(); + assertThat(mColorsProcessed).isEqualTo(1); + } + + /** + * Test that after onComputeColors is called, the colors are computed once the bitmap is loaded + */ + @Test + @EnableFlags(FLAG_OFFLOAD_COLOR_EXTRACTION) + public void testRecomputeColorsBeforeBitmapLoaded() { + resetCounters(); + Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); + spyColorExtractor.onComputeColors(); + spyColorExtractor.onBitmapChanged(bitmap); + assertThat(mColorsProcessed).isEqualTo(1); + } + + /** + * Test that after the dim changes, the colors are computed if the bitmap is already loaded + */ + @Test + @EnableFlags(FLAG_OFFLOAD_COLOR_EXTRACTION) + public void testRecomputeColorsOnDimChanged() { + resetCounters(); + Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); + spyColorExtractor.onBitmapChanged(bitmap); + spyColorExtractor.onDimAmountChanged(0.5f); + assertThat(mColorsProcessed).isEqualTo(1); + } + + /** + * Test that after the dim changes, the colors will be recomputed once the bitmap is loaded + */ + @Test + @EnableFlags(FLAG_OFFLOAD_COLOR_EXTRACTION) + public void testRecomputeColorsOnDimChangedBeforeBitmapLoaded() { + resetCounters(); + Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); + spyColorExtractor.onDimAmountChanged(0.3f); + spyColorExtractor.onBitmapChanged(bitmap); + assertThat(mColorsProcessed).isEqualTo(1); + } + @Test public void testCleanUp() { resetCounters(); @@ -346,6 +415,6 @@ public class WallpaperLocalColorExtractorTest extends SysuiTestCase { assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); spyColorExtractor.cleanUp(); spyColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS)); - assertThat(mColorsProcessed).isEqualTo(0); + assertThat(mLocalColorsProcessed).isEqualTo(0); } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt index f65c74fcebc8..c1d2ad6e1be3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt @@ -22,6 +22,8 @@ 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_MEDIA_IN_SCENE_CONTAINER import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT +import com.android.systemui.Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR +import com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_SYSUI import com.android.systemui.Flags.FLAG_SCENE_CONTAINER /** @@ -29,12 +31,14 @@ import com.android.systemui.Flags.FLAG_SCENE_CONTAINER * that feature. It is also picked up by [SceneContainerRule] to set non-aconfig prerequisites. */ @EnableFlags( - FLAG_SCENE_CONTAINER, - FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, - FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, FLAG_COMPOSE_LOCKSCREEN, - FLAG_MEDIA_IN_SCENE_CONTAINER, + FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, FLAG_KEYGUARD_WM_STATE_REFACTOR, + FLAG_MEDIA_IN_SCENE_CONTAINER, + FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, + FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR, + FLAG_PREDICTIVE_BACK_SYSUI, + FLAG_SCENE_CONTAINER, ) @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt index d791e949f853..12165cdc5658 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt @@ -20,4 +20,4 @@ import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.kosmos.Kosmos val Kosmos.keyguardClockInteractor by - Kosmos.Fixture { KeyguardClockInteractor(keyguardClockRepository = keyguardClockRepository) } + Kosmos.Fixture { KeyguardClockInteractor(keyguardClockRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt index e6651a44236f..f86e9b7216ce 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelKosmos.kt @@ -20,6 +20,8 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -30,5 +32,7 @@ var Kosmos.occludedToLockscreenTransitionViewModel by Fixture { deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, configurationInteractor = configurationInteractor, animationFlow = keyguardTransitionAnimationFlow, + keyguardInteractor = keyguardInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, ) } 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 1b23296ec4d3..afc8f309f6d2 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 @@ -49,6 +49,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.sceneDataSource +import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository @@ -106,6 +107,7 @@ class KosmosJavaAdapter( val sharedNotificationContainerInteractor by lazy { kosmos.sharedNotificationContainerInteractor } + val brightnessMirrorShowingInteractor by lazy { kosmos.brightnessMirrorShowingInteractor } init { kosmos.applicationContext = testCase.context diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt index 1ce26109ed04..0de76c802faf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt @@ -35,7 +35,6 @@ import com.android.systemui.plugins.qs.QSFactory import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractorImpl import com.android.systemui.qs.footer.foregroundServicesRepository import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel -import com.android.systemui.qs.tiles.di.NewQSTileFactory import com.android.systemui.security.data.repository.securityRepository import com.android.systemui.settings.userTracker import com.android.systemui.statusbar.policy.deviceProvisionedController @@ -49,7 +48,6 @@ val Kosmos.qsEventLogger: QsEventLoggerFake by Fixture { QsEventLoggerFake(uiEventLoggerFake, instanceIdSequenceFake) } -var Kosmos.newQSTileFactory by Fixture<NewQSTileFactory>() var Kosmos.qsTileFactory by Fixture<QSFactory>() val Kosmos.fgsManagerController by Fixture { FakeFgsManagerController() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt index 9ef44c4b9085..b870039982f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt @@ -21,7 +21,6 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.qs.external.customTileStatePersister import com.android.systemui.qs.external.tileLifecycleManagerFactory -import com.android.systemui.qs.newQSTileFactory import com.android.systemui.qs.pipeline.data.repository.customTileAddedRepository import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository import com.android.systemui.qs.pipeline.data.repository.minimumTilesRepository @@ -29,6 +28,7 @@ import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository import com.android.systemui.qs.pipeline.shared.logging.qsLogger import com.android.systemui.qs.pipeline.shared.pipelineFlagsRepository import com.android.systemui.qs.qsTileFactory +import com.android.systemui.qs.tiles.di.newQSTileFactory import com.android.systemui.settings.userTracker import com.android.systemui.user.data.repository.userRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt new file mode 100644 index 000000000000..5c4b39081143 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.di + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel +import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider +import com.android.systemui.qs.tiles.viewmodel.qsTileViewModelAdaperFactory +import com.android.systemui.util.mockito.mock +import javax.inject.Provider +import org.mockito.Mockito + +var Kosmos.newFactoryTileMap by Kosmos.Fixture { emptyMap<String, Provider<QSTileViewModel>>() } + +val Kosmos.newQSTileFactory by + Kosmos.Fixture { + NewQSTileFactory( + qSTileConfigProvider, + qsTileViewModelAdaperFactory, + newFactoryTileMap, + mock(Mockito.withSettings().defaultAnswer(Mockito.RETURNS_MOCKS)), + mock(Mockito.withSettings().defaultAnswer(Mockito.RETURNS_MOCKS)), + ) + } diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/strings.xml b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderKosmos.kt index bbab5e0477f7..1d579797bcb3 100644 --- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/strings.xml +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProviderKosmos.kt @@ -1,13 +1,11 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright (c) 2019, 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. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ ---> -<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <!-- Name of overlay [CHAR LIMIT=64] --> - <string name="navigation_bar_mode_title" translatable="false">Gestural Navigation Bar</string> -</resources>
\ No newline at end of file + +package com.android.systemui.qs.tiles.viewmodel + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeQSTileConfigProvider by Kosmos.Fixture { FakeQSTileConfigProvider() } +var Kosmos.qSTileConfigProvider: QSTileConfigProvider by Kosmos.Fixture { fakeQSTileConfigProvider } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapterKosmos.kt new file mode 100644 index 000000000000..a90876551d20 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapterKosmos.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.util.mockito.mock + +val Kosmos.qsTileViewModelAdaperFactory by + Kosmos.Fixture { + object : QSTileViewModelAdapter.Factory { + override fun create(qsTileViewModel: QSTileViewModel): QSTileViewModelAdapter { + return QSTileViewModelAdapter( + applicationCoroutineScope, + mock(), + qsTileViewModel, + ) + } + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt index 4d902fa35204..a654d6fc239a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.ui.adapter import android.content.Context import android.view.View +import com.android.systemui.settings.brightness.MirrorController import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -41,6 +42,9 @@ class FakeQSSceneAdapter( private val _navBarPadding = MutableStateFlow<Int>(0) val navBarPadding = _navBarPadding.asStateFlow() + var brightnessMirrorController: MirrorController? = null + private set + override var isQsFullyCollapsed: Boolean = true override suspend fun inflate(context: Context) { @@ -60,4 +64,12 @@ class FakeQSSceneAdapter( override suspend fun applyBottomNavBarPadding(padding: Int) { _navBarPadding.value = padding } + + override fun requestCloseCustomizer() { + _customizing.value = false + } + + override fun setBrightnessMirrorController(mirrorController: MirrorController?) { + brightnessMirrorController = mirrorController + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt new file mode 100644 index 000000000000..8b7e5d8f54c5 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.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.settings + +import com.android.internal.logging.uiEventLogger +import com.android.systemui.classifier.falsingManager +import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.activityStarter +import com.android.systemui.settings.brightness.BrightnessSliderController +import com.android.systemui.util.time.systemClock + +/** This factory creates empty mocks. */ +var Kosmos.brightnessSliderControllerFactory by + Kosmos.Fixture<BrightnessSliderController.Factory> { + BrightnessSliderController.Factory( + falsingManager, + uiEventLogger, + vibratorHelper, + systemClock, + activityStarter, + ) + } diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/data/repository/BrightnessMirrorShowingRepositoryKosmos.kt index be1f081d5b8f..6db46499cea9 100644 --- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/data/repository/BrightnessMirrorShowingRepositoryKosmos.kt @@ -1,13 +1,11 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright (c) 2023, The Android Open Source Project +/* + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ ---> -<resources> - <!-- If true, attach the navigation bar to the app during app transition --> - <bool name="config_attachNavBarToAppDuringTransition">false</bool> -</resources> + +package com.android.systemui.settings.brightness.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.brightnessMirrorShowingRepository by + Kosmos.Fixture { BrightnessMirrorShowingRepository() } diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/domain/interactor/BrightnessMirrorShowingInteractorKosmos.kt index be1f081d5b8f..8f6b829f0021 100644 --- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/domain/interactor/BrightnessMirrorShowingInteractorKosmos.kt @@ -1,13 +1,11 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright (c) 2023, The Android Open Source Project +/* + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ ---> -<resources> - <!-- If true, attach the navigation bar to the app during app transition --> - <bool name="config_attachNavBarToAppDuringTransition">false</bool> -</resources> + +package com.android.systemui.settings.brightness.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.settings.brightness.data.repository.brightnessMirrorShowingRepository + +val Kosmos.brightnessMirrorShowingInteractor by + Kosmos.Fixture { BrightnessMirrorShowingInteractor(brightnessMirrorShowingRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt new file mode 100644 index 000000000000..8fb370caee09 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.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.settings.brightness.ui.viewmodel + +import android.content.res.mainResources +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor +import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel +import com.android.systemui.settings.brightnessSliderControllerFactory + +val Kosmos.brightnessMirrorViewModel by + Kosmos.Fixture { + BrightnessMirrorViewModel( + brightnessMirrorShowingInteractor, + mainResources, + brightnessSliderControllerFactory, + ) + } 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 4a2eaf0f7bf6..d08855f190ed 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 @@ -21,6 +21,7 @@ package com.android.systemui.shade import com.android.systemui.assist.AssistManager 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.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher @@ -58,6 +59,7 @@ val Kosmos.shadeControllerSceneImpl by statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(), notificationShadeWindowController = mock<NotificationShadeWindowController>(), assistManagerLazy = { mock<AssistManager>() }, + deviceUnlockedInteractor = deviceUnlockedInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt index bada2a61995d..10cc13697d96 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModelKosmos.kt @@ -23,8 +23,8 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor -val Kosmos.notificationStackAppearanceViewModel by Fixture { - NotificationStackAppearanceViewModel( +val Kosmos.notificationScrollViewModel by Fixture { + NotificationScrollViewModel( dumpManager = dumpManager, stackAppearanceInteractor = notificationStackAppearanceInteractor, shadeInteractor = shadeInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index 29faa58d674a..b2492110f849 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import com.android.systemui.dump.dumpManager import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos @@ -26,6 +27,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.notif val Kosmos.notificationsPlaceholderViewModel by Fixture { NotificationsPlaceholderViewModel( + dumpManager = dumpManager, interactor = notificationStackAppearanceInteractor, shadeInteractor = shadeInteractor, flags = sceneContainerFlags, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index de0cc6590593..d2de835ad954 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -43,6 +43,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope 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.domain.interactor.sharedNotificationContainerInteractor import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,6 +56,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, shadeInteractor = shadeInteractor, + notificationStackAppearanceInteractor = notificationStackAppearanceInteractor, alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/FullscreenLightRevealAnimationKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/FullscreenLightRevealAnimationKosmos.kt new file mode 100644 index 000000000000..43d6c48d4069 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/unfold/FullscreenLightRevealAnimationKosmos.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.unfold + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +var Kosmos.fullscreenLightRevealAnimationController by Fixture { + mock<FullscreenLightRevealAnimationController>() +} +var Kosmos.fullscreenLightRevealAnimationControllerFactory by Fixture { + var mockControllerFactory = mock<FullscreenLightRevealAnimationController.Factory>() + whenever(mockControllerFactory.create(any(), any(), any())) + .thenReturn(fullscreenLightRevealAnimationController) + mockControllerFactory +} diff --git a/packages/overlays/Android.bp b/packages/overlays/Android.bp index 5e001fba6aa1..5075f6322d1b 100644 --- a/packages/overlays/Android.bp +++ b/packages/overlays/Android.bp @@ -30,9 +30,6 @@ phony { "FontNotoSerifSourceOverlay", "NavigationBarMode3ButtonOverlay", "NavigationBarModeGesturalOverlay", - "NavigationBarModeGesturalOverlayNarrowBack", - "NavigationBarModeGesturalOverlayWideBack", - "NavigationBarModeGesturalOverlayExtraWideBack", "TransparentNavigationBarOverlay", "NotesRoleEnabledOverlay", "preinstalled-packages-platform-overlays.xml", diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/Android.bp b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/Android.bp deleted file mode 100644 index 28f9f3341307..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/Android.bp +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright 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 { - // 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"], -} - -runtime_resource_overlay { - name: "NavigationBarModeGesturalOverlayExtraWideBack", - theme: "NavigationBarModeGesturalExtraWideBack", - product_specific: true, -} diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/AndroidManifest.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/AndroidManifest.xml deleted file mode 100644 index ba7bebac16a4..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ -<!-- -/** - * 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. - */ ---> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.internal.systemui.navbar.gestural_extra_wide_back" - android:versionCode="1" - android:versionName="1.0"> - <overlay android:targetPackage="android" - android:category="com.android.internal.navigation_bar_mode" - android:priority="1"/> - - <application android:label="@string/navigation_bar_mode_title" android:hasCode="false"/> -</manifest>
\ No newline at end of file diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/config.xml deleted file mode 100644 index 120a4893811f..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/config.xml +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * 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. - */ ---> -<resources> - <!-- Controls the navigation bar interaction mode: - 0: 3 button mode (back, home, overview buttons) - 1: 2 button mode (back, home buttons + swipe up for overview) - 2: gestures only for back, home and overview --> - <integer name="config_navBarInteractionMode">2</integer> - - <!-- Controls whether the nav bar can move from the bottom to the side in landscape. - Only applies if the device display is not square. --> - <bool name="config_navBarCanMove">false</bool> - - <!-- Controls whether the navigation bar lets through taps. --> - <bool name="config_navBarTapThrough">true</bool> - - <!-- Controls whether the IME renders the back and IME switcher buttons or not. --> - <bool name="config_imeDrawsImeNavBar">true</bool> - - <!-- Controls the size of the back gesture inset. --> - <dimen name="config_backGestureInset">40dp</dimen> - - <!-- Controls whether the navbar needs a scrim with - {@link Window#setEnsuringNavigationBarContrastWhenTransparent}. --> - <bool name="config_navBarNeedsScrim">false</bool> - - <!-- Controls the opacity of the navigation bar depending on the visibility of the - various workspace stacks. - 0 - Nav bar is always opaque when either the freeform stack or docked stack is visible. - 1 - Nav bar is always translucent when the freeform stack is visible, otherwise always - opaque. - 2 - Nav bar is never forced opaque. - --> - <integer name="config_navBarOpacityMode">2</integer> - - <!-- Controls whether seamless rotation should be allowed even though the navbar can move - (which normally prevents seamless rotation). --> - <bool name="config_allowSeamlessRotationDespiteNavBarMoving">true</bool> - - <!-- Controls whether the side edge gestures can always trigger the transient nav bar to - show. --> - <bool name="config_navBarAlwaysShowOnSideEdgeGesture">true</bool> - - <!-- If true, attach the navigation bar to the app during app transition --> - <bool name="config_attachNavBarToAppDuringTransition">true</bool> -</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml deleted file mode 100644 index 674bc749bc11..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * 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. - */ ---> -<resources> - <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_height">24dp</dimen> - <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height --> - <dimen name="navigation_bar_height_landscape">24dp</dimen> - <!-- Width of the navigation bar when it is placed vertically on the screen --> - <dimen name="navigation_bar_width">24dp</dimen> - <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_frame_height">48dp</dimen> - <!-- The height of the bottom navigation gesture area. --> - <dimen name="navigation_bar_gesture_height">32dp</dimen> -</resources>
\ No newline at end of file diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/Android.bp b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/Android.bp deleted file mode 100644 index f8a56030e0e4..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/Android.bp +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright 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 { - // 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"], -} - -runtime_resource_overlay { - name: "NavigationBarModeGesturalOverlayNarrowBack", - theme: "NavigationBarModeGesturalNarrowBack", - product_specific: true, -} diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/AndroidManifest.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/AndroidManifest.xml deleted file mode 100644 index 8de91c0cf8ab..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ -<!-- -/** - * 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. - */ ---> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.internal.systemui.navbar.gestural_narrow_back" - android:versionCode="1" - android:versionName="1.0"> - <overlay android:targetPackage="android" - android:category="com.android.internal.navigation_bar_mode" - android:priority="1"/> - - <application android:label="@string/navigation_bar_mode_title" android:hasCode="false"/> -</manifest>
\ No newline at end of file diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/config.xml deleted file mode 100644 index c18d892ec5b0..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/config.xml +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * 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. - */ ---> -<resources> - <!-- Controls the navigation bar interaction mode: - 0: 3 button mode (back, home, overview buttons) - 1: 2 button mode (back, home buttons + swipe up for overview) - 2: gestures only for back, home and overview --> - <integer name="config_navBarInteractionMode">2</integer> - - <!-- Controls whether the nav bar can move from the bottom to the side in landscape. - Only applies if the device display is not square. --> - <bool name="config_navBarCanMove">false</bool> - - <!-- Controls whether the navigation bar lets through taps. --> - <bool name="config_navBarTapThrough">true</bool> - - <!-- Controls whether the IME renders the back and IME switcher buttons or not. --> - <bool name="config_imeDrawsImeNavBar">true</bool> - - <!-- Controls the size of the back gesture inset. --> - <dimen name="config_backGestureInset">18dp</dimen> - - <!-- Controls whether the navbar needs a scrim with - {@link Window#setEnsuringNavigationBarContrastWhenTransparent}. --> - <bool name="config_navBarNeedsScrim">false</bool> - - <!-- Controls the opacity of the navigation bar depending on the visibility of the - various workspace stacks. - 0 - Nav bar is always opaque when either the freeform stack or docked stack is visible. - 1 - Nav bar is always translucent when the freeform stack is visible, otherwise always - opaque. - 2 - Nav bar is never forced opaque. - --> - <integer name="config_navBarOpacityMode">2</integer> - - <!-- Controls whether seamless rotation should be allowed even though the navbar can move - (which normally prevents seamless rotation). --> - <bool name="config_allowSeamlessRotationDespiteNavBarMoving">true</bool> - - <!-- Controls whether the side edge gestures can always trigger the transient nav bar to - show. --> - <bool name="config_navBarAlwaysShowOnSideEdgeGesture">true</bool> - - <!-- If true, attach the navigation bar to the app during app transition --> - <bool name="config_attachNavBarToAppDuringTransition">true</bool> -</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml deleted file mode 100644 index 674bc749bc11..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * 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. - */ ---> -<resources> - <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_height">24dp</dimen> - <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height --> - <dimen name="navigation_bar_height_landscape">24dp</dimen> - <!-- Width of the navigation bar when it is placed vertically on the screen --> - <dimen name="navigation_bar_width">24dp</dimen> - <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_frame_height">48dp</dimen> - <!-- The height of the bottom navigation gesture area. --> - <dimen name="navigation_bar_gesture_height">32dp</dimen> -</resources>
\ No newline at end of file diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/strings.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/strings.xml deleted file mode 100644 index bbab5e0477f7..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/strings.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * 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. - */ ---> -<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <!-- Name of overlay [CHAR LIMIT=64] --> - <string name="navigation_bar_mode_title" translatable="false">Gestural Navigation Bar</string> -</resources>
\ No newline at end of file diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/Android.bp b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/Android.bp deleted file mode 100644 index 60ee6d540dc8..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/Android.bp +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright 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 { - // 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"], -} - -runtime_resource_overlay { - name: "NavigationBarModeGesturalOverlayWideBack", - theme: "NavigationBarModeGesturalWideBack", - product_specific: true, -} diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/AndroidManifest.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/AndroidManifest.xml deleted file mode 100644 index daf461382b28..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ -<!-- -/** - * Copyright (c) 2018, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ ---> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.internal.systemui.navbar.gestural_wide_back" - android:versionCode="1" - android:versionName="1.0"> - <overlay android:targetPackage="android" - android:category="com.android.internal.navigation_bar_mode" - android:priority="1"/> - - <application android:label="@string/navigation_bar_mode_title" android:hasCode="false"/> -</manifest>
\ No newline at end of file diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/config.xml deleted file mode 100644 index 877b5f82c28e..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/config.xml +++ /dev/null @@ -1,62 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * 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. - */ ---> -<resources> - <!-- Controls the navigation bar interaction mode: - 0: 3 button mode (back, home, overview buttons) - 1: 2 button mode (back, home buttons + swipe up for overview) - 2: gestures only for back, home and overview --> - <integer name="config_navBarInteractionMode">2</integer> - - <!-- Controls whether the nav bar can move from the bottom to the side in landscape. - Only applies if the device display is not square. --> - <bool name="config_navBarCanMove">false</bool> - - <!-- Controls whether the navigation bar lets through taps. --> - <bool name="config_navBarTapThrough">true</bool> - - <!-- Controls whether the IME renders the back and IME switcher buttons or not. --> - <bool name="config_imeDrawsImeNavBar">true</bool> - - <!-- Controls the size of the back gesture inset. --> - <dimen name="config_backGestureInset">32dp</dimen> - - <!-- Controls whether the navbar needs a scrim with - {@link Window#setEnsuringNavigationBarContrastWhenTransparent}. --> - <bool name="config_navBarNeedsScrim">false</bool> - - <!-- Controls the opacity of the navigation bar depending on the visibility of the - various workspace stacks. - 0 - Nav bar is always opaque when either the freeform stack or docked stack is visible. - 1 - Nav bar is always translucent when the freeform stack is visible, otherwise always - opaque. - 2 - Nav bar is never forced opaque. - --> - <integer name="config_navBarOpacityMode">2</integer> - - <!-- Controls whether seamless rotation should be allowed even though the navbar can move - (which normally prevents seamless rotation). --> - <bool name="config_allowSeamlessRotationDespiteNavBarMoving">true</bool> - - <!-- Controls whether the side edge gestures can always trigger the transient nav bar to - show. --> - <bool name="config_navBarAlwaysShowOnSideEdgeGesture">true</bool> - - <!-- If true, attach the navigation bar to the app during app transition --> - <bool name="config_attachNavBarToAppDuringTransition">true</bool> -</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml deleted file mode 100644 index 674bc749bc11..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * 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. - */ ---> -<resources> - <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_height">24dp</dimen> - <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height --> - <dimen name="navigation_bar_height_landscape">24dp</dimen> - <!-- Width of the navigation bar when it is placed vertically on the screen --> - <dimen name="navigation_bar_width">24dp</dimen> - <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_frame_height">48dp</dimen> - <!-- The height of the bottom navigation gesture area. --> - <dimen name="navigation_bar_gesture_height">32dp</dimen> -</resources>
\ No newline at end of file diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/strings.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/strings.xml deleted file mode 100644 index bbab5e0477f7..000000000000 --- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/strings.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * 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. - */ ---> -<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <!-- Name of overlay [CHAR LIMIT=64] --> - <string name="navigation_bar_mode_title" translatable="false">Gestural Navigation Bar</string> -</resources>
\ No newline at end of file diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING index a5b28ad550ba..e77f846ffaa6 100644 --- a/ravenwood/TEST_MAPPING +++ b/ravenwood/TEST_MAPPING @@ -5,6 +5,17 @@ }, { "name": "RavenwoodBivalentTest_device" + }, + { + "name": "SystemUIGoogleTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] } ], "ravenwood-presubmit": [ diff --git a/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp b/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp index 5e66b29b370a..83f756e9a0bf 100644 --- a/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp +++ b/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp @@ -42,7 +42,7 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) ALOGI("%s: JNI_OnLoad", __FILE__); int res = jniRegisterNativeMethods(env, - "com/android/platform/test/ravenwood/bivalenttest/RavenwoodJniTest", + "com/android/ravenwoodtest/bivalenttest/RavenwoodJniTest", sMethods, NELEM(sMethods)); if (res < 0) { return res; diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodAndroidApiTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodAndroidApiTest.java new file mode 100644 index 000000000000..d91e73438fa3 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodAndroidApiTest.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.ravenwoodtest.bivalenttest; + +import static org.junit.Assert.assertEquals; + +import android.util.ArrayMap; +import android.util.Size; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +// Tests for calling simple Android APIs. +@RunWith(AndroidJUnit4.class) +public class RavenwoodAndroidApiTest { + @Test + public void testArrayMapSimple() { + final Map<String, String> map = new ArrayMap<>(); + + map.put("key1", "value1"); + assertEquals("value1", map.get("key1")); + } + + @Test + public void testSizeSimple() { + final var size = new Size(1, 2); + + assertEquals(2, size.getHeight()); + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java new file mode 100644 index 000000000000..3a24c0e829a4 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.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.ravenwoodtest.bivalenttest; + +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.ravenwood.RavenwoodClassRule; +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@DisabledOnRavenwood +public class RavenwoodClassRuleDeviceOnlyTest { + @ClassRule + public static final RavenwoodClassRule sRavenwood = new RavenwoodClassRule(); + + @Test + public void testDeviceOnly() { + Assert.assertFalse(RavenwoodRule.isOnRavenwood()); + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java new file mode 100644 index 000000000000..aa33dc3392b0 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.bivalenttest; + +import android.platform.test.ravenwood.RavenwoodClassRule; +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +// TODO: atest RavenwoodBivalentTest_device fails with the following message. +// `RUNNER ERROR: Instrumentation reported numtests=7 but only ran 6` +// @android.platform.test.annotations.DisabledOnNonRavenwood +// Figure it out and then make DisabledOnNonRavenwood support TYPEs as well. +@Ignore +public class RavenwoodClassRuleRavenwoodOnlyTest { + @ClassRule + public static final RavenwoodClassRule sRavenwood = new RavenwoodClassRule(); + + @Test + public void testRavenwoodOnly() { + Assert.assertTrue(RavenwoodRule.isOnRavenwood()); + } +} diff --git a/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodJniTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodJniTest.java index 3b106da74ed4..59467e9d7d14 100644 --- a/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodJniTest.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodJniTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.platform.test.ravenwood.bivalenttest; +package com.android.ravenwoodtest.bivalenttest; import static junit.framework.Assert.assertEquals; diff --git a/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java index 4b650b4bc5e1..3edca7ea5016 100644 --- a/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.platform.test.ravenwood.bivalenttest; +package com.android.ravenwoodtest.bivalenttest; import android.platform.test.annotations.DisabledOnNonRavenwood; import android.platform.test.annotations.DisabledOnRavenwood; diff --git a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java index 2cd585ff6c9c..f1e33cb686f1 100644 --- a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java +++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.platform.test.ravenwood.coretest; +package com.android.ravenwoodtest.coretest; import android.platform.test.ravenwood.RavenwoodRule; @@ -40,7 +40,7 @@ public class RavenwoodTestRunnerValidationTest { public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood); public RavenwoodTestRunnerValidationTest() { - Assume.assumeTrue(mRavenwood._ravenwood_private$isOptionalValidationEnabled()); + Assume.assumeTrue(RavenwoodRule._$RavenwoodPrivate.isOptionalValidationEnabled()); // Because RavenwoodRule will throw this error before executing the test method, // we can't do it in the test method itself. // So instead, we initialize it here. diff --git a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java index 8ca34bafc2c6..2fb8074c850a 100644 --- a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java +++ b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java @@ -31,13 +31,17 @@ import java.lang.annotation.Target; * which means if a test class has this annotation, you can't negate it in subclasses or * on a per-method basis. * + * THIS ANNOTATION CANNOT BE ADDED TO CLASSES AT THIS PONINT. + * See {@link com.android.ravenwoodtest.bivalenttest.RavenwoodClassRuleRavenwoodOnlyTest} + * for the reason. + * * The {@code RAVENWOOD_RUN_DISABLED_TESTS} environmental variable won't work because it won't be * propagated to the device. (We may support it in the future, possibly using a debug. sysprop.) * * @hide */ @Inherited -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DisabledOnNonRavenwood { /** diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java index 9a4d4886d6f0..f4b7ec360dbf 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java @@ -25,6 +25,7 @@ import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInP import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.EnabledOnRavenwood; +import org.junit.Assert; import org.junit.Assume; import org.junit.rules.TestRule; import org.junit.runner.Description; @@ -41,27 +42,16 @@ import org.junit.runners.model.Statement; public class RavenwoodClassRule implements TestRule { @Override public Statement apply(Statement base, Description description) { - // No special treatment when running outside Ravenwood; run tests as-is if (!IS_ON_RAVENWOOD) { - Assume.assumeTrue(shouldEnableOnDevice(description)); - return base; - } - - if (ENABLE_PROBE_IGNORED) { + // This should be "Assume", not Assert, but if we use assume here, the device side + // test runner would complain. + // See the TODO comment in RavenwoodClassRuleRavenwoodOnlyTest. + Assert.assertTrue(shouldEnableOnDevice(description)); + } else if (ENABLE_PROBE_IGNORED) { Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description)); - // Pass through to possible underlying RavenwoodRule for both environment - // configuration and handling method-level annotations - return base; } else { - return new Statement() { - @Override - public void evaluate() throws Throwable { - Assume.assumeTrue(shouldEnableOnRavenwood(description)); - // Pass through to possible underlying RavenwoodRule for both environment - // configuration and handling method-level annotations - base.evaluate(); - } - }; + Assume.assumeTrue(shouldEnableOnRavenwood(description)); } + return base; } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 52ea3402fa62..21d8019fcd87 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -28,7 +28,6 @@ import android.platform.test.annotations.DisabledOnNonRavenwood; import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.EnabledOnRavenwood; import android.platform.test.annotations.IgnoreUnderRavenwood; -import android.util.ArraySet; import org.junit.Assume; import org.junit.rules.TestRule; @@ -278,6 +277,12 @@ public class RavenwoodRule implements TestRule { return false; } } + final var clazz = description.getTestClass(); + if (clazz != null) { + if (clazz.getAnnotation(DisabledOnNonRavenwood.class) != null) { + return false; + } + } return true; } @@ -308,14 +313,17 @@ public class RavenwoodRule implements TestRule { } // Otherwise, consult any class-level annotations - if (description.getTestClass().getAnnotation(EnabledOnRavenwood.class) != null) { - return true; - } - if (description.getTestClass().getAnnotation(DisabledOnRavenwood.class) != null) { - return false; - } - if (description.getTestClass().getAnnotation(IgnoreUnderRavenwood.class) != null) { - return false; + final var clazz = description.getTestClass(); + if (clazz != null) { + if (description.getTestClass().getAnnotation(EnabledOnRavenwood.class) != null) { + return true; + } + if (description.getTestClass().getAnnotation(DisabledOnRavenwood.class) != null) { + return false; + } + if (description.getTestClass().getAnnotation(IgnoreUnderRavenwood.class) != null) { + return false; + } } // When no annotations have been requested, assume test should be included @@ -413,10 +421,9 @@ public class RavenwoodRule implements TestRule { }; } - /** - * Do not use it outside ravenwood core classes. - */ - public boolean _ravenwood_private$isOptionalValidationEnabled() { - return ENABLE_OPTIONAL_VALIDATION; + public static class _$RavenwoodPrivate { + public static boolean isOptionalValidationEnabled() { + return ENABLE_OPTIONAL_VALIDATION; + } } } diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java b/ravenwood/mockito/test/com/android/ravenwoodtest/mockito/RavenwoodMockitoDeviceOnlyTest.java index d02fe69d3168..d566977bd15c 100644 --- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java +++ b/ravenwood/mockito/test/com/android/ravenwoodtest/mockito/RavenwoodMockitoDeviceOnlyTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ravenwood.mockito; +package com.android.ravenwoodtest.mockito; import static com.google.common.truth.Truth.assertThat; diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java b/ravenwood/mockito/test/com/android/ravenwoodtest/mockito/RavenwoodMockitoRavenwoodOnlyTest.java index 0c137d5eaacc..aa2b7611da37 100644 --- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java +++ b/ravenwood/mockito/test/com/android/ravenwoodtest/mockito/RavenwoodMockitoRavenwoodOnlyTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ravenwood.mockito; +package com.android.ravenwoodtest.mockito; import static com.google.common.truth.Truth.assertThat; diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwoodtest/mockito/RavenwoodMockitoTest.java index 95667103bd21..fcc6c9cc447d 100644 --- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java +++ b/ravenwood/mockito/test/com/android/ravenwoodtest/mockito/RavenwoodMockitoTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ravenwood.mockito; +package com.android.ravenwoodtest.mockito; import static com.google.common.truth.Truth.assertThat; diff --git a/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesDependenciesTest.java index efe468d0df25..f833782bc8bb 100644 --- a/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java +++ b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesDependenciesTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.ravenwood; +package com.android.ravenwoodtest.servicestest; import static org.junit.Assert.assertEquals; diff --git a/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesTest.java b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java index c1dee5d2f55b..044239f06297 100644 --- a/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesTest.java +++ b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.ravenwood; +package com.android.ravenwoodtest.servicestest; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp index 467adc7a9628..7a99b605c4fb 100644 --- a/services/accessibility/Android.bp +++ b/services/accessibility/Android.bp @@ -58,6 +58,7 @@ java_library_static { aconfig_declarations { name: "com_android_server_accessibility_flags", package: "com.android.server.accessibility", + container: "system", srcs: [ "accessibility.aconfig", ], diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 04b19ffd4dfc..1d6399e24b2a 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.accessibility" +container: "system" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 0811c872d2eb..bbbc4aef55f3 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2734,10 +2734,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub userState.mComponentNameToServiceMap; boolean isUnlockingOrUnlocked = mUmi.isUserUnlockingOrUnlocked(userState.mUserId); + // Store the list of installed services. + mTempComponentNameSet.clear(); for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) { AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i); ComponentName componentName = ComponentName.unflattenFromString( installedService.getId()); + mTempComponentNameSet.add(componentName); AccessibilityServiceConnection service = componentNameToServiceMap.get(componentName); @@ -2797,6 +2800,25 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub audioManager.setAccessibilityServiceUids(mTempIntArray); } mActivityTaskManagerService.setAccessibilityServiceUids(mTempIntArray); + + // If any services have been removed, remove them from the enabled list and the touch + // exploration granted list. + boolean anyServiceRemoved = + userState.mEnabledServices.removeIf((comp) -> !mTempComponentNameSet.contains(comp)) + || userState.mTouchExplorationGrantedServices.removeIf( + (comp) -> !mTempComponentNameSet.contains(comp)); + if (anyServiceRemoved) { + // Update the enabled services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mEnabledServices, + userState.mUserId); + // Update the touch exploration granted services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + userState.mTouchExplorationGrantedServices, + userState.mUserId); + } updateAccessibilityEnabledSettingLocked(userState); } diff --git a/services/backup/Android.bp b/services/backup/Android.bp index 2a85eb66f6c5..e13746ed13dd 100644 --- a/services/backup/Android.bp +++ b/services/backup/Android.bp @@ -30,5 +30,6 @@ java_library_static { aconfig_declarations { name: "backup_flags", package: "com.android.server.backup", + container: "system", srcs: ["flags.aconfig"], } diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig index 74adfe08dba7..d53f949753d8 100644 --- a/services/backup/flags.aconfig +++ b/services/backup/flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.backup" +container: "system" flag { name: "enable_skipping_restore_launched_apps" @@ -58,4 +59,4 @@ flag { description: "Increase BMM logging coverage in restore at install flow." bug: "331749778" is_fixed_read_only: true -}
\ No newline at end of file +} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 4dae6d5f31e7..30e4a3eb77e6 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -52,6 +52,7 @@ import android.app.ActivityManagerInternal; import android.app.AppOpsManager; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.ecm.EnhancedConfirmationManager; import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.IAssociationRequestCallback; @@ -64,6 +65,7 @@ import android.companion.ObservingDevicePresenceRequest; import android.companion.datatransfer.PermissionSyncRequest; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -80,6 +82,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; +import android.permission.flags.Flags; import android.util.ArraySet; import android.util.ExceptionUtils; import android.util.Slog; @@ -448,15 +451,26 @@ public class CompanionDeviceManagerService extends SystemService { } return Binder.withCleanCallingIdentity(() -> { + final Intent intent; if (!isRestrictedSettingsAllowed(getContext(), callingPackage, callingUid)) { Slog.e(TAG, "Side loaded app must enable restricted " + "setting before request the notification access"); - return null; + if (Flags.enhancedConfirmationModeApisEnabled()) { + intent = getContext() + .getSystemService(EnhancedConfirmationManager.class) + .createRestrictedSettingDialogIntent(callingPackage, + AppOpsManager.OPSTR_ACCESS_NOTIFICATIONS); + } else { + return null; + } + } else { + intent = NotificationAccessConfirmationActivityContract.launcherIntent( + getContext(), userId, component); } + return PendingIntent.getActivityAsUser(getContext(), 0 /* request code */, - NotificationAccessConfirmationActivityContract.launcherIntent( - getContext(), userId, component), + intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT, null /* options */, 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 d7e766eed209..f397814f7ad7 100644 --- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java @@ -16,6 +16,8 @@ package com.android.server.companion.utils; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_SCAN; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED; @@ -209,7 +211,9 @@ public final class PermissionsUtils { */ public static void enforceCallerCanObserveDevicePresenceByUuid(@NonNull Context context) { if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) - != PERMISSION_GRANTED) { + != PERMISSION_GRANTED + || context.checkCallingPermission(BLUETOOTH_SCAN) != PERMISSION_GRANTED + || context.checkCallingPermission(BLUETOOTH_CONNECT) != PERMISSION_GRANTED) { throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have " + "permissions to request observing device presence base on the UUID"); } diff --git a/services/companion/java/com/android/server/companion/virtual/Android.bp b/services/companion/java/com/android/server/companion/virtual/Android.bp index 4a2030f9caff..66313e6ca957 100644 --- a/services/companion/java/com/android/server/companion/virtual/Android.bp +++ b/services/companion/java/com/android/server/companion/virtual/Android.bp @@ -10,6 +10,7 @@ java_aconfig_library { aconfig_declarations { name: "virtualdevice_flags", package: "com.android.server.companion.virtual", + container: "system", srcs: [ "flags.aconfig", ], 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 da90bdbb9041..215f6402fa76 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -28,8 +28,6 @@ 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; import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; -import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery; import android.annotation.EnforcePermission; @@ -1068,6 +1066,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override public boolean hasCustomAudioInputSupport() throws RemoteException { + return hasCustomAudioInputSupportInternal(); + } + + private boolean hasCustomAudioInputSupportInternal() { if (!Flags.vdmPublicApis()) { return false; } @@ -1108,10 +1110,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mParams.dump(fout, indent + indent); fout.println(indent + "mVirtualDisplayIds: "); synchronized (mVirtualDeviceLock) { - fout.println(" mDevicePolicies: " + mDevicePolicies); for (int i = 0; i < mVirtualDisplays.size(); i++) { fout.println(indent + " " + mVirtualDisplays.keyAt(i)); } + fout.println(" mDevicePolicies: " + mDevicePolicies); fout.println(indent + "mDefaultShowPointerIcon: " + mDefaultShowPointerIcon); } mInputController.dump(fout); @@ -1119,6 +1121,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub if (mVirtualCameraController != null) { mVirtualCameraController.dump(fout, indent); } + fout.println( + indent + "hasCustomAudioInputSupport: " + hasCustomAudioInputSupportInternal()); } // For display mirroring, we want to dispatch all key events to the source (default) display, @@ -1150,8 +1154,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub Flags.vdmCustomHome() ? mParams.getHomeComponent() : null; final GenericWindowPolicyController gwpc = new GenericWindowPolicyController( - FLAG_SECURE, - SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, mAttributionSource, getAllowedUserHandles(), activityLaunchAllowedByDefault, @@ -1265,7 +1269,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub // if the secure window is shown on a non-secure virtual display. DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); Display display = displayManager.getDisplay(displayId); - if ((display.getFlags() & FLAG_SECURE) == 0) { + if ((display.getFlags() & Display.FLAG_SECURE) == 0) { showToastWhereUidIsRunning(uid, com.android.internal.R.string.vdm_secure_window, Toast.LENGTH_LONG, mContext.getMainLooper()); diff --git a/services/companion/java/com/android/server/companion/virtual/flags.aconfig b/services/companion/java/com/android/server/companion/virtual/flags.aconfig index 6297e91e8705..616f5d09e13f 100644 --- a/services/companion/java/com/android/server/companion/virtual/flags.aconfig +++ b/services/companion/java/com/android/server/companion/virtual/flags.aconfig @@ -1,6 +1,7 @@ # OLD PACKAGE, DO NOT USE: Prefer `flags.aconfig` in core/java/android/companion/virtual # (or other custom files) to define your flags package: "com.android.server.companion.virtual" +container: "system" flag { name: "dump_history" diff --git a/services/core/Android.bp b/services/core/Android.bp index c7d99424f72b..fd7e4abc6d16 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -254,6 +254,7 @@ java_library_static { "net_flags_lib", "stats_flags_lib", "core_os_flags_lib", + "connectivity_flags_lib", ], javac_shard_size: 50, javacflags: [ diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java index ac19d8bc897f..4694e9fd44bc 100644 --- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -70,6 +70,8 @@ public final class SensitiveContentProtectionManagerService extends SystemServic NotificationListener mNotificationListener; @Nullable private MediaProjectionManager mProjectionManager; + + @GuardedBy("mSensitiveContentProtectionLock") @Nullable private MediaProjectionSession mMediaProjectionSession; @@ -98,6 +100,48 @@ public final class SensitiveContentProtectionManagerService extends SystemServic mIsExempted = isExempted; mSessionId = sessionId; } + + public void logProjectionSessionStart() { + FrameworkStatsLog.write( + SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION, + mSessionId, + mUid, + mIsExempted, + SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__START, + SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS + ); + } + + public void logProjectionSessionStop() { + FrameworkStatsLog.write( + SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION, + mSessionId, + mUid, + mIsExempted, + SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__STOP, + SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS + ); + } + + public void logAppBlocked(int uid) { + FrameworkStatsLog.write( + FrameworkStatsLog.SENSITIVE_CONTENT_APP_PROTECTION, + mSessionId, + uid, + mUid, + FrameworkStatsLog.SENSITIVE_CONTENT_APP_PROTECTION__STATE__BLOCKED + ); + } + + public void logAppUnblocked(int uid) { + FrameworkStatsLog.write( + FrameworkStatsLog.SENSITIVE_CONTENT_APP_PROTECTION, + mSessionId, + uid, + mUid, + FrameworkStatsLog.SENSITIVE_CONTENT_APP_PROTECTION__STATE__UNBLOCKED + ); + } } private final MediaProjectionManager.Callback mProjectionCallback = @@ -112,28 +156,11 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } finally { Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } - FrameworkStatsLog.write( - SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION, - mMediaProjectionSession.mSessionId, - mMediaProjectionSession.mUid, - mMediaProjectionSession.mIsExempted, - SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__START, - SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS - ); } @Override public void onStop(MediaProjectionInfo info) { if (DEBUG) Log.d(TAG, "onStop projection: " + info); - FrameworkStatsLog.write( - SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION, - mMediaProjectionSession.mSessionId, - mMediaProjectionSession.mUid, - mMediaProjectionSession.mIsExempted, - SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__STOP, - SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS - ); - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SensitiveContentProtectionManagerService.onProjectionStop"); try { @@ -242,16 +269,18 @@ public final class SensitiveContentProtectionManagerService extends SystemServic DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0; int uid = mPackageManagerInternal.getPackageUid(projectionInfo.getPackageName(), 0, projectionInfo.getUserHandle().getIdentifier()); - mMediaProjectionSession = new MediaProjectionSession( - uid, isPackageExempted || isFeatureDisabled, new Random().nextLong()); - - if (isPackageExempted || isFeatureDisabled) { - Log.w(TAG, "projection session is exempted, package =" - + projectionInfo.getPackageName() + ", isFeatureDisabled=" + isFeatureDisabled); - return; - } - synchronized (mSensitiveContentProtectionLock) { + mMediaProjectionSession = new MediaProjectionSession( + uid, isPackageExempted || isFeatureDisabled, new Random().nextLong()); + mMediaProjectionSession.logProjectionSessionStart(); + + if (isPackageExempted || isFeatureDisabled) { + Log.w(TAG, "projection session is exempted, package =" + + projectionInfo.getPackageName() + ", isFeatureDisabled=" + + isFeatureDisabled); + return; + } + mProjectionActive = true; if (sensitiveNotificationAppProtection()) { updateAppsThatShouldBlockScreenCapture(); @@ -266,7 +295,10 @@ public final class SensitiveContentProtectionManagerService extends SystemServic private void onProjectionEnd() { synchronized (mSensitiveContentProtectionLock) { mProjectionActive = false; - mMediaProjectionSession = null; + if (mMediaProjectionSession != null) { + mMediaProjectionSession.logProjectionSessionStop(); + mMediaProjectionSession = null; + } // notify windowmanager to clear any sensitive notifications observed during projection // session @@ -437,22 +469,14 @@ public final class SensitiveContentProtectionManagerService extends SystemServic packageInfos.add(packageInfo); if (isShowingSensitiveContent) { mWindowManager.addBlockScreenCaptureForApps(packageInfos); - FrameworkStatsLog.write( - FrameworkStatsLog.SENSITIVE_CONTENT_APP_PROTECTION, - mMediaProjectionSession.mSessionId, - uid, - mMediaProjectionSession.mUid, - FrameworkStatsLog.SENSITIVE_CONTENT_APP_PROTECTION__STATE__BLOCKED - ); + if (mMediaProjectionSession != null) { + mMediaProjectionSession.logAppBlocked(uid); + } } else { mWindowManager.removeBlockScreenCaptureForApps(packageInfos); - FrameworkStatsLog.write( - FrameworkStatsLog.SENSITIVE_CONTENT_APP_PROTECTION, - mMediaProjectionSession.mSessionId, - uid, - mMediaProjectionSession.mUid, - FrameworkStatsLog.SENSITIVE_CONTENT_APP_PROTECTION__STATE__UNBLOCKED - ); + if (mMediaProjectionSession != null) { + mMediaProjectionSession.logAppUnblocked(uid); + } } } } diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING index 9c4c634b7ce7..baae40b88e89 100644 --- a/services/core/java/com/android/server/TEST_MAPPING +++ b/services/core/java/com/android/server/TEST_MAPPING @@ -62,6 +62,18 @@ "file_patterns": ["SensorPrivacyService\\.java"] }, { + "name": "FrameworksMockingServicesTests", + "options": [ + { + "include-filter": "com.android.server.SensitiveContentProtectionManagerServiceContentTest" + }, + { + "include-filter": "com.android.server.SensitiveContentProtectionManagerServiceNotificationTest" + } + ], + "file_patterns": ["SensitiveContentProtectionManagerService\\.java"] + }, + { "name": "FrameworksServicesTests", "options": [ { diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 586b09517e92..603a95c31e5b 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -897,6 +897,11 @@ public class AccountManagerService } } for (String packageNameToNotify : accountRemovedReceivers) { + int currentVisibility = + resolveAccountVisibility(account, packageNameToNotify, accounts); + if (isVisible(currentVisibility)) { + continue; + } sendAccountRemovedBroadcast( account, packageNameToNotify, diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 0a2aaeba57b4..7ea82b095533 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -5555,6 +5555,7 @@ public final class ActiveServices { boolean enqueueOomAdj, @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy) throws TransactionTooLargeException { if (r.app != null && r.app.isThreadReady()) { + r.updateOomAdjSeq(); sendServiceArgsLocked(r, execInFg, false); return null; } diff --git a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java index 55b161ad6348..dcda5c228ceb 100644 --- a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java +++ b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java @@ -55,9 +55,7 @@ class ActivityManagerDebugConfig { static final boolean DEBUG_BACKGROUND_CHECK = DEBUG_ALL || false; static final boolean DEBUG_BACKUP = DEBUG_ALL || false; static final boolean DEBUG_BROADCAST = DEBUG_ALL || false; - static final boolean DEBUG_BROADCAST_BACKGROUND = DEBUG_BROADCAST || false; static final boolean DEBUG_BROADCAST_LIGHT = DEBUG_BROADCAST || false; - static final boolean DEBUG_BROADCAST_DEFERRAL = DEBUG_BROADCAST || false; static final boolean DEBUG_COMPACTION = DEBUG_ALL || false; static final boolean DEBUG_FREEZER = DEBUG_ALL || false; static final boolean DEBUG_LRU = DEBUG_ALL || false; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b20135c958cd..ad15ea90c45c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -635,7 +635,8 @@ public class ActivityManagerService extends IActivityManager.Stub static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; static final String EXTRA_BUGREPORT_TYPE = "android.intent.extra.BUGREPORT_TYPE"; static final String EXTRA_BUGREPORT_NONCE = "android.intent.extra.BUGREPORT_NONCE"; - + static final String EXTRA_EXTRA_ATTACHMENT_URI = + "android.intent.extra.EXTRA_ATTACHMENT_URI"; /** * It is now required for apps to explicitly set either * {@link android.content.Context#RECEIVER_EXPORTED} or @@ -724,6 +725,9 @@ public class ActivityManagerService extends IActivityManager.Stub // Whether we should use SCHED_FIFO for UI and RenderThreads. final boolean mUseFifoUiScheduling; + /** Whether some specified important processes are allowed to use FIFO priority. */ + boolean mAllowSpecifiedFifoScheduling = true; + @GuardedBy("this") private final SparseArray<IUnsafeIntentStrictModeCallback> mStrictModeCallbacks = new SparseArray<>(); @@ -1047,6 +1051,10 @@ public class ActivityManagerService extends IActivityManager.Stub @GuardedBy("this") final SparseArray<ImportanceToken> mImportantProcesses = new SparseArray<ImportanceToken>(); + /** The processes that are allowed to use SCHED_FIFO prorioty. */ + @GuardedBy("mProcLock") + final ArrayList<ProcessRecord> mSpecifiedFifoProcesses = new ArrayList<>(); + /** * List of records for processes that someone had tried to start before the * system was ready. We don't start them at that point, but ensure they @@ -4418,7 +4426,9 @@ public class ActivityManagerService extends IActivityManager.Stub packageName, null, userId); } - if (packageName == null || uninstalling || packageStateStopped) { + final boolean clearPendingIntentsForStoppedApp = (android.content.pm.Flags.stayStopped() + && packageStateStopped); + if (packageName == null || uninstalling || clearPendingIntentsForStoppedApp) { didSomething |= mPendingIntentController.removePendingIntentsForPackage( packageName, userId, appId, doit); } @@ -7654,6 +7664,16 @@ public class ActivityManagerService extends IActivityManager.Stub */ public void requestBugReportWithDescription(@Nullable String shareTitle, @Nullable String shareDescription, int bugreportType, long nonce) { + requestBugReportWithDescription(shareTitle, shareDescription, bugreportType, nonce, null); + } + + /** + * Takes a bugreport using bug report API ({@code BugreportManager}) which gets + * triggered by sending a broadcast to Shell. Optionally adds an extra attachment. + */ + public void requestBugReportWithDescription(@Nullable String shareTitle, + @Nullable String shareDescription, int bugreportType, long nonce, + @Nullable Uri extraAttachment) { String type = null; switch (bugreportType) { case BugreportParams.BUGREPORT_MODE_FULL: @@ -7708,6 +7728,10 @@ public class ActivityManagerService extends IActivityManager.Stub triggerShellBugreport.setPackage(SHELL_APP_PACKAGE); triggerShellBugreport.putExtra(EXTRA_BUGREPORT_TYPE, bugreportType); triggerShellBugreport.putExtra(EXTRA_BUGREPORT_NONCE, nonce); + if (extraAttachment != null) { + triggerShellBugreport.putExtra(EXTRA_EXTRA_ATTACHMENT_URI, extraAttachment); + triggerShellBugreport.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } triggerShellBugreport.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); triggerShellBugreport.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); if (shareTitle != null) { @@ -7761,6 +7785,15 @@ public class ActivityManagerService extends IActivityManager.Stub } /** + * Takes an interactive bugreport with a progress notification. Also attaches given file uri. + */ + @Override + public void requestBugReportWithExtraAttachment(@NonNull Uri extraAttachment) { + requestBugReportWithDescription(null, null, BugreportParams.BUGREPORT_MODE_INTERACTIVE, 0L, + extraAttachment); + } + + /** * Takes an interactive bugreport with a progress notification. Also, shows the given title and * description on the final share notification */ @@ -8218,6 +8251,27 @@ public class ActivityManagerService extends IActivityManager.Stub return false; } + /** + * Switches the priority between SCHED_FIFO and SCHED_OTHER for the main thread and render + * thread of the given process. + */ + @GuardedBy("mProcLock") + static void setFifoPriority(@NonNull ProcessRecord app, boolean enable) { + final int pid = app.getPid(); + final int renderThreadTid = app.getRenderThreadTid(); + if (enable) { + scheduleAsFifoPriority(pid, true /* suppressLogs */); + if (renderThreadTid != 0) { + scheduleAsFifoPriority(renderThreadTid, true /* suppressLogs */); + } + } else { + scheduleAsRegularPriority(pid, true /* suppressLogs */); + if (renderThreadTid != 0) { + scheduleAsRegularPriority(renderThreadTid, true /* suppressLogs */); + } + } + } + @Override public void setRenderThread(int tid) { synchronized (mProcLock) { @@ -8243,7 +8297,7 @@ public class ActivityManagerService extends IActivityManager.Stub // promote to FIFO now if (proc.mState.getCurrentSchedulingGroup() == ProcessList.SCHED_GROUP_TOP_APP) { if (DEBUG_OOM_ADJ) Slog.d("UI_FIFO", "Promoting " + tid + "out of band"); - if (mUseFifoUiScheduling) { + if (proc.useFifoUiScheduling()) { setThreadScheduler(proc.getRenderThreadTid(), SCHED_FIFO | SCHED_RESET_ON_FORK, 1); } else { @@ -11280,6 +11334,9 @@ public class ActivityManagerService extends IActivityManager.Stub if (mAlwaysFinishActivities) { pw.println(" mAlwaysFinishActivities=" + mAlwaysFinishActivities); } + if (mAllowSpecifiedFifoScheduling) { + pw.println(" mAllowSpecifiedFifoScheduling=true"); + } if (dumpAll) { pw.println(" Total persistent processes: " + numPers); pw.println(" mProcessesReady=" + mProcessesReady @@ -17348,6 +17405,12 @@ public class ActivityManagerService extends IActivityManager.Stub } } } + + if (com.android.window.flags.Flags.fifoPriorityForMajorUiProcesses()) { + synchronized (mProcLock) { + adjustFifoProcessesIfNeeded(uid, !active /* allowFifo */); + } + } } final boolean isCameraActiveForUid(@UserIdInt int uid) { @@ -17356,6 +17419,34 @@ public class ActivityManagerService extends IActivityManager.Stub } } + /** + * This is called when the given uid is using camera. If the uid has top process state, then + * cancel the FIFO priority of the high priority processes. + */ + @VisibleForTesting + @GuardedBy("mProcLock") + void adjustFifoProcessesIfNeeded(int preemptiveUid, boolean allowSpecifiedFifo) { + if (allowSpecifiedFifo == mAllowSpecifiedFifoScheduling) { + return; + } + if (!allowSpecifiedFifo) { + final UidRecord uidRec = mProcessList.mActiveUids.get(preemptiveUid); + if (uidRec == null || uidRec.getCurProcState() > PROCESS_STATE_TOP) { + // To avoid frequent switching by background camera usages, e.g. face unlock, + // face detection (auto rotation), screen attention (keep screen on). + return; + } + } + mAllowSpecifiedFifoScheduling = allowSpecifiedFifo; + for (int i = mSpecifiedFifoProcesses.size() - 1; i >= 0; i--) { + final ProcessRecord proc = mSpecifiedFifoProcesses.get(i); + if (proc.mState.getSetSchedGroup() != ProcessList.SCHED_GROUP_TOP_APP) { + continue; + } + setFifoPriority(proc, allowSpecifiedFifo /* enable */); + } + } + @GuardedBy("this") final void doStopUidLocked(int uid, final UidRecord uidRec) { mServices.stopInBackgroundLocked(uid); @@ -17622,7 +17713,8 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public boolean dumpHeap(String process, int userId, boolean managed, boolean mallocInfo, - boolean runGc, String path, ParcelFileDescriptor fd, RemoteCallback finishCallback) { + boolean runGc, String dumpBitmaps, + String path, ParcelFileDescriptor fd, RemoteCallback finishCallback) { try { // note: hijacking SET_ACTIVITY_WATCHER, but should be changed to // its own permission (same as profileControl). @@ -17656,7 +17748,8 @@ public class ActivityManagerService extends IActivityManager.Stub } }, null); - thread.dumpHeap(managed, mallocInfo, runGc, path, fd, intermediateCallback); + thread.dumpHeap(managed, mallocInfo, runGc, dumpBitmaps, + path, fd, intermediateCallback); fd = null; return true; } @@ -17868,9 +17961,35 @@ public class ActivityManagerService extends IActivityManager.Stub mUserController.setStopUserOnSwitch(value); } + /** @deprecated use {@link #stopUserWithCallback(int, IStopUserCallback)} instead */ + @Deprecated + @Override + public int stopUser(final int userId, + boolean stopProfileRegardlessOfParent, final IStopUserCallback callback) { + return stopUserExceptCertainProfiles(userId, stopProfileRegardlessOfParent, callback); + } + + /** Stops the given user. */ + @Override + public int stopUserWithCallback(@UserIdInt int userId, @Nullable IStopUserCallback callback) { + return mUserController.stopUser(userId, /* allowDelayedLocking= */ false, + /* callback= */ callback, /* keyEvictedCallback= */ null); + } + + /** + * Stops the given user. + * + * Usually, callers can just use @link{#stopUserWithCallback(int, IStopUserCallback)} instead. + * + * @param stopProfileRegardlessOfParent whether to stop the profile regardless of who its + * parent is, e.g. even if the parent is the current user; + * its value is irrelevant for non-profile users. + */ @Override - public int stopUser(final int userId, boolean force, final IStopUserCallback callback) { - return mUserController.stopUser(userId, force, /* allowDelayedLocking= */ false, + public int stopUserExceptCertainProfiles(@UserIdInt int userId, + boolean stopProfileRegardlessOfParent, @Nullable IStopUserCallback callback) { + return mUserController.stopUser(userId, + stopProfileRegardlessOfParent, /* allowDelayedLocking= */ false, /* callback= */ callback, /* keyEvictedCallback= */ null); } @@ -17879,11 +17998,9 @@ public class ActivityManagerService extends IActivityManager.Stub * stopping only if {@code config_multiuserDelayUserDataLocking} overlay is set true. * * <p>When delayed locking is not enabled through the overlay, this call becomes the same - * with {@link #stopUser(int, boolean, IStopUserCallback)} call. + * with {@link #stopUserWithCallback(int, IStopUserCallback)} call. * * @param userId User id to stop. - * @param force Force stop the user even if the user is related with system user or current - * user. * @param callback Callback called when user has stopped. * * @return {@link ActivityManager#USER_OP_SUCCESS} when user is stopped successfully. Returns @@ -17893,9 +18010,8 @@ public class ActivityManagerService extends IActivityManager.Stub // TODO(b/302662311): Add javadoc changes corresponding to the user property that allows // delayed locking behavior once the private space flag is finalized. @Override - public int stopUserWithDelayedLocking(final int userId, boolean force, - final IStopUserCallback callback) { - return mUserController.stopUser(userId, force, /* allowDelayedLocking= */ true, + public int stopUserWithDelayedLocking(@UserIdInt int userId, IStopUserCallback callback) { + return mUserController.stopUser(userId, /* allowDelayedLocking= */ true, /* callback= */ callback, /* keyEvictedCallback= */ null); } diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 5a97e87f53f7..e70722ca6579 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -1239,6 +1239,7 @@ final class ActivityManagerShellCommand extends ShellCommand { final PrintWriter err = getErrPrintWriter(); boolean managed = true; boolean mallocInfo = false; + String dumpBitmaps = null; int userId = UserHandle.USER_CURRENT; boolean runGc = false; @@ -1257,6 +1258,11 @@ final class ActivityManagerShellCommand extends ShellCommand { } else if (opt.equals("-m")) { managed = false; mallocInfo = true; + } else if (opt.equals("-b")) { + dumpBitmaps = getNextArg(); + if (dumpBitmaps == null) { + dumpBitmaps = "png"; // default to PNG in dumping bitmaps + } } else { err.println("Error: Unknown option: " + opt); return -1; @@ -1288,8 +1294,8 @@ final class ActivityManagerShellCommand extends ShellCommand { } }, null); - if (!mInterface.dumpHeap(process, userId, managed, mallocInfo, runGc, heapFile, fd, - finishCallback)) { + if (!mInterface.dumpHeap(process, userId, managed, mallocInfo, runGc, dumpBitmaps, + heapFile, fd, finishCallback)) { err.println("HEAP DUMP FAILED on process " + process); return -1; } @@ -2554,7 +2560,8 @@ final class ActivityManagerShellCommand extends ShellCommand { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "shell_runStopUser-" + userId + "-[stopUser]"); try { - int res = mInterface.stopUser(userId, force, callback); + int res = mInterface.stopUserExceptCertainProfiles( + userId, /* stopProfileRegardlessOfParent= */ force, callback); if (res != ActivityManager.USER_OP_SUCCESS) { String txt = ""; switch (res) { @@ -4284,11 +4291,14 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println(" --user <USER_ID> | current: When supplying a process name,"); pw.println(" specify user of process to profile; uses current user if not"); pw.println(" specified."); - pw.println(" dumpheap [--user <USER_ID> current] [-n] [-g] <PROCESS> <FILE>"); + pw.println(" dumpheap [--user <USER_ID> current] [-n] [-g] [-b <format>] "); + pw.println(" <PROCESS> <FILE>"); pw.println(" Dump the heap of a process. The given <PROCESS> argument may"); pw.println(" be either a process name or pid. Options are:"); pw.println(" -n: dump native heap instead of managed heap"); pw.println(" -g: force GC before dumping the heap"); + pw.println(" -b <format>: dump contents of bitmaps in the format specified,"); + pw.println(" which can be \"png\", \"jpg\" or \"webp\"."); pw.println(" --user <USER_ID> | current: When supplying a process name,"); pw.println(" specify user of process to dump; uses current user if not specified."); pw.println(" set-debug-app [-w] [--persistent] <PACKAGE>"); @@ -4385,7 +4395,7 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println(" Stop execution of USER_ID, not allowing it to run any"); pw.println(" code until a later explicit start or switch to it."); pw.println(" -w: wait for stop-user to complete."); - pw.println(" -f: force stop even if there are related users that cannot be stopped."); + pw.println(" -f: force stop, even if user has an unstoppable parent."); pw.println(" is-user-stopped <USER_ID>"); pw.println(" Returns whether <USER_ID> has been stopped or not."); pw.println(" get-started-user-state <USER_ID>"); diff --git a/services/core/java/com/android/server/am/Android.bp b/services/core/java/com/android/server/am/Android.bp index af1200e4bdf8..0294ffe6e151 100644 --- a/services/core/java/com/android/server/am/Android.bp +++ b/services/core/java/com/android/server/am/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "am_flags", package: "com.android.server.am", + container: "system", srcs: ["*.aconfig"], } diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 51aae771542e..6c16fba048bf 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -1051,7 +1051,9 @@ public class AppProfiler { + mProfile.mApp + " to " + mDumpUri.getPath()); } thread.dumpHeap(/* managed= */ true, - /* mallocInfo= */ false, /* runGc= */ false, + /* mallocInfo= */ false, + /* runGc= */ false, + /* dumpbitmaps= */ null, mDumpUri.getPath(), fd, /* finishCallback= */ null); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS index a656287dc3f9..b517631f35c0 100644 --- a/services/core/java/com/android/server/am/OWNERS +++ b/services/core/java/com/android/server/am/OWNERS @@ -9,7 +9,6 @@ mwachens@google.com sudheersai@google.com suprabh@google.com varunshah@google.com -kwekua@google.com bookatz@google.com jji@google.com diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 5a750c2ba6c8..ea7a21dd19cd 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -72,7 +72,6 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYB import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL; import static android.media.audio.Flags.roForegroundAudioControl; -import static android.os.Process.SCHED_OTHER; import static android.os.Process.THREAD_GROUP_BACKGROUND; import static android.os.Process.THREAD_GROUP_DEFAULT; import static android.os.Process.THREAD_GROUP_RESTRICTED; @@ -81,7 +80,6 @@ import static android.os.Process.THREAD_PRIORITY_DISPLAY; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; import static android.os.Process.setProcessGroup; import static android.os.Process.setThreadPriority; -import static android.os.Process.setThreadScheduler; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKUP; @@ -3315,22 +3313,10 @@ public class OomAdjuster { // do nothing if we already switched to RT if (oldSchedGroup != SCHED_GROUP_TOP_APP) { app.getWindowProcessController().onTopProcChanged(); - if (mService.mUseFifoUiScheduling) { + if (app.useFifoUiScheduling()) { // Switch UI pipeline for app to SCHED_FIFO state.setSavedPriority(Process.getThreadPriority(app.getPid())); - mService.scheduleAsFifoPriority(app.getPid(), true); - if (renderThreadTid != 0) { - mService.scheduleAsFifoPriority(renderThreadTid, - /* suppressLogs */true); - if (DEBUG_OOM_ADJ) { - Slog.d("UI_FIFO", "Set RenderThread (TID " + - renderThreadTid + ") to FIFO"); - } - } else { - if (DEBUG_OOM_ADJ) { - Slog.d("UI_FIFO", "Not setting RenderThread TID"); - } - } + ActivityManagerService.setFifoPriority(app, true /* enable */); } else { // Boost priority for top app UI and render threads setThreadPriority(app.getPid(), THREAD_PRIORITY_TOP_APP_BOOST); @@ -3347,22 +3333,10 @@ public class OomAdjuster { } else if (oldSchedGroup == SCHED_GROUP_TOP_APP && curSchedGroup != SCHED_GROUP_TOP_APP) { app.getWindowProcessController().onTopProcChanged(); - if (mService.mUseFifoUiScheduling) { - try { - // Reset UI pipeline to SCHED_OTHER - setThreadScheduler(app.getPid(), SCHED_OTHER, 0); - setThreadPriority(app.getPid(), state.getSavedPriority()); - if (renderThreadTid != 0) { - setThreadScheduler(renderThreadTid, - SCHED_OTHER, 0); - } - } catch (IllegalArgumentException e) { - Slog.w(TAG, - "Failed to set scheduling policy, thread does not exist:\n" - + e); - } catch (SecurityException e) { - Slog.w(TAG, "Failed to set scheduling policy, not allowed:\n" + e); - } + if (app.useFifoUiScheduling()) { + // Reset UI pipeline to SCHED_OTHER + ActivityManagerService.setFifoPriority(app, false /* enable */); + setThreadPriority(app.getPid(), state.getSavedPriority()); } else { // Reset priority for top app UI and render threads setThreadPriority(app.getPid(), 0); @@ -3557,7 +3531,7 @@ public class OomAdjuster { // {@link SCHED_GROUP_TOP_APP}. We don't check render thread because it // is not ready when attaching. app.getWindowProcessController().onTopProcChanged(); - if (mService.mUseFifoUiScheduling) { + if (app.useFifoUiScheduling()) { mService.scheduleAsFifoPriority(app.getPid(), true); } else { setThreadPriority(app.getPid(), THREAD_PRIORITY_TOP_APP_BOOST); diff --git a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java index a8fe734f59b7..ea925716b864 100644 --- a/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java +++ b/services/core/java/com/android/server/am/ProcessCachedOptimizerRecord.java @@ -25,6 +25,8 @@ import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; +import dalvik.annotation.optimization.NeverCompile; + /** * The state info of app when it's cached, used by the optimizer. */ @@ -340,6 +342,7 @@ final class ProcessCachedOptimizerRecord { } @GuardedBy("mProcLock") + @NeverCompile void dump(PrintWriter pw, String prefix, long nowUptime) { pw.print(prefix); pw.print("lastCompactTime="); pw.print(mLastCompactTime); pw.print(" lastCompactProfile="); diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 48a9d6af0df4..6779f7a37f20 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -351,6 +351,7 @@ public final class ProcessList { // LMK_UPDATE_PROPS // LMK_KILL_OCCURRED // LMK_START_MONITORING + // LMK_BOOT_COMPLETED static final byte LMK_TARGET = 0; static final byte LMK_PROCPRIO = 1; static final byte LMK_PROCREMOVE = 2; @@ -361,6 +362,7 @@ public final class ProcessList { static final byte LMK_UPDATE_PROPS = 7; static final byte LMK_KILL_OCCURRED = 8; // Msg to subscribed clients on kill occurred event static final byte LMK_START_MONITORING = 9; // Start monitoring if delayed earlier + static final byte LMK_BOOT_COMPLETED = 10; // Low Memory Killer Daemon command codes. // These must be kept in sync with async_event_type definitions in lmkd.h diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index b93908974a42..08165275757e 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -745,6 +745,9 @@ class ProcessRecord implements WindowProcessListener { mOnewayThread = thread; } mWindowProcessController.setThread(thread); + if (mWindowProcessController.useFifoUiScheduling()) { + mService.mSpecifiedFifoProcesses.add(this); + } } @GuardedBy({"mService", "mProcLock"}) @@ -752,9 +755,19 @@ class ProcessRecord implements WindowProcessListener { mThread = null; mOnewayThread = null; mWindowProcessController.setThread(null); + if (mWindowProcessController.useFifoUiScheduling()) { + mService.mSpecifiedFifoProcesses.remove(this); + } mProfile.onProcessInactive(tracker); } + @GuardedBy(anyOf = {"mService", "mProcLock"}) + boolean useFifoUiScheduling() { + return mService.mUseFifoUiScheduling + || (mService.mAllowSpecifiedFifoScheduling + && mWindowProcessController.useFifoUiScheduling()); + } + @GuardedBy("mService") int getDyingPid() { return mDyingPid; diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 60a8b50feeab..dd4cee47bee9 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -246,6 +246,7 @@ class UserController implements Handler.Callback { * * <p>Note: Current and system user (and their related profiles) are never stopped when * switching users. Due to that, the actual number of running users can exceed mMaxRunningUsers + // TODO(b/310249114): Strongly consider *not* exempting the SYSTEM user's profile. */ @GuardedBy("mLock") private int mMaxRunningUsers; @@ -578,7 +579,8 @@ class UserController implements Handler.Callback { // from outside. Slogf.i(TAG, "Too many running users (%d). Attempting to stop user %d", currentlyRunningLru.size(), userId); - if (stopUsersLU(userId, /* force= */ false, /* allowDelayedLocking= */ true, + if (stopUsersLU(userId, + /* stopProfileRegardlessOfParent= */ false, /* allowDelayedLocking= */ true, /* stopUserCallback= */ null, /* keyEvictedCallback= */ null) == USER_OP_SUCCESS) { // Technically, stopUsersLU can remove more than one user when stopping a parent. @@ -875,7 +877,7 @@ class UserController implements Handler.Callback { Slogf.i(TAG, "Stopping pre-created user " + userInfo.toFullString()); // Pre-created user was started right after creation so services could properly // intialize it; it should be stopped right away as it's not really a "real" user. - stopUser(userInfo.id, /* force= */ true, /* allowDelayedLocking= */ false, + stopUser(userInfo.id, /* allowDelayedLocking= */ false, /* stopUserCallback= */ null, /* keyEvictedCallback= */ null); return; } @@ -930,7 +932,7 @@ class UserController implements Handler.Callback { } int restartUser(final int userId, @UserStartMode int userStartMode) { - return stopUser(userId, /* force= */ true, /* allowDelayedLocking= */ false, + return stopUser(userId, /* allowDelayedLocking= */ false, /* stopUserCallback= */ null, new KeyEvictedCallback() { @Override public void keyEvicted(@UserIdInt int userId) { @@ -966,18 +968,24 @@ class UserController implements Handler.Callback { enforceShellRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES, userId); synchronized (mLock) { - return stopUsersLU(userId, /* force= */ true, /* allowDelayedLocking= */ - false, /* stopUserCallback= */ null, /* keyEvictedCallback= */ null) + return stopUsersLU(userId, /* allowDelayedLocking= */ false, + /* stopUserCallback= */ null, /* keyEvictedCallback= */ null) == ActivityManager.USER_OP_SUCCESS; } } - int stopUser(final int userId, final boolean force, boolean allowDelayedLocking, + int stopUser(final int userId, boolean allowDelayedLocking, + final IStopUserCallback stopUserCallback, KeyEvictedCallback keyEvictedCallback) { + return stopUser(userId, true, allowDelayedLocking, stopUserCallback, keyEvictedCallback); + } + + int stopUser(final int userId, + final boolean stopProfileRegardlessOfParent, final boolean allowDelayedLocking, final IStopUserCallback stopUserCallback, KeyEvictedCallback keyEvictedCallback) { TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("UserController" - + (force ? "-force" : "") + + (stopProfileRegardlessOfParent ? "-stopProfileRegardlessOfParent" : "") + (allowDelayedLocking ? "-allowDelayedLocking" : "") + (stopUserCallback != null ? "-withStopUserCallback" : "") + "-" + userId + "-[stopUser]"); @@ -987,20 +995,32 @@ class UserController implements Handler.Callback { enforceShellRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES, userId); synchronized (mLock) { - return stopUsersLU(userId, force, allowDelayedLocking, stopUserCallback, - keyEvictedCallback); + return stopUsersLU(userId, stopProfileRegardlessOfParent, allowDelayedLocking, + stopUserCallback, keyEvictedCallback); } } finally { t.traceEnd(); } } + /** Stops the user along with its profiles. */ + @GuardedBy("mLock") + private int stopUsersLU(final int userId, boolean allowDelayedLocking, + final IStopUserCallback stopUserCallback, KeyEvictedCallback keyEvictedCallback) { + return stopUsersLU(userId, /* stopProfileRegardlessOfParent= */ true, + allowDelayedLocking, stopUserCallback, keyEvictedCallback); + } + /** * Stops the user along with its profiles. The method calls * {@link #getUsersToStopLU(int)} to determine the list of users that should be stopped. + * + * @param stopProfileRegardlessOfParent whether to stop the profile regardless of who its + * parent is, e.g. even if the parent is the current user */ @GuardedBy("mLock") - private int stopUsersLU(final int userId, boolean force, boolean allowDelayedLocking, + private int stopUsersLU(final int userId, + boolean stopProfileRegardlessOfParent, boolean allowDelayedLocking, final IStopUserCallback stopUserCallback, KeyEvictedCallback keyEvictedCallback) { if (userId == UserHandle.USER_SYSTEM) { return USER_OP_ERROR_IS_SYSTEM; @@ -1008,34 +1028,28 @@ class UserController implements Handler.Callback { if (isCurrentUserLU(userId)) { return USER_OP_IS_CURRENT; } - // TODO(b/324647580): Refactor the idea of "force" and clean up. In the meantime... - final int parentId = mUserProfileGroupIds.get(userId, UserInfo.NO_PROFILE_GROUP_ID); - if (parentId != UserInfo.NO_PROFILE_GROUP_ID && parentId != userId) { - if ((UserHandle.USER_SYSTEM == parentId || isCurrentUserLU(parentId)) && !force) { - return USER_OP_ERROR_RELATED_USERS_CANNOT_STOP; + if (!stopProfileRegardlessOfParent) { + final int parentId = mUserProfileGroupIds.get(userId, UserInfo.NO_PROFILE_GROUP_ID); + if (parentId != UserInfo.NO_PROFILE_GROUP_ID && parentId != userId) { + // TODO(b/310249114): Strongly consider *not* exempting the SYSTEM user's profile. + if ((UserHandle.USER_SYSTEM == parentId || isCurrentUserLU(parentId))) { + return USER_OP_ERROR_RELATED_USERS_CANNOT_STOP; + } } } - TimingsTraceAndSlog t = new TimingsTraceAndSlog(); - int[] usersToStop = getUsersToStopLU(userId); - // If one of related users is system or current, no related users should be stopped + final int[] usersToStop = getUsersToStopLU(userId); + + // Final safety check: abort if one of the users we would plan to stop must not be stopped. + // This should be impossible in the current code, but just in case. for (int relatedUserId : usersToStop) { if ((UserHandle.USER_SYSTEM == relatedUserId) || isCurrentUserLU(relatedUserId)) { - if (DEBUG_MU) { - Slogf.i(TAG, "stopUsersLocked cannot stop related user " + relatedUserId); - } - // We still need to stop the requested user if it's a force stop. - if (force) { - Slogf.i(TAG, - "Force stop user " + userId + ". Related users will not be stopped"); - t.traceBegin("stopSingleUserLU-force-" + userId + "-[stopUser]"); - stopSingleUserLU(userId, allowDelayedLocking, stopUserCallback, - keyEvictedCallback); - t.traceEnd(); - return USER_OP_SUCCESS; - } + Slogf.e(TAG, "Cannot stop user %d because it is related to user %d. ", + userId, relatedUserId); return USER_OP_ERROR_RELATED_USERS_CANNOT_STOP; } } + + TimingsTraceAndSlog t = new TimingsTraceAndSlog(); if (DEBUG_MU) Slogf.i(TAG, "stopUsersLocked usersToStop=" + Arrays.toString(usersToStop)); for (int userIdToStop : usersToStop) { t.traceBegin("stopSingleUserLU-" + userIdToStop + "-[stopUser]"); @@ -1304,8 +1318,8 @@ class UserController implements Handler.Callback { mInjector.activityManagerOnUserStopped(userId); // Clean up all state and processes associated with the user. // Kill all the processes for the user. - t.traceBegin("forceStopUser-" + userId + "-[stopUser]"); - forceStopUser(userId, "finish user"); + t.traceBegin("stopPackagesOfStoppedUser-" + userId + "-[stopUser]"); + stopPackagesOfStoppedUser(userId, "finish user"); t.traceEnd(); } @@ -1516,8 +1530,8 @@ class UserController implements Handler.Callback { return userIds.toArray(); } - private void forceStopUser(@UserIdInt int userId, String reason) { - if (DEBUG_MU) Slogf.i(TAG, "forceStopUser(%d): %s", userId, reason); + private void stopPackagesOfStoppedUser(@UserIdInt int userId, String reason) { + if (DEBUG_MU) Slogf.i(TAG, "stopPackagesOfStoppedUser(%d): %s", userId, reason); mInjector.activityManagerForceStopPackage(userId, reason); if (mInjector.getUserManager().isPreCreated(userId)) { // Don't fire intent for precreated. @@ -1566,8 +1580,7 @@ class UserController implements Handler.Callback { // This is a user to be stopped. Slogf.i(TAG, "Stopping background guest or ephemeral user " + oldUserId); synchronized (mLock) { - stopUsersLU(oldUserId, /* force= */ true, /* allowDelayedLocking= */ false, - null, null); + stopUsersLU(oldUserId, /* allowDelayedLocking= */ false, null, null); } } } @@ -2259,8 +2272,7 @@ class UserController implements Handler.Callback { // If running in background is disabled or mStopUserOnSwitch mode, stop the user. if (hasRestriction || shouldStopUserOnSwitch()) { Slogf.i(TAG, "Stopping user %d and its profiles on user switch", oldUserId); - stopUsersLU(oldUserId, /* force= */ false, /* allowDelayedLocking= */ false, - null, null); + stopUsersLU(oldUserId, /* allowDelayedLocking= */ false, null, null); return; } } @@ -2275,7 +2287,8 @@ class UserController implements Handler.Callback { Slogf.i(TAG, "Stopping profile %d on user switch", profileUserId); synchronized (mLock) { stopUsersLU(profileUserId, - /* force= */ true, /* allowDelayedLocking= */ false, null, null); + /* stopProfileRegardlessOfParent= */ false, + /* allowDelayedLocking= */ false, null, null); } } } diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index fd847f11157f..e1ccf4d2a362 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.am" +container: "system" flag { name: "oomadjuster_correctness_rewrite" diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 951f676dd098..77654d4a5413 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1517,8 +1517,9 @@ public class AudioDeviceBroker { sendLMsgNoDelay(MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY, SENDMSG_QUEUE, deviceState); } - /*package*/ void postUpdatedAdiDeviceState(AdiDeviceState deviceState) { - sendLMsgNoDelay(MSG_L_UPDATED_ADI_DEVICE_STATE, SENDMSG_QUEUE, deviceState); + /*package*/ void postUpdatedAdiDeviceState(AdiDeviceState deviceState, boolean initSA) { + sendILMsgNoDelay( + MSG_IL_UPDATED_ADI_DEVICE_STATE, SENDMSG_QUEUE, initSA ? 1 : 0, deviceState); } /*package*/ static final class CommunicationDeviceInfo { @@ -1820,18 +1821,17 @@ public class AudioDeviceBroker { + "received with null profile proxy: " + btInfo)).printLog(TAG)); } else { - @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec = + final Pair<Integer, Boolean> codecAndChanged = mBtHelper.getCodecWithFallback(btInfo.mDevice, btInfo.mProfile, btInfo.mIsLeOutput, "MSG_L_SET_BT_ACTIVE_DEVICE"); synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { - mDeviceInventory.onSetBtActiveDevice(btInfo, codec, - (btInfo.mProfile - != BluetoothProfile.LE_AUDIO + mDeviceInventory.onSetBtActiveDevice(btInfo, codecAndChanged.first, + (btInfo.mProfile != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput) - ? mAudioService.getBluetoothContextualVolumeStream() - : AudioSystem.STREAM_DEFAULT); + ? mAudioService.getBluetoothContextualVolumeStream() + : AudioSystem.STREAM_DEFAULT); if (btInfo.mProfile == BluetoothProfile.LE_AUDIO || btInfo.mProfile == BluetoothProfile.HEARING_AID) { @@ -1866,13 +1866,13 @@ public class AudioDeviceBroker { break; case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: { final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj; - @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec = - mBtHelper.getCodecWithFallback(btInfo.mDevice, - btInfo.mProfile, btInfo.mIsLeOutput, - "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE"); + final Pair<Integer, Boolean> codecAndChanged = mBtHelper.getCodecWithFallback( + btInfo.mDevice, btInfo.mProfile, btInfo.mIsLeOutput, + "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE"); synchronized (mDeviceStateLock) { - mDeviceInventory.onBluetoothDeviceConfigChange( - btInfo, codec, BtHelper.EVENT_DEVICE_CONFIG_CHANGE); + mDeviceInventory.onBluetoothDeviceConfigChange(btInfo, + codecAndChanged.first, codecAndChanged.second, + BtHelper.EVENT_DEVICE_CONFIG_CHANGE); } } break; case MSG_BROADCAST_AUDIO_BECOMING_NOISY: @@ -2049,8 +2049,8 @@ public class AudioDeviceBroker { } } break; - case MSG_L_UPDATED_ADI_DEVICE_STATE: - mAudioService.onUpdatedAdiDeviceState((AdiDeviceState) msg.obj); + case MSG_IL_UPDATED_ADI_DEVICE_STATE: + mAudioService.onUpdatedAdiDeviceState((AdiDeviceState) msg.obj, msg.arg1 == 1); break; default: @@ -2137,7 +2137,7 @@ public class AudioDeviceBroker { private static final int MSG_CHECK_COMMUNICATION_ROUTE_CLIENT_STATE = 56; private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57; private static final int MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY = 58; - private static final int MSG_L_UPDATED_ADI_DEVICE_STATE = 59; + private static final int MSG_IL_UPDATED_ADI_DEVICE_STATE = 59; diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 14428c41ef26..f38b38154bc3 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -173,7 +173,7 @@ public class AudioDeviceInventory { if (ads.getAudioDeviceCategory() != category && (userDefined || category != AUDIO_DEVICE_CATEGORY_UNKNOWN)) { ads.setAudioDeviceCategory(category); - mDeviceBroker.postUpdatedAdiDeviceState(ads); + mDeviceBroker.postUpdatedAdiDeviceState(ads, false /*initSA*/); mDeviceBroker.postPersistAudioDeviceSettings(); } mDeviceBroker.postSynchronizeAdiDevicesInInventory(ads); @@ -186,7 +186,7 @@ public class AudioDeviceInventory { mDeviceInventory.put(ads.getDeviceId(), ads); checkDeviceInventorySize_l(); - mDeviceBroker.postUpdatedAdiDeviceState(ads); + mDeviceBroker.postUpdatedAdiDeviceState(ads, true /*initSA*/); mDeviceBroker.postPersistAudioDeviceSettings(); } } @@ -216,7 +216,7 @@ public class AudioDeviceInventory { checkDeviceInventorySize_l(); } if (updatedCategory.get()) { - mDeviceBroker.postUpdatedAdiDeviceState(deviceState); + mDeviceBroker.postUpdatedAdiDeviceState(deviceState, false /*initSA*/); } mDeviceBroker.postSynchronizeAdiDevicesInInventory(deviceState); } @@ -318,7 +318,7 @@ public class AudioDeviceInventory { } ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); - mDeviceBroker.postUpdatedAdiDeviceState(ads2); + mDeviceBroker.postUpdatedAdiDeviceState(ads2, false /*initSA*/); AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "synchronizeBleDeviceInInventory synced device pair ads1=" + updatedDevice + " ads2=" + ads2).printLog(TAG)); @@ -339,7 +339,7 @@ public class AudioDeviceInventory { } ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); - mDeviceBroker.postUpdatedAdiDeviceState(ads2); + mDeviceBroker.postUpdatedAdiDeviceState(ads2, false /*initSA*/); AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "synchronizeBleDeviceInInventory synced device pair ads1=" + updatedDevice + " peer ads2=" + ads2).printLog(TAG)); @@ -364,7 +364,7 @@ public class AudioDeviceInventory { } ads.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); - mDeviceBroker.postUpdatedAdiDeviceState(ads); + mDeviceBroker.postUpdatedAdiDeviceState(ads, false /*initSA*/); AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "synchronizeDeviceProfilesInInventory synced device pair ads1=" + updatedDevice + " ads2=" + ads).printLog(TAG)); @@ -868,7 +868,8 @@ public class AudioDeviceInventory { @GuardedBy("mDeviceBroker.mDeviceStateLock") /*package*/ void onBluetoothDeviceConfigChange( @NonNull AudioDeviceBroker.BtDeviceInfo btInfo, - @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, int event) { + @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, + boolean codecChanged, int event) { MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "onBluetoothDeviceConfigChange") .set(MediaMetrics.Property.EVENT, BtHelper.deviceEventToString(event)); @@ -916,14 +917,12 @@ public class AudioDeviceInventory { if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) { - boolean codecChange = false; if (btInfo.mProfile == BluetoothProfile.A2DP || btInfo.mProfile == BluetoothProfile.LE_AUDIO || btInfo.mProfile == BluetoothProfile.LE_AUDIO_BROADCAST) { - if (di.mDeviceCodecFormat != codec) { + if (codecChanged) { di.mDeviceCodecFormat = codec; mConnectedDevices.replace(key, di); - codecChange = true; final int res = mAudioSystem.handleDeviceConfigChange( btInfo.mAudioSystemDevice, address, BtHelper.getName(btDevice), codec); @@ -947,7 +946,7 @@ public class AudioDeviceInventory { } } } - if (!codecChange) { + if (!codecChanged) { updateBluetoothPreferredModes_l(btDevice /*connectedDevice*/); } } diff --git a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java index 85acf707677a..570d4e9857e6 100644 --- a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java +++ b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java @@ -53,6 +53,12 @@ class AudioManagerShellCommand extends ShellCommand { return getSoundDoseValue(); case "reset-sound-dose-timeout": return resetSoundDoseTimeout(); + case "set-volume": + return setVolume(); + case "adj-mute": + return adjMute(); + case "adj-unmute": + return adjUnmute(); } return 0; } @@ -78,6 +84,12 @@ class AudioManagerShellCommand extends ShellCommand { pw.println(" Returns the current sound dose value"); pw.println(" reset-sound-dose-timeout"); pw.println(" Resets the sound dose timeout used for momentary exposure"); + pw.println(" set-volume STREAM_TYPE VOLUME_INDEX"); + pw.println(" Sets the volume for STREAM_TYPE to VOLUME_INDEX"); + pw.println(" adj-mute STREAM_TYPE"); + pw.println(" mutes the STREAM_TYPE"); + pw.println(" adj-unmute STREAM_TYPE"); + pw.println(" unmutes the STREAM_TYPE"); } private int setSurroundFormatEnabled() { @@ -216,4 +228,54 @@ class AudioManagerShellCommand extends ShellCommand { getOutPrintWriter().println("Reset sound dose momentary exposure timeout"); return 0; } + + private int setVolume() { + final Context context = mService.mContext; + final AudioManager am = context.getSystemService(AudioManager.class); + final int stream = readIntArg(); + final int index = readIntArg(); + getOutPrintWriter().println("calling AudioManager.setStreamVolume(" + + stream + ", " + index + ", 0)"); + am.setStreamVolume(stream, index, 0); + return 0; + } + + private int adjMute() { + final Context context = mService.mContext; + final AudioManager am = context.getSystemService(AudioManager.class); + final int stream = readIntArg(); + getOutPrintWriter().println("calling AudioManager.adjustStreamVolume(" + + stream + ", AudioManager.ADJUST_MUTE, 0)"); + am.adjustStreamVolume(stream, AudioManager.ADJUST_MUTE, 0); + return 0; + } + + private int adjUnmute() { + final Context context = mService.mContext; + final AudioManager am = context.getSystemService(AudioManager.class); + final int stream = readIntArg(); + getOutPrintWriter().println("calling AudioManager.adjustStreamVolume(" + + stream + ", AudioManager.ADJUST_UNMUTE, 0)"); + am.adjustStreamVolume(stream, AudioManager.ADJUST_UNMUTE, 0); + return 0; + } + + private int readIntArg() throws IllegalArgumentException { + String argText = getNextArg(); + + if (argText == null) { + getErrPrintWriter().println("Error: no argument provided"); + throw new IllegalArgumentException("No argument provided"); + } + + int argIntVal = Integer.MIN_VALUE; + try { + argIntVal = Integer.parseInt(argText); + } catch (NumberFormatException e) { + getErrPrintWriter().println("Error: wrong format for argument " + argText); + throw new IllegalArgumentException("Wrong format for argument " + argText); + } + + return argIntVal; + } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index ed58c4033c4c..5ba0af402e4d 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -47,6 +47,7 @@ import static com.android.media.audio.Flags.alarmMinVolumeZero; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; import static com.android.media.audio.Flags.setStreamVolumeOrder; +import static com.android.media.audio.Flags.vgsVssSyncMuteOrder; import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE; import static com.android.server.utils.EventLogger.Event.ALOGE; import static com.android.server.utils.EventLogger.Event.ALOGI; @@ -4544,6 +4545,8 @@ public class AudioService extends IAudioService.Stub + setStreamVolumeOrder()); pw.println("\tandroid.media.audio.roForegroundAudioControl:" + roForegroundAudioControl()); + pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:" + + vgsVssSyncMuteOrder()); } private void dumpAudioMode(PrintWriter pw) { @@ -8317,13 +8320,23 @@ public class AudioService extends IAudioService.Stub synced = true; continue; } + if (vgsVssSyncMuteOrder()) { + if ((isMuted() != streamMuted) && isVssMuteBijective( + stream)) { + mStreamStates[stream].mute(isMuted(), + "VGS.applyAllVolumes#1"); + } + } if (indexForStream != index) { mStreamStates[stream].setIndex(index * 10, device, caller, true /*hasModifyAudioSettings*/); } - if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) { - mStreamStates[stream].mute(isMuted(), - "VGS.applyAllVolumes#1"); + if (!vgsVssSyncMuteOrder()) { + if ((isMuted() != streamMuted) && isVssMuteBijective( + stream)) { + mStreamStates[stream].mute(isMuted(), + "VGS.applyAllVolumes#1"); + } } } } @@ -8855,6 +8868,7 @@ public class AudioService extends IAudioService.Stub boolean changed; int oldIndex; final boolean isCurrentDevice; + final StringBuilder aliasStreamIndexes = new StringBuilder(); synchronized (mSettingsLock) { synchronized (VolumeStreamState.class) { oldIndex = getIndex(device); @@ -8881,13 +8895,17 @@ public class AudioService extends IAudioService.Stub (changed || !aliasStreamState.hasIndexForDevice(device))) { final int scaledIndex = rescaleIndex(aliasIndex, mStreamType, streamType); - aliasStreamState.setIndex(scaledIndex, device, caller, - hasModifyAudioSettings); + boolean changedAlias = aliasStreamState.setIndex(scaledIndex, device, + caller, hasModifyAudioSettings); if (isCurrentDevice) { - aliasStreamState.setIndex(scaledIndex, + changedAlias |= aliasStreamState.setIndex(scaledIndex, getDeviceForStream(streamType), caller, hasModifyAudioSettings); } + if (changedAlias) { + aliasStreamIndexes.append(AudioSystem.streamToString(streamType)) + .append(":").append((scaledIndex + 5) / 10).append(" "); + } } } // Mirror changes in SPEAKER ringtone volume on SCO when @@ -8927,8 +8945,15 @@ public class AudioService extends IAudioService.Stub oldIndex); mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS, mStreamVolumeAlias[mStreamType]); - AudioService.sVolumeLogger.enqueue(new VolChangedBroadcastEvent( - mStreamType, mStreamVolumeAlias[mStreamType], index, oldIndex)); + if (mStreamType == mStreamVolumeAlias[mStreamType]) { + String aliasStreamIndexesString = ""; + if (!aliasStreamIndexes.isEmpty()) { + aliasStreamIndexesString = + " aliased streams: " + aliasStreamIndexes; + } + AudioService.sVolumeLogger.enqueue(new VolChangedBroadcastEvent( + mStreamType, aliasStreamIndexesString, index, oldIndex)); + } sendBroadcastToAll(mVolumeChanged, mVolumeChangedOptions); } } @@ -11271,7 +11296,8 @@ public class AudioService extends IAudioService.Stub mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(deviceState); mDeviceBroker.postPersistAudioDeviceSettings(); - mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes()); + mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes(), + false /* initState */); mSoundDoseHelper.setAudioDeviceCategory(addr, internalType, btAudioDeviceCategory == AUDIO_DEVICE_CATEGORY_HEADPHONES); } @@ -11342,11 +11368,11 @@ public class AudioService extends IAudioService.Stub /** Update the sound dose and spatializer state based on the new AdiDeviceState. */ @VisibleForTesting(visibility = PACKAGE) - public void onUpdatedAdiDeviceState(AdiDeviceState deviceState) { + public void onUpdatedAdiDeviceState(AdiDeviceState deviceState, boolean initSA) { if (deviceState == null) { return; } - mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes()); + mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes(), initSA); mSoundDoseHelper.setAudioDeviceCategory(deviceState.getDeviceAddress(), deviceState.getInternalDeviceType(), deviceState.getAudioDeviceCategory() == AUDIO_DEVICE_CATEGORY_HEADPHONES); diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java index 3b1c011f09c0..749044e05cbc 100644 --- a/services/core/java/com/android/server/audio/AudioServiceEvents.java +++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java @@ -151,13 +151,13 @@ public class AudioServiceEvents { static final class VolChangedBroadcastEvent extends EventLogger.Event { final int mStreamType; - final int mAliasStreamType; + final String mAliasStreamIndexes; final int mIndex; final int mOldIndex; - VolChangedBroadcastEvent(int stream, int alias, int index, int oldIndex) { + VolChangedBroadcastEvent(int stream, String aliasIndexes, int index, int oldIndex) { mStreamType = stream; - mAliasStreamType = alias; + mAliasStreamIndexes = aliasIndexes; mIndex = index; mOldIndex = oldIndex; } @@ -167,8 +167,8 @@ public class AudioServiceEvents { return new StringBuilder("sending VOLUME_CHANGED stream:") .append(AudioSystem.streamToString(mStreamType)) .append(" index:").append(mIndex) - .append(" (was:").append(mOldIndex) - .append(") alias:").append(AudioSystem.streamToString(mAliasStreamType)) + .append(" (was:").append(mOldIndex).append(")") + .append(mAliasStreamIndexes) .toString(); } } diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index f3a5fdb3cacc..edeabdc5243c 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -98,9 +98,16 @@ public class BtHelper { private @Nullable BluetoothLeAudio mLeAudio; + private @Nullable BluetoothLeAudioCodecConfig mLeAudioCodecConfig; + // Reference to BluetoothA2dp to query for AbsoluteVolume. private @Nullable BluetoothA2dp mA2dp; + private @Nullable BluetoothCodecConfig mA2dpCodecConfig; + + private @AudioSystem.AudioFormatNativeEnumForBtCodec + int mLeAudioBroadcastCodec = AudioSystem.AUDIO_FORMAT_DEFAULT; + // If absolute volume is supported in AVRCP device private boolean mAvrcpAbsVolSupported = false; @@ -265,12 +272,15 @@ public class BtHelper { } } - /*package*/ synchronized @AudioSystem.AudioFormatNativeEnumForBtCodec int getCodec( + private synchronized Pair<Integer, Boolean> getCodec( @NonNull BluetoothDevice device, @AudioService.BtProfile int profile) { + switch (profile) { case BluetoothProfile.A2DP: { + boolean changed = mA2dpCodecConfig != null; if (mA2dp == null) { - return AudioSystem.AUDIO_FORMAT_DEFAULT; + mA2dpCodecConfig = null; + return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed); } BluetoothCodecStatus btCodecStatus = null; try { @@ -279,17 +289,24 @@ public class BtHelper { Log.e(TAG, "Exception while getting status of " + device, e); } if (btCodecStatus == null) { - return AudioSystem.AUDIO_FORMAT_DEFAULT; + mA2dpCodecConfig = null; + return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed); } final BluetoothCodecConfig btCodecConfig = btCodecStatus.getCodecConfig(); if (btCodecConfig == null) { - return AudioSystem.AUDIO_FORMAT_DEFAULT; + mA2dpCodecConfig = null; + return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed); } - return AudioSystem.bluetoothA2dpCodecToAudioFormat(btCodecConfig.getCodecType()); + changed = !btCodecConfig.equals(mA2dpCodecConfig); + mA2dpCodecConfig = btCodecConfig; + return new Pair<>(AudioSystem.bluetoothA2dpCodecToAudioFormat( + btCodecConfig.getCodecType()), changed); } case BluetoothProfile.LE_AUDIO: { + boolean changed = mLeAudioCodecConfig != null; if (mLeAudio == null) { - return AudioSystem.AUDIO_FORMAT_DEFAULT; + mLeAudioCodecConfig = null; + return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed); } BluetoothLeAudioCodecStatus btLeCodecStatus = null; int groupId = mLeAudio.getGroupId(device); @@ -299,42 +316,54 @@ public class BtHelper { Log.e(TAG, "Exception while getting status of " + device, e); } if (btLeCodecStatus == null) { - return AudioSystem.AUDIO_FORMAT_DEFAULT; + mLeAudioCodecConfig = null; + return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed); } BluetoothLeAudioCodecConfig btLeCodecConfig = btLeCodecStatus.getOutputCodecConfig(); if (btLeCodecConfig == null) { - return AudioSystem.AUDIO_FORMAT_DEFAULT; + mLeAudioCodecConfig = null; + return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, changed); } - return AudioSystem.bluetoothLeCodecToAudioFormat(btLeCodecConfig.getCodecType()); + changed = !btLeCodecConfig.equals(mLeAudioCodecConfig); + mLeAudioCodecConfig = btLeCodecConfig; + return new Pair<>(AudioSystem.bluetoothLeCodecToAudioFormat( + btLeCodecConfig.getCodecType()), changed); + } + case BluetoothProfile.LE_AUDIO_BROADCAST: { + // We assume LC3 for LE Audio broadcast codec as there is no API to get the codec + // config on LE Broadcast profile proxy. + boolean changed = mLeAudioBroadcastCodec != AudioSystem.AUDIO_FORMAT_LC3; + mLeAudioBroadcastCodec = AudioSystem.AUDIO_FORMAT_LC3; + return new Pair<>(mLeAudioBroadcastCodec, changed); } default: - return AudioSystem.AUDIO_FORMAT_DEFAULT; + return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, false); } } - /*package*/ synchronized @AudioSystem.AudioFormatNativeEnumForBtCodec - int getCodecWithFallback( - @NonNull BluetoothDevice device, @AudioService.BtProfile int profile, - boolean isLeOutput, @NonNull String source) { + /*package*/ synchronized Pair<Integer, Boolean> + getCodecWithFallback(@NonNull BluetoothDevice device, + @AudioService.BtProfile int profile, + boolean isLeOutput, @NonNull String source) { // For profiles other than A2DP and LE Audio output, the audio codec format must be // AUDIO_FORMAT_DEFAULT as native audio policy manager expects a specific audio format // only if audio HW module selection based on format is supported for the device type. if (!(profile == BluetoothProfile.A2DP || (isLeOutput && ((profile == BluetoothProfile.LE_AUDIO) || (profile == BluetoothProfile.LE_AUDIO_BROADCAST))))) { - return AudioSystem.AUDIO_FORMAT_DEFAULT; + return new Pair<>(AudioSystem.AUDIO_FORMAT_DEFAULT, false); } - @AudioSystem.AudioFormatNativeEnumForBtCodec int codec = + Pair<Integer, Boolean> codecAndChanged = getCodec(device, profile); - if (codec == AudioSystem.AUDIO_FORMAT_DEFAULT) { + if (codecAndChanged.first == AudioSystem.AUDIO_FORMAT_DEFAULT) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "getCodec DEFAULT from " + source + " fallback to " + (profile == BluetoothProfile.A2DP ? "SBC" : "LC3"))); - return profile == BluetoothProfile.A2DP - ? AudioSystem.AUDIO_FORMAT_SBC : AudioSystem.AUDIO_FORMAT_LC3; + return new Pair<>(profile == BluetoothProfile.A2DP + ? AudioSystem.AUDIO_FORMAT_SBC : AudioSystem.AUDIO_FORMAT_LC3, true); } - return codec; + return codecAndChanged; } // @GuardedBy("mDeviceBroker.mSetModeLock") @@ -539,15 +568,19 @@ public class BtHelper { break; case BluetoothProfile.A2DP: mA2dp = null; + mA2dpCodecConfig = null; break; case BluetoothProfile.HEARING_AID: mHearingAid = null; break; case BluetoothProfile.LE_AUDIO: mLeAudio = null; + mLeAudioCodecConfig = null; break; - case BluetoothProfile.A2DP_SINK: case BluetoothProfile.LE_AUDIO_BROADCAST: + mLeAudioBroadcastCodec = AudioSystem.AUDIO_FORMAT_DEFAULT; + break; + case BluetoothProfile.A2DP_SINK: // nothing to do in BtHelper break; default: diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index 08da32e8831c..28af22214f8d 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -235,6 +235,10 @@ public final class PlaybackActivityMonitor public int trackPlayer(PlayerBase.PlayerIdCard pic) { final int newPiid = AudioSystem.newAudioPlayerId(); if (DEBUG) { Log.v(TAG, "trackPlayer() new piid=" + newPiid); } + if (newPiid == PLAYER_PIID_INVALID) { + Log.w(TAG, "invalid piid assigned from AudioSystem"); + return newPiid; + } final AudioPlaybackConfiguration apc = new AudioPlaybackConfiguration(pic, newPiid, Binder.getCallingUid(), Binder.getCallingPid()); @@ -365,15 +369,14 @@ public final class PlaybackActivityMonitor sEventLogger.enqueue(new PlayerEvent(piid, event, eventValue)); if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_PORT_ID) { - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_II_UPDATE_PORT_EVENT, eventValue, piid)); + mPortIdToPiid.put(eventValue, piid); return; } else if (event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) { for (Integer uidInteger: mBannedUids) { if (checkBanPlayer(apc, uidInteger.intValue())) { // player was banned, do not update its state sEventLogger.enqueue(new EventLogger.StringEvent( - "not starting piid:" + piid + " ,is banned")); + "not starting piid:" + piid + ", is banned")); return; } } @@ -429,16 +432,12 @@ public final class PlaybackActivityMonitor synchronized (mPlayerLock) { int piid = mPortIdToPiid.get(portId, PLAYER_PIID_INVALID); if (piid == PLAYER_PIID_INVALID) { - if (DEBUG) { - Log.v(TAG, "No piid assigned for invalid/internal port id " + portId); - } + Log.w(TAG, "No piid assigned for invalid/internal port id " + portId); return; } final AudioPlaybackConfiguration apc = mPlayers.get(piid); if (apc == null) { - if (DEBUG) { - Log.v(TAG, "No AudioPlaybackConfiguration assigned for piid " + piid); - } + Log.w(TAG, "No AudioPlaybackConfiguration assigned for piid " + piid); return; } @@ -470,11 +469,18 @@ public final class PlaybackActivityMonitor public void releasePlayer(int piid, int binderUid) { if (DEBUG) { Log.v(TAG, "releasePlayer() for piid=" + piid); } boolean change = false; + if (piid == PLAYER_PIID_INVALID) { + Log.w(TAG, "Received releasePlayer with invalid piid: " + piid); + sEventLogger.enqueue(new EventLogger.StringEvent("releasePlayer with invalid piid:" + + piid + ", uid:" + binderUid)); + return; + } + synchronized(mPlayerLock) { final AudioPlaybackConfiguration apc = mPlayers.get(new Integer(piid)); if (checkConfigurationCaller(piid, apc, binderUid)) { sEventLogger.enqueue(new EventLogger.StringEvent( - "releasing player piid:" + piid)); + "releasing player piid:" + piid + ", uid:" + binderUid)); mPlayers.remove(new Integer(piid)); mDuckingManager.removeReleased(apc); mFadeOutManager.removeReleased(apc); @@ -484,8 +490,10 @@ public final class PlaybackActivityMonitor AudioPlaybackConfiguration.PLAYER_DEVICEID_INVALID); // remove all port ids mapped to the released player - mEventHandler.sendMessage( - mEventHandler.obtainMessage(MSG_I_CLEAR_PORTS_FOR_PIID, piid, /*arg2=*/0)); + int portIdx; + while ((portIdx = mPortIdToPiid.indexOfValue(piid)) >= 0) { + mPortIdToPiid.removeAt(portIdx); + } if (change && mDoNotLogPiidList.contains(piid)) { // do not dispatch a change for a "do not log" player @@ -1609,14 +1617,6 @@ public final class PlaybackActivityMonitor private static final int MSG_L_TIMEOUT_MUTE_AWAIT_CONNECTION = 1; /** - * assign new port id to piid - * args: - * msg.arg1: port id - * msg.arg2: piid - */ - private static final int MSG_II_UPDATE_PORT_EVENT = 2; - - /** * event for player getting muted * args: * msg.arg1: piid @@ -1624,14 +1624,7 @@ public final class PlaybackActivityMonitor * msg.obj: extras describing the mute reason * type: PersistableBundle */ - private static final int MSG_IIL_UPDATE_PLAYER_MUTED_EVENT = 3; - - /** - * clear all ports assigned to a given piid - * args: - * msg.arg1: the piid - */ - private static final int MSG_I_CLEAR_PORTS_FOR_PIID = 4; + private static final int MSG_IIL_UPDATE_PLAYER_MUTED_EVENT = 2; /** * event for player reporting playback format and spatialization status @@ -1641,7 +1634,7 @@ public final class PlaybackActivityMonitor * msg.obj: extras describing the sample rate, channel mask, spatialized * type: PersistableBundle */ - private static final int MSG_IIL_UPDATE_PLAYER_FORMAT = 5; + private static final int MSG_IIL_UPDATE_PLAYER_FORMAT = 3; private void initEventHandler() { mEventThread = new HandlerThread(TAG); @@ -1660,11 +1653,6 @@ public final class PlaybackActivityMonitor mMuteAwaitConnectionTimeoutCb.accept((AudioDeviceAttributes) msg.obj); break; - case MSG_II_UPDATE_PORT_EVENT: - synchronized (mPlayerLock) { - mPortIdToPiid.put(/*portId*/msg.arg1, /*piid*/msg.arg2); - } - break; case MSG_IIL_UPDATE_PLAYER_MUTED_EVENT: // TODO: replace PersistableBundle with own struct PersistableBundle extras = (PersistableBundle) msg.obj; @@ -1680,10 +1668,7 @@ public final class PlaybackActivityMonitor sEventLogger.enqueue( new PlayerEvent(piid, PLAYER_UPDATE_MUTED, eventValue)); - final AudioPlaybackConfiguration apc; - synchronized (mPlayerLock) { - apc = mPlayers.get(piid); - } + final AudioPlaybackConfiguration apc = mPlayers.get(piid); if (apc == null || !apc.handleMutedEvent(eventValue)) { break; // do not dispatch } @@ -1691,21 +1676,6 @@ public final class PlaybackActivityMonitor } break; - case MSG_I_CLEAR_PORTS_FOR_PIID: - int piid = msg.arg1; - if (piid == AudioPlaybackConfiguration.PLAYER_PIID_INVALID) { - Log.w(TAG, "Received clear ports with invalid piid"); - break; - } - - synchronized (mPlayerLock) { - int portIdx; - while ((portIdx = mPortIdToPiid.indexOfValue(piid)) >= 0) { - mPortIdToPiid.removeAt(portIdx); - } - } - break; - case MSG_IIL_UPDATE_PLAYER_FORMAT: final PersistableBundle formatExtras = (PersistableBundle) msg.obj; if (formatExtras == null) { diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 3b5fa7f00891..38fa79f7f44a 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -295,11 +295,11 @@ public class SpatializerHelper { // could have been called another time after boot in case of audioserver restart addCompatibleAudioDevice( new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""), - false /*forceEnable*/); + false /*forceEnable*/, false /*forceInit*/); // not force-enabling as this device might already be in the device list addCompatibleAudioDevice( new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""), - false /*forceEnable*/); + false /*forceEnable*/, false /*forceInit*/); } catch (RemoteException e) { resetCapabilities(); } finally { @@ -526,7 +526,7 @@ public class SpatializerHelper { } synchronized void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { - addCompatibleAudioDevice(ada, true /*forceEnable*/); + addCompatibleAudioDevice(ada, true /*forceEnable*/, false /*forceInit*/); } /** @@ -540,7 +540,7 @@ public class SpatializerHelper { */ @GuardedBy("this") private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada, - boolean forceEnable) { + boolean forceEnable, boolean forceInit) { if (!isDeviceCompatibleWithSpatializationModes(ada)) { return; } @@ -548,6 +548,9 @@ public class SpatializerHelper { final AdiDeviceState deviceState = findSACompatibleDeviceStateForAudioDeviceAttributes(ada); AdiDeviceState updatedDevice = null; // non-null on update. if (deviceState != null) { + if (forceInit) { + initSAState(deviceState); + } if (forceEnable && !deviceState.isSAEnabled()) { updatedDevice = deviceState; updatedDevice.setSAEnabled(true); @@ -756,10 +759,10 @@ public class SpatializerHelper { } } - synchronized void refreshDevice(@NonNull AudioDeviceAttributes ada) { + synchronized void refreshDevice(@NonNull AudioDeviceAttributes ada, boolean initState) { final AdiDeviceState deviceState = findSACompatibleDeviceStateForAudioDeviceAttributes(ada); if (isAvailableForAdiDeviceState(deviceState)) { - addCompatibleAudioDevice(ada, /*forceEnable=*/deviceState.isSAEnabled()); + addCompatibleAudioDevice(ada, /*forceEnable=*/deviceState.isSAEnabled(), initState); setHeadTrackerEnabled(deviceState.isHeadTrackerEnabled(), ada); } else { removeCompatibleAudioDevice(ada); diff --git a/services/core/java/com/android/server/biometrics/Android.bp b/services/core/java/com/android/server/biometrics/Android.bp index 6cbe4adbcdbe..ed5de0305863 100644 --- a/services/core/java/com/android/server/biometrics/Android.bp +++ b/services/core/java/com/android/server/biometrics/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "biometrics_flags", package: "com.android.server.biometrics", + container: "system", srcs: [ "biometrics.aconfig", ], diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index 3d4801b3e9aa..c03b3b86ad03 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -56,7 +56,7 @@ import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.IBinder; import android.os.RemoteException; -import android.security.KeyStore; +import android.security.KeyStoreAuthorization; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -111,7 +111,7 @@ public final class AuthSession implements IBinder.DeathRecipient { @NonNull private final BiometricContext mBiometricContext; private final IStatusBarService mStatusBarService; @VisibleForTesting final IBiometricSysuiReceiver mSysuiReceiver; - private final KeyStore mKeyStore; + private final KeyStoreAuthorization mKeyStoreAuthorization; private final Random mRandom; private final ClientDeathReceiver mClientDeathReceiver; final PreAuthInfo mPreAuthInfo; @@ -158,7 +158,7 @@ public final class AuthSession implements IBinder.DeathRecipient { @NonNull BiometricContext biometricContext, @NonNull IStatusBarService statusBarService, @NonNull IBiometricSysuiReceiver sysuiReceiver, - @NonNull KeyStore keystore, + @NonNull KeyStoreAuthorization keyStoreAuthorization, @NonNull Random random, @NonNull ClientDeathReceiver clientDeathReceiver, @NonNull PreAuthInfo preAuthInfo, @@ -172,8 +172,8 @@ public final class AuthSession implements IBinder.DeathRecipient { @NonNull PromptInfo promptInfo, boolean debugEnabled, @NonNull List<FingerprintSensorPropertiesInternal> fingerprintSensorProperties) { - this(context, biometricContext, statusBarService, sysuiReceiver, keystore, random, - clientDeathReceiver, preAuthInfo, token, requestId, operationId, userId, + this(context, biometricContext, statusBarService, sysuiReceiver, keyStoreAuthorization, + random, clientDeathReceiver, preAuthInfo, token, requestId, operationId, userId, sensorReceiver, clientReceiver, opPackageName, promptInfo, debugEnabled, fingerprintSensorProperties, BiometricFrameworkStatsLogger.getInstance()); } @@ -183,7 +183,7 @@ public final class AuthSession implements IBinder.DeathRecipient { @NonNull BiometricContext biometricContext, @NonNull IStatusBarService statusBarService, @NonNull IBiometricSysuiReceiver sysuiReceiver, - @NonNull KeyStore keystore, + @NonNull KeyStoreAuthorization keyStoreAuthorization, @NonNull Random random, @NonNull ClientDeathReceiver clientDeathReceiver, @NonNull PreAuthInfo preAuthInfo, @@ -203,7 +203,7 @@ public final class AuthSession implements IBinder.DeathRecipient { mBiometricContext = biometricContext; mStatusBarService = statusBarService; mSysuiReceiver = sysuiReceiver; - mKeyStore = keystore; + mKeyStoreAuthorization = keyStoreAuthorization; mRandom = random; mClientDeathReceiver = clientDeathReceiver; mPreAuthInfo = preAuthInfo; @@ -814,14 +814,14 @@ public final class AuthSession implements IBinder.DeathRecipient { switch (reason) { case BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED: if (credentialAttestation != null) { - mKeyStore.addAuthToken(credentialAttestation); + mKeyStoreAuthorization.addAuthToken(credentialAttestation); } else { Slog.e(TAG, "credentialAttestation is null"); } case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED: case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED: if (mTokenEscrow != null) { - final int result = mKeyStore.addAuthToken(mTokenEscrow); + final int result = mKeyStoreAuthorization.addAuthToken(mTokenEscrow); Slog.d(TAG, "addAuthToken: " + result); } else { Slog.e(TAG, "mTokenEscrow is null"); diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 894b4d5d0b36..3737d6fb4f3f 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -65,15 +65,11 @@ import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.ServiceSpecificException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import android.security.Authorization; import android.security.GateKeeper; -import android.security.KeyStore; -import android.security.authorization.IKeystoreAuthorization; -import android.security.authorization.ResponseCode; +import android.security.KeyStoreAuthorization; import android.service.gatekeeper.IGateKeeperService; import android.text.TextUtils; import android.util.ArraySet; @@ -123,11 +119,9 @@ public class BiometricService extends SystemService { @VisibleForTesting IStatusBarService mStatusBarService; @VisibleForTesting - KeyStore mKeyStore; - @VisibleForTesting ITrustManager mTrustManager; @VisibleForTesting - IKeystoreAuthorization mKeystoreAuthorization; + KeyStoreAuthorization mKeyStoreAuthorization; @VisibleForTesting IGateKeeperService mGateKeeper; @@ -674,19 +668,7 @@ public class BiometricService extends SystemService { int[] authTypesArray = hardwareAuthenticators.stream() .mapToInt(Integer::intValue) .toArray(); - try { - return mKeystoreAuthorization.getLastAuthTime(secureUserId, authTypesArray); - } catch (RemoteException e) { - Slog.w(TAG, "Error getting last auth time: " + e); - return BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; - } catch (ServiceSpecificException e) { - // This is returned when the feature flag test fails in keystore2 - if (e.errorCode == ResponseCode.PERMISSION_DENIED) { - throw new UnsupportedOperationException(); - } - - return BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; - } + return mKeyStoreAuthorization.getLastAuthTime(secureUserId, authTypesArray); } @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @@ -1011,8 +993,8 @@ public class BiometricService extends SystemService { return ActivityManager.getService(); } - public IKeystoreAuthorization getKeystoreAuthorizationService() { - return Authorization.getService(); + public KeyStoreAuthorization getKeyStoreAuthorization() { + return KeyStoreAuthorization.getInstance(); } public IGateKeeperService getGateKeeperService() { @@ -1036,10 +1018,6 @@ public class BiometricService extends SystemService { return new SettingObserver(context, handler, callbacks); } - public KeyStore getKeyStore() { - return KeyStore.getInstance(); - } - /** * Allows to enable/disable debug logs. */ @@ -1138,7 +1116,7 @@ public class BiometricService extends SystemService { mBiometricContext = injector.getBiometricContext(context); mUserManager = injector.getUserManager(context); mBiometricCameraManager = injector.getBiometricCameraManager(context); - mKeystoreAuthorization = injector.getKeystoreAuthorizationService(); + mKeyStoreAuthorization = injector.getKeyStoreAuthorization(); mGateKeeper = injector.getGateKeeperService(); mBiometricNotificationLogger = injector.getNotificationLogger(); @@ -1159,7 +1137,6 @@ public class BiometricService extends SystemService { @Override public void onStart() { - mKeyStore = mInjector.getKeyStore(); mStatusBarService = mInjector.getStatusBarService(); mTrustManager = mInjector.getTrustManager(); mInjector.publishBinderService(this, mImpl); @@ -1481,7 +1458,7 @@ public class BiometricService extends SystemService { final boolean debugEnabled = mInjector.isDebugEnabled(getContext(), userId); mAuthSession = new AuthSession(getContext(), mBiometricContext, mStatusBarService, - createSysuiReceiver(requestId), mKeyStore, mRandom, + createSysuiReceiver(requestId), mKeyStoreAuthorization, mRandom, createClientDeathReceiver(requestId), preAuthInfo, token, requestId, operationId, userId, createBiometricSensorReceiver(requestId), receiver, opPackageName, promptInfo, debugEnabled, diff --git a/services/core/java/com/android/server/biometrics/biometrics.aconfig b/services/core/java/com/android/server/biometrics/biometrics.aconfig index b12d831ffe24..7a9491e44cd7 100644 --- a/services/core/java/com/android/server/biometrics/biometrics.aconfig +++ b/services/core/java/com/android/server/biometrics/biometrics.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.biometrics" +container: "system" flag { name: "face_vhal_feature" 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 62c21cf36f69..4fa8741c867e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -30,7 +30,7 @@ import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricRequestConstants; import android.os.IBinder; import android.os.RemoteException; -import android.security.KeyStore; +import android.security.KeyStoreAuthorization; import android.util.EventLog; import android.util.Slog; @@ -255,7 +255,7 @@ 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); + final int result = KeyStoreAuthorization.getInstance().addAuthToken(byteToken); if (result != 0) { Slog.d(TAG, "Error adding auth token : " + result); } else { diff --git a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java index 3ede0a2597d9..028b9b0bcbc0 100644 --- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java +++ b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java @@ -26,7 +26,7 @@ import com.android.server.SystemService; import java.util.ArrayList; -public class BroadcastRadioService extends SystemService { +public final class BroadcastRadioService extends SystemService { private final IRadioService mServiceImpl; public BroadcastRadioService(Context context) { diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java index 42b2682cf530..16514fa813dc 100644 --- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java +++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java @@ -52,7 +52,7 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { private static final List<String> SERVICE_NAMES = Arrays.asList( IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab"); - private final BroadcastRadioServiceImpl mHalAidl; + private final BroadcastRadioServiceImpl mAidlHalClient; private final BroadcastRadioService mService; /** @@ -77,14 +77,14 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { @VisibleForTesting IRadioServiceAidlImpl(BroadcastRadioService service, BroadcastRadioServiceImpl halAidl) { mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null"); - mHalAidl = Objects.requireNonNull(halAidl, + mAidlHalClient = Objects.requireNonNull(halAidl, "Broadcast radio service implementation for AIDL HAL cannot be null"); } @Override public List<RadioManager.ModuleProperties> listModules() { mService.enforcePolicyAccess(); - return mHalAidl.listModules(); + return mAidlHalClient.listModules(); } @Override @@ -97,7 +97,7 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { if (callback == null) { throw new IllegalArgumentException("Callback must not be null"); } - return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback); + return mAidlHalClient.openSession(moduleId, bandConfig, withAudio, callback); } @Override @@ -110,7 +110,7 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { Objects.requireNonNull(listener, "Announcement listener cannot be null"); mService.enforcePolicyAccess(); - return mHalAidl.addAnnouncementListener(enabledTypes, listener); + return mAidlHalClient.addAnnouncementListener(enabledTypes, listener); } @Override @@ -126,10 +126,10 @@ final class IRadioServiceAidlImpl extends IRadioService.Stub { radioPrintWriter.printf("BroadcastRadioService\n"); radioPrintWriter.increaseIndent(); - radioPrintWriter.printf("AIDL HAL:\n"); + radioPrintWriter.printf("AIDL HAL client:\n"); radioPrintWriter.increaseIndent(); - mHalAidl.dumpInfo(radioPrintWriter); + mAidlHalClient.dumpInfo(radioPrintWriter); radioPrintWriter.decreaseIndent(); radioPrintWriter.decreaseIndent(); diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java index bc72a4be18be..ab083429a200 100644 --- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java +++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java @@ -49,8 +49,8 @@ import java.util.OptionalInt; final class IRadioServiceHidlImpl extends IRadioService.Stub { private static final String TAG = "BcRadioSrvHidl"; - private final com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1; - private final com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2; + private final com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1Client; + private final com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2Client; private final Object mLock = new Object(); @@ -61,10 +61,10 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { IRadioServiceHidlImpl(BroadcastRadioService service) { mService = Objects.requireNonNull(service, "broadcast radio service cannot be null"); - mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(); - mV1Modules = mHal1.loadModules(); + mHal1Client = new com.android.server.broadcastradio.hal1.BroadcastRadioService(); + mV1Modules = mHal1Client.loadModules(); OptionalInt max = mV1Modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max(); - mHal2 = new com.android.server.broadcastradio.hal2.BroadcastRadioService( + mHal2Client = new com.android.server.broadcastradio.hal2.BroadcastRadioService( max.isPresent() ? max.getAsInt() + 1 : 0); } @@ -73,17 +73,17 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { com.android.server.broadcastradio.hal1.BroadcastRadioService hal1, com.android.server.broadcastradio.hal2.BroadcastRadioService hal2) { mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null"); - mHal1 = Objects.requireNonNull(hal1, + mHal1Client = Objects.requireNonNull(hal1, "Broadcast radio service implementation for HIDL 1 HAL cannot be null"); - mV1Modules = mHal1.loadModules(); - mHal2 = Objects.requireNonNull(hal2, + mV1Modules = mHal1Client.loadModules(); + mHal2Client = Objects.requireNonNull(hal2, "Broadcast radio service implementation for HIDL 2 HAL cannot be null"); } @Override public List<RadioManager.ModuleProperties> listModules() { mService.enforcePolicyAccess(); - Collection<RadioManager.ModuleProperties> v2Modules = mHal2.listModules(); + Collection<RadioManager.ModuleProperties> v2Modules = mHal2Client.listModules(); List<RadioManager.ModuleProperties> modules; synchronized (mLock) { modules = new ArrayList<>(mV1Modules.size() + v2Modules.size()); @@ -102,10 +102,10 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { mService.enforcePolicyAccess(); Objects.requireNonNull(callback, "Callback must not be null"); synchronized (mLock) { - if (mHal2.hasModule(moduleId)) { - return mHal2.openSession(moduleId, bandConfig, withAudio, callback); + if (mHal2Client.hasModule(moduleId)) { + return mHal2Client.openSession(moduleId, bandConfig, withAudio, callback); } else { - return mHal1.openTuner(moduleId, bandConfig, withAudio, callback); + return mHal1Client.openTuner(moduleId, bandConfig, withAudio, callback); } } } @@ -121,12 +121,12 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { mService.enforcePolicyAccess(); synchronized (mLock) { - if (!mHal2.hasAnyModules()) { + if (!mHal2Client.hasAnyModules()) { Slog.w(TAG, "There are no HAL 2.0 modules registered"); return new AnnouncementAggregator(listener, mLock); } - return mHal2.addAnnouncementListener(enabledTypes, listener); + return mHal2Client.addAnnouncementListener(enabledTypes, listener); } } @@ -143,18 +143,18 @@ final class IRadioServiceHidlImpl extends IRadioService.Stub { radioPw.printf("BroadcastRadioService\n"); radioPw.increaseIndent(); - radioPw.printf("HAL1: %s\n", mHal1); + radioPw.printf("HAL1 client: %s\n", mHal1Client); radioPw.increaseIndent(); synchronized (mLock) { - radioPw.printf("Modules of HAL1: %s\n", mV1Modules); + radioPw.printf("Modules of HAL1 client: %s\n", mV1Modules); } radioPw.decreaseIndent(); - radioPw.printf("HAL2:\n"); + radioPw.printf("HAL2 client:\n"); radioPw.increaseIndent(); - mHal2.dumpInfo(radioPw); + mHal2Client.dumpInfo(radioPw); radioPw.decreaseIndent(); radioPw.decreaseIndent(); diff --git a/services/core/java/com/android/server/broadcastradio/RadioServiceUserController.java b/services/core/java/com/android/server/broadcastradio/RadioServiceUserController.java index 4b847a27c4de..c705ebe686f2 100644 --- a/services/core/java/com/android/server/broadcastradio/RadioServiceUserController.java +++ b/services/core/java/com/android/server/broadcastradio/RadioServiceUserController.java @@ -48,8 +48,8 @@ public final class RadioServiceUserController { * @return foreground user id. */ public static int getCurrentUser() { - final long identity = Binder.clearCallingIdentity(); int userId = UserHandle.USER_NULL; + final long identity = Binder.clearCallingIdentity(); try { userId = ActivityManager.getCurrentUser(); } catch (RuntimeException e) { diff --git a/services/core/java/com/android/server/broadcastradio/hal1/Convert.java b/services/core/java/com/android/server/broadcastradio/hal1/Convert.java index 219ee4c3229a..08ff6627785a 100644 --- a/services/core/java/com/android/server/broadcastradio/hal1/Convert.java +++ b/services/core/java/com/android/server/broadcastradio/hal1/Convert.java @@ -18,12 +18,13 @@ package com.android.server.broadcastradio.hal1; import android.annotation.NonNull; import android.annotation.Nullable; -import android.util.Slog; + +import com.android.server.utils.Slogf; import java.util.Map; import java.util.Set; -class Convert { +final class Convert { private static final String TAG = "BcRadio1Srv.Convert"; @@ -34,12 +35,12 @@ class Convert { * side, which requires several separate java calls for each element. * * @param map map to convert. - * @returns array (sized the same as map) of two-element string arrays - * (first element is the key, second is value). + * @return array (sized the same as map) of two-element string arrays + * (first element is the key, second is value). */ static @NonNull String[][] stringMapToNative(@Nullable Map<String, String> map) { if (map == null) { - Slog.v(TAG, "map is null, returning zero-elements array"); + Slogf.v(TAG, "map is null, returning zero-elements array"); return new String[0][0]; } @@ -54,7 +55,7 @@ class Convert { i++; } - Slog.v(TAG, "converted " + i + " element(s)"); + Slogf.v(TAG, "converted " + i + " element(s)"); return arr; } } diff --git a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java index 8e5f6b5b8624..7cac4091c583 100644 --- a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java +++ b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java @@ -26,7 +26,6 @@ import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.os.IBinder; import android.os.RemoteException; -import android.util.Slog; import com.android.server.broadcastradio.RadioServiceUserController; import com.android.server.utils.Slogf; @@ -44,9 +43,9 @@ class Tuner extends ITuner.Stub { private final long mNativeContext; private final Object mLock = new Object(); - @NonNull private final TunerCallback mTunerCallback; - @NonNull private final ITunerCallback mClientCallback; - @NonNull private final IBinder.DeathRecipient mDeathRecipient; + private final TunerCallback mTunerCallback; + private final ITunerCallback mClientCallback; + private final IBinder.DeathRecipient mDeathRecipient; private boolean mIsClosed = false; private boolean mIsMuted = false; @@ -122,7 +121,7 @@ class Tuner extends ITuner.Stub { private boolean checkConfiguredLocked() { if (mTunerCallback.isInitialConfigurationDone()) return true; - Slog.w(TAG, "Initial configuration is still pending, skipping the operation"); + Slogf.w(TAG, "Initial configuration is still pending, skipping the operation"); return false; } @@ -159,14 +158,14 @@ class Tuner extends ITuner.Stub { checkNotClosedLocked(); if (mIsMuted == mute) return; mIsMuted = mute; - Slog.w(TAG, "Mute via RadioService is not implemented - please handle it via app"); + Slogf.w(TAG, "Mute via RadioService is not implemented - please handle it via app"); } } @Override public boolean isMuted() { if (!mWithAudio) { - Slog.w(TAG, "Tuner did not request audio, pretending it was muted"); + Slogf.w(TAG, "Tuner did not request audio, pretending it was muted"); return true; } synchronized (mLock) { @@ -210,7 +209,7 @@ class Tuner extends ITuner.Stub { if (selector == null) { throw new IllegalArgumentException("The argument must not be a null pointer"); } - Slog.i(TAG, "Tuning to " + selector); + Slogf.i(TAG, "Tuning to " + selector); synchronized (mLock) { checkNotClosedLocked(); if (!checkConfiguredLocked()) return; diff --git a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java index aa43b7581fe7..e013643a812d 100644 --- a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java +++ b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java @@ -25,7 +25,8 @@ import android.hardware.radio.RadioManager; import android.hardware.radio.RadioTuner; import android.os.IBinder; import android.os.RemoteException; -import android.util.Slog; + +import com.android.server.utils.Slogf; import java.util.List; import java.util.Map; @@ -42,8 +43,8 @@ class TunerCallback implements ITunerCallback { */ private final long mNativeContext; - @NonNull private final Tuner mTuner; - @NonNull private final ITunerCallback mClientCallback; + private final Tuner mTuner; + private final ITunerCallback mClientCallback; private final AtomicReference<ProgramList.Filter> mProgramListFilter = new AtomicReference<>(); private boolean mInitialConfigurationDone = false; @@ -76,7 +77,7 @@ class TunerCallback implements ITunerCallback { try { func.run(); } catch (RemoteException e) { - Slog.e(TAG, "client died", e); + Slogf.e(TAG, "client died", e); } } @@ -107,7 +108,7 @@ class TunerCallback implements ITunerCallback { @Override public void onTuneFailed(int result, ProgramSelector selector) { - Slog.e(TAG, "Not applicable for HAL 1.x"); + Slogf.e(TAG, "Not applicable for HAL 1.x"); } @Override @@ -160,7 +161,7 @@ class TunerCallback implements ITunerCallback { try { modified = mTuner.getProgramList(filter.getVendorFilter()); } catch (IllegalStateException ex) { - Slog.d(TAG, "Program list not ready yet"); + Slogf.d(TAG, "Program list not ready yet"); return; } Set<RadioManager.ProgramInfo> modifiedSet = modified.stream().collect(Collectors.toSet()); @@ -175,12 +176,12 @@ class TunerCallback implements ITunerCallback { @Override public void onConfigFlagUpdated(int flag, boolean value) { - Slog.w(TAG, "Not applicable for HAL 1.x"); + Slogf.w(TAG, "Not applicable for HAL 1.x"); } @Override public void onParametersUpdated(Map<String, String> parameters) { - Slog.w(TAG, "Not applicable for HAL 1.x"); + Slogf.w(TAG, "Not applicable for HAL 1.x"); } @Override diff --git a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java index 85c13aee37c9..0327ee79d39c 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java @@ -23,20 +23,20 @@ import android.hardware.radio.IAnnouncementListener; import android.hardware.radio.ICloseHandle; import android.os.IBinder; import android.os.RemoteException; -import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import com.android.server.utils.Slogf; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; -public class AnnouncementAggregator extends ICloseHandle.Stub { +public final class AnnouncementAggregator extends ICloseHandle.Stub { private static final String TAG = "BcRadio2Srv.AnnAggr"; private final Object mLock; - @NonNull private final IAnnouncementListener mListener; + private final IAnnouncementListener mListener; private final IBinder.DeathRecipient mDeathRecipient = new DeathRecipient(); @GuardedBy("mLock") @@ -77,14 +77,16 @@ public class AnnouncementAggregator extends ICloseHandle.Stub { public void binderDied() { try { close(); - } catch (RemoteException ex) {} + } catch (RemoteException ex) { + Slogf.e(TAG, ex, "Cannot close Announcement aggregator for DeathRecipient"); + } } } private void onListUpdated() { synchronized (mLock) { if (mIsClosed) { - Slog.e(TAG, "Announcement aggregator is closed, it shouldn't receive callbacks"); + Slogf.e(TAG, "Announcement aggregator is closed, it shouldn't receive callbacks"); return; } List<Announcement> combined = new ArrayList<>(); @@ -94,7 +96,7 @@ public class AnnouncementAggregator extends ICloseHandle.Stub { try { mListener.onListUpdated(combined); } catch (RemoteException ex) { - Slog.e(TAG, "mListener.onListUpdated() failed: ", ex); + Slogf.e(TAG, "mListener.onListUpdated() failed: ", ex); } } } @@ -111,7 +113,7 @@ public class AnnouncementAggregator extends ICloseHandle.Stub { try { closeHandle = module.addAnnouncementListener(enabledTypes, watcher); } catch (RemoteException ex) { - Slog.e(TAG, "Failed to add announcement listener", ex); + Slogf.e(TAG, "Failed to add announcement listener", ex); return; } watcher.setCloseHandle(closeHandle); diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java index 1e31f200fd47..3198842c1ff3 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java @@ -29,8 +29,8 @@ import android.hidl.manager.V1_0.IServiceManager; import android.hidl.manager.V1_0.IServiceNotification; import android.os.IHwBinder.DeathRecipient; import android.os.RemoteException; +import android.util.ArrayMap; import android.util.IndentingPrintWriter; -import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -38,7 +38,6 @@ import com.android.server.broadcastradio.RadioServiceUserController; import com.android.server.utils.Slogf; import java.util.Collection; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -55,16 +54,17 @@ public final class BroadcastRadioService { private int mNextModuleId; @GuardedBy("mLock") - private final Map<String, Integer> mServiceNameToModuleIdMap = new HashMap<>(); + private final Map<String, Integer> mServiceNameToModuleIdMap = new ArrayMap<>(); // Map from module ID to RadioModule created by mServiceListener.onRegistration(). @GuardedBy("mLock") - private final Map<Integer, RadioModule> mModules = new HashMap<>(); + private final Map<Integer, RadioModule> mModules = new ArrayMap<>(); - private IServiceNotification.Stub mServiceListener = new IServiceNotification.Stub() { + private final IServiceNotification.Stub mServiceListener = new IServiceNotification.Stub() { @Override public void onRegistration(String fqName, String serviceName, boolean preexisting) { - Slog.v(TAG, "onRegistration(" + fqName + ", " + serviceName + ", " + preexisting + ")"); + Slogf.v(TAG, "onRegistration(" + fqName + ", " + serviceName + ", " + preexisting + + ")"); Integer moduleId; synchronized (mLock) { // If the service has been registered before, reuse its previous module ID. @@ -75,13 +75,13 @@ public final class BroadcastRadioService { moduleId = mNextModuleId; } - RadioModule module = RadioModule.tryLoadingModule(moduleId, serviceName); - if (module == null) { + RadioModule radioModule = RadioModule.tryLoadingModule(moduleId, serviceName); + if (radioModule == null) { return; } - Slog.v(TAG, "loaded broadcast radio module " + moduleId + ": " + serviceName + Slogf.v(TAG, "loaded broadcast radio module " + moduleId + ": " + serviceName + " (HAL 2.0)"); - RadioModule prevModule = mModules.put(moduleId, module); + RadioModule prevModule = mModules.put(moduleId, radioModule); if (prevModule != null) { prevModule.closeSessions(RadioTuner.ERROR_HARDWARE_FAILURE); } @@ -92,7 +92,7 @@ public final class BroadcastRadioService { } try { - module.getService().linkToDeath(mDeathRecipient, moduleId); + radioModule.getService().linkToDeath(mDeathRecipient, moduleId); } catch (RemoteException ex) { // Service has already died, so remove its entry from mModules. mModules.remove(moduleId); @@ -101,10 +101,10 @@ public final class BroadcastRadioService { } }; - private DeathRecipient mDeathRecipient = new DeathRecipient() { + private final DeathRecipient mDeathRecipient = new DeathRecipient() { @Override public void serviceDied(long cookie) { - Slog.v(TAG, "serviceDied(" + cookie + ")"); + Slogf.v(TAG, "serviceDied(" + cookie + ")"); synchronized (mLock) { int moduleId = (int) cookie; RadioModule prevModule = mModules.remove(moduleId); @@ -114,7 +114,7 @@ public final class BroadcastRadioService { for (Map.Entry<String, Integer> entry : mServiceNameToModuleIdMap.entrySet()) { if (entry.getValue() == moduleId) { - Slog.i(TAG, "service " + entry.getKey() + Slogf.i(TAG, "service " + entry.getKey() + " died; removed RadioModule with ID " + moduleId); return; } @@ -128,12 +128,12 @@ public final class BroadcastRadioService { try { IServiceManager manager = IServiceManager.getService(); if (manager == null) { - Slog.e(TAG, "failed to get HIDL Service Manager"); + Slogf.e(TAG, "failed to get HIDL Service Manager"); return; } manager.registerForNotifications(IBroadcastRadio.kInterfaceName, "", mServiceListener); } catch (RemoteException ex) { - Slog.e(TAG, "failed to register for service notifications: ", ex); + Slogf.e(TAG, "failed to register for service notifications: ", ex); } } @@ -144,12 +144,12 @@ public final class BroadcastRadioService { try { manager.registerForNotifications(IBroadcastRadio.kInterfaceName, "", mServiceListener); } catch (RemoteException ex) { - Slog.e(TAG, "Failed to register for service notifications: ", ex); + Slogf.e(TAG, "Failed to register for service notifications: ", ex); } } public @NonNull Collection<RadioManager.ModuleProperties> listModules() { - Slog.v(TAG, "List HIDL 2.0 modules"); + Slogf.v(TAG, "List HIDL 2.0 modules"); synchronized (mLock) { return mModules.values().stream().map(module -> module.getProperties()) .collect(Collectors.toList()); @@ -170,7 +170,7 @@ public final class BroadcastRadioService { public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig, boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException { - Slog.v(TAG, "Open HIDL 2.0 session with module id " + moduleId); + Slogf.v(TAG, "Open HIDL 2.0 session with module id " + moduleId); if (!RadioServiceUserController.isCurrentOrSystemUser()) { Slogf.e(TAG, "Cannot open tuner on HAL 2.0 client for non-current user"); throw new IllegalStateException("Cannot open session for non-current user"); @@ -198,7 +198,7 @@ public final class BroadcastRadioService { public ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes, @NonNull IAnnouncementListener listener) { - Slog.v(TAG, "Add announcementListener"); + Slogf.v(TAG, "Add announcementListener"); AnnouncementAggregator aggregator = new AnnouncementAggregator(listener, mLock); boolean anySupported = false; synchronized (mLock) { @@ -207,12 +207,12 @@ public final class BroadcastRadioService { aggregator.watchModule(module, enabledTypes); anySupported = true; } catch (UnsupportedOperationException ex) { - Slog.v(TAG, "Announcements not supported for this module", ex); + Slogf.v(TAG, "Announcements not supported for this module", ex); } } } if (!anySupported) { - Slog.i(TAG, "There are no HAL modules that support announcements"); + Slogf.i(TAG, "There are no HAL modules that support announcements"); } return aggregator; } diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java index fb1138f4bc24..34bfa6cb2d46 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java @@ -37,21 +37,22 @@ import android.hardware.radio.RadioManager; import android.hardware.radio.RadioMetadata; import android.hardware.radio.RadioTuner; import android.os.ParcelableException; -import android.util.Slog; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.SparseArray; + +import com.android.server.utils.Slogf; import java.util.ArrayList; 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.Objects; import java.util.Set; import java.util.stream.Collectors; -class Convert { +final class Convert { private static final String TAG = "BcRadio2Srv.convert"; @@ -111,7 +112,7 @@ class Convert { elem.key = entry.getKey(); elem.value = entry.getValue(); if (elem.key == null || elem.value == null) { - Slog.w(TAG, "VendorKeyValue contains null pointers"); + Slogf.w(TAG, "VendorKeyValue contains null pointers"); continue; } list.add(elem); @@ -120,20 +121,21 @@ class Convert { return list; } - static @NonNull Map<String, String> - vendorInfoFromHal(@Nullable List<VendorKeyValue> info) { - if (info == null) return Collections.emptyMap(); + static @NonNull Map<String, String> vendorInfoFromHal(@Nullable List<VendorKeyValue> info) { + Map<String, String> vendorInfoMap = new ArrayMap<>(); + if (info == null) { + return vendorInfoMap; + } - Map<String, String> map = new HashMap<>(); for (VendorKeyValue kvp : info) { if (kvp.key == null || kvp.value == null) { - Slog.w(TAG, "VendorKeyValue contains null pointers"); + Slogf.w(TAG, "VendorKeyValue contains null pointers"); continue; } - map.put(kvp.key, kvp.value); + vendorInfoMap.put(kvp.key, kvp.value); } - return map; + return vendorInfoMap; } private static @ProgramSelector.ProgramType int identifierTypeToProgramType( @@ -168,7 +170,7 @@ class Convert { private static @NonNull int[] identifierTypesToProgramTypes(@NonNull int[] idTypes) { - Set<Integer> pTypes = new HashSet<>(); + Set<Integer> pTypes = new ArraySet<>(); for (int idType : idTypes) { int pType = identifierTypeToProgramType(idType); @@ -202,7 +204,7 @@ class Convert { for (AmFmBandRange range : config.ranges) { FrequencyBand bandType = Utils.getBand(range.lowerBound); if (bandType == FrequencyBand.UNKNOWN) { - Slog.e(TAG, "Unknown frequency band at " + range.lowerBound + "kHz"); + Slogf.e(TAG, "Unknown frequency band at " + range.lowerBound + "kHz"); continue; } if (bandType == FrequencyBand.FM) { @@ -304,7 +306,7 @@ class Convert { @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) { if (sel.primaryId.type != 0) return false; if (sel.primaryId.value != 0) return false; - if (sel.secondaryIds.size() != 0) return false; + if (!sel.secondaryIds.isEmpty()) return false; return true; } @@ -319,7 +321,7 @@ class Convert { return new ProgramSelector( identifierTypeToProgramType(sel.primaryId.type), Objects.requireNonNull(programIdentifierFromHal(sel.primaryId)), - secondaryIds, null); + secondaryIds, /* vendorIds= */ null); } private enum MetadataType { @@ -335,40 +337,40 @@ class Convert { } } - private static final Map<Integer, MetadataDef> metadataKeys; + private static final SparseArray<MetadataDef> METADATA_KEYS; static { - metadataKeys = new HashMap<>(); - metadataKeys.put(MetadataKey.RDS_PS, new MetadataDef( + METADATA_KEYS = new SparseArray<>(); + METADATA_KEYS.put(MetadataKey.RDS_PS, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_RDS_PS)); - metadataKeys.put(MetadataKey.RDS_PTY, new MetadataDef( + METADATA_KEYS.put(MetadataKey.RDS_PTY, new MetadataDef( MetadataType.INT, RadioMetadata.METADATA_KEY_RDS_PTY)); - metadataKeys.put(MetadataKey.RBDS_PTY, new MetadataDef( + METADATA_KEYS.put(MetadataKey.RBDS_PTY, new MetadataDef( MetadataType.INT, RadioMetadata.METADATA_KEY_RBDS_PTY)); - metadataKeys.put(MetadataKey.RDS_RT, new MetadataDef( + METADATA_KEYS.put(MetadataKey.RDS_RT, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_RDS_RT)); - metadataKeys.put(MetadataKey.SONG_TITLE, new MetadataDef( + METADATA_KEYS.put(MetadataKey.SONG_TITLE, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_TITLE)); - metadataKeys.put(MetadataKey.SONG_ARTIST, new MetadataDef( + METADATA_KEYS.put(MetadataKey.SONG_ARTIST, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_ARTIST)); - metadataKeys.put(MetadataKey.SONG_ALBUM, new MetadataDef( + METADATA_KEYS.put(MetadataKey.SONG_ALBUM, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_ALBUM)); - metadataKeys.put(MetadataKey.STATION_ICON, new MetadataDef( + METADATA_KEYS.put(MetadataKey.STATION_ICON, new MetadataDef( MetadataType.INT, RadioMetadata.METADATA_KEY_ICON)); - metadataKeys.put(MetadataKey.ALBUM_ART, new MetadataDef( + METADATA_KEYS.put(MetadataKey.ALBUM_ART, new MetadataDef( MetadataType.INT, RadioMetadata.METADATA_KEY_ART)); - metadataKeys.put(MetadataKey.PROGRAM_NAME, new MetadataDef( + METADATA_KEYS.put(MetadataKey.PROGRAM_NAME, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_PROGRAM_NAME)); - metadataKeys.put(MetadataKey.DAB_ENSEMBLE_NAME, new MetadataDef( + METADATA_KEYS.put(MetadataKey.DAB_ENSEMBLE_NAME, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME)); - metadataKeys.put(MetadataKey.DAB_ENSEMBLE_NAME_SHORT, new MetadataDef( + METADATA_KEYS.put(MetadataKey.DAB_ENSEMBLE_NAME_SHORT, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT)); - metadataKeys.put(MetadataKey.DAB_SERVICE_NAME, new MetadataDef( + METADATA_KEYS.put(MetadataKey.DAB_SERVICE_NAME, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME)); - metadataKeys.put(MetadataKey.DAB_SERVICE_NAME_SHORT, new MetadataDef( + METADATA_KEYS.put(MetadataKey.DAB_SERVICE_NAME_SHORT, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME_SHORT)); - metadataKeys.put(MetadataKey.DAB_COMPONENT_NAME, new MetadataDef( + METADATA_KEYS.put(MetadataKey.DAB_COMPONENT_NAME, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME)); - metadataKeys.put(MetadataKey.DAB_COMPONENT_NAME_SHORT, new MetadataDef( + METADATA_KEYS.put(MetadataKey.DAB_COMPONENT_NAME_SHORT, new MetadataDef( MetadataType.STRING, RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME_SHORT)); } @@ -376,9 +378,9 @@ class Convert { RadioMetadata.Builder builder = new RadioMetadata.Builder(); for (Metadata entry : meta) { - MetadataDef keyDef = metadataKeys.get(entry.key); + MetadataDef keyDef = METADATA_KEYS.get(entry.key); if (keyDef == null) { - Slog.i(TAG, "Ignored unknown metadata entry: " + MetadataKey.toString(entry.key)); + Slogf.i(TAG, "Ignored unknown metadata entry: " + MetadataKey.toString(entry.key)); continue; } if (keyDef.type == MetadataType.STRING) { diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java index 48112c452f02..b8d12280ac05 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioEventLogger.java @@ -19,7 +19,8 @@ package com.android.server.broadcastradio.hal2; import android.util.IndentingPrintWriter; import android.util.LocalLog; import android.util.Log; -import android.util.Slog; + +import com.android.server.utils.Slogf; final class RadioEventLogger { private final String mTag; @@ -30,11 +31,12 @@ final class RadioEventLogger { mEventLogger = new LocalLog(loggerQueueSize); } + @SuppressWarnings("AnnotateFormatMethod") void logRadioEvent(String logFormat, Object... args) { String log = String.format(logFormat, args); mEventLogger.log(log); if (Log.isLoggable(mTag, Log.DEBUG)) { - Slog.d(mTag, log); + Slogf.d(mTag, log); } } diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java index a54af2ef6e44..0e11df8282a7 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java @@ -39,16 +39,16 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; +import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.MutableInt; -import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.broadcastradio.RadioServiceUserController; +import com.android.server.utils.Slogf; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -59,12 +59,12 @@ final class RadioModule { private static final String TAG = "BcRadio2Srv.module"; private static final int RADIO_EVENT_LOGGER_QUEUE_SIZE = 25; - @NonNull private final IBroadcastRadio mService; - @NonNull private final RadioManager.ModuleProperties mProperties; + private final IBroadcastRadio mService; + private final RadioManager.ModuleProperties mProperties; private final Object mLock = new Object(); - @NonNull private final Handler mHandler; - @NonNull private final RadioEventLogger mEventLogger; + private final Handler mHandler; + private final RadioEventLogger mEventLogger; @GuardedBy("mLock") private ITunerSession mHalTunerSession; @@ -144,7 +144,7 @@ final class RadioModule { // Collection of active AIDL tuner sessions created through openSession(). @GuardedBy("mLock") - private final Set<TunerSession> mAidlTunerSessions = new HashSet<>(); + private final Set<TunerSession> mAidlTunerSessions = new ArraySet<>(); @VisibleForTesting RadioModule(@NonNull IBroadcastRadio service, @@ -158,10 +158,10 @@ final class RadioModule { @Nullable static RadioModule tryLoadingModule(int idx, @NonNull String fqName) { try { - Slog.i(TAG, "Try loading module for idx " + idx + ", fqName " + fqName); + Slogf.i(TAG, "Try loading module for idx " + idx + ", fqName " + fqName); IBroadcastRadio service = IBroadcastRadio.getService(fqName); if (service == null) { - Slog.w(TAG, "No service found for fqName " + fqName); + Slogf.w(TAG, "No service found for fqName " + fqName); return null; } @@ -180,7 +180,7 @@ final class RadioModule { return new RadioModule(service, prop); } catch (RemoteException ex) { - Slog.e(TAG, "Failed to load module " + fqName, ex); + Slogf.e(TAG, "Failed to load module " + fqName, ex); return null; } } @@ -256,8 +256,8 @@ final class RadioModule { } if (idTypes == null) { - idTypes = new HashSet<>(filter.getIdentifierTypes()); - ids = new HashSet<>(filter.getIdentifiers()); + idTypes = new ArraySet<>(filter.getIdentifierTypes()); + ids = new ArraySet<>(filter.getIdentifiers()); includeCategories = filter.areCategoriesIncluded(); excludeModifications = filter.areModificationsExcluded(); continue; @@ -305,7 +305,7 @@ final class RadioModule { try { mHalTunerSession.stopProgramListUpdates(); } catch (RemoteException ex) { - Slog.e(TAG, "mHalTunerSession.stopProgramListUpdates() failed: ", ex); + Slogf.e(TAG, "mHalTunerSession.stopProgramListUpdates() failed: ", ex); } return; } @@ -327,7 +327,7 @@ final class RadioModule { newFilter)); Convert.throwOnError("startProgramListUpdates", halResult); } catch (RemoteException ex) { - Slog.e(TAG, "mHalTunerSession.startProgramListUpdates() failed: ", ex); + Slogf.e(TAG, "mHalTunerSession.startProgramListUpdates() failed: ", ex); } } @@ -348,7 +348,7 @@ final class RadioModule { try { mHalTunerSession.close(); } catch (RemoteException ex) { - Slog.e(TAG, "mHalTunerSession.close() failed: ", ex); + Slogf.e(TAG, "mHalTunerSession.close() failed: ", ex); } mHalTunerSession = null; } @@ -385,18 +385,17 @@ final class RadioModule { runnable.run(tunerSession.mCallback); } catch (DeadObjectException ex) { // The other side died without calling close(), so just purge it from our records. - Slog.e(TAG, "Removing dead TunerSession"); + Slogf.e(TAG, "Removing dead TunerSession"); if (deadSessions == null) { deadSessions = new ArrayList<>(); } deadSessions.add(tunerSession); } catch (RemoteException ex) { - Slog.e(TAG, "Failed to invoke ITunerCallback: ", ex); + Slogf.e(TAG, "Failed to invoke ITunerCallback: ", ex); } } if (deadSessions != null) { - onTunerSessionsClosedLocked(deadSessions.toArray( - new TunerSession[deadSessions.size()])); + onTunerSessionsClosedLocked(deadSessions.toArray(new TunerSession[0])); } } @@ -429,7 +428,7 @@ final class RadioModule { try { hwCloseHandle.value.close(); } catch (RemoteException ex) { - Slog.e(TAG, "Failed closing announcement listener", ex); + Slogf.e(TAG, "Failed closing announcement listener", ex); } hwCloseHandle.value = null; } @@ -447,7 +446,9 @@ final class RadioModule { rawImage[i] = rawList.get(i); } - if (rawImage == null || rawImage.length == 0) return null; + if (rawImage.length == 0) { + return null; + } return BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length); } diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java index 978dc01d1219..6d435e38117f 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java @@ -30,27 +30,25 @@ import android.hardware.radio.RadioManager; import android.os.Binder; import android.os.RemoteException; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.MutableBoolean; import android.util.MutableInt; -import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.server.broadcastradio.RadioServiceUserController; import com.android.server.utils.Slogf; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -class TunerSession extends ITuner.Stub { +final class TunerSession extends ITuner.Stub { private static final String TAG = "BcRadio2Srv.session"; - private static final String kAudioDeviceName = "Radio tuner source"; private static final int TUNER_EVENT_LOGGER_QUEUE_SIZE = 25; private final Object mLock = new Object(); - @NonNull private final RadioEventLogger mEventLogger; + private final RadioEventLogger mEventLogger; private final RadioModule mModule; private final ITunerSession mHwSession; @@ -99,7 +97,7 @@ class TunerSession extends ITuner.Stub { try { mCallback.onError(error); } catch (RemoteException ex) { - Slog.w(TAG, "mCallback.onError() failed: ", ex); + Slogf.w(TAG, "mCallback.onError() failed: ", ex); } } mModule.onTunerSessionClosed(this); @@ -129,7 +127,7 @@ class TunerSession extends ITuner.Stub { checkNotClosedLocked(); mDummyConfig = Objects.requireNonNull(config); } - Slog.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL 2.0"); + Slogf.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL 2.0"); mModule.fanoutAidlCallback(cb -> cb.onConfigurationChanged(config)); } @@ -148,7 +146,7 @@ class TunerSession extends ITuner.Stub { if (mIsMuted == mute) return; mIsMuted = mute; } - Slog.w(TAG, "Mute via RadioService is not implemented - please handle it via app"); + Slogf.w(TAG, "Mute via RadioService is not implemented - please handle it via app"); } @Override @@ -205,7 +203,7 @@ class TunerSession extends ITuner.Stub { @Override public void cancel() { - Slog.i(TAG, "Cancel"); + Slogf.i(TAG, "Cancel"); if (!RadioServiceUserController.isCurrentOrSystemUser()) { Slogf.w(TAG, "Cannot cancel on HAL 2.0 client from non-current user"); return; @@ -218,7 +216,8 @@ class TunerSession extends ITuner.Stub { @Override public void cancelAnnouncement() { - Slog.w(TAG, "Announcements control doesn't involve cancelling at the HAL level in HAL 2.0"); + Slogf.w(TAG, + "Announcements control doesn't involve cancelling at the HAL level in HAL 2.0"); } @Override @@ -229,7 +228,7 @@ class TunerSession extends ITuner.Stub { @Override public boolean startBackgroundScan() { - Slog.w(TAG, "Explicit background scan trigger is not supported with HAL 2.0"); + Slogf.w(TAG, "Explicit background scan trigger is not supported with HAL 2.0"); if (!RadioServiceUserController.isCurrentOrSystemUser()) { Slogf.w(TAG, "Cannot start background scan on HAL 2.0 client from non-current user"); @@ -240,7 +239,7 @@ class TunerSession extends ITuner.Stub { } @Override - public void startProgramListUpdates(ProgramList.Filter filter) throws RemoteException { + public void startProgramListUpdates(ProgramList.Filter filter) { mEventLogger.logRadioEvent("start programList updates %s", filter); if (!RadioServiceUserController.isCurrentOrSystemUser()) { Slogf.w(TAG, @@ -250,8 +249,8 @@ class TunerSession extends ITuner.Stub { // If the AIDL client provides a null filter, it wants all updates, so use the most broad // filter. if (filter == null) { - filter = new ProgramList.Filter(new HashSet<Integer>(), - new HashSet<android.hardware.radio.ProgramSelector.Identifier>(), true, false); + filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); } synchronized (mLock) { checkNotClosedLocked(); @@ -285,7 +284,7 @@ class TunerSession extends ITuner.Stub { if (mProgramInfoCache == null) { return; } - clientUpdateChunks = mProgramInfoCache.filterAndUpdateFrom(halCache, true); + clientUpdateChunks = mProgramInfoCache.filterAndUpdateFrom(halCache, /* purge= */ true); } dispatchClientUpdateChunks(clientUpdateChunks); } @@ -298,7 +297,7 @@ class TunerSession extends ITuner.Stub { try { mCallback.onProgramListUpdated(chunk); } catch (RemoteException ex) { - Slog.w(TAG, "mCallback.onProgramListUpdated() failed: ", ex); + Slogf.w(TAG, "mCallback.onProgramListUpdated() failed: ", ex); } } } diff --git a/services/core/java/com/android/server/connectivity/Android.bp b/services/core/java/com/android/server/connectivity/Android.bp new file mode 100644 index 000000000000..a374ec2cea9a --- /dev/null +++ b/services/core/java/com/android/server/connectivity/Android.bp @@ -0,0 +1,10 @@ +aconfig_declarations { + name: "connectivity_flags", + package: "com.android.server.connectivity", + srcs: ["flags.aconfig"], +} + +java_aconfig_library { + name: "connectivity_flags_lib", + aconfig_declarations: "connectivity_flags", +} diff --git a/services/core/java/com/android/server/connectivity/flags.aconfig b/services/core/java/com/android/server/connectivity/flags.aconfig new file mode 100644 index 000000000000..32593d4bcdaa --- /dev/null +++ b/services/core/java/com/android/server/connectivity/flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.connectivity" + +flag { + name: "replace_vpn_profile_store" + namespace: "android_core_networking" + description: "This flag controls the usage of VpnBlobStore to replace LegacyVpnProfileStore." + bug: "307903113" +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java index 10030b3c9176..dc0e80c686a8 100644 --- a/services/core/java/com/android/server/display/BrightnessRangeController.java +++ b/services/core/java/com/android/server/display/BrightnessRangeController.java @@ -117,6 +117,7 @@ class BrightnessRangeController { () -> mNormalBrightnessModeController.setAutoBrightnessState(state), () -> mHbmController.setAutoBrightnessEnabled(state) ); + mHdrClamper.setAutoBrightnessState(state); } void onBrightnessChanged(float brightness, float unthrottledBrightness, diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 40f0362ff8f3..31092f27c838 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -757,6 +757,7 @@ public final class DisplayManagerService extends SystemService { mContext.getSystemService(DeviceStateManager.class).registerCallback( new HandlerExecutor(mHandler), new DeviceStateListener()); + mLogicalDisplayMapper.onWindowManagerReady(); scheduleTraversalLocked(false); } } diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 6203a32151a0..bca53cf02f69 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -41,10 +41,12 @@ import android.view.DisplayInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.foldables.FoldGracePeriodProvider; +import com.android.server.LocalServices; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.layout.DisplayIdProducer; import com.android.server.display.layout.Layout; import com.android.server.display.utils.DebugUtils; +import com.android.server.policy.WindowManagerPolicy; import com.android.server.utils.FoldSettingProvider; import java.io.PrintWriter; @@ -189,6 +191,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { * #updateLogicalDisplaysLocked} to establish which Virtual Devices own which Virtual Displays. */ private final ArrayMap<String, Integer> mVirtualDeviceDisplayMapping = new ArrayMap<>(); + private WindowManagerPolicy mWindowManagerPolicy; private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1; private final DisplayIdProducer mIdProducer = (isDefault) -> @@ -274,6 +277,10 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { mListener.onTraversalRequested(); } + public void onWindowManagerReady() { + mWindowManagerPolicy = LocalServices.getService(WindowManagerPolicy.class); + } + public LogicalDisplay getDisplayLocked(int displayId) { return getDisplayLocked(displayId, /* includeDisabled= */ true); } @@ -1114,14 +1121,22 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { final int logicalDisplayId = displayLayout.getLogicalDisplayId(); LogicalDisplay newDisplay = getDisplayLocked(logicalDisplayId); + boolean newDisplayCreated = false; if (newDisplay == null) { newDisplay = createNewLogicalDisplayLocked( null /*displayDevice*/, logicalDisplayId); + newDisplayCreated = true; } // Now swap the underlying display devices between the old display and the new display final LogicalDisplay oldDisplay = getDisplayLocked(device); if (newDisplay != oldDisplay) { + // Display is swapping, notify WindowManager, so it can prepare for + // the display switch + if (!newDisplayCreated && mWindowManagerPolicy != null) { + mWindowManagerPolicy.onDisplaySwitchStart(newDisplay.getDisplayIdLocked()); + } + newDisplay.swapDisplaysLocked(oldDisplay); } DisplayDeviceConfig config = device.getDisplayDeviceConfig(); diff --git a/services/core/java/com/android/server/display/NormalBrightnessModeController.java b/services/core/java/com/android/server/display/NormalBrightnessModeController.java index 135ebd8f4fbf..e94cf00437eb 100644 --- a/services/core/java/com/android/server/display/NormalBrightnessModeController.java +++ b/services/core/java/com/android/server/display/NormalBrightnessModeController.java @@ -79,10 +79,12 @@ class NormalBrightnessModeController { maxBrightnessPoints = mMaxBrightnessLimits.get(BrightnessLimitMapType.ADAPTIVE); } - if (maxBrightnessPoints == null) { + // AutoBrightnessController sends ambientLux values *only* when auto brightness enabled. + // Temporary disabling this Controller if auto brightness is off, to avoid capping + // brightness based on stale ambient lux. The issue is tracked here: b/322445088 + if (mAutoBrightnessEnabled && maxBrightnessPoints == null) { maxBrightnessPoints = mMaxBrightnessLimits.get(BrightnessLimitMapType.DEFAULT); } - if (maxBrightnessPoints != null) { for (Map.Entry<Float, Float> brightnessPoint : maxBrightnessPoints.entrySet()) { float ambientBoundary = brightnessPoint.getKey(); diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java index 01a8d360a526..f1cb66c0efbb 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java @@ -24,6 +24,7 @@ import android.os.PowerManager; import android.view.SurfaceControlHdrLayerInfoListener; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.AutomaticBrightnessController; import com.android.server.display.config.HdrBrightnessData; import java.io.PrintWriter; @@ -56,6 +57,8 @@ public class HdrClamper { private float mTransitionRate = -1f; private float mDesiredTransitionRate = -1f; + private boolean mAutoBrightnessEnabled = false; + public HdrClamper(BrightnessClamperController.ClamperChangeListener clamperChangeListener, Handler handler) { this(clamperChangeListener, handler, new Injector()); @@ -122,6 +125,18 @@ public class HdrClamper { recalculateBrightnessCap(data, mAmbientLux, mHdrVisible); } + /** + * Sets state of auto brightness to temporary disabling this Clamper if auto brightness is off. + * The issue is tracked here: b/322445088 + */ + public void setAutoBrightnessState(int state) { + boolean isEnabled = state == AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED; + if (isEnabled != mAutoBrightnessEnabled) { + mAutoBrightnessEnabled = isEnabled; + recalculateBrightnessCap(mHdrBrightnessData, mAmbientLux, mHdrVisible); + } + } + /** Clean up all resources */ @SuppressLint("AndroidFrameworkRequiresPermission") public void stop() { @@ -145,6 +160,7 @@ public class HdrClamper { : mHdrBrightnessData.toString())); pw.println(" mHdrListener registered=" + (mRegisteredDisplayToken != null)); pw.println(" mAmbientLux=" + mAmbientLux); + pw.println(" mAutoBrightnessEnabled=" + mAutoBrightnessEnabled); } private void reset() { @@ -163,7 +179,10 @@ public class HdrClamper { private void recalculateBrightnessCap(HdrBrightnessData data, float ambientLux, boolean hdrVisible) { - if (data == null || !hdrVisible) { + // AutoBrightnessController sends ambientLux values *only* when auto brightness enabled. + // Temporary disabling this Clamper if auto brightness is off, to avoid capping + // brightness based on stale ambient lux. The issue is tracked here: b/322445088 + if (data == null || !hdrVisible || !mAutoBrightnessEnabled) { reset(); return; } diff --git a/services/core/java/com/android/server/display/feature/Android.bp b/services/core/java/com/android/server/display/feature/Android.bp index a0ead384c1d2..daf8832fd1d7 100644 --- a/services/core/java/com/android/server/display/feature/Android.bp +++ b/services/core/java/com/android/server/display/feature/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "display_flags", package: "com.android.server.display.feature.flags", + container: "system", srcs: [ "*.aconfig", ], diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index d4319cc2b633..c68ef9bd4cce 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.display.feature.flags" +container: "system" # Important: Flags must be accessed through DisplayManagerFlags. diff --git a/services/core/java/com/android/server/feature/Android.bp b/services/core/java/com/android/server/feature/Android.bp index 067288d6650d..b0fbab6657b0 100644 --- a/services/core/java/com/android/server/feature/Android.bp +++ b/services/core/java/com/android/server/feature/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "dropbox_flags", package: "com.android.server.feature.flags", + container: "system", srcs: [ "dropbox_flags.aconfig", ], 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 14e964b26c6b..98978f03fae5 100644 --- a/services/core/java/com/android/server/feature/dropbox_flags.aconfig +++ b/services/core/java/com/android/server/feature/dropbox_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.feature.flags" +container: "system" flag{ name: "enable_read_dropbox_permission" diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java index 7b844a099841..f3836794c32e 100644 --- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java +++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java @@ -165,7 +165,11 @@ public final class FontManagerService extends IFontManager.Stub { @Override public void onBootPhase(int phase) { - if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { + final int latestFontLoadBootPhase = + (Flags.completeFontLoadInSystemServicesReady()) + ? SystemService.PHASE_SYSTEM_SERVICES_READY + : SystemService.PHASE_ACTIVITY_MANAGER_READY; + if (phase == latestFontLoadBootPhase) { // Wait for FontManagerService to start since it will be needed after this point. mServiceStarted.join(); } diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java index 9d04682f2374..ea240c75452d 100644 --- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java +++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java @@ -196,7 +196,12 @@ final class UpdatableFontDir { File signatureFile = new File(dir, FONT_SIGNATURE_FILE); if (!signatureFile.exists()) { Slog.i(TAG, "The signature file is missing."); - return; + if (com.android.text.flags.Flags.fixFontUpdateFailure()) { + return; + } else { + FileUtils.deleteContentsAndDir(dir); + continue; + } } byte[] signature; try { @@ -221,33 +226,39 @@ final class UpdatableFontDir { FontFileInfo fontFileInfo = validateFontFile(fontFile, signature); if (fontConfig == null) { - // Use preinstalled font config for checking revision number. - fontConfig = mConfigSupplier.apply(Collections.emptyMap()); + if (com.android.text.flags.Flags.fixFontUpdateFailure()) { + // Use preinstalled font config for checking revision number. + fontConfig = mConfigSupplier.apply(Collections.emptyMap()); + } else { + fontConfig = getSystemFontConfig(); + } } addFileToMapIfSameOrNewer(fontFileInfo, fontConfig, true /* deleteOldFile */); } - // Treat as error if post script name of font family was not installed. - for (int i = 0; i < config.fontFamilies.size(); ++i) { - FontUpdateRequest.Family family = config.fontFamilies.get(i); - for (int j = 0; j < family.getFonts().size(); ++j) { - FontUpdateRequest.Font font = family.getFonts().get(j); - if (mFontFileInfoMap.containsKey(font.getPostScriptName())) { - continue; + if (com.android.text.flags.Flags.fixFontUpdateFailure()) { + // Treat as error if post script name of font family was not installed. + for (int i = 0; i < config.fontFamilies.size(); ++i) { + FontUpdateRequest.Family family = config.fontFamilies.get(i); + for (int j = 0; j < family.getFonts().size(); ++j) { + FontUpdateRequest.Font font = family.getFonts().get(j); + if (mFontFileInfoMap.containsKey(font.getPostScriptName())) { + continue; + } + + if (fontConfig == null) { + fontConfig = mConfigSupplier.apply(Collections.emptyMap()); + } + + if (getFontByPostScriptName(font.getPostScriptName(), fontConfig) != null) { + continue; + } + + Slog.e(TAG, "Unknown font that has PostScript name " + + font.getPostScriptName() + " is requested in FontFamily " + + family.getName()); + return; } - - if (fontConfig == null) { - fontConfig = mConfigSupplier.apply(Collections.emptyMap()); - } - - if (getFontByPostScriptName(font.getPostScriptName(), fontConfig) != null) { - continue; - } - - Slog.e(TAG, "Unknown font that has PostScript name " - + font.getPostScriptName() + " is requested in FontFamily " - + family.getName()); - return; } } @@ -262,7 +273,9 @@ final class UpdatableFontDir { mFontFileInfoMap.clear(); mLastModifiedMillis = 0; FileUtils.deleteContents(mFilesDir); - mConfigFile.delete(); + if (com.android.text.flags.Flags.fixFontUpdateFailure()) { + mConfigFile.delete(); + } } } } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 77119d5ac384..f32c11d90c0d 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1197,54 +1197,11 @@ public class InputManagerService extends IInputManager.Stub } @Override // Binder call - public KeyboardLayout[] getKeyboardLayoutsForInputDevice( - final InputDeviceIdentifier identifier) { - return mKeyboardLayoutManager.getKeyboardLayoutsForInputDevice(identifier); - } - - @Override // Binder call public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) { return mKeyboardLayoutManager.getKeyboardLayout(keyboardLayoutDescriptor); } @Override // Binder call - public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) { - return mKeyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice(identifier); - } - - @EnforcePermission(Manifest.permission.SET_KEYBOARD_LAYOUT) - @Override // Binder call - public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor) { - super.setCurrentKeyboardLayoutForInputDevice_enforcePermission(); - mKeyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice(identifier, - keyboardLayoutDescriptor); - } - - @Override // Binder call - public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) { - return mKeyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice(identifier); - } - - @EnforcePermission(Manifest.permission.SET_KEYBOARD_LAYOUT) - @Override // Binder call - public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor) { - super.addKeyboardLayoutForInputDevice_enforcePermission(); - mKeyboardLayoutManager.addKeyboardLayoutForInputDevice(identifier, - keyboardLayoutDescriptor); - } - - @EnforcePermission(Manifest.permission.SET_KEYBOARD_LAYOUT) - @Override // Binder call - public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor) { - super.removeKeyboardLayoutForInputDevice_enforcePermission(); - mKeyboardLayoutManager.removeKeyboardLayoutForInputDevice(identifier, - keyboardLayoutDescriptor); - } - - @Override // Binder call public KeyboardLayoutSelectionResult getKeyboardLayoutForInputDevice( InputDeviceIdentifier identifier, @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype) { @@ -1270,11 +1227,6 @@ public class InputManagerService extends IInputManager.Stub imeInfo, imeSubtype); } - - public void switchKeyboardLayout(int deviceId, int direction) { - mKeyboardLayoutManager.switchKeyboardLayout(deviceId, direction); - } - public void setFocusedApplication(int displayId, InputApplicationHandle application) { mNative.setFocusedApplication(displayId, application); } diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index 661008103a25..9ba647fb1d2d 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -60,7 +60,6 @@ import android.os.UserManager; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; -import android.util.FeatureFlagUtils; import android.util.Log; import android.util.Slog; import android.util.SparseArray; @@ -69,7 +68,6 @@ import android.view.KeyCharacterMap; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; -import android.widget.Toast; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -96,7 +94,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Stream; /** * A component of {@link InputManagerService} responsible for managing Physical Keyboard layouts. @@ -112,9 +109,8 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int MSG_UPDATE_EXISTING_DEVICES = 1; - private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 2; - private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 3; - private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 4; + private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 2; + private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 3; private final Context mContext; private final NativeInputManagerService mNative; @@ -126,13 +122,14 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { // Connected keyboards with associated keyboard layouts (either auto-detected or manually // selected layout). private final SparseArray<KeyboardConfiguration> mConfiguredKeyboards = new SparseArray<>(); - private Toast mSwitchedKeyboardLayoutToast; // This cache stores "best-matched" layouts so that we don't need to run the matching // algorithm repeatedly. @GuardedBy("mKeyboardLayoutCache") private final Map<String, KeyboardLayoutSelectionResult> mKeyboardLayoutCache = new ArrayMap<>(); + + private HashSet<String> mAvailableLayouts = new HashSet<>(); private final Object mImeInfoLock = new Object(); @Nullable @GuardedBy("mImeInfoLock") @@ -206,68 +203,51 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } boolean needToShowNotification = false; - if (!useNewSettingsUi()) { - synchronized (mDataStore) { - String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier()); - if (layout == null) { - layout = getDefaultKeyboardLayout(inputDevice); - if (layout != null) { - setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout); - } - } - if (layout == null) { - // In old settings show notification always until user manually selects a - // layout in the settings. - needToShowNotification = true; - } - } - } else { - Set<String> selectedLayouts = new HashSet<>(); - List<ImeInfo> imeInfoList = getImeInfoListForLayoutMapping(); - List<KeyboardLayoutSelectionResult> resultList = new ArrayList<>(); - boolean hasMissingLayout = false; - for (ImeInfo imeInfo : imeInfoList) { - // Check if the layout has been previously configured - KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal( - keyboardIdentifier, imeInfo); - boolean noLayoutFound = result.getLayoutDescriptor() == null; - if (!noLayoutFound) { - selectedLayouts.add(result.getLayoutDescriptor()); - } - resultList.add(result); - hasMissingLayout |= noLayoutFound; + Set<String> selectedLayouts = new HashSet<>(); + List<ImeInfo> imeInfoList = getImeInfoListForLayoutMapping(); + List<KeyboardLayoutSelectionResult> resultList = new ArrayList<>(); + boolean hasMissingLayout = false; + for (ImeInfo imeInfo : imeInfoList) { + // Check if the layout has been previously configured + KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal( + keyboardIdentifier, imeInfo); + if (result.getLayoutDescriptor() != null) { + selectedLayouts.add(result.getLayoutDescriptor()); + } else { + hasMissingLayout = true; } + resultList.add(result); + } - if (DEBUG) { - Slog.d(TAG, - "Layouts selected for input device: " + keyboardIdentifier - + " -> selectedLayouts: " + selectedLayouts); - } + if (DEBUG) { + Slog.d(TAG, + "Layouts selected for input device: " + keyboardIdentifier + + " -> selectedLayouts: " + selectedLayouts); + } - // If even one layout not configured properly, we need to ask user to configure - // the keyboard properly from the Settings. - if (hasMissingLayout) { - selectedLayouts.clear(); - } + // If even one layout not configured properly, we need to ask user to configure + // the keyboard properly from the Settings. + if (hasMissingLayout) { + selectedLayouts.clear(); + } - config.setConfiguredLayouts(selectedLayouts); + config.setConfiguredLayouts(selectedLayouts); - synchronized (mDataStore) { - try { - final String key = keyboardIdentifier.toString(); - if (mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) { - // Need to show the notification only if layout selection changed - // from the previous configuration - needToShowNotification = true; - } + synchronized (mDataStore) { + try { + final String key = keyboardIdentifier.toString(); + if (mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) { + // Need to show the notification only if layout selection changed + // from the previous configuration + needToShowNotification = true; + } - if (shouldLogConfiguration) { - logKeyboardConfigurationEvent(inputDevice, imeInfoList, resultList, - !mDataStore.hasInputDeviceEntry(key)); - } - } finally { - mDataStore.saveIfNeeded(); + if (shouldLogConfiguration) { + logKeyboardConfigurationEvent(inputDevice, imeInfoList, resultList, + !mDataStore.hasInputDeviceEntry(key)); } + } finally { + mDataStore.saveIfNeeded(); } } if (needToShowNotification) { @@ -275,63 +255,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } - private String getDefaultKeyboardLayout(final InputDevice inputDevice) { - final Locale systemLocale = mContext.getResources().getConfiguration().locale; - // If our locale doesn't have a language for some reason, then we don't really have a - // reasonable default. - if (TextUtils.isEmpty(systemLocale.getLanguage())) { - return null; - } - final List<KeyboardLayout> layouts = new ArrayList<>(); - visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> { - // Only select a default when we know the layout is appropriate. For now, this - // means it's a custom layout for a specific keyboard. - if (layout.getVendorId() != inputDevice.getVendorId() - || layout.getProductId() != inputDevice.getProductId()) { - return; - } - final LocaleList locales = layout.getLocales(); - for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) { - final Locale locale = locales.get(localeIndex); - if (locale != null && isCompatibleLocale(systemLocale, locale)) { - layouts.add(layout); - break; - } - } - }); - - if (layouts.isEmpty()) { - return null; - } - - // First sort so that ones with higher priority are listed at the top - Collections.sort(layouts); - // Next we want to try to find an exact match of language, country and variant. - for (KeyboardLayout layout : layouts) { - final LocaleList locales = layout.getLocales(); - for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) { - final Locale locale = locales.get(localeIndex); - if (locale != null && locale.getCountry().equals(systemLocale.getCountry()) - && locale.getVariant().equals(systemLocale.getVariant())) { - return layout.getDescriptor(); - } - } - } - // Then try an exact match of language and country - for (KeyboardLayout layout : layouts) { - final LocaleList locales = layout.getLocales(); - for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) { - final Locale locale = locales.get(localeIndex); - if (locale != null && locale.getCountry().equals(systemLocale.getCountry())) { - return layout.getDescriptor(); - } - } - } - - // Give up and just use the highest priority layout with matching language - return layouts.get(0).getDescriptor(); - } - private static boolean isCompatibleLocale(Locale systemLocale, Locale keyboardLocale) { // Different languages are never compatible if (!systemLocale.getLanguage().equals(keyboardLocale.getLanguage())) { @@ -343,11 +266,19 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { || systemLocale.getCountry().equals(keyboardLocale.getCountry()); } + @MainThread private void updateKeyboardLayouts() { // Scan all input devices state for keyboard layouts that have been uninstalled. - final HashSet<String> availableKeyboardLayouts = new HashSet<String>(); + final HashSet<String> availableKeyboardLayouts = new HashSet<>(); visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> availableKeyboardLayouts.add(layout.getDescriptor())); + + // If available layouts don't change, there is no need to reload layouts. + if (mAvailableLayouts.equals(availableKeyboardLayouts)) { + return; + } + mAvailableLayouts = availableKeyboardLayouts; + synchronized (mDataStore) { try { mDataStore.removeUninstalledKeyboardLayouts(availableKeyboardLayouts); @@ -374,53 +305,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } @AnyThread - public KeyboardLayout[] getKeyboardLayoutsForInputDevice( - final InputDeviceIdentifier identifier) { - if (useNewSettingsUi()) { - // Provide all supported keyboard layouts since Ime info is not provided - return getKeyboardLayouts(); - } - final String[] enabledLayoutDescriptors = - getEnabledKeyboardLayoutsForInputDevice(identifier); - final ArrayList<KeyboardLayout> enabledLayouts = - new ArrayList<>(enabledLayoutDescriptors.length); - final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>(); - visitAllKeyboardLayouts(new KeyboardLayoutVisitor() { - boolean mHasSeenDeviceSpecificLayout; - - @Override - public void visitKeyboardLayout(Resources resources, - int keyboardLayoutResId, KeyboardLayout layout) { - // First check if it's enabled. If the keyboard layout is enabled then we always - // want to return it as a possible layout for the device. - for (String s : enabledLayoutDescriptors) { - if (s != null && s.equals(layout.getDescriptor())) { - enabledLayouts.add(layout); - return; - } - } - // Next find any potential layouts that aren't yet enabled for the device. For - // devices that have special layouts we assume there's a reason that the generic - // layouts don't work for them so we don't want to return them since it's likely - // to result in a poor user experience. - if (layout.getVendorId() == identifier.getVendorId() - && layout.getProductId() == identifier.getProductId()) { - if (!mHasSeenDeviceSpecificLayout) { - mHasSeenDeviceSpecificLayout = true; - potentialLayouts.clear(); - } - potentialLayouts.add(layout); - } else if (layout.getVendorId() == -1 && layout.getProductId() == -1 - && !mHasSeenDeviceSpecificLayout) { - potentialLayouts.add(layout); - } - } - }); - return Stream.concat(enabledLayouts.stream(), potentialLayouts.stream()).toArray( - KeyboardLayout[]::new); - } - - @AnyThread @Nullable public KeyboardLayout getKeyboardLayout(@NonNull String keyboardLayoutDescriptor) { Objects.requireNonNull(keyboardLayoutDescriptor, @@ -580,195 +464,16 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { return LocaleList.forLanguageTags(languageTags.replace('|', ',')); } - @AnyThread - @Nullable - public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) { - if (useNewSettingsUi()) { - Slog.e(TAG, "getCurrentKeyboardLayoutForInputDevice API not supported"); - return null; - } - String key = new KeyboardIdentifier(identifier).toString(); - synchronized (mDataStore) { - String layout; - // try loading it using the layout descriptor if we have it - layout = mDataStore.getCurrentKeyboardLayout(key); - if (layout == null && !key.equals(identifier.getDescriptor())) { - // if it doesn't exist fall back to the device descriptor - layout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor()); - } - if (DEBUG) { - Slog.d(TAG, "getCurrentKeyboardLayoutForInputDevice() " - + identifier.toString() + ": " + layout); - } - return layout; - } - } - - @AnyThread - public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor) { - if (useNewSettingsUi()) { - Slog.e(TAG, "setCurrentKeyboardLayoutForInputDevice API not supported"); - return; - } - - Objects.requireNonNull(keyboardLayoutDescriptor, - "keyboardLayoutDescriptor must not be null"); - String key = new KeyboardIdentifier(identifier).toString(); - synchronized (mDataStore) { - try { - if (mDataStore.setCurrentKeyboardLayout(key, keyboardLayoutDescriptor)) { - if (DEBUG) { - Slog.d(TAG, "setCurrentKeyboardLayoutForInputDevice() " + identifier - + " key: " + key - + " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor); - } - mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); - } - } finally { - mDataStore.saveIfNeeded(); - } - } - } - - @AnyThread - public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) { - if (useNewSettingsUi()) { - Slog.e(TAG, "getEnabledKeyboardLayoutsForInputDevice API not supported"); - return new String[0]; - } - String key = new KeyboardIdentifier(identifier).toString(); - synchronized (mDataStore) { - String[] layouts = mDataStore.getKeyboardLayouts(key); - if ((layouts == null || layouts.length == 0) - && !key.equals(identifier.getDescriptor())) { - layouts = mDataStore.getKeyboardLayouts(identifier.getDescriptor()); - } - return layouts; - } - } - - @AnyThread - public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor) { - if (useNewSettingsUi()) { - Slog.e(TAG, "addKeyboardLayoutForInputDevice API not supported"); - return; - } - Objects.requireNonNull(keyboardLayoutDescriptor, - "keyboardLayoutDescriptor must not be null"); - - String key = new KeyboardIdentifier(identifier).toString(); - synchronized (mDataStore) { - try { - String oldLayout = mDataStore.getCurrentKeyboardLayout(key); - if (oldLayout == null && !key.equals(identifier.getDescriptor())) { - oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor()); - } - if (mDataStore.addKeyboardLayout(key, keyboardLayoutDescriptor) - && !Objects.equals(oldLayout, - mDataStore.getCurrentKeyboardLayout(key))) { - mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); - } - } finally { - mDataStore.saveIfNeeded(); - } - } - } - - @AnyThread - public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, - String keyboardLayoutDescriptor) { - if (useNewSettingsUi()) { - Slog.e(TAG, "removeKeyboardLayoutForInputDevice API not supported"); - return; - } - Objects.requireNonNull(keyboardLayoutDescriptor, - "keyboardLayoutDescriptor must not be null"); - - String key = new KeyboardIdentifier(identifier).toString(); - synchronized (mDataStore) { - try { - String oldLayout = mDataStore.getCurrentKeyboardLayout(key); - if (oldLayout == null && !key.equals(identifier.getDescriptor())) { - oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor()); - } - boolean removed = mDataStore.removeKeyboardLayout(key, keyboardLayoutDescriptor); - if (!key.equals(identifier.getDescriptor())) { - // We need to remove from both places to ensure it is gone - removed |= mDataStore.removeKeyboardLayout(identifier.getDescriptor(), - keyboardLayoutDescriptor); - } - if (removed && !Objects.equals(oldLayout, - mDataStore.getCurrentKeyboardLayout(key))) { - mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); - } - } finally { - mDataStore.saveIfNeeded(); - } - } - } - - @AnyThread - public void switchKeyboardLayout(int deviceId, int direction) { - if (useNewSettingsUi()) { - Slog.e(TAG, "switchKeyboardLayout API not supported"); - return; - } - mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget(); - } - - @MainThread - private void handleSwitchKeyboardLayout(int deviceId, int direction) { - final InputDevice device = getInputDevice(deviceId); - if (device != null) { - final boolean changed; - final String keyboardLayoutDescriptor; - - String key = new KeyboardIdentifier(device.getIdentifier()).toString(); - synchronized (mDataStore) { - try { - changed = mDataStore.switchKeyboardLayout(key, direction); - keyboardLayoutDescriptor = mDataStore.getCurrentKeyboardLayout( - key); - } finally { - mDataStore.saveIfNeeded(); - } - } - - if (changed) { - if (mSwitchedKeyboardLayoutToast != null) { - mSwitchedKeyboardLayoutToast.cancel(); - mSwitchedKeyboardLayoutToast = null; - } - if (keyboardLayoutDescriptor != null) { - KeyboardLayout keyboardLayout = getKeyboardLayout(keyboardLayoutDescriptor); - if (keyboardLayout != null) { - mSwitchedKeyboardLayoutToast = Toast.makeText( - mContext, keyboardLayout.getLabel(), Toast.LENGTH_SHORT); - mSwitchedKeyboardLayoutToast.show(); - } - } - - reloadKeyboardLayouts(); - } - } - } - @Nullable @AnyThread public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier, String languageTag, String layoutType) { String keyboardLayoutDescriptor; - if (useNewSettingsUi()) { - synchronized (mImeInfoLock) { - KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal( - new KeyboardIdentifier(identifier, languageTag, layoutType), - mCurrentImeInfo); - keyboardLayoutDescriptor = result.getLayoutDescriptor(); - } - } else { - keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier); + synchronized (mImeInfoLock) { + KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal( + new KeyboardIdentifier(identifier, languageTag, layoutType), + mCurrentImeInfo); + keyboardLayoutDescriptor = result.getLayoutDescriptor(); } if (keyboardLayoutDescriptor == null) { return null; @@ -797,10 +502,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { public KeyboardLayoutSelectionResult getKeyboardLayoutForInputDevice( InputDeviceIdentifier identifier, @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype) { - if (!useNewSettingsUi()) { - Slog.e(TAG, "getKeyboardLayoutForInputDevice() API not supported"); - return FAILED; - } InputDevice inputDevice = getInputDevice(identifier); if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { return FAILED; @@ -820,10 +521,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype, String keyboardLayoutDescriptor) { - if (!useNewSettingsUi()) { - Slog.e(TAG, "setKeyboardLayoutForInputDevice() API not supported"); - return; - } Objects.requireNonNull(keyboardLayoutDescriptor, "keyboardLayoutDescriptor must not be null"); InputDevice inputDevice = getInputDevice(identifier); @@ -854,10 +551,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier, @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype) { - if (!useNewSettingsUi()) { - Slog.e(TAG, "getKeyboardLayoutListForInputDevice() API not supported"); - return new KeyboardLayout[0]; - } InputDevice inputDevice = getInputDevice(identifier); if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { return new KeyboardLayout[0]; @@ -923,10 +616,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { public void onInputMethodSubtypeChanged(@UserIdInt int userId, @Nullable InputMethodSubtypeHandle subtypeHandle, @Nullable InputMethodSubtype subtype) { - if (!useNewSettingsUi()) { - Slog.e(TAG, "onInputMethodSubtypeChanged() API not supported"); - return; - } if (subtypeHandle == null) { if (DEBUG) { Slog.d(TAG, "No InputMethod is running, ignoring change"); @@ -1289,9 +978,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { onInputDeviceAdded(deviceId); } return true; - case MSG_SWITCH_KEYBOARD_LAYOUT: - handleSwitchKeyboardLayout(msg.arg1, msg.arg2); - return true; case MSG_RELOAD_KEYBOARD_LAYOUTS: reloadKeyboardLayouts(); return true; @@ -1303,10 +989,6 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } - private boolean useNewSettingsUi() { - return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI); - } - @Nullable private InputDevice getInputDevice(int deviceId) { InputManager inputManager = mContext.getSystemService(InputManager.class); diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java index 31083fd5de03..7859253d66e0 100644 --- a/services/core/java/com/android/server/input/PersistentDataStore.java +++ b/services/core/java/com/android/server/input/PersistentDataStore.java @@ -27,7 +27,6 @@ import android.util.Xml; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -42,7 +41,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -74,7 +72,7 @@ final class PersistentDataStore { new HashMap<String, InputDeviceState>(); // The interface for methods which should be replaced by the test harness. - private Injector mInjector; + private final Injector mInjector; // True if the data has been loaded. private boolean mLoaded; @@ -83,7 +81,7 @@ final class PersistentDataStore { private boolean mDirty; // Storing key remapping - private Map<Integer, Integer> mKeyRemapping = new HashMap<>(); + private final Map<Integer, Integer> mKeyRemapping = new HashMap<>(); public PersistentDataStore() { this(new Injector()); @@ -130,22 +128,6 @@ final class PersistentDataStore { } @Nullable - public String getCurrentKeyboardLayout(String inputDeviceDescriptor) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); - return state != null ? state.getCurrentKeyboardLayout() : null; - } - - public boolean setCurrentKeyboardLayout(String inputDeviceDescriptor, - String keyboardLayoutDescriptor) { - InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); - if (state.setCurrentKeyboardLayout(keyboardLayoutDescriptor)) { - setDirty(); - return true; - } - return false; - } - - @Nullable public String getKeyboardLayout(String inputDeviceDescriptor, String key) { InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); return state != null ? state.getKeyboardLayout(key) : null; @@ -171,43 +153,6 @@ final class PersistentDataStore { return false; } - public String[] getKeyboardLayouts(String inputDeviceDescriptor) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); - if (state == null) { - return (String[])ArrayUtils.emptyArray(String.class); - } - return state.getKeyboardLayouts(); - } - - public boolean addKeyboardLayout(String inputDeviceDescriptor, - String keyboardLayoutDescriptor) { - InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); - if (state.addKeyboardLayout(keyboardLayoutDescriptor)) { - setDirty(); - return true; - } - return false; - } - - public boolean removeKeyboardLayout(String inputDeviceDescriptor, - String keyboardLayoutDescriptor) { - InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); - if (state.removeKeyboardLayout(keyboardLayoutDescriptor)) { - setDirty(); - return true; - } - return false; - } - - public boolean switchKeyboardLayout(String inputDeviceDescriptor, int direction) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); - if (state != null && state.switchKeyboardLayout(direction)) { - setDirty(); - return true; - } - return false; - } - public boolean setKeyboardBacklightBrightness(String inputDeviceDescriptor, int lightId, int brightness) { InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); @@ -417,9 +362,6 @@ final class PersistentDataStore { "x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" }; private final TouchCalibration[] mTouchCalibration = new TouchCalibration[4]; - @Nullable - private String mCurrentKeyboardLayout; - private final ArrayList<String> mKeyboardLayouts = new ArrayList<String>(); private final SparseIntArray mKeyboardBacklightBrightnessMap = new SparseIntArray(); private final Map<String, String> mKeyboardLayoutMap = new ArrayMap<>(); @@ -465,49 +407,6 @@ final class PersistentDataStore { return true; } - @Nullable - public String getCurrentKeyboardLayout() { - return mCurrentKeyboardLayout; - } - - public boolean setCurrentKeyboardLayout(String keyboardLayout) { - if (Objects.equals(mCurrentKeyboardLayout, keyboardLayout)) { - return false; - } - addKeyboardLayout(keyboardLayout); - mCurrentKeyboardLayout = keyboardLayout; - return true; - } - - public String[] getKeyboardLayouts() { - if (mKeyboardLayouts.isEmpty()) { - return (String[])ArrayUtils.emptyArray(String.class); - } - return mKeyboardLayouts.toArray(new String[mKeyboardLayouts.size()]); - } - - public boolean addKeyboardLayout(String keyboardLayout) { - int index = Collections.binarySearch(mKeyboardLayouts, keyboardLayout); - if (index >= 0) { - return false; - } - mKeyboardLayouts.add(-index - 1, keyboardLayout); - if (mCurrentKeyboardLayout == null) { - mCurrentKeyboardLayout = keyboardLayout; - } - return true; - } - - public boolean removeKeyboardLayout(String keyboardLayout) { - int index = Collections.binarySearch(mKeyboardLayouts, keyboardLayout); - if (index < 0) { - return false; - } - mKeyboardLayouts.remove(index); - updateCurrentKeyboardLayoutIfRemoved(keyboardLayout, index); - return true; - } - public boolean setKeyboardBacklightBrightness(int lightId, int brightness) { if (mKeyboardBacklightBrightnessMap.get(lightId, INVALID_VALUE) == brightness) { return false; @@ -521,48 +420,8 @@ final class PersistentDataStore { return brightness == INVALID_VALUE ? OptionalInt.empty() : OptionalInt.of(brightness); } - private void updateCurrentKeyboardLayoutIfRemoved( - String removedKeyboardLayout, int removedIndex) { - if (Objects.equals(mCurrentKeyboardLayout, removedKeyboardLayout)) { - if (!mKeyboardLayouts.isEmpty()) { - int index = removedIndex; - if (index == mKeyboardLayouts.size()) { - index = 0; - } - mCurrentKeyboardLayout = mKeyboardLayouts.get(index); - } else { - mCurrentKeyboardLayout = null; - } - } - } - - public boolean switchKeyboardLayout(int direction) { - final int size = mKeyboardLayouts.size(); - if (size < 2) { - return false; - } - int index = Collections.binarySearch(mKeyboardLayouts, mCurrentKeyboardLayout); - assert index >= 0; - if (direction > 0) { - index = (index + 1) % size; - } else { - index = (index + size - 1) % size; - } - mCurrentKeyboardLayout = mKeyboardLayouts.get(index); - return true; - } - public boolean removeUninstalledKeyboardLayouts(Set<String> availableKeyboardLayouts) { boolean changed = false; - for (int i = mKeyboardLayouts.size(); i-- > 0; ) { - String keyboardLayout = mKeyboardLayouts.get(i); - if (!availableKeyboardLayouts.contains(keyboardLayout)) { - Slog.i(TAG, "Removing uninstalled keyboard layout " + keyboardLayout); - mKeyboardLayouts.remove(i); - updateCurrentKeyboardLayoutIfRemoved(keyboardLayout, i); - changed = true; - } - } List<String> removedEntries = new ArrayList<>(); for (String key : mKeyboardLayoutMap.keySet()) { if (!availableKeyboardLayouts.contains(mKeyboardLayoutMap.get(key))) { @@ -582,27 +441,7 @@ final class PersistentDataStore { throws IOException, XmlPullParserException { final int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { - if (parser.getName().equals("keyboard-layout")) { - String descriptor = parser.getAttributeValue(null, "descriptor"); - if (descriptor == null) { - throw new XmlPullParserException( - "Missing descriptor attribute on keyboard-layout."); - } - String current = parser.getAttributeValue(null, "current"); - if (mKeyboardLayouts.contains(descriptor)) { - throw new XmlPullParserException( - "Found duplicate keyboard layout."); - } - - mKeyboardLayouts.add(descriptor); - if (current != null && current.equals("true")) { - if (mCurrentKeyboardLayout != null) { - throw new XmlPullParserException( - "Found multiple current keyboard layouts."); - } - mCurrentKeyboardLayout = descriptor; - } - } else if (parser.getName().equals("keyed-keyboard-layout")) { + if (parser.getName().equals("keyed-keyboard-layout")) { String key = parser.getAttributeValue(null, "key"); if (key == null) { throw new XmlPullParserException( @@ -676,27 +515,9 @@ final class PersistentDataStore { } } } - - // Maintain invariant that layouts are sorted. - Collections.sort(mKeyboardLayouts); - - // Maintain invariant that there is always a current keyboard layout unless - // there are none installed. - if (mCurrentKeyboardLayout == null && !mKeyboardLayouts.isEmpty()) { - mCurrentKeyboardLayout = mKeyboardLayouts.get(0); - } } public void saveToXml(TypedXmlSerializer serializer) throws IOException { - for (String layout : mKeyboardLayouts) { - serializer.startTag(null, "keyboard-layout"); - serializer.attribute(null, "descriptor", layout); - if (layout.equals(mCurrentKeyboardLayout)) { - serializer.attributeBoolean(null, "current", true); - } - serializer.endTag(null, "keyboard-layout"); - } - for (String key : mKeyboardLayoutMap.keySet()) { serializer.startTag(null, "keyed-keyboard-layout"); serializer.attribute(null, "key", key); diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index c80f988bd61b..593174420d94 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -107,7 +107,7 @@ import android.os.storage.StorageManager; import android.provider.DeviceConfig; import android.provider.Settings; import android.security.AndroidKeyStoreMaintenance; -import android.security.Authorization; +import android.security.KeyStoreAuthorization; import android.security.keystore.KeyProperties; import android.security.keystore.KeyProtection; import android.security.keystore.recovery.KeyChainProtectionParams; @@ -293,6 +293,7 @@ public class LockSettingsService extends ILockSettings.Stub { private final SyntheticPasswordManager mSpManager; private final KeyStore mKeyStore; + private final KeyStoreAuthorization mKeyStoreAuthorization; private final RecoverableKeyStoreManager mRecoverableKeyStoreManager; private final UnifiedProfilePasswordCache mUnifiedProfilePasswordCache; @@ -627,6 +628,10 @@ public class LockSettingsService extends ILockSettings.Stub { } } + public KeyStoreAuthorization getKeyStoreAuthorization() { + return KeyStoreAuthorization.getInstance(); + } + public @NonNull UnifiedProfilePasswordCache getUnifiedProfilePasswordCache(KeyStore ks) { return new UnifiedProfilePasswordCache(ks); } @@ -650,6 +655,7 @@ public class LockSettingsService extends ILockSettings.Stub { mInjector = injector; mContext = injector.getContext(); mKeyStore = injector.getKeyStore(); + mKeyStoreAuthorization = injector.getKeyStoreAuthorization(); mRecoverableKeyStoreManager = injector.getRecoverableKeyStoreManager(); mHandler = injector.getHandler(injector.getServiceThread()); mStrongAuth = injector.getStrongAuth(); @@ -1460,7 +1466,7 @@ public class LockSettingsService extends ILockSettings.Stub { } private void unlockKeystore(int userId, SyntheticPassword sp) { - Authorization.onDeviceUnlocked(userId, sp.deriveKeyStorePassword()); + mKeyStoreAuthorization.onDeviceUnlocked(userId, sp.deriveKeyStorePassword()); } @VisibleForTesting /** Note: this method is overridden in unit tests */ diff --git a/services/core/java/com/android/server/media/AudioManagerRouteController.java b/services/core/java/com/android/server/media/AudioManagerRouteController.java index e7f717aa6c3b..f27ade42a738 100644 --- a/services/core/java/com/android/server/media/AudioManagerRouteController.java +++ b/services/core/java/com/android/server/media/AudioManagerRouteController.java @@ -136,7 +136,7 @@ import java.util.Objects; mBluetoothRouteController = new BluetoothDeviceRoutesManager( - mContext, btAdapter, this::rebuildAvailableRoutesAndNotify); + mContext, mHandler, btAdapter, this::rebuildAvailableRoutesAndNotify); // Just build routes but don't notify. The caller may not expect the listener to be invoked // before this constructor has finished executing. rebuildAvailableRoutes(); @@ -204,23 +204,24 @@ import java.util.Objects; Slog.w(TAG, "transferTo: Ignoring transfer request to unknown route id : " + routeId); return; } - // TODO: b/329929065 - Push audio manager and bluetooth operations to the handler, so that - // they don't run on a binder thread, so as to prevent possible deadlocks (these operations - // may need system_server binder threads to complete). - if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) { - // By default, the last connected device is the active route so we don't need to apply a - // routing audio policy. - mBluetoothRouteController.activateBluetoothDeviceWithAddress( - mediaRoute2InfoHolder.mMediaRoute2Info.getAddress()); - mAudioManager.removePreferredDeviceForStrategy(mStrategyForMedia); - } else { - AudioDeviceAttributes attr = - new AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_OUTPUT, - mediaRoute2InfoHolder.mAudioDeviceInfoType, - /* address= */ ""); // This is not a BT device, hence no address needed. - mAudioManager.setPreferredDeviceForStrategy(mStrategyForMedia, attr); - } + Runnable transferAction = getTransferActionForRoute(mediaRoute2InfoHolder); + Runnable guardedTransferAction = + () -> { + try { + transferAction.run(); + } catch (Throwable throwable) { + // We swallow the exception to avoid crashing system_server, since this + // doesn't run on a binder thread. + Slog.e( + TAG, + "Unexpected exception while transferring to route id: " + routeId, + throwable); + mHandler.post(this::rebuildAvailableRoutesAndNotify); + } + }; + // We post the transfer operation to the handler to avoid making these calls on a binder + // thread. See class javadoc for details. + mHandler.post(guardedTransferAction); } @RequiresPermission( @@ -236,6 +237,28 @@ import java.util.Objects; return true; } + private Runnable getTransferActionForRoute(MediaRoute2InfoHolder mediaRoute2InfoHolder) { + if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) { + String deviceAddress = mediaRoute2InfoHolder.mMediaRoute2Info.getAddress(); + return () -> { + // By default, the last connected device is the active route so we don't + // need to apply a routing audio policy. + mBluetoothRouteController.activateBluetoothDeviceWithAddress(deviceAddress); + mAudioManager.removePreferredDeviceForStrategy(mStrategyForMedia); + }; + + } else { + AudioDeviceAttributes deviceAttributes = + new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + mediaRoute2InfoHolder.mAudioDeviceInfoType, + /* address= */ ""); // This is not a BT device, hence no address needed. + return () -> + mAudioManager.setPreferredDeviceForStrategy( + mStrategyForMedia, deviceAttributes); + } + } + @RequiresPermission( anyOf = { Manifest.permission.MODIFY_AUDIO_ROUTING, diff --git a/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java b/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java index b881ef6195d5..8b65ea305ad8 100644 --- a/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java +++ b/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java @@ -31,6 +31,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.MediaRoute2Info; +import android.os.Handler; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; @@ -77,26 +78,35 @@ import java.util.stream.Collectors; @NonNull private final Context mContext; - @NonNull - private final BluetoothAdapter mBluetoothAdapter; + @NonNull private final Handler mHandler; + @NonNull private final BluetoothAdapter mBluetoothAdapter; @NonNull private final BluetoothRouteController.BluetoothRoutesUpdatedListener mListener; @NonNull private final BluetoothProfileMonitor mBluetoothProfileMonitor; - BluetoothDeviceRoutesManager(@NonNull Context context, + BluetoothDeviceRoutesManager( + @NonNull Context context, + @NonNull Handler handler, @NonNull BluetoothAdapter bluetoothAdapter, @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) { - this(context, bluetoothAdapter, - new BluetoothProfileMonitor(context, bluetoothAdapter), listener); + this( + context, + handler, + bluetoothAdapter, + new BluetoothProfileMonitor(context, bluetoothAdapter), + listener); } @VisibleForTesting - BluetoothDeviceRoutesManager(@NonNull Context context, + BluetoothDeviceRoutesManager( + @NonNull Context context, + @NonNull Handler handler, @NonNull BluetoothAdapter bluetoothAdapter, @NonNull BluetoothProfileMonitor bluetoothProfileMonitor, @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) { mContext = Objects.requireNonNull(context); + mHandler = handler; mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter); mBluetoothProfileMonitor = Objects.requireNonNull(bluetoothProfileMonitor); mListener = Objects.requireNonNull(listener); @@ -298,6 +308,26 @@ import java.util.stream.Collectors; }; } + private void handleBluetoothAdapterStateChange(int state) { + if (state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_TURNING_OFF) { + synchronized (BluetoothDeviceRoutesManager.this) { + mBluetoothRoutes.clear(); + } + notifyBluetoothRoutesUpdated(); + } else if (state == BluetoothAdapter.STATE_ON) { + updateBluetoothRoutes(); + + boolean shouldCallListener; + synchronized (BluetoothDeviceRoutesManager.this) { + shouldCallListener = !mBluetoothRoutes.isEmpty(); + } + + if (shouldCallListener) { + notifyBluetoothRoutesUpdated(); + } + } + } + private static class BluetoothRouteInfo { private BluetoothDevice mBtDevice; private MediaRoute2Info mRoute; @@ -308,23 +338,10 @@ import java.util.stream.Collectors; @Override public void onReceive(Context context, Intent intent) { int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); - if (state == BluetoothAdapter.STATE_OFF - || state == BluetoothAdapter.STATE_TURNING_OFF) { - synchronized (BluetoothDeviceRoutesManager.this) { - mBluetoothRoutes.clear(); - } - notifyBluetoothRoutesUpdated(); - } else if (state == BluetoothAdapter.STATE_ON) { - updateBluetoothRoutes(); - - boolean shouldCallListener; - synchronized (BluetoothDeviceRoutesManager.this) { - shouldCallListener = !mBluetoothRoutes.isEmpty(); - } - - if (shouldCallListener) { - notifyBluetoothRoutesUpdated(); - } + if (Flags.enableMr2ServiceNonMainBgThread()) { + mHandler.post(() -> handleBluetoothAdapterStateChange(state)); + } else { + handleBluetoothAdapterStateChange(state); } } } @@ -337,8 +354,16 @@ import java.util.stream.Collectors; case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED: case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED: case BluetoothDevice.ACTION_ALIAS_CHANGED: - updateBluetoothRoutes(); - notifyBluetoothRoutesUpdated(); + if (Flags.enableMr2ServiceNonMainBgThread()) { + mHandler.post( + () -> { + updateBluetoothRoutes(); + notifyBluetoothRoutesUpdated(); + }); + } else { + updateBluetoothRoutes(); + notifyBluetoothRoutesUpdated(); + } } } } diff --git a/services/core/java/com/android/server/media/MediaKeyDispatcher.java b/services/core/java/com/android/server/media/MediaKeyDispatcher.java index 66cafabb1e6e..e4f2ec36d758 100644 --- a/services/core/java/com/android/server/media/MediaKeyDispatcher.java +++ b/services/core/java/com/android/server/media/MediaKeyDispatcher.java @@ -44,7 +44,6 @@ import java.util.Map; * Note: When instantiating this class, {@link MediaSessionService} will only use the constructor * without any parameters. */ -// TODO: Move this class to apex/media/ public abstract class MediaKeyDispatcher { @IntDef(flag = true, value = { KEY_EVENT_SINGLE_TAP, diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index 67d3fe995160..db83d4b76778 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -232,7 +232,9 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider if (!mRunning) { return false; } - if (!getSessionInfos().isEmpty() || mIsManagerScanning) { + boolean bindDueToManagerScan = + mIsManagerScanning && Flags.enablePreventionOfManagerScansWhenNoAppsScan(); + if (!getSessionInfos().isEmpty() || bindDueToManagerScan) { // We bind if any manager is scanning (regardless of whether an app is scanning) to give // the opportunity for providers to publish routing sessions that were established // directly between the app and the provider (typically via AndroidX MediaRouter). See diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index aa71e054ddee..e50189b2e79d 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -189,12 +189,10 @@ class MediaRouter2ServiceImpl { mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); mAppOpsManager = mContext.getSystemService(AppOpsManager.class); - if (!Flags.disableScreenOffBroadcastReceiver()) { - IntentFilter screenOnOffIntentFilter = new IntentFilter(); - screenOnOffIntentFilter.addAction(ACTION_SCREEN_ON); - screenOnOffIntentFilter.addAction(ACTION_SCREEN_OFF); - mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter); - } + IntentFilter screenOnOffIntentFilter = new IntentFilter(); + screenOnOffIntentFilter.addAction(ACTION_SCREEN_ON); + screenOnOffIntentFilter.addAction(ACTION_SCREEN_OFF); + mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter); // Passing null package name to listen to all events. mAppOpsManager.startWatchingMode( @@ -3435,9 +3433,7 @@ class MediaRouter2ServiceImpl { @NonNull private static List<RouterRecord> getIndividuallyActiveRouters( MediaRouter2ServiceImpl service, List<RouterRecord> allRouterRecords) { - if (!Flags.disableScreenOffBroadcastReceiver() - && !service.mPowerManager.isInteractive() - && !Flags.enableScreenOffScanning()) { + if (!service.mPowerManager.isInteractive() && !Flags.enableScreenOffScanning()) { return Collections.emptyList(); } @@ -3453,9 +3449,7 @@ class MediaRouter2ServiceImpl { private static boolean areManagersScanning( MediaRouter2ServiceImpl service, List<ManagerRecord> managerRecords) { - if (!Flags.disableScreenOffBroadcastReceiver() - && !service.mPowerManager.isInteractive() - && !Flags.enableScreenOffScanning()) { + if (!service.mPowerManager.isInteractive() && !Flags.enableScreenOffScanning()) { return false; } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 4bdca29d3bd0..1a129cb080a8 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -53,6 +53,7 @@ import android.media.RoutingSessionInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; @@ -70,6 +71,7 @@ import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.DumpUtils; +import com.android.media.flags.Flags; import com.android.server.LocalServices; import com.android.server.Watchdog; import com.android.server.pm.UserManagerInternal; @@ -94,6 +96,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub implements Watchdog.Monitor { private static final String TAG = "MediaRouterService"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final String WORKER_THREAD_NAME = "MediaRouterServiceThread"; /** * Timeout in milliseconds for a selected route to transition from a disconnected state to a @@ -126,7 +129,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub private final IAudioService mAudioService; private final AudioPlayerStateMonitor mAudioPlayerStateMonitor; - private final Handler mHandler = new Handler(); + private final Handler mHandler; private final IntArray mActivePlayerMinPriorityQueue = new IntArray(); private final IntArray mActivePlayerUidMinPriorityQueue = new IntArray(); @@ -142,7 +145,14 @@ public final class MediaRouterService extends IMediaRouterService.Stub @RequiresPermission(Manifest.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS) public MediaRouterService(Context context) { - mLooper = Looper.getMainLooper(); + if (Flags.enableMr2ServiceNonMainBgThread()) { + HandlerThread handlerThread = new HandlerThread(WORKER_THREAD_NAME); + handlerThread.start(); + mLooper = handlerThread.getLooper(); + } else { + mLooper = Looper.myLooper(); + } + mHandler = new Handler(mLooper); mService2 = new MediaRouter2ServiceImpl(context, mLooper); mContext = context; Watchdog.getInstance().addMonitor(this); diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java index 0cd7654f70ea..dfb2b0a750e3 100644 --- a/services/core/java/com/android/server/media/MediaSession2Record.java +++ b/services/core/java/com/android/server/media/MediaSession2Record.java @@ -157,6 +157,11 @@ public class MediaSession2Record extends MediaSessionRecordImpl { } @Override + public void expireTempEngaged() { + // NA as MediaSession2 doesn't support UserEngagementStates for FGS. + } + + @Override public boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService, KeyEvent ke, int sequenceId, ResultReceiver cb) { // TODO(jaewan): Implement. diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index eb4e6e44c8a4..8f164d361a56 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -24,6 +24,7 @@ import static android.media.session.MediaController.PlaybackInfo.PLAYBACK_TYPE_L import static android.media.session.MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE; import android.Manifest; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -85,6 +86,8 @@ import com.android.server.LocalServices; import com.android.server.uri.UriGrantsManagerInternal; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -225,6 +228,49 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private int mPolicies; + private @UserEngagementState int mUserEngagementState = USER_DISENGAGED; + + @IntDef({USER_PERMANENTLY_ENGAGED, USER_TEMPORARY_ENGAGED, USER_DISENGAGED}) + @Retention(RetentionPolicy.SOURCE) + private @interface UserEngagementState {} + + /** + * Indicates that the session is active and in one of the user engaged states. + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_PERMANENTLY_ENGAGED = 0; + + /** + * Indicates that the session is active and in {@link PlaybackState#STATE_PAUSED} state. + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_TEMPORARY_ENGAGED = 1; + + /** + * Indicates that the session is either not active or in one of the user disengaged states + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_DISENGAGED = 2; + + /** + * Indicates the duration of the temporary engaged states. + * + * <p>Some {@link MediaSession} states like {@link PlaybackState#STATE_PAUSED} are temporarily + * engaged, meaning the corresponding session is only considered in an engaged state for the + * duration of this timeout, and only if coming from an engaged state. + * + * <p>For example, if a session is transitioning from a user-engaged state {@link + * PlaybackState#STATE_PLAYING} to a temporary user-engaged state {@link + * PlaybackState#STATE_PAUSED}, then the session will be considered in a user-engaged state for + * the duration of this timeout, starting at the transition instant. However, a temporary + * user-engaged state is not considered user-engaged when transitioning from a non-user engaged + * state {@link PlaybackState#STATE_STOPPED}. + */ + private static final int TEMP_USER_ENGAGED_TIMEOUT = 600000; + public MediaSessionRecord( int ownerPid, int ownerUid, @@ -548,6 +594,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde mSessionCb.mCb.asBinder().unlinkToDeath(this, 0); mDestroyed = true; mPlaybackState = null; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); mHandler.post(MessageHandler.MSG_DESTROYED); } } @@ -559,6 +606,12 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } + @Override + public void expireTempEngaged() { + mHandler.removeCallbacks(mHandleTempEngagedSessionTimeout); + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); + } + /** * Sends media button. * @@ -849,7 +902,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } if (deadCallbackHolders != null) { - mControllerCallbackHolders.removeAll(deadCallbackHolders); + removeControllerHoldersSafely(deadCallbackHolders); } } @@ -876,7 +929,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } if (deadCallbackHolders != null) { - mControllerCallbackHolders.removeAll(deadCallbackHolders); + removeControllerHoldersSafely(deadCallbackHolders); } } @@ -911,7 +964,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } if (deadCallbackHolders != null) { - mControllerCallbackHolders.removeAll(deadCallbackHolders); + removeControllerHoldersSafely(deadCallbackHolders); } } @@ -938,7 +991,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } if (deadCallbackHolders != null) { - mControllerCallbackHolders.removeAll(deadCallbackHolders); + removeControllerHoldersSafely(deadCallbackHolders); } } @@ -965,7 +1018,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } if (deadCallbackHolders != null) { - mControllerCallbackHolders.removeAll(deadCallbackHolders); + removeControllerHoldersSafely(deadCallbackHolders); } } @@ -992,7 +1045,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } if (deadCallbackHolders != null) { - mControllerCallbackHolders.removeAll(deadCallbackHolders); + removeControllerHoldersSafely(deadCallbackHolders); } } @@ -1017,7 +1070,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } if (deadCallbackHolders != null) { - mControllerCallbackHolders.removeAll(deadCallbackHolders); + removeControllerHoldersSafely(deadCallbackHolders); } } @@ -1042,7 +1095,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } // After notifying clear all listeners - mControllerCallbackHolders.clear(); + removeControllerHoldersSafely(null); } private PlaybackState getStateWithUpdatedPosition() { @@ -1090,6 +1143,17 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde return -1; } + private void removeControllerHoldersSafely( + Collection<ISessionControllerCallbackHolder> holders) { + synchronized (mLock) { + if (holders == null) { + mControllerCallbackHolders.clear(); + } else { + mControllerCallbackHolders.removeAll(holders); + } + } + } + private PlaybackInfo getVolumeAttributes() { int volumeType; AudioAttributes attributes; @@ -1118,6 +1182,11 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } }; + private final Runnable mHandleTempEngagedSessionTimeout = + () -> { + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); + }; + @RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS) private static boolean componentNameExists( @NonNull ComponentName componentName, @NonNull Context context, int userId) { @@ -1134,6 +1203,40 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde return !resolveInfos.isEmpty(); } + private void updateUserEngagedStateIfNeededLocked(boolean isTimeoutExpired) { + int oldUserEngagedState = mUserEngagementState; + int newUserEngagedState; + if (!isActive() || mPlaybackState == null) { + newUserEngagedState = USER_DISENGAGED; + } else if (isActive() && mPlaybackState.isActive()) { + newUserEngagedState = USER_PERMANENTLY_ENGAGED; + } else if (mPlaybackState.getState() == PlaybackState.STATE_PAUSED) { + newUserEngagedState = + oldUserEngagedState == USER_PERMANENTLY_ENGAGED || !isTimeoutExpired + ? USER_TEMPORARY_ENGAGED + : USER_DISENGAGED; + } else { + newUserEngagedState = USER_DISENGAGED; + } + if (oldUserEngagedState == newUserEngagedState) { + return; + } + + if (newUserEngagedState == USER_TEMPORARY_ENGAGED) { + mHandler.postDelayed(mHandleTempEngagedSessionTimeout, TEMP_USER_ENGAGED_TIMEOUT); + } else if (oldUserEngagedState == USER_TEMPORARY_ENGAGED) { + mHandler.removeCallbacks(mHandleTempEngagedSessionTimeout); + } + + boolean wasUserEngaged = oldUserEngagedState != USER_DISENGAGED; + boolean isNowUserEngaged = newUserEngagedState != USER_DISENGAGED; + mUserEngagementState = newUserEngagedState; + if (wasUserEngaged != isNowUserEngaged) { + mService.onSessionUserEngagementStateChange( + /* mediaSessionRecord= */ this, /* isUserEngaged= */ isNowUserEngaged); + } + } + private final class SessionStub extends ISession.Stub { @Override public void destroySession() throws RemoteException { @@ -1171,8 +1274,10 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde .logFgsApiEnd(ActivityManager.FOREGROUND_SERVICE_API_TYPE_MEDIA_PLAYBACK, callingUid, callingPid); } - - mIsActive = active; + synchronized (mLock) { + mIsActive = active; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ false); + } long token = Binder.clearCallingIdentity(); try { mService.onSessionActiveStateChanged(MediaSessionRecord.this, mPlaybackState); @@ -1330,6 +1435,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde && TRANSITION_PRIORITY_STATES.contains(newState)); synchronized (mLock) { mPlaybackState = state; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ false); } final long token = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java index 09991995099e..b57b14835987 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java +++ b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java @@ -196,6 +196,12 @@ public abstract class MediaSessionRecordImpl { */ public abstract boolean isClosed(); + /** + * Note: This method is only used for testing purposes If the session is temporary engaged, the + * timeout will expire and it will become disengaged. + */ + public abstract void expireTempEngaged(); + @Override public final boolean equals(Object o) { if (this == o) return true; diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 53c32cf31d21..2ca76a578f53 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -367,11 +367,13 @@ public class MediaSessionService extends SystemService implements Monitor { } boolean isUserEngaged = isUserEngaged(record, playbackState); - Log.d(TAG, "onSessionActiveStateChanged: " - + "record=" + record - + "playbackState=" + playbackState - + "allowRunningInForeground=" + isUserEngaged); - setForegroundServiceAllowance(record, /* allowRunningInForeground= */ isUserEngaged); + Log.d( + TAG, + "onSessionActiveStateChanged:" + + " record=" + + record + + " playbackState=" + + playbackState); reportMediaInteractionEvent(record, isUserEngaged); mHandler.postSessionsChanged(record); } @@ -479,11 +481,13 @@ public class MediaSessionService extends SystemService implements Monitor { } user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority); boolean isUserEngaged = isUserEngaged(record, playbackState); - Log.d(TAG, "onSessionPlaybackStateChanged: " - + "record=" + record - + "playbackState=" + playbackState - + "allowRunningInForeground=" + isUserEngaged); - setForegroundServiceAllowance(record, /* allowRunningInForeground= */ isUserEngaged); + Log.d( + TAG, + "onSessionPlaybackStateChanged:" + + " record=" + + record + + " playbackState=" + + playbackState); reportMediaInteractionEvent(record, isUserEngaged); } } @@ -650,33 +654,54 @@ public class MediaSessionService extends SystemService implements Monitor { session.close(); Log.d(TAG, "destroySessionLocked: record=" + session); - setForegroundServiceAllowance(session, /* allowRunningInForeground= */ false); + reportMediaInteractionEvent(session, /* userEngaged= */ false); mHandler.postSessionsChanged(session); } - private void setForegroundServiceAllowance( - MediaSessionRecordImpl record, boolean allowRunningInForeground) { - if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { - return; - } - ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = - record.getForegroundServiceDelegationOptions(); - if (foregroundServiceDelegationOptions == null) { - return; - } - if (allowRunningInForeground) { - onUserSessionEngaged(record); + void onSessionUserEngagementStateChange( + MediaSessionRecordImpl mediaSessionRecord, boolean isUserEngaged) { + if (isUserEngaged) { + addUserEngagedSession(mediaSessionRecord); + startFgsIfSessionIsLinkedToNotification(mediaSessionRecord); } else { - onUserDisengaged(record); + removeUserEngagedSession(mediaSessionRecord); + stopFgsIfNoSessionIsLinkedToNotification(mediaSessionRecord); } } - private void onUserSessionEngaged(MediaSessionRecordImpl mediaSessionRecord) { + private void addUserEngagedSession(MediaSessionRecordImpl mediaSessionRecord) { synchronized (mLock) { int uid = mediaSessionRecord.getUid(); mUserEngagedSessionsForFgs.putIfAbsent(uid, new HashSet<>()); mUserEngagedSessionsForFgs.get(uid).add(mediaSessionRecord); + } + } + + private void removeUserEngagedSession(MediaSessionRecordImpl mediaSessionRecord) { + synchronized (mLock) { + int uid = mediaSessionRecord.getUid(); + Set<MediaSessionRecordImpl> mUidUserEngagedSessionsForFgs = + mUserEngagedSessionsForFgs.get(uid); + if (mUidUserEngagedSessionsForFgs == null) { + return; + } + + mUidUserEngagedSessionsForFgs.remove(mediaSessionRecord); + if (mUidUserEngagedSessionsForFgs.isEmpty()) { + mUserEngagedSessionsForFgs.remove(uid); + } + } + } + + private void startFgsIfSessionIsLinkedToNotification( + MediaSessionRecordImpl mediaSessionRecord) { + Log.d(TAG, "startFgsIfSessionIsLinkedToNotification: record=" + mediaSessionRecord); + if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { + return; + } + synchronized (mLock) { + int uid = mediaSessionRecord.getUid(); for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, Set.of())) { if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) { mActivityManagerInternal.startForegroundServiceDelegate( @@ -688,30 +713,34 @@ public class MediaSessionService extends SystemService implements Monitor { } } - private void onUserDisengaged(MediaSessionRecordImpl mediaSessionRecord) { + private void stopFgsIfNoSessionIsLinkedToNotification( + MediaSessionRecordImpl mediaSessionRecord) { + Log.d(TAG, "stopFgsIfNoSessionIsLinkedToNotification: record=" + mediaSessionRecord); + if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { + return; + } synchronized (mLock) { int uid = mediaSessionRecord.getUid(); - if (mUserEngagedSessionsForFgs.containsKey(uid)) { - mUserEngagedSessionsForFgs.get(uid).remove(mediaSessionRecord); - if (mUserEngagedSessionsForFgs.get(uid).isEmpty()) { - mUserEngagedSessionsForFgs.remove(uid); - } + ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = + mediaSessionRecord.getForegroundServiceDelegationOptions(); + if (foregroundServiceDelegationOptions == null) { + return; } - boolean shouldStopFgs = true; - for (MediaSessionRecordImpl sessionRecord : + for (MediaSessionRecordImpl record : mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { - for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, - Set.of())) { - if (sessionRecord.isLinkedToNotification(mediaNotification)) { - shouldStopFgs = false; + for (Notification mediaNotification : + mMediaNotifications.getOrDefault(uid, Set.of())) { + if (record.isLinkedToNotification(mediaNotification)) { + // A user engaged session linked with a media notification is found. + // We shouldn't call stop FGS in this case. + return; } } } - if (shouldStopFgs) { - mActivityManagerInternal.stopForegroundServiceDelegate( - mediaSessionRecord.getForegroundServiceDelegationOptions()); - } + + mActivityManagerInternal.stopForegroundServiceDelegate( + foregroundServiceDelegationOptions); } } @@ -2502,7 +2531,6 @@ public class MediaSessionService extends SystemService implements Monitor { } MediaSessionRecord session = null; MediaButtonReceiverHolder mediaButtonReceiverHolder = null; - if (mCustomMediaKeyDispatcher != null) { MediaSession.Token token = mCustomMediaKeyDispatcher.getMediaSession( keyEvent, uid, asSystemService); @@ -2630,6 +2658,18 @@ public class MediaSessionService extends SystemService implements Monitor { && streamType <= AudioManager.STREAM_NOTIFICATION; } + @Override + public void expireTempEngagedSessions() { + synchronized (mLock) { + for (Set<MediaSessionRecordImpl> uidSessions : + mUserEngagedSessionsForFgs.values()) { + for (MediaSessionRecordImpl sessionRecord : uidSessions) { + sessionRecord.expireTempEngaged(); + } + } + } + } + private class MediaKeyListenerResultReceiver extends ResultReceiver implements Runnable { private final String mPackageName; private final int mPid; @@ -3127,7 +3167,6 @@ public class MediaSessionService extends SystemService implements Monitor { super.onNotificationPosted(sbn); Notification postedNotification = sbn.getNotification(); int uid = sbn.getUid(); - if (!postedNotification.isMediaNotification()) { return; } @@ -3138,8 +3177,8 @@ public class MediaSessionService extends SystemService implements Monitor { mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = mediaSessionRecord.getForegroundServiceDelegationOptions(); - if (mediaSessionRecord.isLinkedToNotification(postedNotification) - && foregroundServiceDelegationOptions != null) { + if (foregroundServiceDelegationOptions != null + && mediaSessionRecord.isLinkedToNotification(postedNotification)) { mActivityManagerInternal.startForegroundServiceDelegate( foregroundServiceDelegationOptions, /* connection= */ null); @@ -3173,21 +3212,7 @@ public class MediaSessionService extends SystemService implements Monitor { return; } - boolean shouldStopFgs = true; - for (MediaSessionRecordImpl mediaSessionRecord : - mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { - for (Notification mediaNotification : - mMediaNotifications.getOrDefault(uid, Set.of())) { - if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) { - shouldStopFgs = false; - } - } - } - if (shouldStopFgs - && notificationRecord.getForegroundServiceDelegationOptions() != null) { - mActivityManagerInternal.stopForegroundServiceDelegate( - notificationRecord.getForegroundServiceDelegationOptions()); - } + stopFgsIfNoSessionIsLinkedToNotification(notificationRecord); } } diff --git a/services/core/java/com/android/server/media/MediaShellCommand.java b/services/core/java/com/android/server/media/MediaShellCommand.java index a56380827f2c..a20de3198d2c 100644 --- a/services/core/java/com/android/server/media/MediaShellCommand.java +++ b/services/core/java/com/android/server/media/MediaShellCommand.java @@ -92,6 +92,8 @@ public class MediaShellCommand extends ShellCommand { runMonitor(); } else if (cmd.equals("volume")) { runVolume(); + } else if (cmd.equals("expire-temp-engaged-sessions")) { + expireTempEngagedSessions(); } else { showError("Error: unknown command '" + cmd + "'"); return -1; @@ -367,4 +369,8 @@ public class MediaShellCommand extends ShellCommand { private void runVolume() throws Exception { VolumeCtrl.run(this); } + + private void expireTempEngagedSessions() throws Exception { + mSessionService.expireTempEngagedSessions(); + } } diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 802acba2e412..8df38a8d565a 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -653,9 +653,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { return; } - // TODO: b/310145678 - Post this to mHandler once mHandler does not run on the main - // thread. - updateVolume(); + if (Flags.enableMr2ServiceNonMainBgThread()) { + mHandler.post(SystemMediaRoute2Provider.this::updateVolume); + } else { + updateVolume(); + } } } } diff --git a/services/core/java/com/android/server/media/TEST_MAPPING b/services/core/java/com/android/server/media/TEST_MAPPING index b3e5b9e96889..43e2afd8827d 100644 --- a/services/core/java/com/android/server/media/TEST_MAPPING +++ b/services/core/java/com/android/server/media/TEST_MAPPING @@ -2,9 +2,7 @@ "presubmit": [ { "name": "CtsMediaBetterTogetherTestCases" - } - ], - "postsubmit": [ + }, { "name": "MediaRouterServiceTests" } diff --git a/services/core/java/com/android/server/net/Android.bp b/services/core/java/com/android/server/net/Android.bp index 71d8e6ba367e..3ac2d232dfc8 100644 --- a/services/core/java/com/android/server/net/Android.bp +++ b/services/core/java/com/android/server/net/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "net_flags", package: "com.android.server.net", + container: "system", srcs: ["*.aconfig"], } diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig index 419665a0a5ab..d9491de52d87 100644 --- a/services/core/java/com/android/server/net/flags.aconfig +++ b/services/core/java/com/android/server/net/flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.net" +container: "system" flag { name: "network_blocked_for_top_sleeping_and_above" diff --git a/services/core/java/com/android/server/notification/Android.bp b/services/core/java/com/android/server/notification/Android.bp index 9be43581aed5..d757470d86e9 100644 --- a/services/core/java/com/android/server/notification/Android.bp +++ b/services/core/java/com/android/server/notification/Android.bp @@ -10,6 +10,7 @@ java_aconfig_library { aconfig_declarations { name: "notification_flags", package: "com.android.server.notification", + container: "system", srcs: [ "flags.aconfig", ], diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index e1e2b3efd1eb..3949dfe01761 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -1617,7 +1617,8 @@ abstract public class ManagedServices { intent.putExtra(Intent.EXTRA_CLIENT_LABEL, mConfig.clientLabel); final ActivityOptions activityOptions = ActivityOptions.makeBasic(); - activityOptions.setIgnorePendingIntentCreatorForegroundState(true); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED); final PendingIntent pendingIntent = PendingIntent.getActivity( mContext, 0, new Intent(mConfig.settingsAction), PendingIntent.FLAG_IMMUTABLE, activityOptions.toBundle()); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 24f6c7048120..956e10c79246 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -1320,7 +1320,7 @@ public class NotificationManagerService extends SystemService { // Notifications that have been interacted with should no longer be lifetime // extended. if (lifetimeExtensionRefactor()) { - // Enqueue a cancellation; this cancellation should only work if + // This cancellation should only work if // the notification still has FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY // We wait for 200 milliseconds before posting the cancel, to allow the app // time to update the notification in response instead. @@ -1337,7 +1337,7 @@ public class NotificationManagerService extends SystemService { FLAG_NO_DISMISS /*=mustNotHaveFlags*/, false /*=sendDelete*/, r.getUserId(), - REASON_APP_CANCEL, + REASON_CLICK, -1 /*=rank*/, -1 /*=count*/, null /*=listener*/, @@ -1855,14 +1855,6 @@ public class NotificationManagerService extends SystemService { FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true, record.getUserId(), REASON_TIMEOUT, null); - // If cancellation will be prevented due to lifetime extension, we send an - // update to system UI. - final int packageImportance = getPackageImportanceWithIdentity( - record.getSbn().getPackageName()); - synchronized (mNotificationLock) { - maybeNotifySystemUiListenerLifetimeExtendedLocked(record, - record.getSbn().getPackageName(), packageImportance); - } } else { cancelNotification(record.getSbn().getUid(), record.getSbn().getInitialPid(), @@ -3673,15 +3665,6 @@ public class NotificationManagerService extends SystemService { if (lifetimeExtensionRefactor()) { // Also don't allow client apps to cancel lifetime extended notifs. mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; - // If cancellation will be prevented due to lifetime extension, we send an update to - // system UI. - NotificationRecord record = null; - final int packageImportance = getPackageImportanceWithIdentity(pkg); - synchronized (mNotificationLock) { - record = findNotificationLocked(pkg, tag, id, userId); - maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg, - packageImportance); - } } cancelNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(), @@ -4878,7 +4861,7 @@ public class NotificationManagerService extends SystemService { || isNotificationRecent(r.getUpdateTimeMs()); cancelNotificationFromListenerLocked(info, callingUid, callingPid, r.getSbn().getPackageName(), r.getSbn().getTag(), - r.getSbn().getId(), userId, reason, packageImportance); + r.getSbn().getId(), userId, reason); } } else { for (NotificationRecord notificationRecord : mNotificationList) { @@ -5018,14 +5001,10 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") private void cancelNotificationFromListenerLocked(ManagedServiceInfo info, int callingUid, int callingPid, String pkg, String tag, int id, int userId, - int reason, int packageImportance) { + int reason) { int mustNotHaveFlags = FLAG_ONGOING_EVENT; if (lifetimeExtensionRefactor()) { mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; - // If cancellation will be prevented due to lifetime extension, we send an update - // to system UI. - NotificationRecord record = findNotificationLocked(pkg, tag, id, userId); - maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg, packageImportance); } cancelNotification(callingUid, callingPid, pkg, tag, id, 0 /* mustHaveFlags */, mustNotHaveFlags, @@ -5168,13 +5147,7 @@ public class NotificationManagerService extends SystemService { final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); final long identity = Binder.clearCallingIdentity(); - final int packageImportance; try { - if (lifetimeExtensionRefactor()) { - packageImportance = getPackageImportanceWithIdentity(pkg); - } else { - packageImportance = IMPORTANCE_NONE; - } synchronized (mNotificationLock) { final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); int cancelReason = REASON_LISTENER_CANCEL; @@ -5187,7 +5160,7 @@ public class NotificationManagerService extends SystemService { + " use cancelNotification(key) instead."); } else { cancelNotificationFromListenerLocked(info, callingUid, callingPid, - pkg, tag, id, info.userid, cancelReason, packageImportance); + pkg, tag, id, info.userid, cancelReason); } } } finally { @@ -8178,19 +8151,30 @@ public class NotificationManagerService extends SystemService { EventLogTags.writeNotificationCancel(mCallingUid, mCallingPid, mPkg, mId, mTag, mUserId, mMustHaveFlags, mMustNotHaveFlags, mReason, listenerName); } - + int packageImportance = IMPORTANCE_NONE; + if (lifetimeExtensionRefactor()) { + packageImportance = getPackageImportanceWithIdentity(mPkg); + } synchronized (mNotificationLock) { // Look for the notification, searching both the posted and enqueued lists. NotificationRecord r = findNotificationLocked(mPkg, mTag, mId, mUserId); + if (r != null) { // The notification was found, check if it should be removed. - // Ideally we'd do this in the caller of this method. However, that would // require the caller to also find the notification. if (mReason == REASON_CLICK) { mUsageStats.registerClickedByUser(r); } + // If cancellation will be prevented due to lifetime extension, we need to + // send an update to system UI. This must be checked before flags are checked. + // We do not want to send this update. + if (lifetimeExtensionRefactor() && mReason != REASON_CLICK) { + maybeNotifySystemUiListenerLifetimeExtendedLocked(r, mPkg, + packageImportance); + } + if ((mReason == REASON_LISTENER_CANCEL && r.getNotification().isBubbleNotification()) || (mReason == REASON_CLICK && r.canBubble() diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index d4641c45b8a6..077ed5a72ae9 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.notification" +container: "system" flag { name: "expire_bitmaps" diff --git a/services/core/java/com/android/server/os/Android.bp b/services/core/java/com/android/server/os/Android.bp index 565dc3b644ea..c5886708ad85 100644 --- a/services/core/java/com/android/server/os/Android.bp +++ b/services/core/java/com/android/server/os/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "core_os_flags", package: "com.android.server.os", + container: "system", srcs: [ "*.aconfig", ], diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java index 4eb8b2b980cb..c8fd7e47d80a 100644 --- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java +++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java @@ -184,6 +184,16 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { throwInvalidBugreportFileForCallerException( bugreportFile, callingInfo.second); } + + boolean keepBugreportOnRetrieval = false; + if (onboardingBugreportV2Enabled()) { + keepBugreportOnRetrieval = mBugreportFilesToPersist.contains( + bugreportFile); + } + + if (!keepBugreportOnRetrieval) { + bugreportFilesForUid.remove(bugreportFile); + } } else { ArraySet<String> bugreportFilesForCaller = mBugreportFiles.get(callingInfo); if (bugreportFilesForCaller != null diff --git a/services/core/java/com/android/server/os/core_os_flags.aconfig b/services/core/java/com/android/server/os/core_os_flags.aconfig index cbc0d54046df..ae33df83e3aa 100644 --- a/services/core/java/com/android/server/os/core_os_flags.aconfig +++ b/services/core/java/com/android/server/os/core_os_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.os" +container: "system" flag { name: "proto_tombstone" diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java index 017cf55541fc..306f77d3d11e 100644 --- a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java +++ b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java @@ -25,6 +25,7 @@ import static com.android.server.pm.CrossProfileIntentFilter.FLAG_IS_PACKAGE_FOR import android.content.Intent; import android.hardware.usb.UsbManager; +import android.nfc.NfcAdapter; import android.provider.AlarmClock; import android.provider.MediaStore; @@ -361,6 +362,7 @@ public class DefaultCrossProfileIntentFiltersUtils { .addCategory(Intent.CATEGORY_DEFAULT) .build(); + public static List<DefaultCrossProfileIntentFilter> getDefaultManagedProfileFilters() { List<DefaultCrossProfileIntentFilter> filters = new ArrayList<DefaultCrossProfileIntentFilter>(); @@ -637,6 +639,33 @@ public class DefaultCrossProfileIntentFiltersUtils { .addDataType("video/*") .build(); + /** NFC TAG intent is always resolved by the primary user. */ + static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_NFC_TAG_DISCOVERED = + new DefaultCrossProfileIntentFilter.Builder( + DefaultCrossProfileIntentFilter.Direction.TO_PROFILE, + /* flags= */0, + /* letsPersonalDataIntoProfile= */ false) + .addAction(NfcAdapter.ACTION_TAG_DISCOVERED) + .build(); + + /** NFC TAG intent is always resolved by the primary user. */ + static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_NFC_TECH_DISCOVERED = + new DefaultCrossProfileIntentFilter.Builder( + DefaultCrossProfileIntentFilter.Direction.TO_PROFILE, + /* flags= */0, + /* letsPersonalDataIntoProfile= */ false) + .addAction(NfcAdapter.ACTION_TECH_DISCOVERED) + .build(); + + /** NFC TAG intent is always resolved by the primary user. */ + static final DefaultCrossProfileIntentFilter PARENT_TO_CLONE_NFC_NDEF_DISCOVERED = + new DefaultCrossProfileIntentFilter.Builder( + DefaultCrossProfileIntentFilter.Direction.TO_PROFILE, + /* flags= */0, + /* letsPersonalDataIntoProfile= */ false) + .addAction(NfcAdapter.ACTION_NDEF_DISCOVERED) + .build(); + public static List<DefaultCrossProfileIntentFilter> getDefaultCloneProfileFilters() { return Arrays.asList( PARENT_TO_CLONE_SEND_ACTION, @@ -652,7 +681,10 @@ public class DefaultCrossProfileIntentFiltersUtils { CLONE_TO_PARENT_SMS_MMS, CLONE_TO_PARENT_PHOTOPICKER_SELECTION, CLONE_TO_PARENT_ACTION_PICK_IMAGES, - CLONE_TO_PARENT_ACTION_PICK_IMAGES_WITH_DATA_TYPES + CLONE_TO_PARENT_ACTION_PICK_IMAGES_WITH_DATA_TYPES, + PARENT_TO_CLONE_NFC_TAG_DISCOVERED, + PARENT_TO_CLONE_NFC_TECH_DISCOVERED, + PARENT_TO_CLONE_NFC_NDEF_DISCOVERED ); } diff --git a/services/core/java/com/android/server/pm/NoFilteringResolver.java b/services/core/java/com/android/server/pm/NoFilteringResolver.java index b87256dbbd9a..712413c49f18 100644 --- a/services/core/java/com/android/server/pm/NoFilteringResolver.java +++ b/services/core/java/com/android/server/pm/NoFilteringResolver.java @@ -60,9 +60,12 @@ public class NoFilteringResolver extends CrossProfileResolver { public static boolean isIntentRedirectionAllowed(Context context, AppCloningDeviceConfigHelper appCloningDeviceConfigHelper, boolean resolveForStart, long flags) { + boolean canMatchCloneProfile = (flags & PackageManager.MATCH_CLONE_PROFILE) != 0 + || (flags & PackageManager.MATCH_CLONE_PROFILE_LONG) != 0; return isAppCloningBuildingBlocksEnabled(context, appCloningDeviceConfigHelper) - && (resolveForStart || (((flags & PackageManager.MATCH_CLONE_PROFILE) != 0) - && hasPermission(context, Manifest.permission.QUERY_CLONED_APPS))); + && (resolveForStart + || (canMatchCloneProfile + && hasPermission(context, Manifest.permission.QUERY_CLONED_APPS))); } public NoFilteringResolver(ComponentResolverApi componentResolver, diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 29320aeefde9..a5cd821e319d 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -838,7 +838,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements if ((params.installFlags & PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK) != 0 && !PackageManagerServiceUtils.isSystemOrRootOrShell(callingUid) - && !Build.IS_DEBUGGABLE) { + && !Build.IS_DEBUGGABLE + && !PackageManagerServiceUtils.isAdoptedShell(callingUid, mContext)) { // If the bypass flag is set, but not running as system root or shell then remove // the flag params.installFlags &= ~PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 3eeeae7dc260..80a5f3a4c579 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -1085,11 +1085,17 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final boolean isUpdateOwnershipEnforcementEnabled = mPm.isUpdateOwnershipEnforcementAvailable() && existingUpdateOwnerPackageName != null; + // For an installation that un-archives an app, if the installer doesn't have the + // INSTALL_PACKAGES permission, the user should have already been prompted to confirm the + // un-archive request. There's no need for another confirmation during the installation. + final boolean isInstallUnarchive = + (params.installFlags & PackageManager.INSTALL_UNARCHIVE) != 0; // Device owners and affiliated profile owners are allowed to silently install packages, so // the permission check is waived if the installer is the device owner. final boolean noUserActionNecessary = isInstallerRoot || isInstallerSystem - || isInstallerDeviceOwnerOrAffiliatedProfileOwner() || isEmergencyInstall; + || isInstallerDeviceOwnerOrAffiliatedProfileOwner() || isEmergencyInstall + || isInstallUnarchive; if (noUserActionNecessary) { return userActionNotTypicallyNeededResponse; diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 034e467d32f8..179379487aba 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -3387,6 +3387,18 @@ class PackageManagerShellCommand extends ShellCommand { } sessionParams.setEnableRollback(true, rollbackStrategy); break; + case "--rollback-impact-level": + if (!Flags.recoverabilityDetection()) { + throw new IllegalArgumentException("Unknown option " + opt); + } + int rollbackImpactLevel = Integer.parseInt(peekNextArg()); + if (rollbackImpactLevel < PackageManager.ROLLBACK_USER_IMPACT_LOW + || rollbackImpactLevel + > PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL) { + throw new IllegalArgumentException( + rollbackImpactLevel + " is not a valid rollback impact level."); + } + sessionParams.setRollbackImpactLevel(rollbackImpactLevel); case "--staged-ready-timeout": params.stagedReadyTimeoutMs = Long.parseLong(getNextArgRequired()); break; @@ -4571,6 +4583,11 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --full: cause the app to be installed as a non-ephemeral full app"); pw.println(" --enable-rollback: enable rollbacks for the upgrade."); pw.println(" 0=restore (default), 1=wipe, 2=retain"); + if (Flags.recoverabilityDetection()) { + pw.println( + " --rollback-impact-level: set device impact required for rollback."); + pw.println(" 0=low (default), 1=high, 2=manual only"); + } pw.println(" --install-location: force the install location:"); pw.println(" 0=auto, 1=internal only, 2=prefer external"); pw.println(" --install-reason: indicates why the app is being installed:"); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index ff41245e1e13..e3261bd1322a 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1882,11 +1882,10 @@ public class UserManagerService extends IUserManager.Stub { && android.multiuser.Flags.enablePrivateSpaceFeatures()) { // Allow delayed locking since some profile types want to be able to unlock again via // biometrics. - ActivityManager.getService() - .stopUserWithDelayedLocking(userId, /* force= */ true, null); + ActivityManager.getService().stopUserWithDelayedLocking(userId, null); return; } - ActivityManager.getService().stopUser(userId, /* force= */ true, null); + ActivityManager.getService().stopUserWithCallback(userId, null); } private void logQuietModeEnabled(@UserIdInt int userId, boolean enableQuietMode, @@ -6132,7 +6131,7 @@ public class UserManagerService extends IUserManager.Stub { if (DBG) Slog.i(LOG_TAG, "Stopping user " + userId); int res; try { - res = ActivityManager.getService().stopUser(userId, /* force= */ true, + res = ActivityManager.getService().stopUserWithCallback(userId, new IStopUserCallback.Stub() { @Override public void userStopped(int userIdParam) { diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index 324637caa82f..4e02470e087d 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -721,7 +721,8 @@ public class UserRestrictionsUtils { if (!am.isProfileForeground(UserHandle.of(userId)) && userId != UserHandle.USER_SYSTEM) { try { - ActivityManager.getService().stopUser(userId, false, null); + ActivityManager.getService().stopUserExceptCertainProfiles( + userId, false, null); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } diff --git a/services/core/java/com/android/server/policy/Android.bp b/services/core/java/com/android/server/policy/Android.bp index fa55bf0a30e5..325b6cbf5f2c 100644 --- a/services/core/java/com/android/server/policy/Android.bp +++ b/services/core/java/com/android/server/policy/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "policy_flags", package: "com.android.server.policy", + container: "system", srcs: ["*.aconfig"], } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 76bf8fd45a43..b4919a4fb9ff 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -174,7 +174,6 @@ import android.service.dreams.IDreamManager; import android.service.vr.IPersistentVrStateCallbacks; import android.speech.RecognizerIntent; import android.telecom.TelecomManager; -import android.util.FeatureFlagUtils; import android.util.Log; import android.util.MathUtils; import android.util.MutableBoolean; @@ -4121,14 +4120,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { private void handleSwitchKeyboardLayout(@NonNull KeyEvent event, int direction, IBinder focusedToken) { - if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI)) { - IBinder targetWindowToken = - mWindowManagerInternal.getTargetWindowTokenFromInputToken(focusedToken); - InputMethodManagerInternal.get().onSwitchKeyboardLayoutShortcut(direction, - event.getDisplayId(), targetWindowToken); - } else { - mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction); - } + IBinder targetWindowToken = + mWindowManagerInternal.getTargetWindowTokenFromInputToken(focusedToken); + InputMethodManagerInternal.get().onSwitchKeyboardLayoutShortcut(direction, + event.getDisplayId(), targetWindowToken); } private boolean interceptFallback(IBinder focusedToken, KeyEvent fallbackEvent, @@ -5664,6 +5659,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } + @Override + public void onDisplaySwitchStart(int displayId) { + if (displayId == DEFAULT_DISPLAY) { + mDefaultDisplayPolicy.onDisplaySwitchStart(); + } + } + private long getKeyguardDrawnTimeout() { final boolean bootCompleted = LocalServices.getService(SystemServiceManager.class).isBootCompleted(); diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java index 5956594acd26..9ca4e273ac39 100644 --- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java +++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java @@ -251,12 +251,6 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { */ public int getCameraLensCoverState(); - /** - * Switch the keyboard layout for the given device. - * Direction should be +1 or -1 to go to the next or previous keyboard layout. - */ - public void switchKeyboardLayout(int deviceId, int direction); - public void shutdown(boolean confirm); public void reboot(boolean confirm); public void rebootSafeMode(boolean confirm); @@ -895,6 +889,9 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { void onScreenOff(); } + /** Called when the physical display starts to switch, e.g. fold/unfold. */ + void onDisplaySwitchStart(int displayId); + /** * Return whether the default display is on and not blocked by a black surface. */ diff --git a/services/core/java/com/android/server/policy/window_policy_flags.aconfig b/services/core/java/com/android/server/policy/window_policy_flags.aconfig index 2154a26acbaf..7914b949ad5f 100644 --- a/services/core/java/com/android/server/policy/window_policy_flags.aconfig +++ b/services/core/java/com/android/server/policy/window_policy_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.policy" +container: "system" flag { name: "support_input_wakeup_delegate" diff --git a/services/core/java/com/android/server/power/Android.bp b/services/core/java/com/android/server/power/Android.bp index 863ff76cb0c7..5d4065df91d6 100644 --- a/services/core/java/com/android/server/power/Android.bp +++ b/services/core/java/com/android/server/power/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "backstage_power_flags", package: "com.android.server.power.optimization", + container: "system", srcs: [ "stats/*.aconfig", ], diff --git a/services/core/java/com/android/server/power/batterysaver/OWNERS b/services/core/java/com/android/server/power/batterysaver/OWNERS index cf23bea97289..dc2d0b320d58 100644 --- a/services/core/java/com/android/server/power/batterysaver/OWNERS +++ b/services/core/java/com/android/server/power/batterysaver/OWNERS @@ -1,3 +1,2 @@ -kwekua@google.com omakoto@google.com -yamasani@google.com
\ No newline at end of file +yamasani@google.com diff --git a/services/core/java/com/android/server/power/feature/Android.bp b/services/core/java/com/android/server/power/feature/Android.bp index 2295b41009de..fee3114015a6 100644 --- a/services/core/java/com/android/server/power/feature/Android.bp +++ b/services/core/java/com/android/server/power/feature/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "power_flags", package: "com.android.server.power.feature.flags", + container: "system", srcs: [ "*.aconfig", ], diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig index f5dfb5cc3afe..ca58153cf25b 100644 --- a/services/core/java/com/android/server/power/feature/power_flags.aconfig +++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.power.feature.flags" +container: "system" # Important: Flags must be accessed through PowerManagerFlags. diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp index 8a98de673c3d..d7dd902dbabc 100644 --- a/services/core/java/com/android/server/power/hint/Android.bp +++ b/services/core/java/com/android/server/power/hint/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "power_hint_flags", package: "com.android.server.power.hint", + container: "system", srcs: [ "flags.aconfig", ], diff --git a/services/core/java/com/android/server/power/hint/flags.aconfig b/services/core/java/com/android/server/power/hint/flags.aconfig index f4afcb141b19..099774420d23 100644 --- a/services/core/java/com/android/server/power/hint/flags.aconfig +++ b/services/core/java/com/android/server/power/hint/flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.power.hint" +container: "system" flag { name: "powerhint_thread_cleanup" 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 c42cceab55be..54ae84f4535b 100644 --- a/services/core/java/com/android/server/power/stats/flags.aconfig +++ b/services/core/java/com/android/server/power/stats/flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.power.optimization" +container: "system" flag { name: "power_monitor_api" diff --git a/services/core/java/com/android/server/rollback/README.md b/services/core/java/com/android/server/rollback/README.md index f6bcbd057307..a38315c29d53 100644 --- a/services/core/java/com/android/server/rollback/README.md +++ b/services/core/java/com/android/server/rollback/README.md @@ -203,6 +203,15 @@ a different `RollbackDataPolicy`, like `ROLLBACK_DATA_POLICY_RETAIN` (1) or $ adb install --enable-rollback 1 FooV2.apk ``` +### Setting Rollback Impact Level + +The `adb install` command accepts the `--rollback-impact-level [0/1/2]` flag to control +when a rollback can be performed by `PackageWatchdog`. + +The default rollback impact level is `ROLLBACK_USER_IMPACT_LOW` (0). To use a +different impact level, use `ROLLBACK_USER_IMPACT_HIGH` (1) or `ROLLBACK_USER_IMPACT_ONLY_MANUAL` +(2). + ### Triggering Rollback Manually If rollback is available for an application, the pm command can be used to @@ -225,6 +234,8 @@ $ adb shell dumpsys rollback 469808841: -state: committed -timestamp: 2019-04-23T14:57:35.944Z + -rollbackLifetimeMillis: 0 + -rollbackUserImpact: 1 -packages: com.android.tests.rollback.testapp.B 2 -> 1 [0] -causePackages: @@ -232,6 +243,8 @@ $ adb shell dumpsys rollback 649899517: -state: committed -timestamp: 2019-04-23T12:55:21.342Z + -rollbackLifetimeMillis: 0 + -rollbackUserImpact: 0 -stagedSessionId: 343374391 -packages: com.android.tests.rollback.testapex 2 -> 1 [0] diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java index d1f91c89a04e..8f39ffb3f53c 100644 --- a/services/core/java/com/android/server/rollback/Rollback.java +++ b/services/core/java/com/android/server/rollback/Rollback.java @@ -27,6 +27,7 @@ import android.annotation.WorkerThread; import android.content.Context; import android.content.Intent; import android.content.IntentSender; +import android.content.pm.Flags; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; @@ -960,6 +961,9 @@ class Rollback { ipw.println("-stateDescription: " + mStateDescription); ipw.println("-timestamp: " + getTimestamp()); ipw.println("-rollbackLifetimeMillis: " + getRollbackLifetimeMillis()); + if (Flags.recoverabilityDetection()) { + ipw.println("-rollbackImpactLevel: " + info.getRollbackImpactLevel()); + } ipw.println("-isStaged: " + isStaged()); ipw.println("-originalSessionId: " + getOriginalSessionId()); ipw.println("-packages:"); diff --git a/services/core/java/com/android/server/stats/Android.bp b/services/core/java/com/android/server/stats/Android.bp index e597c3a6b6e6..f7955e86660a 100644 --- a/services/core/java/com/android/server/stats/Android.bp +++ b/services/core/java/com/android/server/stats/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "stats_flags", package: "com.android.server.stats", + container: "system", srcs: [ "stats_flags.aconfig", ], diff --git a/services/core/java/com/android/server/stats/stats_flags.aconfig b/services/core/java/com/android/server/stats/stats_flags.aconfig index 5101a6982fe1..101b98e1785d 100644 --- a/services/core/java/com/android/server/stats/stats_flags.aconfig +++ b/services/core/java/com/android/server/stats/stats_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.stats" +container: "system" flag { name: "add_mobile_bytes_transfer_by_proc_state_puller" diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index 2b05993c86ba..f62c76e1505a 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -61,7 +61,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import android.security.Authorization; +import android.security.KeyStoreAuthorization; import android.service.trust.GrantTrustResult; import android.service.trust.TrustAgentService; import android.text.TextUtils; @@ -155,6 +155,7 @@ public class TrustManagerService extends SystemService { /* package */ final TrustArchive mArchive = new TrustArchive(); private final Context mContext; private final LockPatternUtils mLockPatternUtils; + private final KeyStoreAuthorization mKeyStoreAuthorization; private final UserManager mUserManager; private final ActivityManager mActivityManager; private FingerprintManager mFingerprintManager; @@ -252,25 +253,27 @@ public class TrustManagerService extends SystemService { * cases. */ protected static class Injector { - private final LockPatternUtils mLockPatternUtils; - private final Looper mLooper; + private final Context mContext; - public Injector(LockPatternUtils lockPatternUtils, Looper looper) { - mLockPatternUtils = lockPatternUtils; - mLooper = looper; + public Injector(Context context) { + mContext = context; } LockPatternUtils getLockPatternUtils() { - return mLockPatternUtils; + return new LockPatternUtils(mContext); + } + + KeyStoreAuthorization getKeyStoreAuthorization() { + return KeyStoreAuthorization.getInstance(); } Looper getLooper() { - return mLooper; + return Looper.myLooper(); } } public TrustManagerService(Context context) { - this(context, new Injector(new LockPatternUtils(context), Looper.myLooper())); + this(context, new Injector(context)); } protected TrustManagerService(Context context, Injector injector) { @@ -280,6 +283,7 @@ public class TrustManagerService extends SystemService { mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); mLockPatternUtils = injector.getLockPatternUtils(); + mKeyStoreAuthorization = injector.getKeyStoreAuthorization(); mStrongAuthTracker = new StrongAuthTracker(context, injector.getLooper()); mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); } @@ -915,16 +919,16 @@ public class TrustManagerService extends SystemService { int authUserId = mLockPatternUtils.isProfileWithUnifiedChallenge(userId) ? resolveProfileParent(userId) : userId; - Authorization.onDeviceLocked(userId, getBiometricSids(authUserId), + mKeyStoreAuthorization.onDeviceLocked(userId, getBiometricSids(authUserId), isWeakUnlockMethodEnabled(authUserId)); } else { - Authorization.onDeviceLocked(userId, getBiometricSids(userId), false); + mKeyStoreAuthorization.onDeviceLocked(userId, getBiometricSids(userId), false); } } else { // Notify Keystore that the device is now unlocked for the user. Note that for unlocks // with LSKF, this is redundant with the call from LockSettingsService which provides // the password. However, for unlocks with biometric or trust agent, this is required. - Authorization.onDeviceUnlocked(userId, /* password= */ null); + mKeyStoreAuthorization.onDeviceUnlocked(userId, /* password= */ null); } } diff --git a/services/core/java/com/android/server/utils/Android.bp b/services/core/java/com/android/server/utils/Android.bp index 3a334bee93ff..ffb9aec1cbef 100644 --- a/services/core/java/com/android/server/utils/Android.bp +++ b/services/core/java/com/android/server/utils/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "com.android.server.utils-aconfig", package: "com.android.server.utils", + container: "system", srcs: ["*.aconfig"], } diff --git a/services/core/java/com/android/server/utils/flags.aconfig b/services/core/java/com/android/server/utils/flags.aconfig index 163116b9c5c7..6f37837d4f3b 100644 --- a/services/core/java/com/android/server/utils/flags.aconfig +++ b/services/core/java/com/android/server/utils/flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.utils" +container: "system" flag { name: "anr_timer_service" diff --git a/services/core/java/com/android/server/utils/quota/OWNERS b/services/core/java/com/android/server/utils/quota/OWNERS index a2943f39db24..469acb23f0b9 100644 --- a/services/core/java/com/android/server/utils/quota/OWNERS +++ b/services/core/java/com/android/server/utils/quota/OWNERS @@ -1,4 +1,3 @@ dplotnikov@google.com -kwekua@google.com omakoto@google.com yamasani@google.com diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index 6fc9d9a8ce1d..ad54efcd11d3 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -32,8 +32,9 @@ import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -42,10 +43,11 @@ import java.util.concurrent.atomic.AtomicInteger; * The base class for all vibrations. */ abstract class Vibration { - private static final SimpleDateFormat DEBUG_TIME_FORMAT = - new SimpleDateFormat("HH:mm:ss.SSS"); - private static final SimpleDateFormat DEBUG_DATE_TIME_FORMAT = - new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); + private static final DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern( + "HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); + private static final DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( + "MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); + // Used to generate globally unique vibration ids. private static final AtomicInteger sNextVibrationId = new AtomicInteger(1); // 0 = no callback @@ -240,10 +242,12 @@ abstract class Vibration { @Override public String toString() { - return "createTime: " + DEBUG_DATE_TIME_FORMAT.format(new Date(mCreateTime)) - + ", startTime: " + DEBUG_DATE_TIME_FORMAT.format(new Date(mStartTime)) - + ", endTime: " - + (mEndTime == 0 ? null : DEBUG_DATE_TIME_FORMAT.format(new Date(mEndTime))) + return "createTime: " + DEBUG_DATE_TIME_FORMATTER.format( + Instant.ofEpochMilli(mCreateTime)) + + ", startTime: " + DEBUG_DATE_TIME_FORMATTER.format( + Instant.ofEpochMilli(mStartTime)) + + ", endTime: " + (mEndTime == 0 ? null : DEBUG_DATE_TIME_FORMATTER.format( + Instant.ofEpochMilli(mEndTime))) + ", durationMs: " + mDurationMs + ", status: " + mStatus.name().toLowerCase(Locale.ROOT) + ", playedEffect: " + mPlayedEffect @@ -267,12 +271,14 @@ abstract class Vibration { boolean isExternalVibration = mPlayedEffect == null; String timingsStr = String.format(Locale.ROOT, "%s | %8s | %20s | duration: %5dms | start: %12s | end: %12s", - DEBUG_DATE_TIME_FORMAT.format(new Date(mCreateTime)), + DEBUG_DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(mCreateTime)), isExternalVibration ? "external" : "effect", mStatus.name().toLowerCase(Locale.ROOT), mDurationMs, - mStartTime == 0 ? "" : DEBUG_TIME_FORMAT.format(new Date(mStartTime)), - mEndTime == 0 ? "" : DEBUG_TIME_FORMAT.format(new Date(mEndTime))); + mStartTime == 0 ? "" + : DEBUG_TIME_FORMATTER.format(Instant.ofEpochMilli(mStartTime)), + mEndTime == 0 ? "" + : DEBUG_TIME_FORMATTER.format(Instant.ofEpochMilli(mEndTime))); String paramStr = String.format(Locale.ROOT, " | scale: %8s (adaptive=%.2f) | flags: %4s | usage: %s", VibrationScaler.scaleLevelToString(mScaleLevel), mAdaptiveScale, @@ -307,10 +313,12 @@ abstract class Vibration { pw.increaseIndent(); pw.println("status = " + mStatus.name().toLowerCase(Locale.ROOT)); pw.println("durationMs = " + mDurationMs); - pw.println("createTime = " + DEBUG_DATE_TIME_FORMAT.format(new Date(mCreateTime))); - pw.println("startTime = " + DEBUG_DATE_TIME_FORMAT.format(new Date(mStartTime))); - pw.println("endTime = " - + (mEndTime == 0 ? null : DEBUG_DATE_TIME_FORMAT.format(new Date(mEndTime)))); + pw.println("createTime = " + DEBUG_DATE_TIME_FORMATTER.format( + Instant.ofEpochMilli(mCreateTime))); + pw.println("startTime = " + DEBUG_DATE_TIME_FORMATTER.format( + Instant.ofEpochMilli(mStartTime))); + pw.println("endTime = " + (mEndTime == 0 ? null + : DEBUG_DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(mEndTime)))); pw.println("playedEffect = " + mPlayedEffect); pw.println("originalEffect = " + mOriginalEffect); pw.println("scale = " + VibrationScaler.scaleLevelToString(mScaleLevel)); diff --git a/services/core/java/com/android/server/vibrator/VibratorControlService.java b/services/core/java/com/android/server/vibrator/VibratorControlService.java index 10317c997581..b33fa6f56a23 100644 --- a/services/core/java/com/android/server/vibrator/VibratorControlService.java +++ b/services/core/java/com/android/server/vibrator/VibratorControlService.java @@ -51,8 +51,9 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -66,8 +67,8 @@ final class VibratorControlService extends IVibratorControlService.Stub { private static final int UNRECOGNIZED_VIBRATION_TYPE = -1; private static final int NO_SCALE = -1; - private static final SimpleDateFormat DEBUG_DATE_TIME_FORMAT = - new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); + private static final DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( + "MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); private final VibrationParamsRecords mVibrationParamsRecords; private final VibratorControllerHolder mVibratorControllerHolder; @@ -567,7 +568,7 @@ final class VibratorControlService extends IVibratorControlService.Stub { public void dump(IndentingPrintWriter pw) { String line = String.format(Locale.ROOT, "%s | %6s | scale: %5s | typesMask: %6s | usages: %s", - DEBUG_DATE_TIME_FORMAT.format(new Date(mCreateTime)), + DEBUG_DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(mCreateTime)), mOperation.name().toLowerCase(Locale.ROOT), (mScale == NO_SCALE) ? "" : String.format(Locale.ROOT, "%.2f", mScale), Long.toBinaryString(mTypesMask), createVibrationUsagesString()); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 5175b74116f4..6905802809c3 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -16,6 +16,7 @@ package com.android.server.wallpaper; +import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; import static android.app.WallpaperManager.getOrientation; import static android.app.WallpaperManager.getRotatedOrientation; import static android.view.Display.DEFAULT_DISPLAY; @@ -86,7 +87,7 @@ public class WallpaperCropper { public interface WallpaperCropUtils { /** - * Equivalent to {@link #getCrop(Point, Point, SparseArray, boolean)} + * Equivalent to {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)} */ Rect getCrop(Point displaySize, Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl); @@ -120,16 +121,23 @@ public class WallpaperCropper { public Rect getCrop(Point displaySize, Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl) { - // Case 1: if no crops are provided, center align the full image + int orientation = getOrientation(displaySize); + + // Case 1: if no crops are provided, show the full image (from the left, or right if RTL). if (suggestedCrops == null || suggestedCrops.size() == 0) { - Rect crop = new Rect(0, 0, displaySize.x, displaySize.y); - float scale = Math.min( - ((float) bitmapSize.x) / displaySize.x, - ((float) bitmapSize.y) / displaySize.y); - crop.scale(scale); - crop.offset((bitmapSize.x - crop.width()) / 2, - (bitmapSize.y - crop.height()) / 2); - return crop; + Rect crop = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + + // The first exception is if the device is a foldable and we're on the folded screen. + // In that case, show the center of what's on the unfolded screen. + int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation); + if (unfoldedOrientation != ORIENTATION_UNKNOWN) { + // Let the system know that we're showing the full image on the unfolded screen + SparseArray<Rect> newSuggestedCrops = new SparseArray<>(); + newSuggestedCrops.put(unfoldedOrientation, crop); + // This will fall into "Case 4" of this function and center the folded screen + return getCrop(displaySize, bitmapSize, newSuggestedCrops, rtl); + } + return getAdjustedCrop(crop, bitmapSize, displaySize, true, rtl, ADD); } // If any suggested crop is invalid, fallback to case 1 @@ -142,8 +150,6 @@ public class WallpaperCropper { } } - int orientation = getOrientation(displaySize); - // Case 2: if the orientation exists in the suggested crops, adjust the suggested crop Rect suggestedCrop = suggestedCrops.get(orientation); if (suggestedCrop != null) { @@ -168,11 +174,21 @@ public class WallpaperCropper { suggestedCrop = suggestedCrops.get(unfoldedOrientation); suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation); if (suggestedCrop != null) { - // only keep the visible part (without parallax) + // compute the visible part (without parallax) of the unfolded screen Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl); - return getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE); + // compute the folded crop, at the center of the crop of the unfolded screen + Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE); + // if we removed some width, add it back to add a parallax effect + if (res.width() < adjustedCrop.width()) { + if (rtl) res.left = Math.min(res.left, adjustedCrop.left); + else res.right = Math.max(res.right, adjustedCrop.right); + // use getAdjustedCrop(parallax=true) to make sure we don't exceed MAX_PARALLAX + res = getAdjustedCrop(res, bitmapSize, displaySize, true, rtl, ADD); + } + return res; } + // Case 5: if the device is a foldable, if we're looking for an unfolded orientation and // have the suggested crop of the relative folded orientation, reuse it by adding content. int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation); @@ -274,11 +290,8 @@ public class WallpaperCropper { if (additionalWidthForParallax > MAX_PARALLAX) { int widthToRemove = (int) Math.ceil( (additionalWidthForParallax - MAX_PARALLAX) * screenRatio * crop.height()); - if (rtl) { - adjustedCrop.left += widthToRemove; - } else { - adjustedCrop.right -= widthToRemove; - } + adjustedCrop.left += widthToRemove / 2; + adjustedCrop.right -= widthToRemove / 2 + widthToRemove % 2; } } else { // TODO (b/281648899) the third case is not always correct, fix that. @@ -366,6 +379,24 @@ public class WallpaperCropper { */ SparseArray<Rect> getDefaultCrops(SparseArray<Rect> suggestedCrops, Point bitmapSize) { + // If the suggested crops is single-element map with (ORIENTATION_UNKNOWN, cropHint), + // Crop the bitmap using the cropHint and compute the crops for cropped bitmap. + Rect cropHint = suggestedCrops.get(ORIENTATION_UNKNOWN); + if (cropHint != null) { + Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + if (suggestedCrops.size() != 1 || !bitmapRect.contains(cropHint)) { + Slog.w(TAG, "Couldn't get default crops from suggested crops " + suggestedCrops + + " for bitmap of size " + bitmapSize + "; ignoring suggested crops"); + return getDefaultCrops(new SparseArray<>(), bitmapSize); + } + Point cropSize = new Point(cropHint.width(), cropHint.height()); + SparseArray<Rect> relativeDefaultCrops = getDefaultCrops(new SparseArray<>(), cropSize); + for (int i = 0; i < relativeDefaultCrops.size(); i++) { + relativeDefaultCrops.valueAt(i).offset(cropHint.left, cropHint.top); + } + return relativeDefaultCrops; + } + SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes(); boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL; @@ -422,26 +453,74 @@ public class WallpaperCropper { } else { boolean needCrop = false; boolean needScale; - boolean multiCrop = multiCrop() && wallpaper.mSupportsMultiCrop; Point bitmapSize = new Point(options.outWidth, options.outHeight); + Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); + if (multiCrop()) { + // Check that the suggested crops per screen orientation are all within the bitmap. + for (int i = 0; i < wallpaper.mCropHints.size(); i++) { + int orientation = wallpaper.mCropHints.keyAt(i); + Rect crop = wallpaper.mCropHints.valueAt(i); + if (crop.isEmpty() || !bitmapRect.contains(crop)) { + Slog.w(TAG, "Invalid crop " + crop + " for orientation " + orientation + + " and bitmap size " + bitmapSize + "; clearing suggested crops."); + wallpaper.mCropHints.clear(); + wallpaper.cropHint.set(bitmapRect); + break; + } + } + } final Rect cropHint; - if (multiCrop) { - SparseArray<Rect> defaultDisplayCrops = - getDefaultCrops(wallpaper.mCropHints, bitmapSize); - // adapt the entries in wallpaper.mCropHints for the actual display + + // A wallpaper with cropHints = Map.of(ORIENTATION_UNKNOWN, rect) is treated like + // a wallpaper with cropHints = null and cropHint = rect. + Rect tempCropHint = wallpaper.mCropHints.get(ORIENTATION_UNKNOWN); + if (multiCrop() && tempCropHint != null) { + wallpaper.cropHint.set(tempCropHint); + wallpaper.mCropHints.clear(); + } + if (multiCrop() && wallpaper.mCropHints.size() > 0) { + // Some suggested crops per screen orientation were provided, + // use them to compute the default crops for this device + SparseArray<Rect> defaultCrops = getDefaultCrops(wallpaper.mCropHints, bitmapSize); + // Adapt the provided crops to match the actual crops for the default display SparseArray<Rect> updatedCropHints = new SparseArray<>(); for (int i = 0; i < wallpaper.mCropHints.size(); i++) { int orientation = wallpaper.mCropHints.keyAt(i); - Rect defaultCrop = defaultDisplayCrops.get(orientation); + Rect defaultCrop = defaultCrops.get(orientation); if (defaultCrop != null) { updatedCropHints.put(orientation, defaultCrop); } } wallpaper.mCropHints = updatedCropHints; - cropHint = getTotalCrop(defaultDisplayCrops); + + // Finally, compute the cropHint based on the default crops + cropHint = getTotalCrop(defaultCrops); wallpaper.cropHint.set(cropHint); + if (DEBUG) { + Slog.d(TAG, "Generated default crops for wallpaper: " + defaultCrops + + " based on suggested crops: " + wallpaper.mCropHints); + } + } else if (multiCrop()) { + // No crops per screen orientation were provided, but an overall cropHint may be + // defined in wallpaper.cropHint. Compute the default crops for the sub-image + // defined by the cropHint, then recompute the cropHint based on the default crops. + // If the cropHint is empty or invalid, ignore it and use the full image. + if (wallpaper.cropHint.isEmpty()) wallpaper.cropHint.set(bitmapRect); + if (!bitmapRect.contains(wallpaper.cropHint)) { + Slog.w(TAG, "Ignoring wallpaper.cropHint = " + wallpaper.cropHint + + "; not within the bitmap of size " + bitmapSize); + wallpaper.cropHint.set(bitmapRect); + } + Point cropSize = new Point(wallpaper.cropHint.width(), wallpaper.cropHint.height()); + SparseArray<Rect> defaultCrops = getDefaultCrops(new SparseArray<>(), cropSize); + cropHint = getTotalCrop(defaultCrops); + cropHint.offset(wallpaper.cropHint.left, wallpaper.cropHint.top); + wallpaper.cropHint.set(cropHint); + if (DEBUG) { + Slog.d(TAG, "Generated default crops for wallpaper: " + defaultCrops); + } } else { cropHint = new Rect(wallpaper.cropHint); } @@ -455,11 +534,11 @@ public class WallpaperCropper { } // Empty crop means use the full image - if (cropHint.isEmpty()) { + if (!multiCrop() && cropHint.isEmpty()) { cropHint.left = cropHint.top = 0; cropHint.right = options.outWidth; cropHint.bottom = options.outHeight; - } else { + } else if (!multiCrop()) { // force the crop rect to lie within the measured bounds int dx = cropHint.right > options.outWidth ? options.outWidth - cropHint.right : 0; int dy = cropHint.bottom > options.outHeight @@ -473,19 +552,19 @@ public class WallpaperCropper { if (cropHint.top < 0) { cropHint.top = 0; } - - // Don't bother cropping if what we're left with is identity - needCrop = (options.outHeight > cropHint.height() - || options.outWidth > cropHint.width()); } + // Don't bother cropping if what we're left with is identity + needCrop = (options.outHeight > cropHint.height() + || options.outWidth > cropHint.width()); + // scale if the crop height winds up not matching the recommended metrics needScale = cropHint.height() > wpData.mHeight || cropHint.height() > GLHelper.getMaxTextureSize() || cropHint.width() > GLHelper.getMaxTextureSize(); //make sure screen aspect ratio is preserved if width is scaled under screen size - if (needScale && !multiCrop) { + if (needScale && !multiCrop()) { final float scaleByHeight = (float) wpData.mHeight / (float) cropHint.height(); final int newWidth = (int) (cropHint.width() * scaleByHeight); if (newWidth < displayInfo.logicalWidth) { @@ -543,7 +622,7 @@ public class WallpaperCropper { final Rect estimateCrop = new Rect(cropHint); estimateCrop.scale(1f / options.inSampleSize); float hRatio = (float) wpData.mHeight / estimateCrop.height(); - if (multiCrop) { + if (multiCrop()) { // make sure the crop height is at most the display largest dimension hRatio = (float) mWallpaperDisplayHelper.getDefaultDisplayLargestDimension() / estimateCrop.height(); @@ -557,7 +636,7 @@ public class WallpaperCropper { if (DEBUG) { Slog.w(TAG, "Invalid crop dimensions, trying to adjust."); } - if (multiCrop) { + if (multiCrop()) { // clear custom crop guidelines, fallback to system default wallpaper.mCropHints.clear(); generateCropInternal(wallpaper); @@ -618,7 +697,7 @@ public class WallpaperCropper { final Bitmap finalCrop = Bitmap.createScaledBitmap(cropped, safeWidth, safeHeight, true); - if (multiCrop) { + if (multiCrop()) { wallpaper.mSampleSize = ((float) cropHint.height()) / finalCrop.getHeight(); } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index 02594d2d8d22..b792f7909fc8 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -172,11 +172,6 @@ class WallpaperData { SparseArray<Rect> mCropHints = new SparseArray<>(); /** - * cropHints will be ignored if this flag is false - */ - boolean mSupportsMultiCrop; - - /** * The phone orientation when the wallpaper was set. Only relevant for image wallpapers */ int mOrientationWhenSet = ORIENTATION_UNKNOWN; @@ -204,7 +199,6 @@ class WallpaperData { if (source.mCropHints != null) { this.mCropHints = source.mCropHints.clone(); } - this.mSupportsMultiCrop = source.mSupportsMultiCrop; this.allowBackup = source.allowBackup; this.primaryColors = source.primaryColors; this.mWallpaperDimAmount = source.mWallpaperDimAmount; diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index 7f53ea372687..4aefb54889aa 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -324,10 +324,7 @@ public class WallpaperDataParser { getAttributeInt(parser, "totalCropTop", 0), getAttributeInt(parser, "totalCropRight", 0), getAttributeInt(parser, "totalCropBottom", 0)); - wallpaper.mSupportsMultiCrop = multiCrop() && ( - parser.getAttributeBoolean(null, "supportsMultiCrop", false) - || mImageWallpaper.equals(wallpaper.wallpaperComponent)); - if (wallpaper.mSupportsMultiCrop) { + if (multiCrop() && mImageWallpaper.equals(wallpaper.nextWallpaperComponent)) { wallpaper.mCropHints = new SparseArray<>(); for (Pair<Integer, String> pair: screenDimensionPairs()) { Rect cropHint = new Rect( @@ -342,16 +339,14 @@ public class WallpaperDataParser { } if (wallpaper.mCropHints.size() == 0 && totalCropHint.isEmpty()) { // migration case: the crops per screen orientation are not specified. - int orientation = legacyCropHint.width() < legacyCropHint.height() - ? WallpaperManager.PORTRAIT : WallpaperManager.LANDSCAPE; if (!legacyCropHint.isEmpty()) { - wallpaper.mCropHints.put(orientation, legacyCropHint); + wallpaper.cropHint.set(legacyCropHint); } } else { wallpaper.cropHint.set(totalCropHint); } wallpaper.mSampleSize = parser.getAttributeFloat(null, "sampleSize", 1f); - } else { + } else if (!multiCrop()) { wallpaper.cropHint.set(legacyCropHint); } final DisplayData wpData = mWallpaperDisplayHelper @@ -467,13 +462,12 @@ public class WallpaperDataParser { out.startTag(null, tag); out.attributeInt(null, "id", wallpaper.wallpaperId); - out.attributeBoolean(null, "supportsMultiCrop", wallpaper.mSupportsMultiCrop); - - if (multiCrop() && wallpaper.mSupportsMultiCrop) { + if (multiCrop() && mImageWallpaper.equals(wallpaper.wallpaperComponent)) { if (wallpaper.mCropHints == null) { Slog.e(TAG, "cropHints should not be null when saved"); wallpaper.mCropHints = new SparseArray<>(); } + Rect rectToPutInLegacyCrop = new Rect(wallpaper.cropHint); for (Pair<Integer, String> pair : screenDimensionPairs()) { Rect cropHint = wallpaper.mCropHints.get(pair.first); if (cropHint == null) continue; @@ -493,12 +487,14 @@ public class WallpaperDataParser { } } if (pair.first == orientationToPutInLegacyCrop) { - out.attributeInt(null, "cropLeft", cropHint.left); - out.attributeInt(null, "cropTop", cropHint.top); - out.attributeInt(null, "cropRight", cropHint.right); - out.attributeInt(null, "cropBottom", cropHint.bottom); + rectToPutInLegacyCrop.set(cropHint); } } + out.attributeInt(null, "cropLeft", rectToPutInLegacyCrop.left); + out.attributeInt(null, "cropTop", rectToPutInLegacyCrop.top); + out.attributeInt(null, "cropRight", rectToPutInLegacyCrop.right); + out.attributeInt(null, "cropBottom", rectToPutInLegacyCrop.bottom); + out.attributeInt(null, "totalCropLeft", wallpaper.cropHint.left); out.attributeInt(null, "totalCropTop", wallpaper.cropHint.top); out.attributeInt(null, "totalCropRight", wallpaper.cropHint.right); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 885baf65013f..802f196adf3b 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -42,6 +42,7 @@ import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_ORIG; import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked; import static com.android.window.flags.Flags.multiCrop; +import static com.android.window.flags.Flags.offloadColorExtraction; import android.annotation.NonNull; import android.app.ActivityManager; @@ -380,7 +381,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } // Outside of the lock since it will synchronize itself - notifyWallpaperColorsChanged(wallpaper); + if (!offloadColorExtraction()) notifyWallpaperColorsChanged(wallpaper); } @Override @@ -403,12 +404,16 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } void notifyWallpaperColorsChanged(@NonNull WallpaperData wallpaper) { + notifyWallpaperColorsChanged(wallpaper, wallpaper.mWhich); + } + + private void notifyWallpaperColorsChanged(@NonNull WallpaperData wallpaper, int which) { if (DEBUG) { Slog.i(TAG, "Notifying wallpaper colors changed"); } if (wallpaper.connection != null) { wallpaper.connection.forEachDisplayConnector(connector -> - notifyWallpaperColorsChangedOnDisplay(wallpaper, connector.mDisplayId)); + notifyWallpaperColorsChangedOnDisplay(wallpaper, connector.mDisplayId, which)); } } @@ -425,6 +430,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub private void notifyWallpaperColorsChangedOnDisplay(@NonNull WallpaperData wallpaper, int displayId) { + notifyWallpaperColorsChangedOnDisplay(wallpaper, displayId, wallpaper.mWhich); + } + + private void notifyWallpaperColorsChangedOnDisplay(@NonNull WallpaperData wallpaper, + int displayId, int which) { boolean needsExtraction; synchronized (mLock) { final RemoteCallbackList<IWallpaperManagerCallback> currentUserColorListeners = @@ -449,8 +459,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub notify = extractColors(wallpaper); } if (notify) { - notifyColorListeners(getAdjustedWallpaperColorsOnDimming(wallpaper), - wallpaper.mWhich, wallpaper.userId, displayId); + notifyColorListeners(getAdjustedWallpaperColorsOnDimming(wallpaper), which, + wallpaper.userId, displayId); } } @@ -504,6 +514,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub * @return true unless the wallpaper changed during the color computation */ private boolean extractColors(WallpaperData wallpaper) { + if (offloadColorExtraction()) return !mImageWallpaper.equals(wallpaper.wallpaperComponent); String cropFile = null; boolean defaultImageWallpaper = false; int wallpaperId; @@ -803,7 +814,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub null /* options */); mWindowManagerInternal.setWallpaperShowWhenLocked( mToken, (wallpaper.mWhich & FLAG_LOCK) != 0); - if (multiCrop() && wallpaper.mSupportsMultiCrop) { + if (multiCrop() && mImageWallpaper.equals(wallpaper.wallpaperComponent)) { mWindowManagerInternal.setWallpaperCropHints(mToken, mWallpaperCropper.getRelativeCropHints(wallpaper)); } else { @@ -1148,10 +1159,16 @@ public class WallpaperManagerService extends IWallpaperManager.Stub synchronized (mLock) { // Do not broadcast changes on ImageWallpaper since it's handled // internally by this class. - if (mImageWallpaper.equals(mWallpaper.wallpaperComponent)) { + boolean isImageWallpaper = mImageWallpaper.equals(mWallpaper.wallpaperComponent); + if (isImageWallpaper && (!offloadColorExtraction() || primaryColors == null)) { return; } mWallpaper.primaryColors = primaryColors; + // only save the colors for ImageWallpaper - for live wallpapers, the colors + // are always recomputed after a reboot. + if (offloadColorExtraction() && isImageWallpaper) { + saveSettingsLocked(mWallpaper.userId); + } } notifyWallpaperColorsChangedOnDisplay(mWallpaper, displayId); } @@ -1177,7 +1194,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub try { // This will trigger onComputeColors in the wallpaper engine. // It's fine to be locked in here since the binder is oneway. - connector.mEngine.requestWallpaperColors(); + if (!offloadColorExtraction() || mWallpaper.primaryColors == null) { + connector.mEngine.requestWallpaperColors(); + } } catch (RemoteException e) { Slog.w(TAG, "Failed to request wallpaper colors", e); } @@ -1811,6 +1830,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub // Offload color extraction to another thread since switchUser will be called // from the main thread. FgThread.getHandler().post(() -> { + if (offloadColorExtraction()) return; notifyWallpaperColorsChanged(systemWallpaper); if (lockWallpaper != systemWallpaper) notifyWallpaperColorsChanged(lockWallpaper); notifyWallpaperColorsChanged(mFallbackWallpaper); @@ -1917,11 +1937,17 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final ComponentName component; final int finalWhich; - if ((which & FLAG_LOCK) > 0 && lockWallpaper != null) { - clearWallpaperBitmaps(lockWallpaper); - } - if ((which & FLAG_SYSTEM) > 0) { - clearWallpaperBitmaps(wallpaper); + // Clear any previous ImageWallpaper related fields + List<WallpaperData> toClear = new ArrayList<>(); + if ((which & FLAG_LOCK) > 0 && lockWallpaper != null) toClear.add(lockWallpaper); + if ((which & FLAG_SYSTEM) > 0) toClear.add(wallpaper); + for (WallpaperData wallpaperToClear : toClear) { + clearWallpaperBitmaps(wallpaperToClear); + if (multiCrop()) { + wallpaperToClear.mCropHints.clear(); + wallpaperToClear.cropHint.set(0, 0, 0, 0); + wallpaperToClear.mSampleSize = 1; + } } // lock only case: set the system wallpaper component to both screens @@ -2225,7 +2251,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub checkPermission(READ_WALLPAPER_INTERNAL); WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId) : mWallpaperMap.get(userId); - if (wallpaper == null || !wallpaper.mSupportsMultiCrop) return null; + if (wallpaper == null || !mImageWallpaper.equals(wallpaper.wallpaperComponent)) { + return null; + } SparseArray<Rect> relativeSuggestedCrops = mWallpaperCropper.getRelativeCropHints(wallpaper); Point croppedBitmapSize = new Point( @@ -2255,7 +2283,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub @Override public List<Rect> getFutureBitmapCrops(Point bitmapSize, List<Point> displaySizes, int[] screenOrientations, List<Rect> crops) { - SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops, ORIENTATION_UNKNOWN); + SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops); SparseArray<Rect> defaultCrops = mWallpaperCropper.getDefaultCrops(cropMap, bitmapSize); List<Rect> result = new ArrayList<>(); boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) @@ -2272,7 +2300,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub throw new UnsupportedOperationException( "This method should only be called with the multi crop flag enabled"); } - SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops, ORIENTATION_UNKNOWN); + SparseArray<Rect> cropMap = getCropMap(screenOrientations, crops); SparseArray<Rect> defaultCrops = mWallpaperCropper.getDefaultCrops(cropMap, bitmapSize); return WallpaperCropper.getTotalCrop(defaultCrops); } @@ -2714,8 +2742,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub }); // Need to extract colors again to re-calculate dark hints after // applying dimming. - wp.mIsColorExtractedFromDim = true; - pendingColorExtraction.add(wp); + if (!offloadColorExtraction()) { + wp.mIsColorExtractedFromDim = true; + pendingColorExtraction.add(wp); + } changed = true; } } @@ -2724,7 +2754,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } for (WallpaperData wp: pendingColorExtraction) { - notifyWallpaperColorsChanged(wp); + if (!offloadColorExtraction()) notifyWallpaperColorsChanged(wp); } } finally { Binder.restoreCallingIdentity(ident); @@ -2860,10 +2890,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return null; } - int currentOrientation = mWallpaperDisplayHelper.getDefaultDisplayCurrentOrientation(); - SparseArray<Rect> cropMap = !multiCrop() ? null - : getCropMap(screenOrientations, crops, currentOrientation); - Rect cropHint = multiCrop() || crops == null ? null : crops.get(0); + SparseArray<Rect> cropMap = !multiCrop() ? null : getCropMap(screenOrientations, crops); + Rect cropHint = multiCrop() || crops == null || crops.isEmpty() ? new Rect() : crops.get(0); final boolean fromForegroundApp = !multiCrop() ? false : isFromForegroundApp(callingPackage); @@ -2912,12 +2940,16 @@ public class WallpaperManagerService extends IWallpaperManager.Stub wallpaper.setComplete = completion; wallpaper.fromForegroundApp = multiCrop() ? fromForegroundApp : isFromForegroundApp(callingPackage); - if (!multiCrop()) wallpaper.cropHint.set(cropHint); - if (multiCrop()) wallpaper.mSupportsMultiCrop = true; - if (multiCrop()) wallpaper.mCropHints = cropMap; + wallpaper.cropHint.set(cropHint); + if (multiCrop()) { + wallpaper.mCropHints = cropMap; + wallpaper.mSampleSize = 1f; + wallpaper.mOrientationWhenSet = + mWallpaperDisplayHelper.getDefaultDisplayCurrentOrientation(); + } wallpaper.allowBackup = allowBackup; wallpaper.mWallpaperDimAmount = getWallpaperDimAmount(); - wallpaper.mOrientationWhenSet = currentOrientation; + if (offloadColorExtraction()) wallpaper.primaryColors = null; } return pfd; } finally { @@ -2926,16 +2958,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } - private SparseArray<Rect> getCropMap(int[] screenOrientations, List<Rect> crops, - int currentOrientation) { + private SparseArray<Rect> getCropMap(int[] screenOrientations, List<Rect> crops) { if ((crops == null ^ screenOrientations == null) || (crops != null && crops.size() != screenOrientations.length)) { throw new IllegalArgumentException( "Illegal crops/orientations lists: must both be null, or both the same size"); } SparseArray<Rect> cropMap = new SparseArray<>(); - boolean unknown = false; - if (crops != null && crops.size() != 0) { + if (crops != null && !crops.isEmpty()) { for (int i = 0; i < crops.size(); i++) { Rect crop = crops.get(i); int width = crop.width(), height = crop.height(); @@ -2943,22 +2973,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub throw new IllegalArgumentException("Invalid crop rect supplied: " + crop); } int orientation = screenOrientations[i]; - if (orientation == ORIENTATION_UNKNOWN) { - if (currentOrientation == ORIENTATION_UNKNOWN) { - throw new IllegalArgumentException( - "Invalid orientation: " + ORIENTATION_UNKNOWN); - } - unknown = true; - orientation = currentOrientation; + if (orientation == ORIENTATION_UNKNOWN && cropMap.size() > 1) { + throw new IllegalArgumentException("Invalid crops supplied: the UNKNOWN" + + "screen orientation should only be used in a singleton map"); } cropMap.put(orientation, crop); } } - if (unknown && cropMap.size() > 1) { - throw new IllegalArgumentException("Invalid crops supplied: the UNKNOWN screen " - + "orientation should only be used in a singleton map (in which case it" - + "represents the current orientation of the default display)"); - } return cropMap; } @@ -2975,7 +2996,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub WallpaperData lockWP = new WallpaperData(userId, FLAG_LOCK); lockWP.wallpaperId = sysWP.wallpaperId; lockWP.cropHint.set(sysWP.cropHint); - lockWP.mSupportsMultiCrop = sysWP.mSupportsMultiCrop; if (sysWP.mCropHints != null) { lockWP.mCropHints = sysWP.mCropHints.clone(); } @@ -3072,6 +3092,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT); boolean shouldNotifyColors = false; + + // If the lockscreen wallpaper is set to the same as the home screen, notify that the + // lockscreen wallpaper colors changed, even if we don't bind a new wallpaper engine. + boolean shouldNotifyLockscreenColors = false; boolean bindSuccess; final WallpaperData newWallpaper; @@ -3096,7 +3120,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final long ident = Binder.clearCallingIdentity(); try { - newWallpaper.mSupportsMultiCrop = mImageWallpaper.equals(name); newWallpaper.imageWallpaperPending = false; newWallpaper.mWhich = which; newWallpaper.mSystemWasBoth = systemIsBoth; @@ -3118,7 +3141,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub bindSuccess = bindWallpaperComponentLocked(name, /* force */ forceRebind, /* fromUser */ true, newWallpaper, reply); if (bindSuccess) { - if (!same) { + if (!same || (offloadColorExtraction() && forceRebind)) { newWallpaper.primaryColors = null; } else { if (newWallpaper.connection != null) { @@ -3142,6 +3165,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub newWallpaper.wallpaperId = makeWallpaperIdLocked(); notifyCallbacksLocked(newWallpaper); shouldNotifyColors = true; + if (offloadColorExtraction()) { + shouldNotifyColors = false; + shouldNotifyLockscreenColors = !force && same && !systemIsBoth + && which == (FLAG_SYSTEM | FLAG_LOCK); + } if (which == (FLAG_SYSTEM | FLAG_LOCK)) { if (DEBUG) { @@ -3170,6 +3198,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (shouldNotifyColors) { notifyWallpaperColorsChanged(newWallpaper); } + if (shouldNotifyLockscreenColors) { + notifyWallpaperColorsChanged(newWallpaper, FLAG_LOCK); + } + return bindSuccess; } diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index 5184e49385b2..c2b9128daac5 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -96,7 +96,6 @@ import static com.android.server.wm.AppTransitionProto.LAST_USED_APP_TRANSITION; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerInternal.AppTransitionListener; -import static com.android.server.wm.WindowManagerInternal.KeyguardExitAnimationStartListener; import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_AFTER_ANIM; import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_NONE; @@ -105,7 +104,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; -import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -117,7 +115,6 @@ import android.os.IBinder; import android.os.IRemoteCallback; import android.os.RemoteException; import android.os.SystemClock; -import android.os.SystemProperties; import android.os.UserHandle; import android.util.Pair; import android.util.Slog; @@ -137,6 +134,7 @@ import android.view.animation.TranslateAnimation; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.TransitionAnimation; +import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.DumpUtils.Dump; import com.android.internal.util.function.pooled.PooledLambda; @@ -244,7 +242,7 @@ public class AppTransition implements Dump { mHandler = new Handler(service.mH.getLooper()); mDisplayContent = displayContent; mTransitionAnimation = new TransitionAnimation( - context, ProtoLog.isEnabled(WM_DEBUG_ANIM), TAG); + context, ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG), TAG); final TypedArray windowStyle = mContext.getTheme().obtainStyledAttributes( com.android.internal.R.styleable.Window); diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index f6681c571090..f7b4a6748411 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -20,6 +20,7 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_NONE; import static android.view.WindowManager.TRANSIT_TO_BACK; @@ -779,6 +780,10 @@ class BackNavigationController { && wc.asTaskFragment() == null) { continue; } + // Only care if visibility changed. + if (targets.get(i).getTransitMode(wc) == TRANSIT_CHANGE) { + continue; + } // WC can be visible due to setLaunchBehind if (wc.isVisibleRequested()) { mTmpOpenApps.add(wc); @@ -843,14 +848,14 @@ class BackNavigationController { * @param targets The final animation targets derived in transition. * @param finishedTransition The finished transition target. */ - boolean onTransitionFinish(ArrayList<Transition.ChangeInfo> targets, + void onTransitionFinish(ArrayList<Transition.ChangeInfo> targets, @NonNull Transition finishedTransition) { if (finishedTransition == mWaitTransitionFinish) { clearBackAnimations(); } if (!mBackAnimationInProgress || mPendingAnimationBuilder == null) { - return false; + return; } ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Handling the deferred animation after transition finished"); @@ -878,7 +883,7 @@ class BackNavigationController { + " open: " + Arrays.toString(mPendingAnimationBuilder.mOpenTargets) + " close: " + mPendingAnimationBuilder.mCloseTarget); cancelPendingAnimation(); - return false; + return; } // Ensure the final animation targets which hidden by transition could be visible. @@ -887,9 +892,14 @@ class BackNavigationController { wc.prepareSurfaces(); } - scheduleAnimation(mPendingAnimationBuilder); - mPendingAnimationBuilder = null; - return true; + // The pending builder could be cleared due to prepareSurfaces + // => updateNonSystemOverlayWindowsVisibilityIfNeeded + // => setForceHideNonSystemOverlayWindowIfNeeded + // => updateFocusedWindowLocked => onFocusWindowChanged. + if (mPendingAnimationBuilder != null) { + scheduleAnimation(mPendingAnimationBuilder); + mPendingAnimationBuilder = null; + } } private void cancelPendingAnimation() { @@ -1552,15 +1562,17 @@ class BackNavigationController { return this; } + // WC must be Activity/TaskFragment/Task boolean containTarget(@NonNull WindowContainer wc) { if (mOpenTargets != null) { for (int i = mOpenTargets.length - 1; i >= 0; --i) { - if (wc == mOpenTargets[i] || mOpenTargets[i].hasChild(wc)) { + if (wc == mOpenTargets[i] || mOpenTargets[i].hasChild(wc) + || wc.hasChild(mOpenTargets[i])) { return true; } } } - return wc == mCloseTarget || mCloseTarget.hasChild(wc); + return wc == mCloseTarget || mCloseTarget.hasChild(wc) || wc.hasChild(mCloseTarget); } /** diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 47f4a66995af..0e446b8eaf8c 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -38,9 +38,11 @@ import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY; import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; +import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balRequireOptInSameUid; +import static com.android.window.flags.Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid; import static com.android.window.flags.Flags.balShowToastsBlocked; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -539,6 +541,16 @@ public class BackgroundActivityStartController { sb.append("; balAllowedByPiSender: ").append(mBalAllowedByPiSender); sb.append("; resultIfPiSenderAllowsBal: ").append(mResultForRealCaller); } + // features + sb.append("; balImproveRealCallerVisibilityCheck: ") + .append(balImproveRealCallerVisibilityCheck()); + sb.append("; balRequireOptInByPendingIntentCreator: ") + .append(balRequireOptInByPendingIntentCreator()); + sb.append("; balRequireOptInSameUid: ").append(balRequireOptInSameUid()); + sb.append("; balRespectAppSwitchStateWhenCheckBoundByForegroundUid: ") + .append(balRespectAppSwitchStateWhenCheckBoundByForegroundUid()); + sb.append("; balDontBringExistingBackgroundTaskStackToFg: ") + .append(balDontBringExistingBackgroundTaskStackToFg()); sb.append("]"); return sb.toString(); } diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index a29cb60ff545..ca5f26aa4cc8 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -26,6 +26,10 @@ import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFi import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; +import android.os.Message; +import android.os.Trace; +import android.util.Log; +import android.util.Slog; import android.view.DisplayInfo; import android.window.DisplayAreaInfo; import android.window.TransitionRequestInfo; @@ -35,6 +39,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.protolog.common.ProtoLog; import com.android.server.wm.utils.DisplayInfoOverrides.DisplayInfoFieldsUpdater; +import com.android.window.flags.Flags; import java.util.Arrays; import java.util.Objects; @@ -65,6 +70,12 @@ public class DeferredDisplayUpdater implements DisplayUpdater { WM_OVERRIDE_FIELDS.setFields(out, override); }; + private static final String TAG = "DeferredDisplayUpdater"; + + private static final String TRACE_TAG_WAIT_FOR_TRANSITION = + "Screen unblock: wait for transition"; + private static final int WAIT_FOR_TRANSITION_TIMEOUT = 1000; + private final DisplayContent mDisplayContent; @NonNull @@ -88,6 +99,18 @@ public class DeferredDisplayUpdater implements DisplayUpdater { @NonNull private final DisplayInfo mOutputDisplayInfo = new DisplayInfo(); + /** Whether {@link #mScreenUnblocker} should wait for transition to be ready. */ + private boolean mShouldWaitForTransitionWhenScreenOn; + + /** The message to notify PhoneWindowManager#finishWindowsDrawn. */ + @Nullable + private Message mScreenUnblocker; + + private final Runnable mScreenUnblockTimeoutRunnable = () -> { + Slog.e(TAG, "Timeout waiting for the display switch transition to start"); + continueScreenUnblocking(); + }; + public DeferredDisplayUpdater(@NonNull DisplayContent displayContent) { mDisplayContent = displayContent; mNonOverrideDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo()); @@ -248,6 +271,7 @@ public class DeferredDisplayUpdater implements DisplayUpdater { getCurrentDisplayChange(fromRotation, startBounds); displayChange.setPhysicalDisplayChanged(true); + transition.addTransactionCompletedListener(this::continueScreenUnblocking); mDisplayContent.mTransitionController.requestStartTransition(transition, /* startTask= */ null, /* remoteTransition= */ null, displayChange); @@ -277,6 +301,58 @@ public class DeferredDisplayUpdater implements DisplayUpdater { return !Objects.equals(first.uniqueId, second.uniqueId); } + @Override + public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation, + DisplayAreaInfo newDisplayAreaInfo) { + // Unblock immediately in case there is no transition. This is unlikely to happen. + if (mScreenUnblocker != null && !mDisplayContent.mTransitionController.inTransition()) { + mScreenUnblocker.sendToTarget(); + mScreenUnblocker = null; + } + } + + @Override + public void onDisplaySwitching(boolean switching) { + mShouldWaitForTransitionWhenScreenOn = switching; + } + + @Override + public boolean waitForTransition(@NonNull Message screenUnblocker) { + if (!Flags.waitForTransitionOnDisplaySwitch()) return false; + if (!mShouldWaitForTransitionWhenScreenOn) { + return false; + } + mScreenUnblocker = screenUnblocker; + if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) { + Trace.beginAsyncSection(TRACE_TAG_WAIT_FOR_TRANSITION, screenUnblocker.hashCode()); + } + + mDisplayContent.mWmService.mH.removeCallbacks(mScreenUnblockTimeoutRunnable); + mDisplayContent.mWmService.mH.postDelayed(mScreenUnblockTimeoutRunnable, + WAIT_FOR_TRANSITION_TIMEOUT); + return true; + } + + /** + * Continues the screen unblocking flow, could be called either on a binder thread as + * a result of surface transaction completed listener or from {@link WindowManagerService#mH} + * handler in case of timeout + */ + private void continueScreenUnblocking() { + synchronized (mDisplayContent.mWmService.mGlobalLock) { + mShouldWaitForTransitionWhenScreenOn = false; + mDisplayContent.mWmService.mH.removeCallbacks(mScreenUnblockTimeoutRunnable); + if (mScreenUnblocker == null) { + return; + } + mScreenUnblocker.sendToTarget(); + if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) { + Trace.endAsyncSection(TRACE_TAG_WAIT_FOR_TRANSITION, mScreenUnblocker.hashCode()); + } + mScreenUnblocker = null; + } + } + /** * Diff result: fields are the same */ diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 54abbc3f6dd5..cde3e68e43c9 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -470,7 +470,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private final DisplayRotation mDisplayRotation; @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy; DisplayFrames mDisplayFrames; - private final DisplayUpdater mDisplayUpdater; + final DisplayUpdater mDisplayUpdater; private boolean mInTouchMode; diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 16f7373ebc5e..a5037ea0ff07 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -779,6 +779,11 @@ public class DisplayPolicy { return mLidState; } + private void onDisplaySwitchFinished() { + mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + mDisplayContent.mDisplayUpdater.onDisplaySwitching(false); + } + public void setAwake(boolean awake) { synchronized (mLock) { if (awake == mAwake) { @@ -797,7 +802,7 @@ public class DisplayPolicy { mService.mAtmService.mKeyguardController.updateDeferTransitionForAod( mAwake /* waiting */); if (!awake) { - mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + onDisplaySwitchFinished(); } } } @@ -866,7 +871,7 @@ public class DisplayPolicy { /** It is called after {@link #finishScreenTurningOn}. This runs on PowerManager's thread. */ public void screenTurnedOn() { - mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + onDisplaySwitchFinished(); } public void screenTurnedOff() { @@ -2187,6 +2192,11 @@ public class DisplayPolicy { mDisplayContent.mTransitionController.getCollectingTransitionId(); } + /** If this is called, expect that there will be an onDisplayChanged about unique id. */ + public void onDisplaySwitchStart() { + mDisplayContent.mDisplayUpdater.onDisplaySwitching(true); + } + @NavigationBarPosition int navigationBarPosition(int displayRotation) { if (mNavigationBar != null) { diff --git a/services/core/java/com/android/server/wm/DisplayUpdater.java b/services/core/java/com/android/server/wm/DisplayUpdater.java index e611177210e8..918b180ab1cb 100644 --- a/services/core/java/com/android/server/wm/DisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DisplayUpdater.java @@ -17,6 +17,7 @@ package com.android.server.wm; import android.annotation.NonNull; +import android.os.Message; import android.view.Surface; import android.window.DisplayAreaInfo; @@ -49,4 +50,16 @@ interface DisplayUpdater { @Surface.Rotation int previousRotation, @Surface.Rotation int newRotation, @NonNull DisplayAreaInfo newDisplayAreaInfo) { } + + /** + * Called with {@code true} when physical display is going to switch. And {@code false} when + * the display is turned on or the device goes to sleep. + */ + default void onDisplaySwitching(boolean switching) { + } + + /** Returns {@code true} if the transition will control when to turn on the screen. */ + default boolean waitForTransition(@NonNull Message screenUnBlocker) { + return false; + } } diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index b8bb258aa2ce..0ad601de95ec 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -61,6 +61,7 @@ import android.view.WindowManager; import com.android.internal.policy.IKeyguardDismissCallback; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.policy.WindowManagerPolicy; +import com.android.window.flags.Flags; import java.io.PrintWriter; @@ -225,13 +226,16 @@ class KeyguardController { if (keyguardShowing) { state.mDismissalRequested = false; } - if (goingAwayRemoved) { - // Keyguard dismiss is canceled. Send a transition to undo the changes and clean up - // before holding the sleep token again. + if (goingAwayRemoved || (keyguardShowing && Flags.keyguardAppearTransition())) { + // Keyguard decided to show or stopped going away. Send a transition to animate back + // to the locked state before holding the sleep token again final DisplayContent dc = mRootWindowContainer.getDefaultDisplay(); dc.requestTransitionAndLegacyPrepare( TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING); - mWindowManager.executeAppTransition(); + if (Flags.keyguardAppearTransition()) { + dc.mWallpaperController.adjustWallpaperWindows(); + } + dc.executeAppTransition(); } } diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java index 3ef6eeb2ecf3..63bbb3140bfb 100644 --- a/services/core/java/com/android/server/wm/RemoteAnimationController.java +++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java @@ -44,6 +44,7 @@ import android.view.SurfaceControl.Transaction; import android.view.WindowManager; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.FastPrintWriter; import com.android.server.wm.SurfaceAnimator.AnimationType; @@ -209,7 +210,7 @@ class RemoteAnimationController implements DeathRecipient { Slog.e(TAG, "Failed to start remote animation", e); onAnimationFinished(); } - if (ProtoLog.isEnabled(WM_DEBUG_REMOTE_ANIMATIONS)) { + if (ProtoLog.isEnabled(WM_DEBUG_REMOTE_ANIMATIONS, LogLevel.DEBUG)) { ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation(): Notify animation start:"); writeStartDebugStatement(); } diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index d67684c7038e..c632714bd8ce 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -32,6 +32,7 @@ import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLog; import java.io.PrintWriter; @@ -192,7 +193,7 @@ public class SurfaceAnimator { return; } mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback); - if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) { + if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG)) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); mAnimation.dump(pw, ""); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 3779d9eef727..1b380aadee35 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -112,6 +112,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.Executor; import java.util.function.Predicate; /** @@ -233,6 +234,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ private ArrayList<Task> mTransientHideTasks; + @VisibleForTesting + ArrayList<Runnable> mTransactionCompletedListeners = null; + /** Custom activity-level animation options and callbacks. */ private TransitionInfo.AnimationOptions mOverrideOptions; private IRemoteCallback mClientAnimationStartCallback = null; @@ -1640,6 +1644,14 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { commitVisibleActivities(transaction); commitVisibleWallpapers(); + if (mTransactionCompletedListeners != null) { + for (int i = 0; i < mTransactionCompletedListeners.size(); i++) { + final Runnable listener = mTransactionCompletedListeners.get(i); + transaction.addTransactionCompletedListener(Runnable::run, + (stats) -> listener.run()); + } + } + // Fall-back to the default display if there isn't one participating. final DisplayContent primaryDisplay = !mTargetDisplays.isEmpty() ? mTargetDisplays.get(0) : mController.mAtm.mRootWindowContainer.getDefaultDisplay(); @@ -1862,6 +1874,17 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } /** + * Adds a listener that will be executed after the start transaction of this transition + * is presented on the screen, the listener will be executed on a binder thread + */ + void addTransactionCompletedListener(Runnable listener) { + if (mTransactionCompletedListeners == null) { + mTransactionCompletedListeners = new ArrayList<>(); + } + mTransactionCompletedListeners.add(listener); + } + + /** * Checks if the transition contains order changes. * * This is a shallow check that doesn't account for collection in parallel, unlike diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index a2f6fb4c08ad..59bda54eb089 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -22,7 +22,6 @@ import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER; @@ -858,10 +857,6 @@ class WallpaperController { } public void updateWallpaperTokens(boolean keyguardLocked) { - if (DEBUG_WALLPAPER) { - Slog.v(TAG, "Wallpaper vis: target " + mWallpaperTarget + " prev=" - + mPrevWallpaperTarget); - } updateWallpaperTokens(mWallpaperTarget != null || mPrevWallpaperTarget != null, keyguardLocked); } @@ -870,6 +865,8 @@ class WallpaperController { * Change the visibility of the top wallpaper to {@param visibility} and hide all the others. */ private void updateWallpaperTokens(boolean visibility, boolean keyguardLocked) { + ProtoLog.v(WM_DEBUG_WALLPAPER, "updateWallpaperTokens requestedVisibility=%b on" + + " keyguardLocked=%b", visibility, keyguardLocked); WindowState topWallpaper = mFindResults.getTopWallpaper(keyguardLocked); WallpaperWindowToken topWallpaperToken = topWallpaper == null ? null : topWallpaper.mToken.asWallpaperToken(); diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 55eeaf22cca8..5c24eee63317 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -274,6 +274,12 @@ class WallpaperWindowToken extends WindowToken { } @Override + boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) { + // TODO(b/233286785): Support sync state for wallpaper. See WindowState#prepareSync. + return !mVisibleRequested || !hasVisibleNotDrawnWallpaper(); + } + + @Override public String toString() { if (stringName == null) { StringBuilder sb = new StringBuilder(); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 80889d1ac972..90ac57613cff 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -113,6 +113,7 @@ import android.window.WindowContainerToken; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; +import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ToBooleanFunction; import com.android.server.wm.SurfaceAnimator.Animatable; @@ -3405,7 +3406,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // ActivityOption#makeCustomAnimation or WindowManager#overridePendingTransition. a.restrictDuration(MAX_APP_TRANSITION_DURATION); } - if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) { + if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG)) { ProtoLog.i(WM_DEBUG_ANIM, "Loaded animation %s for %s, duration: %d, stack=%s", a, this, ((a != null) ? a.getDuration() : 0), Debug.getCallers(20)); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 04ca0ae0982d..8a68afbd501f 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -3685,12 +3685,6 @@ public class WindowManagerService extends IWindowManager.Stub // Called by window manager policy. Not exposed externally. @Override - public void switchKeyboardLayout(int deviceId, int direction) { - mInputManager.switchKeyboardLayout(deviceId, direction); - } - - // Called by window manager policy. Not exposed externally. - @Override public void shutdown(boolean confirm) { // Pass in the UI context, since ShutdownThread requires it (to show UI). ShutdownThread.shutdown(ActivityThread.currentActivityThread().getSystemUiContext(), @@ -8077,6 +8071,10 @@ public class WindowManagerService extends IWindowManager.Stub } boolean allWindowsDrawn = false; synchronized (mGlobalLock) { + if (mRoot.getDefaultDisplay().mDisplayUpdater.waitForTransition(message)) { + // Use the ready-to-play of transition as the signal. + return; + } container.waitForAllWindowsDrawn(); mWindowPlacerLocked.requestTraversal(); mH.removeMessages(H.WAITING_FOR_DRAWN_TIMEOUT, container); @@ -10164,10 +10162,12 @@ public class WindowManagerService extends IWindowManager.Stub // TODO(b/323580163): Check if already shown and update shown state. if (mSensitiveContentPackages.shouldBlockScreenCaptureForApp(w.getOwningPackage(), w.getOwningUid(), w.getWindowToken())) { - Toast.makeText(mContext, Looper.getMainLooper(), - mContext.getString(R.string.screen_not_shared_sensitive_content), - Toast.LENGTH_SHORT) - .show(); + mH.post(() -> { + Toast.makeText(mContext, Looper.getMainLooper(), + mContext.getString(R.string.screen_not_shared_sensitive_content), + Toast.LENGTH_SHORT) + .show(); + }); } } } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 6ac2774941e1..aac567c2e455 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -202,6 +202,12 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio // Whether this process has ever started a service with the BIND_INPUT_METHOD permission. private volatile boolean mHasImeService; + /** + * Whether this process can use realtime prioirity (SCHED_FIFO) for its UI and render threads + * when this process is SCHED_GROUP_TOP_APP. + */ + private final boolean mUseFifoUiScheduling; + /** Whether {@link #mActivities} is not empty. */ private volatile boolean mHasActivities; /** All activities running in the process (exclude destroying). */ @@ -340,6 +346,8 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio // TODO(b/151161907): Remove after support for display-independent (raw) SysUi configs. mIsActivityConfigOverrideAllowed = false; } + mUseFifoUiScheduling = com.android.window.flags.Flags.fifoPriorityForMajorUiProcesses() + && (isSysUiPackage || mAtm.isCallerRecents(uid)); mCanUseSystemGrammaticalGender = mAtm.mGrammaticalManagerInternal != null && mAtm.mGrammaticalManagerInternal.canGetSystemGrammaticalGender(mUid, @@ -1901,6 +1909,11 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } + /** Returns {@code true} if the process prefers to use fifo scheduling. */ + public boolean useFifoUiScheduling() { + return mUseFifoUiScheduling; + } + @HotPath(caller = HotPath.OOM_ADJUSTMENT) public void onTopProcChanged() { if (mAtm.mVrController.isInterestingToSchedGroup()) { @@ -2078,6 +2091,9 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } pw.println(); } + if (mUseFifoUiScheduling) { + pw.println(prefix + " mUseFifoUiScheduling=true"); + } final int stateFlags = mActivityStateFlags; if (stateFlags != ACTIVITY_STATE_FLAG_MASK_MIN_TASK_LAYER) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index b716dc67f1a3..7a0245bc1acc 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -247,6 +247,7 @@ import android.window.OnBackInvokedCallbackInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.KeyInterceptionInfo; +import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.ToBooleanFunction; @@ -4739,7 +4740,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } void onExitAnimationDone() { - if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) { + if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.VERBOSE)) { final AnimationAdapter animationAdapter = mSurfaceAnimator.getAnimation(); StringWriter sw = new StringWriter(); if (animationAdapter != null) { diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index a242d4242388..6fd7aa0e4a78 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -61,6 +61,7 @@ import android.view.WindowManager.LayoutParams; import android.view.animation.Animation; import android.view.animation.AnimationUtils; +import com.android.internal.protolog.common.LogLevel; import com.android.internal.protolog.common.ProtoLog; import com.android.server.policy.WindowManagerPolicy; @@ -586,7 +587,7 @@ class WindowStateAnimator { mWin.mAttrs, attr, TRANSIT_OLD_NONE); } } - if (ProtoLog.isEnabled(WM_DEBUG_ANIM)) { + if (ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.VERBOSE)) { ProtoLog.v(WM_DEBUG_ANIM, "applyAnimation: win=%s" + " anim=%d attr=0x%x a=%s transit=%d type=%d isEntrance=%b Callers %s", this, anim, attr, a, transit, mAttrType, isEntrance, Debug.getCallers(20)); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java index 82f9aadba9f4..d24afabe95a4 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java @@ -92,7 +92,7 @@ final class BundlePolicySerializer extends PolicySerializer<Bundle> { while (count > 0 && (type = parser.next()) != XmlPullParser.END_DOCUMENT) { if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_VALUE)) { - values.add(parser.nextText().trim()); + values.add(parser.nextText()); count--; } } @@ -111,7 +111,7 @@ final class BundlePolicySerializer extends PolicySerializer<Bundle> { restrictions.putParcelableArray(key, bundleList.toArray(new Bundle[bundleList.size()])); } else { - String value = parser.nextText().trim(); + String value = parser.nextText(); if (ATTR_TYPE_BOOLEAN.equals(valType)) { restrictions.putBoolean(key, Boolean.parseBoolean(value)); } else if (ATTR_TYPE_INTEGER.equals(valType)) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index b34092ca280f..1dd719eb02aa 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -11509,10 +11509,17 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void setApplicationRestrictions(ComponentName who, String callerPackage, - String packageName, Bundle restrictions) { + String packageName, Bundle restrictions, boolean parent) { final CallerIdentity caller = getCallerIdentity(who, callerPackage); checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_APPLICATION_RESTRICTIONS); + // This check is eventually made in UMS, checking here to fail early. + String validationResult = + FrameworkParsingPackageUtils.validateName(packageName, false, false); + if (validationResult != null) { + throw new IllegalArgumentException("Invalid package name: " + validationResult); + } + if (isUnicornFlagEnabled()) { EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( who, @@ -11520,12 +11527,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { caller.getPackageName(), caller.getUserId() ); - // This check is eventually made in UMS, checking here to fail early. - String validationResult = - FrameworkParsingPackageUtils.validateName(packageName, false, false); - if (validationResult != null) { - throw new IllegalArgumentException("Invalid package name: " + validationResult); - } if (restrictions == null || restrictions.isEmpty()) { mDevicePolicyEngine.removeLocalPolicy( @@ -11541,6 +11542,57 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } setBackwardsCompatibleAppRestrictions( caller, packageName, restrictions, caller.getUserHandle()); + } else if (Flags.dmrhCanSetAppRestriction()) { + final boolean isRoleHolder; + if (who != null) { + // DO or PO + Preconditions.checkCallAuthorization( + (isProfileOwner(caller) || isDefaultDeviceOwner(caller))); + Preconditions.checkCallAuthorization(!parent, + "DO or PO cannot call this on parent"); + // Caller has opted to be treated as DPC (by passing a non-null who), so don't + // consider it as the DMRH, even if the caller is both the DPC and the DMRH. + isRoleHolder = false; + } else { + // Delegates, or the DMRH. Only DMRH can call this on COPE parent + isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller); + if (parent) { + Preconditions.checkCallAuthorization(isRoleHolder); + Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(), + "Role Holder can only operate parent app restriction on COPE devices"); + } else { + Preconditions.checkCallAuthorization(isRoleHolder + || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS)); + } + } + // DMRH caller uses policy engine, others still use legacy code path + if (isRoleHolder) { + EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null, + caller.getPackageName()); + int affectedUserId = parent + ? getProfileParentId(caller.getUserId()) : caller.getUserId(); + if (restrictions == null || restrictions.isEmpty()) { + mDevicePolicyEngine.removeLocalPolicy( + PolicyDefinition.APPLICATION_RESTRICTIONS(packageName), + enforcingAdmin, + affectedUserId); + } else { + mDevicePolicyEngine.setLocalPolicy( + PolicyDefinition.APPLICATION_RESTRICTIONS(packageName), + enforcingAdmin, + new BundlePolicyValue(restrictions), + affectedUserId); + } + Intent changeIntent = new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); + changeIntent.setPackage(packageName); + changeIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mContext.sendBroadcastAsUser(changeIntent, UserHandle.of(affectedUserId)); + } else { + mInjector.binderWithCleanCallingIdentity(() -> { + mUserManager.setApplicationRestrictions(packageName, restrictions, + caller.getUserHandle()); + }); + } } else { Preconditions.checkCallAuthorization((caller.hasAdminComponent() && (isProfileOwner(caller) || isDefaultDeviceOwner(caller))) @@ -12823,7 +12875,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Slogf.i(LOG_TAG, "Stopping user %d", userId); final long id = mInjector.binderClearCallingIdentity(); try { - switch (mInjector.getIActivityManager().stopUser(userId, true /*force*/, null)) { + switch (mInjector.getIActivityManager().stopUserWithCallback(userId, null)) { case ActivityManager.USER_OP_SUCCESS: return UserManager.USER_OPERATION_SUCCESS; case ActivityManager.USER_OP_IS_CURRENT: @@ -12872,7 +12924,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public Bundle getApplicationRestrictions(ComponentName who, String callerPackage, - String packageName) { + String packageName, boolean parent) { final CallerIdentity caller = getCallerIdentity(who, callerPackage); if (isUnicornFlagEnabled()) { @@ -12891,6 +12943,50 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return Bundle.EMPTY; } return policies.get(enforcingAdmin).getValue(); + } else if (Flags.dmrhCanSetAppRestriction()) { + final boolean isRoleHolder; + if (who != null) { + // Caller is DO or PO. They cannot call this on parent + Preconditions.checkCallAuthorization(!parent + && (isProfileOwner(caller) || isDefaultDeviceOwner(caller))); + // Caller has opted to be treated as DPC (by passing a non-null who), so don't + // consider it as the DMRH, even if the caller is both the DPC and the DMRH. + isRoleHolder = false; + } else { + // Caller is delegates or the DMRH. Only DMRH can call this on parent + isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller); + if (parent) { + Preconditions.checkCallAuthorization(isRoleHolder); + Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(), + "Role Holder can only operate parent app restriction on COPE devices"); + } else { + Preconditions.checkCallAuthorization(isRoleHolder + || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS)); + } + } + if (isRoleHolder) { + EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null, + caller.getPackageName()); + int affectedUserId = parent + ? getProfileParentId(caller.getUserId()) : caller.getUserId(); + LinkedHashMap<EnforcingAdmin, PolicyValue<Bundle>> policies = + mDevicePolicyEngine.getLocalPoliciesSetByAdmins( + PolicyDefinition.APPLICATION_RESTRICTIONS(packageName), + affectedUserId); + if (!policies.containsKey(enforcingAdmin)) { + return Bundle.EMPTY; + } + return policies.get(enforcingAdmin).getValue(); + } else { + return mInjector.binderWithCleanCallingIdentity(() -> { + Bundle bundle = mUserManager.getApplicationRestrictions(packageName, + caller.getUserHandle()); + // if no restrictions were saved, mUserManager.getApplicationRestrictions + // returns null, but DPM method should return an empty Bundle as per JavaDoc + return bundle != null ? bundle : Bundle.EMPTY; + }); + } + } else { Preconditions.checkCallAuthorization((caller.hasAdminComponent() && (isProfileOwner(caller) || isDefaultDeviceOwner(caller))) @@ -15811,19 +15907,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { for (EnforcingAdmin admin : policies.keySet()) { restrictions.add(policies.get(admin).getValue()); } - if (!restrictions.isEmpty()) { - return restrictions; - } return mInjector.binderWithCleanCallingIdentity(() -> { - // Could be a device that has a DPC that hasn't migrated yet, so just return any + // Could be a device that has a DPC that hasn't migrated yet, so also return any // restrictions saved in userManager. Bundle bundle = mUserManager.getApplicationRestrictions( packageName, UserHandle.of(userId)); - if (bundle == null || bundle.isEmpty()) { - return new ArrayList<>(); + if (bundle != null && !bundle.isEmpty()) { + restrictions.add(bundle); } - return List.of(bundle); + return restrictions; }); } diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp index 6ad8d790485c..6393e11b7432 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp @@ -1,6 +1,7 @@ aconfig_declarations { name: "device_state_flags", package: "com.android.server.policy.feature.flags", + container: "system", srcs: [ "device_state_flags.aconfig", ], diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig index 29e258cc90ff..21e33dd1b99a 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.policy.feature.flags" +container: "system" flag { name: "enable_dual_display_blocking" diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 0a7f49da0c31..3c6b500d9ced 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -108,29 +108,50 @@ import com.android.internal.util.EmergencyAffordanceManager; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.ILockSettings; import com.android.internal.widget.LockSettingsInternal; +import com.android.server.accessibility.AccessibilityManagerService; +import com.android.server.accounts.AccountManagerService; import com.android.server.adaptiveauth.AdaptiveAuthService; +import com.android.server.adb.AdbService; +import com.android.server.alarm.AlarmManagerService; import com.android.server.am.ActivityManagerService; +import com.android.server.ambientcontext.AmbientContextManagerService; +import com.android.server.app.GameManagerService; import com.android.server.appbinding.AppBindingService; +import com.android.server.apphibernation.AppHibernationService; import com.android.server.appop.AppOpMigrationHelper; import com.android.server.appop.AppOpMigrationHelperImpl; +import com.android.server.appprediction.AppPredictionManagerService; +import com.android.server.appwidget.AppWidgetService; import com.android.server.art.ArtModuleServiceInitializer; import com.android.server.art.DexUseManagerLocal; import com.android.server.attention.AttentionManagerService; import com.android.server.audio.AudioService; +import com.android.server.autofill.AutofillManagerService; +import com.android.server.backup.BackupManagerService; import com.android.server.biometrics.AuthService; import com.android.server.biometrics.BiometricService; import com.android.server.biometrics.sensors.face.FaceService; import com.android.server.biometrics.sensors.fingerprint.FingerprintService; import com.android.server.biometrics.sensors.iris.IrisService; +import com.android.server.blob.BlobStoreManagerService; import com.android.server.broadcastradio.BroadcastRadioService; import com.android.server.camera.CameraServiceProxy; import com.android.server.clipboard.ClipboardService; +import com.android.server.companion.CompanionDeviceManagerService; +import com.android.server.companion.virtual.VirtualDeviceManagerService; import com.android.server.compat.PlatformCompat; import com.android.server.compat.PlatformCompatNative; +import com.android.server.compat.overrides.AppCompatOverridesService; +import com.android.server.connectivity.IpConnectivityMetrics; import com.android.server.connectivity.PacProxyService; +import com.android.server.content.ContentService; import com.android.server.contentcapture.ContentCaptureManagerInternal; +import com.android.server.contentcapture.ContentCaptureManagerService; +import com.android.server.contentsuggestions.ContentSuggestionsManagerService; +import com.android.server.contextualsearch.ContextualSearchManagerService; import com.android.server.coverage.CoverageService; import com.android.server.cpu.CpuMonitorService; +import com.android.server.credentials.CredentialManagerService; import com.android.server.criticalevents.CriticalEventLog; import com.android.server.devicepolicy.DevicePolicyManagerService; import com.android.server.devicestate.DeviceStateManagerService; @@ -147,14 +168,20 @@ import com.android.server.incident.IncidentCompanionService; import com.android.server.input.InputManagerService; import com.android.server.inputmethod.InputMethodManagerService; import com.android.server.integrity.AppIntegrityManagerService; +import com.android.server.job.JobSchedulerService; import com.android.server.lights.LightsService; import com.android.server.locales.LocaleManagerService; import com.android.server.location.LocationManagerService; import com.android.server.location.altitude.AltitudeService; +import com.android.server.locksettings.LockSettingsService; import com.android.server.logcat.LogcatManagerService; +import com.android.server.media.MediaResourceMonitorService; import com.android.server.media.MediaRouterService; +import com.android.server.media.MediaSessionService; import com.android.server.media.metrics.MediaMetricsManagerService; import com.android.server.media.projection.MediaProjectionManagerService; +import com.android.server.midi.MidiService; +import com.android.server.musicrecognition.MusicRecognitionManagerService; import com.android.server.net.NetworkManagementService; import com.android.server.net.NetworkPolicyManagerService; import com.android.server.net.watchlist.NetworkWatchlistService; @@ -195,12 +222,16 @@ import com.android.server.power.ShutdownThread; import com.android.server.power.ThermalManagerService; import com.android.server.power.hint.HintManagerService; import com.android.server.powerstats.PowerStatsService; +import com.android.server.print.PrintManagerService; import com.android.server.profcollect.ProfcollectForwardingService; import com.android.server.recoverysystem.RecoverySystemService; import com.android.server.resources.ResourcesManagerService; import com.android.server.restrictions.RestrictionsManagerService; import com.android.server.role.RoleServicePlatformHelper; +import com.android.server.rollback.RollbackManagerService; import com.android.server.rotationresolver.RotationResolverManagerService; +import com.android.server.search.SearchManagerService; +import com.android.server.searchui.SearchUiManagerService; import com.android.server.security.AttestationVerificationManagerService; import com.android.server.security.FileIntegrityService; import com.android.server.security.KeyAttestationApplicationIdProviderService; @@ -210,16 +241,28 @@ import com.android.server.selinux.SelinuxAuditLogsService; import com.android.server.sensorprivacy.SensorPrivacyService; import com.android.server.sensors.SensorService; import com.android.server.signedconfig.SignedConfigService; +import com.android.server.slice.SliceManagerService; +import com.android.server.smartspace.SmartspaceManagerService; import com.android.server.soundtrigger.SoundTriggerService; import com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareService; +import com.android.server.speech.SpeechRecognitionManagerService; +import com.android.server.stats.bootstrap.StatsBootstrapAtomService; +import com.android.server.stats.pull.StatsPullAtomService; import com.android.server.statusbar.StatusBarManagerService; import com.android.server.storage.DeviceStorageMonitorService; +import com.android.server.systemcaptions.SystemCaptionsManagerService; import com.android.server.telecom.TelecomLoaderService; import com.android.server.testharness.TestHarnessModeService; import com.android.server.textclassifier.TextClassificationManagerService; import com.android.server.textservices.TextServicesManagerService; +import com.android.server.texttospeech.TextToSpeechManagerService; +import com.android.server.timedetector.GnssTimeUpdateService; import com.android.server.timedetector.NetworkTimeUpdateService; +import com.android.server.timedetector.TimeDetectorService; +import com.android.server.timezonedetector.TimeZoneDetectorService; +import com.android.server.timezonedetector.location.LocationTimeZoneManagerService; import com.android.server.tracing.TracingServiceProxy; +import com.android.server.translation.TranslationManagerService; import com.android.server.trust.TrustManagerService; import com.android.server.tv.TvInputManagerService; import com.android.server.tv.TvRemoteService; @@ -227,10 +270,15 @@ import com.android.server.tv.interactive.TvInteractiveAppManagerService; import com.android.server.tv.tunerresourcemanager.TunerResourceManagerService; import com.android.server.twilight.TwilightService; import com.android.server.uri.UriGrantsManagerService; +import com.android.server.usage.StorageStatsService; import com.android.server.usage.UsageStatsService; +import com.android.server.usb.UsbService; import com.android.server.utils.TimingsTraceAndSlog; import com.android.server.vibrator.VibratorManagerService; +import com.android.server.voiceinteraction.VoiceInteractionManagerService; import com.android.server.vr.VrManagerService; +import com.android.server.wallpaper.WallpaperManagerService; +import com.android.server.wallpapereffectsgeneration.WallpaperEffectsGenerationManagerService; import com.android.server.wearable.WearableSensingManagerService; import com.android.server.webkit.WebViewUpdateService; import com.android.server.wm.ActivityTaskManagerService; @@ -268,44 +316,20 @@ public final class SystemServer implements Dumpable { * Implementation class names. TODO: Move them to a codegen class or load * them from the build system somehow. */ - private static final String BACKUP_MANAGER_SERVICE_CLASS = - "com.android.server.backup.BackupManagerService$Lifecycle"; - private static final String APPWIDGET_SERVICE_CLASS = - "com.android.server.appwidget.AppWidgetService"; private static final String ARC_NETWORK_SERVICE_CLASS = "com.android.server.arc.net.ArcNetworkService"; private static final String ARC_PERSISTENT_DATA_BLOCK_SERVICE_CLASS = "com.android.server.arc.persistent_data_block.ArcPersistentDataBlockService"; private static final String ARC_SYSTEM_HEALTH_SERVICE = "com.android.server.arc.health.ArcSystemHealthService"; - private static final String VOICE_RECOGNITION_MANAGER_SERVICE_CLASS = - "com.android.server.voiceinteraction.VoiceInteractionManagerService"; - private static final String APP_HIBERNATION_SERVICE_CLASS = - "com.android.server.apphibernation.AppHibernationService"; - private static final String PRINT_MANAGER_SERVICE_CLASS = - "com.android.server.print.PrintManagerService"; - private static final String COMPANION_DEVICE_MANAGER_SERVICE_CLASS = - "com.android.server.companion.CompanionDeviceManagerService"; - private static final String VIRTUAL_DEVICE_MANAGER_SERVICE_CLASS = - "com.android.server.companion.virtual.VirtualDeviceManagerService"; private static final String STATS_COMPANION_APEX_PATH = "/apex/com.android.os.statsd/javalib/service-statsd.jar"; + private static final String STATS_COMPANION_LIFECYCLE_CLASS = + "com.android.server.stats.StatsCompanion$Lifecycle"; private static final String SCHEDULING_APEX_PATH = "/apex/com.android.scheduling/javalib/service-scheduling.jar"; private static final String REBOOT_READINESS_LIFECYCLE_CLASS = "com.android.server.scheduling.RebootReadinessManagerService$Lifecycle"; - private static final String CONNECTIVITY_SERVICE_APEX_PATH = - "/apex/com.android.tethering/javalib/service-connectivity.jar"; - private static final String STATS_COMPANION_LIFECYCLE_CLASS = - "com.android.server.stats.StatsCompanion$Lifecycle"; - private static final String STATS_PULL_ATOM_SERVICE_CLASS = - "com.android.server.stats.pull.StatsPullAtomService"; - private static final String STATS_BOOTSTRAP_ATOM_SERVICE_LIFECYCLE_CLASS = - "com.android.server.stats.bootstrap.StatsBootstrapAtomService$Lifecycle"; - private static final String USB_SERVICE_CLASS = - "com.android.server.usb.UsbService$Lifecycle"; - private static final String MIDI_SERVICE_CLASS = - "com.android.server.midi.MidiService$Lifecycle"; private static final String WIFI_APEX_SERVICE_JAR_PATH = "/apex/com.android.wifi/javalib/service-wifi.jar"; private static final String WIFI_SERVICE_CLASS = @@ -320,16 +344,6 @@ public final class SystemServer implements Dumpable { "com.android.server.wifi.p2p.WifiP2pService"; private static final String LOWPAN_SERVICE_CLASS = "com.android.server.lowpan.LowpanService"; - private static final String JOB_SCHEDULER_SERVICE_CLASS = - "com.android.server.job.JobSchedulerService"; - private static final String LOCK_SETTINGS_SERVICE_CLASS = - "com.android.server.locksettings.LockSettingsService$Lifecycle"; - private static final String STORAGE_MANAGER_SERVICE_CLASS = - "com.android.server.StorageManagerService$Lifecycle"; - private static final String STORAGE_STATS_SERVICE_CLASS = - "com.android.server.usage.StorageStatsService$Lifecycle"; - private static final String SEARCH_MANAGER_SERVICE_CLASS = - "com.android.server.search.SearchManagerService$Lifecycle"; private static final String THERMAL_OBSERVER_CLASS = "com.android.clockwork.ThermalObserver"; private static final String WEAR_CONNECTIVITY_SERVICE_CLASS = @@ -354,91 +368,26 @@ public final class SystemServer implements Dumpable { "com.android.clockwork.settings.WearSettingsService"; private static final String WRIST_ORIENTATION_SERVICE_CLASS = "com.android.clockwork.wristorientation.WristOrientationService"; - private static final String ACCOUNT_SERVICE_CLASS = - "com.android.server.accounts.AccountManagerService$Lifecycle"; - private static final String CONTENT_SERVICE_CLASS = - "com.android.server.content.ContentService$Lifecycle"; - private static final String WALLPAPER_SERVICE_CLASS = - "com.android.server.wallpaper.WallpaperManagerService$Lifecycle"; - private static final String AUTO_FILL_MANAGER_SERVICE_CLASS = - "com.android.server.autofill.AutofillManagerService"; - private static final String CREDENTIAL_MANAGER_SERVICE_CLASS = - "com.android.server.credentials.CredentialManagerService"; - private static final String CONTENT_CAPTURE_MANAGER_SERVICE_CLASS = - "com.android.server.contentcapture.ContentCaptureManagerService"; - private static final String TRANSLATION_MANAGER_SERVICE_CLASS = - "com.android.server.translation.TranslationManagerService"; - private static final String MUSIC_RECOGNITION_MANAGER_SERVICE_CLASS = - "com.android.server.musicrecognition.MusicRecognitionManagerService"; - private static final String AMBIENT_CONTEXT_MANAGER_SERVICE_CLASS = - "com.android.server.ambientcontext.AmbientContextManagerService"; - private static final String SYSTEM_CAPTIONS_MANAGER_SERVICE_CLASS = - "com.android.server.systemcaptions.SystemCaptionsManagerService"; - private static final String TEXT_TO_SPEECH_MANAGER_SERVICE_CLASS = - "com.android.server.texttospeech.TextToSpeechManagerService"; + private static final String IOT_SERVICE_CLASS = "com.android.things.server.IoTSystemService"; - private static final String SLICE_MANAGER_SERVICE_CLASS = - "com.android.server.slice.SliceManagerService$Lifecycle"; private static final String CAR_SERVICE_HELPER_SERVICE_CLASS = "com.android.internal.car.CarServiceHelperService"; - private static final String TIME_DETECTOR_SERVICE_CLASS = - "com.android.server.timedetector.TimeDetectorService$Lifecycle"; - private static final String TIME_ZONE_DETECTOR_SERVICE_CLASS = - "com.android.server.timezonedetector.TimeZoneDetectorService$Lifecycle"; - private static final String LOCATION_TIME_ZONE_MANAGER_SERVICE_CLASS = - "com.android.server.timezonedetector.location.LocationTimeZoneManagerService$Lifecycle"; - private static final String GNSS_TIME_UPDATE_SERVICE_CLASS = - "com.android.server.timedetector.GnssTimeUpdateService$Lifecycle"; - private static final String ACCESSIBILITY_MANAGER_SERVICE_CLASS = - "com.android.server.accessibility.AccessibilityManagerService$Lifecycle"; - private static final String ADB_SERVICE_CLASS = - "com.android.server.adb.AdbService$Lifecycle"; - private static final String SPEECH_RECOGNITION_MANAGER_SERVICE_CLASS = - "com.android.server.speech.SpeechRecognitionManagerService"; - private static final String WALLPAPER_EFFECTS_GENERATION_MANAGER_SERVICE_CLASS = - "com.android.server.wallpapereffectsgeneration.WallpaperEffectsGenerationManagerService"; - private static final String APP_PREDICTION_MANAGER_SERVICE_CLASS = - "com.android.server.appprediction.AppPredictionManagerService"; - private static final String CONTENT_SUGGESTIONS_SERVICE_CLASS = - "com.android.server.contentsuggestions.ContentSuggestionsManagerService"; - private static final String SEARCH_UI_MANAGER_SERVICE_CLASS = - "com.android.server.searchui.SearchUiManagerService"; - private static final String SMARTSPACE_MANAGER_SERVICE_CLASS = - "com.android.server.smartspace.SmartspaceManagerService"; - private static final String CONTEXTUAL_SEARCH_MANAGER_SERVICE_CLASS = - "com.android.server.contextualsearch.ContextualSearchManagerService"; - private static final String DEVICE_IDLE_CONTROLLER_CLASS = - "com.android.server.DeviceIdleController"; - private static final String BLOB_STORE_MANAGER_SERVICE_CLASS = - "com.android.server.blob.BlobStoreManagerService"; private static final String APPSEARCH_MODULE_LIFECYCLE_CLASS = "com.android.server.appsearch.AppSearchModule$Lifecycle"; private static final String ISOLATED_COMPILATION_SERVICE_CLASS = "com.android.server.compos.IsolatedCompilationService"; - private static final String ROLLBACK_MANAGER_SERVICE_CLASS = - "com.android.server.rollback.RollbackManagerService"; - private static final String ALARM_MANAGER_SERVICE_CLASS = - "com.android.server.alarm.AlarmManagerService"; - private static final String MEDIA_SESSION_SERVICE_CLASS = - "com.android.server.media.MediaSessionService"; - private static final String MEDIA_RESOURCE_MONITOR_SERVICE_CLASS = - "com.android.server.media.MediaResourceMonitorService"; + private static final String CONNECTIVITY_SERVICE_APEX_PATH = + "/apex/com.android.tethering/javalib/service-connectivity.jar"; private static final String CONNECTIVITY_SERVICE_INITIALIZER_CLASS = "com.android.server.ConnectivityServiceInitializer"; private static final String NETWORK_STATS_SERVICE_INITIALIZER_CLASS = "com.android.server.NetworkStatsServiceInitializer"; - private static final String IP_CONNECTIVITY_METRICS_CLASS = - "com.android.server.connectivity.IpConnectivityMetrics"; private static final String MEDIA_COMMUNICATION_SERVICE_CLASS = "com.android.server.media.MediaCommunicationService"; - private static final String APP_COMPAT_OVERRIDES_SERVICE_CLASS = - "com.android.server.compat.overrides.AppCompatOverridesService$Lifecycle"; private static final String HEALTHCONNECT_MANAGER_SERVICE_CLASS = "com.android.server.healthconnect.HealthConnectManagerService"; private static final String ROLE_SERVICE_CLASS = "com.android.role.RoleService"; - private static final String GAME_MANAGER_SERVICE_CLASS = - "com.android.server.app.GameManagerService$Lifecycle"; private static final String ENHANCED_CONFIRMATION_SERVICE_CLASS = "com.android.ecm.EnhancedConfirmationService"; @@ -461,6 +410,7 @@ public final class SystemServer implements Dumpable { + "OnDevicePersonalizationSystemService$Lifecycle"; private static final String UPDATABLE_DEVICE_CONFIG_SERVICE_CLASS = "com.android.server.deviceconfig.DeviceConfigInit$Lifecycle"; + private static final String DEVICE_LOCK_SERVICE_CLASS = "com.android.server.devicelock.DeviceLockService"; private static final String DEVICE_LOCK_APEX_PATH = @@ -1435,7 +1385,7 @@ public final class SystemServer implements Dumpable { // Manages apk rollbacks. t.traceBegin("StartRollbackManagerService"); - mSystemServiceManager.startService(ROLLBACK_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(RollbackManagerService.class); t.traceEnd(); // Tracks native tombstones. @@ -1580,11 +1530,11 @@ public final class SystemServer implements Dumpable { // The AccountManager must come before the ContentService t.traceBegin("StartAccountManagerService"); - mSystemServiceManager.startService(ACCOUNT_SERVICE_CLASS); + mSystemServiceManager.startService(AccountManagerService.Lifecycle.class); t.traceEnd(); t.traceBegin("StartContentService"); - mSystemServiceManager.startService(CONTENT_SERVICE_CLASS); + mSystemServiceManager.startService(ContentService.Lifecycle.class); t.traceEnd(); t.traceBegin("InstallSystemProviders"); @@ -1639,7 +1589,7 @@ public final class SystemServer implements Dumpable { // TODO(aml-jobscheduler): Think about how to do it properly. t.traceBegin("StartAlarmManagerService"); - mSystemServiceManager.startService(ALARM_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(AlarmManagerService.class); t.traceEnd(); t.traceBegin("StartInputManagerService"); @@ -1721,7 +1671,7 @@ public final class SystemServer implements Dumpable { } t.traceBegin("IpConnectivityMetrics"); - mSystemServiceManager.startService(IP_CONNECTIVITY_METRICS_CLASS); + mSystemServiceManager.startService(IpConnectivityMetrics.class); t.traceEnd(); t.traceBegin("NetworkWatchlistService"); @@ -1796,7 +1746,7 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartAccessibilityManagerService"); try { - mSystemServiceManager.startService(ACCESSIBILITY_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(AccessibilityManagerService.Lifecycle.class); } catch (Throwable e) { reportWtf("starting Accessibility Manager", e); } @@ -1819,7 +1769,7 @@ public final class SystemServer implements Dumpable { * NotificationManagerService is dependant on StorageManagerService, * (for media / usb notifications) so we must start StorageManagerService first. */ - mSystemServiceManager.startService(STORAGE_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(StorageManagerService.Lifecycle.class); storageManager = IStorageManager.Stub.asInterface( ServiceManager.getService("mount")); } catch (Throwable e) { @@ -1829,7 +1779,7 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartStorageStatsService"); try { - mSystemServiceManager.startService(STORAGE_STATS_SERVICE_CLASS); + mSystemServiceManager.startService(StorageStatsService.Lifecycle.class); } catch (Throwable e) { reportWtf("starting StorageStatsService", e); } @@ -1860,7 +1810,7 @@ public final class SystemServer implements Dumpable { t.traceEnd(); t.traceBegin("StartAppHibernationService"); - mSystemServiceManager.startService(APP_HIBERNATION_SERVICE_CLASS); + mSystemServiceManager.startService(AppHibernationService.class); t.traceEnd(); t.traceBegin("ArtManagerLocal"); @@ -1892,7 +1842,7 @@ public final class SystemServer implements Dumpable { } else { t.traceBegin("StartLockSettingsService"); try { - mSystemServiceManager.startService(LOCK_SETTINGS_SERVICE_CLASS); + mSystemServiceManager.startService(LockSettingsService.Lifecycle.class); lockSettings = ILockSettings.Stub.asInterface( ServiceManager.getService("lock_settings")); } catch (Throwable e) { @@ -1925,7 +1875,7 @@ public final class SystemServer implements Dumpable { } t.traceBegin("StartDeviceIdleController"); - mSystemServiceManager.startService(DEVICE_IDLE_CONTROLLER_CLASS); + mSystemServiceManager.startService(DeviceIdleController.class); t.traceEnd(); // Always start the Device Policy Manager, so that the API is compatible with @@ -1948,7 +1898,7 @@ public final class SystemServer implements Dumpable { if (deviceHasConfigString(context, R.string.config_defaultMusicRecognitionService)) { t.traceBegin("StartMusicRecognitionManagerService"); - mSystemServiceManager.startService(MUSIC_RECOGNITION_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(MusicRecognitionManagerService.class); t.traceEnd(); } else { Slog.d(TAG, @@ -1966,7 +1916,7 @@ public final class SystemServer implements Dumpable { if (deviceHasConfigString( context, R.string.config_defaultAmbientContextDetectionService)) { t.traceBegin("StartAmbientContextService"); - mSystemServiceManager.startService(AMBIENT_CONTEXT_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(AmbientContextManagerService.class); t.traceEnd(); } else { Slog.d(TAG, "AmbientContextManagerService not defined by OEM or disabled by flag"); @@ -1974,13 +1924,13 @@ public final class SystemServer implements Dumpable { // System Speech Recognition Service t.traceBegin("StartSpeechRecognitionManagerService"); - mSystemServiceManager.startService(SPEECH_RECOGNITION_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(SpeechRecognitionManagerService.class); t.traceEnd(); // App prediction manager service if (deviceHasConfigString(context, R.string.config_defaultAppPredictionService)) { t.traceBegin("StartAppPredictionService"); - mSystemServiceManager.startService(APP_PREDICTION_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(AppPredictionManagerService.class); t.traceEnd(); } else { Slog.d(TAG, "AppPredictionService not defined by OEM"); @@ -1989,7 +1939,7 @@ public final class SystemServer implements Dumpable { // Content suggestions manager service if (deviceHasConfigString(context, R.string.config_defaultContentSuggestionsService)) { t.traceBegin("StartContentSuggestionsService"); - mSystemServiceManager.startService(CONTENT_SUGGESTIONS_SERVICE_CLASS); + mSystemServiceManager.startService(ContentSuggestionsManagerService.class); t.traceEnd(); } else { Slog.d(TAG, "ContentSuggestionsService not defined by OEM"); @@ -1998,14 +1948,14 @@ public final class SystemServer implements Dumpable { // Search UI manager service if (deviceHasConfigString(context, R.string.config_defaultSearchUiService)) { t.traceBegin("StartSearchUiService"); - mSystemServiceManager.startService(SEARCH_UI_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(SearchUiManagerService.class); t.traceEnd(); } // Smartspace manager service if (deviceHasConfigString(context, R.string.config_defaultSmartspaceService)) { t.traceBegin("StartSmartspaceService"); - mSystemServiceManager.startService(SMARTSPACE_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(SmartspaceManagerService.class); t.traceEnd(); } else { Slog.d(TAG, "SmartspaceManagerService not defined by OEM or disabled by flag"); @@ -2015,7 +1965,7 @@ public final class SystemServer implements Dumpable { if (deviceHasConfigString(context, R.string.config_defaultContextualSearchPackageName)) { t.traceBegin("StartContextualSearchService"); - mSystemServiceManager.startService(CONTEXTUAL_SEARCH_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(ContextualSearchManagerService.class); t.traceEnd(); } else { Slog.d(TAG, "ContextualSearchManagerService not defined or disabled by flag"); @@ -2214,7 +2164,7 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartTimeDetectorService"); try { - mSystemServiceManager.startService(TIME_DETECTOR_SERVICE_CLASS); + mSystemServiceManager.startService(TimeDetectorService.Lifecycle.class); } catch (Throwable e) { reportWtf("starting TimeDetectorService service", e); } @@ -2235,7 +2185,7 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartTimeZoneDetectorService"); try { - mSystemServiceManager.startService(TIME_ZONE_DETECTOR_SERVICE_CLASS); + mSystemServiceManager.startService(TimeZoneDetectorService.Lifecycle.class); } catch (Throwable e) { reportWtf("starting TimeZoneDetectorService service", e); } @@ -2251,7 +2201,7 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartLocationTimeZoneManagerService"); try { - mSystemServiceManager.startService(LOCATION_TIME_ZONE_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(LocationTimeZoneManagerService.Lifecycle.class); } catch (Throwable e) { reportWtf("starting LocationTimeZoneManagerService service", e); } @@ -2260,7 +2210,7 @@ public final class SystemServer implements Dumpable { if (context.getResources().getBoolean(R.bool.config_enableGnssTimeUpdateService)) { t.traceBegin("StartGnssTimeUpdateService"); try { - mSystemServiceManager.startService(GNSS_TIME_UPDATE_SERVICE_CLASS); + mSystemServiceManager.startService(GnssTimeUpdateService.Lifecycle.class); } catch (Throwable e) { reportWtf("starting GnssTimeUpdateService service", e); } @@ -2270,7 +2220,7 @@ public final class SystemServer implements Dumpable { if (!isWatch) { t.traceBegin("StartSearchManagerService"); try { - mSystemServiceManager.startService(SEARCH_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(SearchManagerService.Lifecycle.class); } catch (Throwable e) { reportWtf("starting Search Service", e); } @@ -2279,7 +2229,7 @@ public final class SystemServer implements Dumpable { if (context.getResources().getBoolean(R.bool.config_enableWallpaperService)) { t.traceBegin("StartWallpaperManagerService"); - mSystemServiceManager.startService(WALLPAPER_SERVICE_CLASS); + mSystemServiceManager.startService(WallpaperManagerService.Lifecycle.class); t.traceEnd(); } else { Slog.i(TAG, "Wallpaper service disabled by config"); @@ -2289,8 +2239,7 @@ public final class SystemServer implements Dumpable { if (deviceHasConfigString(context, R.string.config_defaultWallpaperEffectsGenerationService)) { t.traceBegin("StartWallpaperEffectsGenerationService"); - mSystemServiceManager.startService( - WALLPAPER_EFFECTS_GENERATION_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(WallpaperEffectsGenerationManagerService.class); t.traceEnd(); } @@ -2345,14 +2294,14 @@ public final class SystemServer implements Dumpable { if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_MIDI)) { // Start MIDI Manager service t.traceBegin("StartMidiManager"); - mSystemServiceManager.startService(MIDI_SERVICE_CLASS); + mSystemServiceManager.startService(MidiService.Lifecycle.class); t.traceEnd(); } // Start ADB Debugging Service t.traceBegin("StartAdbService"); try { - mSystemServiceManager.startService(ADB_SERVICE_CLASS); + mSystemServiceManager.startService(AdbService.Lifecycle.class); } catch (Throwable e) { Slog.e(TAG, "Failure starting AdbService"); } @@ -2364,7 +2313,7 @@ public final class SystemServer implements Dumpable { || Build.IS_EMULATOR) { // Manage USB host and device support t.traceBegin("StartUsbService"); - mSystemServiceManager.startService(USB_SERVICE_CLASS); + mSystemServiceManager.startService(UsbService.Lifecycle.class); t.traceEnd(); } @@ -2396,7 +2345,7 @@ public final class SystemServer implements Dumpable { // TODO(aml-jobscheduler): Think about how to do it properly. t.traceBegin("StartJobScheduler"); - mSystemServiceManager.startService(JOB_SCHEDULER_SERVICE_CLASS); + mSystemServiceManager.startService(JobSchedulerService.class); t.traceEnd(); t.traceBegin("StartSoundTrigger"); @@ -2409,14 +2358,14 @@ public final class SystemServer implements Dumpable { if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_BACKUP)) { t.traceBegin("StartBackupManager"); - mSystemServiceManager.startService(BACKUP_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(BackupManagerService.Lifecycle.class); t.traceEnd(); } if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS) || context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) { t.traceBegin("StartAppWidgetService"); - mSystemServiceManager.startService(APPWIDGET_SERVICE_CLASS); + mSystemServiceManager.startService(AppWidgetService.class); t.traceEnd(); } @@ -2425,7 +2374,7 @@ public final class SystemServer implements Dumpable { // of initializing various settings. It will internally modify its behavior // based on that feature. t.traceBegin("StartVoiceRecognitionManager"); - mSystemServiceManager.startService(VOICE_RECOGNITION_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(VoiceInteractionManagerService.class); t.traceEnd(); if (GestureLauncherService.isGestureLauncherEnabled(context.getResources())) { @@ -2486,7 +2435,7 @@ public final class SystemServer implements Dumpable { } t.traceBegin(START_BLOB_STORE_SERVICE); - mSystemServiceManager.startService(BLOB_STORE_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(BlobStoreManagerService.class); t.traceEnd(); // Dreams (interactive idle-time views, a/k/a screen savers, and doze mode) @@ -2507,7 +2456,7 @@ public final class SystemServer implements Dumpable { if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_PRINTING)) { t.traceBegin("StartPrintManager"); - mSystemServiceManager.startService(PRINT_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(PrintManagerService.class); t.traceEnd(); } @@ -2517,13 +2466,13 @@ public final class SystemServer implements Dumpable { if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)) { t.traceBegin("StartCompanionDeviceManager"); - mSystemServiceManager.startService(COMPANION_DEVICE_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(CompanionDeviceManagerService.class); t.traceEnd(); } if (context.getResources().getBoolean(R.bool.config_enableVirtualDeviceManager)) { t.traceBegin("StartVirtualDeviceManager"); - mSystemServiceManager.startService(VIRTUAL_DEVICE_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(VirtualDeviceManagerService.class); t.traceEnd(); } @@ -2532,7 +2481,7 @@ public final class SystemServer implements Dumpable { t.traceEnd(); t.traceBegin("StartMediaSessionService"); - mSystemServiceManager.startService(MEDIA_SESSION_SERVICE_CLASS); + mSystemServiceManager.startService(MediaSessionService.class); t.traceEnd(); if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_HDMI_CEC)) { @@ -2563,7 +2512,7 @@ public final class SystemServer implements Dumpable { if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { t.traceBegin("StartMediaResourceMonitor"); - mSystemServiceManager.startService(MEDIA_RESOURCE_MONITOR_SERVICE_CLASS); + mSystemServiceManager.startService(MediaResourceMonitorService.class); t.traceEnd(); } @@ -2735,7 +2684,7 @@ public final class SystemServer implements Dumpable { if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_SLICES_DISABLED)) { t.traceBegin("StartSliceManagerService"); - mSystemServiceManager.startService(SLICE_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(SliceManagerService.Lifecycle.class); t.traceEnd(); } @@ -2759,12 +2708,12 @@ public final class SystemServer implements Dumpable { // Statsd pulled atoms t.traceBegin("StartStatsPullAtomService"); - mSystemServiceManager.startService(STATS_PULL_ATOM_SERVICE_CLASS); + mSystemServiceManager.startService(StatsPullAtomService.class); t.traceEnd(); // Log atoms to statsd from bootstrap processes. t.traceBegin("StatsBootstrapAtomService"); - mSystemServiceManager.startService(STATS_BOOTSTRAP_ATOM_SERVICE_LIFECYCLE_CLASS); + mSystemServiceManager.startService(StatsBootstrapAtomService.Lifecycle.class); t.traceEnd(); // Incidentd and dumpstated helper @@ -2811,7 +2760,7 @@ public final class SystemServer implements Dumpable { if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTOFILL)) { t.traceBegin("StartAutoFillService"); - mSystemServiceManager.startService(AUTO_FILL_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(AutofillManagerService.class); t.traceEnd(); } @@ -2820,13 +2769,12 @@ public final class SystemServer implements Dumpable { DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CREDENTIAL, CredentialManager.DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER, true); if (credentialManagerEnabled) { - if(isWatch && - !android.credentials.flags.Flags.wearCredentialManagerEnabled()) { - Slog.d(TAG, "CredentialManager disabled on wear."); + if (isWatch && !android.credentials.flags.Flags.wearCredentialManagerEnabled()) { + Slog.d(TAG, "CredentialManager disabled on wear."); } else { - t.traceBegin("StartCredentialManagerService"); - mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS); - t.traceEnd(); + t.traceBegin("StartCredentialManagerService"); + mSystemServiceManager.startService(CredentialManagerService.class); + t.traceEnd(); } } else { Slog.d(TAG, "CredentialManager disabled."); @@ -2836,7 +2784,7 @@ public final class SystemServer implements Dumpable { // Translation manager service if (deviceHasConfigString(context, R.string.config_defaultTranslationService)) { t.traceBegin("StartTranslationManagerService"); - mSystemServiceManager.startService(TRANSLATION_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(TranslationManagerService.class); t.traceEnd(); } else { Slog.d(TAG, "TranslationService not defined by OEM"); @@ -2980,7 +2928,7 @@ public final class SystemServer implements Dumpable { t.traceEnd(); t.traceBegin("GameManagerService"); - mSystemServiceManager.startService(GAME_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(GameManagerService.Lifecycle.class); t.traceEnd(); if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB)) { @@ -3012,7 +2960,7 @@ public final class SystemServer implements Dumpable { t.traceEnd(); t.traceBegin("AppCompatOverridesService"); - mSystemServiceManager.startService(APP_COMPAT_OVERRIDES_SERVICE_CLASS); + mSystemServiceManager.startService(AppCompatOverridesService.Lifecycle.class); t.traceEnd(); t.traceBegin("HealthConnectManagerService"); @@ -3397,14 +3345,14 @@ public final class SystemServer implements Dumpable { } t.traceBegin("StartSystemCaptionsManagerService"); - mSystemServiceManager.startService(SYSTEM_CAPTIONS_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(SystemCaptionsManagerService.class); t.traceEnd(); } private void startTextToSpeechManagerService(@NonNull Context context, @NonNull TimingsTraceAndSlog t) { t.traceBegin("StartTextToSpeechManagerService"); - mSystemServiceManager.startService(TEXT_TO_SPEECH_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(TextToSpeechManagerService.class); t.traceEnd(); } @@ -3439,7 +3387,7 @@ public final class SystemServer implements Dumpable { } t.traceBegin("StartContentCaptureService"); - mSystemServiceManager.startService(CONTENT_CAPTURE_MANAGER_SERVICE_CLASS); + mSystemServiceManager.startService(ContentCaptureManagerService.class); ContentCaptureManagerInternal ccmi = LocalServices.getService(ContentCaptureManagerInternal.class); diff --git a/services/permission/TEST_MAPPING b/services/permission/TEST_MAPPING index 00bfcd3007a4..4de4a56aa806 100644 --- a/services/permission/TEST_MAPPING +++ b/services/permission/TEST_MAPPING @@ -103,6 +103,28 @@ "include-filter": "android.permission.cts.PermissionMaxSdkVersionTest" } ] + }, + { + "name": "CtsVirtualDevicesAudioTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "include-filter": "android.virtualdevice.cts.audio.VirtualAudioPermissionTest" + } + ] + }, + { + "name": "CtsVirtualDevicesAppLaunchTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "include-filter": "android.virtualdevice.cts.applaunch.VirtualDevicePermissionTest" + } + ] } ], "imports": [ diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt index 36bea7d2eea2..36758341d4d7 100644 --- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt @@ -16,6 +16,7 @@ package com.android.server.permission.access +import android.permission.flags.Flags import android.util.Slog import com.android.modules.utils.BinaryXmlPullParser import com.android.modules.utils.BinaryXmlSerializer @@ -78,6 +79,9 @@ private constructor( setPackageStates(packageStates) setDisabledSystemPackageStates(disabledSystemPackageStates) packageStates.forEach { (_, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } mutateAppIdPackageNames() .mutateOrPut(packageState.appId) { MutableIndexedListSet() } .add(packageState.packageName) @@ -103,6 +107,9 @@ private constructor( newState.mutateUserStatesNoWrite()[userId] = MutableUserState() forEachSchemePolicy { with(it) { onUserAdded(userId) } } newState.externalState.packageStates.forEach { (_, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } upgradePackageVersion(packageState, userId) } } @@ -126,6 +133,9 @@ private constructor( setPackageStates(packageStates) setDisabledSystemPackageStates(disabledSystemPackageStates) packageStates.forEach { (packageName, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } if (packageState.volumeUuid == volumeUuid) { // The APK for a package on a mounted storage volume may still be unavailable // due to APK being deleted, e.g. after an OTA. @@ -151,6 +161,9 @@ private constructor( with(it) { onStorageVolumeMounted(volumeUuid, packageNames, isSystemUpdated) } } packageStates.forEach { (_, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } if (packageState.volumeUuid == volumeUuid) { newState.userStates.forEachIndexed { _, userId, _ -> upgradePackageVersion(packageState, userId) diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 47fd970c760e..63fb468c8f54 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -81,6 +81,9 @@ class AppIdPermissionPolicy : SchemePolicy() { override fun MutateStateScope.onUserAdded(userId: Int) { newState.externalState.packageStates.forEach { (_, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } evaluateAllPermissionStatesForPackageAndUser(packageState, userId, null) } newState.externalState.appIdPackageNames.forEachIndexed { _, appId, _ -> diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index b32c54496002..44ed3df34f24 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -1445,6 +1445,9 @@ class PermissionService(private val service: AccessCheckingService) : val packageStates = packageManagerLocal.withUnfilteredSnapshot().use { it.packageStates } service.mutateState { packageStates.forEach { (packageName, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } val androidPackage = packageState.androidPackage ?: return@forEach androidPackage.requestedPermissions.forEach { permissionName -> updatePermissionFlags( @@ -1598,7 +1601,7 @@ class PermissionService(private val service: AccessCheckingService) : ) { with(policy) { getPermissionFlags(appId, userId, permissionName) } } else { - if (permissionName !in DEVICE_AWARE_PERMISSIONS) { + if (permissionName !in PermissionManager.DEVICE_AWARE_PERMISSIONS) { Slog.i( LOG_TAG, "$permissionName is not device aware permission, " + @@ -1623,7 +1626,7 @@ class PermissionService(private val service: AccessCheckingService) : ) { with(policy) { setPermissionFlags(appId, userId, permissionName, flags) } } else { - if (permissionName !in DEVICE_AWARE_PERMISSIONS) { + if (permissionName !in PermissionManager.DEVICE_AWARE_PERMISSIONS) { Slog.i( LOG_TAG, "$permissionName is not device aware permission, " + @@ -1877,6 +1880,9 @@ class PermissionService(private val service: AccessCheckingService) : packageManagerLocal.withUnfilteredSnapshot().use { snapshot -> service.mutateState { snapshot.packageStates.forEach { (_, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } with(policy) { resetRuntimePermissions(packageState.packageName, userId) } with(devicePolicy) { resetRuntimePermissions(packageState.packageName, userId) } } @@ -1918,8 +1924,11 @@ class PermissionService(private val service: AccessCheckingService) : } packageManagerLocal.withUnfilteredSnapshot().use { snapshot -> - snapshot.packageStates.forEach packageStates@{ (_, packageState) -> - val androidPackage = packageState.androidPackage ?: return@packageStates + snapshot.packageStates.forEach { (_, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } + val androidPackage = packageState.androidPackage ?: return@forEach if (permissionName in androidPackage.requestedPermissions) { packageNames += androidPackage.packageName } @@ -1934,6 +1943,9 @@ class PermissionService(private val service: AccessCheckingService) : val permissions = service.getState { with(policy) { getPermissions() } } packageManagerLocal.withUnfilteredSnapshot().use { snapshot -> snapshot.packageStates.forEach packageStates@{ (_, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@packageStates + } val androidPackage = packageState.androidPackage ?: return@packageStates androidPackage.requestedPermissions.forEach requestedPermissions@{ permissionName -> val permission = permissions[permissionName] ?: return@requestedPermissions @@ -2060,6 +2072,9 @@ class PermissionService(private val service: AccessCheckingService) : val appIdPackageNames = MutableIndexedMap<Int, MutableIndexedSet<String>>() packageStates.forEach { (_, packageState) -> + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return@forEach + } appIdPackageNames .getOrPut(packageState.appId) { MutableIndexedSet() } .add(packageState.packageName) @@ -2313,6 +2328,10 @@ class PermissionService(private val service: AccessCheckingService) : isInstantApp: Boolean, oldPackage: AndroidPackage? ) { + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return + } + synchronized(storageVolumeLock) { // Accumulating the package names here because we want to maintain the same call order // of onPackageAdded() and reuse this order in onStorageVolumeAdded(). We need the @@ -2339,6 +2358,10 @@ class PermissionService(private val service: AccessCheckingService) : params: PermissionManagerServiceInternal.PackageInstalledParams, userId: Int ) { + if (Flags.ignoreApexPermissions() && androidPackage.isApex) { + return + } + if (params === PermissionManagerServiceInternal.PackageInstalledParams.DEFAULT) { // TODO: We should actually stop calling onPackageInstalled() when we are passing // PackageInstalledParams.DEFAULT in InstallPackageHelper, because there's actually no @@ -2391,6 +2414,10 @@ class PermissionService(private val service: AccessCheckingService) : sharedUserPkgs: List<AndroidPackage>, userId: Int ) { + if (Flags.ignoreApexPermissions() && packageState.isApex) { + return + } + val userIds = if (userId == UserHandle.USER_ALL) { userManagerService.userIdsIncludingPreCreated @@ -2820,15 +2847,6 @@ class PermissionService(private val service: AccessCheckingService) : PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM or PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER - /** These permissions are supported for virtual devices. */ - // TODO: b/298661870 - Use new API to get the list of device aware permissions. - val DEVICE_AWARE_PERMISSIONS = - if (Flags.deviceAwarePermissionsEnabled()) { - setOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) - } else { - emptySet<String>() - } - fun getFullerPermission(permissionName: String): String? = FULLER_PERMISSIONS[permissionName] } diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 5a50510082d6..1a03e780521a 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -79,12 +79,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.internal.foldables.FoldGracePeriodProvider; +import com.android.internal.util.test.LocalServiceKeeperRule; +import com.android.server.LocalServices; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.layout.DisplayIdProducer; import com.android.server.display.layout.Layout; +import com.android.server.policy.WindowManagerPolicy; import com.android.server.utils.FoldSettingProvider; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -124,6 +128,9 @@ public class LogicalDisplayMapperTest { private DeviceStateToLayoutMap mDeviceStateToLayoutMapSpy; + @Rule + public LocalServiceKeeperRule mLocalServiceKeeperRule = new LocalServiceKeeperRule(); + @Mock LogicalDisplayMapper.Listener mListenerMock; @Mock Context mContextMock; @Mock FoldSettingProvider mFoldSettingProviderMock; @@ -133,6 +140,7 @@ public class LogicalDisplayMapperTest { @Mock IThermalService mIThermalServiceMock; @Mock DisplayManagerFlags mFlagsMock; @Mock DisplayAdapter mDisplayAdapterMock; + @Mock WindowManagerPolicy mWindowManagerPolicy; @Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor; @Captor ArgumentCaptor<Integer> mDisplayEventCaptor; @@ -143,6 +151,9 @@ public class LogicalDisplayMapperTest { System.setProperty("dexmaker.share_classloader", "true"); MockitoAnnotations.initMocks(this); + mLocalServiceKeeperRule.overrideLocalService(WindowManagerPolicy.class, + mWindowManagerPolicy); + mDeviceStateToLayoutMapSpy = spy(new DeviceStateToLayoutMap(mIdProducer, mFlagsMock, NON_EXISTING_FILE)); mDisplayDeviceRepo = new DisplayDeviceRepository( @@ -194,6 +205,7 @@ public class LogicalDisplayMapperTest { mDisplayDeviceRepo, mListenerMock, new DisplayManagerService.SyncRoot(), mHandler, mDeviceStateToLayoutMapSpy, mFlagsMock); + mLogicalDisplayMapper.onWindowManagerReady(); } @@ -757,6 +769,44 @@ public class LogicalDisplayMapperTest { } @Test + public void testDisplaySwappedAfterDeviceStateChange_windowManagerIsNotified() { + FoldableDisplayDevices foldableDisplayDevices = createFoldableDeviceStateToLayoutMap(); + mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_OPEN); + mLogicalDisplayMapper.onEarlyInteractivityChange(true); + mLogicalDisplayMapper.onBootCompleted(); + advanceTime(1000); + clearInvocations(mWindowManagerPolicy); + + // Switch from 'inner' to 'outer' display (fold a foldable device) + mLogicalDisplayMapper.setDeviceStateLocked(DEVICE_STATE_CLOSED); + // Continue folding device state transition by turning off the inner display + foldableDisplayDevices.mInner.setState(STATE_OFF); + notifyDisplayChanges(foldableDisplayDevices.mOuter); + advanceTime(TIMEOUT_STATE_TRANSITION_MILLIS); + + verify(mWindowManagerPolicy).onDisplaySwitchStart(DEFAULT_DISPLAY); + } + + @Test + public void testCreateNewLogicalDisplay_windowManagerIsNotNotifiedAboutSwitch() { + DisplayDevice device1 = createDisplayDevice(TYPE_EXTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + when(mDeviceStateToLayoutMapSpy.size()).thenReturn(1); + LogicalDisplay display1 = add(device1); + + assertTrue(display1.isEnabledLocked()); + + DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + when(mDeviceStateToLayoutMapSpy.size()).thenReturn(2); + add(device2); + + // As it is not a display switch but adding a new display, we should not notify + // about display switch start to window manager + verify(mWindowManagerPolicy, never()).onDisplaySwitchStart(anyInt()); + } + + @Test public void testDoNotWaitForSleepWhenFoldSettingStayAwake() { // Test device should be marked ready for transition immediately when 'Continue using app // on fold' set to 'Always' diff --git a/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java index c379d6b79ee7..3fd3cef07dd5 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/NormalBrightnessModeControllerTest.java @@ -43,6 +43,11 @@ public class NormalBrightnessModeControllerTest { private final NormalBrightnessModeController mController = new NormalBrightnessModeController(); + // AutoBrightnessController sends ambientLux values *only* when auto brightness enabled. + // NormalBrightnessModeController is temporary disabled if auto brightness is off, + // to avoid capping brightness based on stale ambient lux. Temporary disabling tests with + // auto brightness off and default config pres + // The issue is tracked here: b/322445088 @Keep private static Object[][] brightnessData() { return new Object[][]{ @@ -59,10 +64,10 @@ public class NormalBrightnessModeControllerTest { ImmutableMap.of(99f, 0.1f, 101f, 0.2f) ), 0.2f}, // Auto brightness - off, config only for default - {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of( - BrightnessLimitMapType.DEFAULT, - ImmutableMap.of(99f, 0.1f, 101f, 0.2f) - ), 0.2f}, + // {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of( + // BrightnessLimitMapType.DEFAULT, + // ImmutableMap.of(99f, 0.1f, 101f, 0.2f) + // ), 0.2f}, // Auto brightness - off, config only for adaptive {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of( BrightnessLimitMapType.ADAPTIVE, @@ -81,12 +86,12 @@ public class NormalBrightnessModeControllerTest { ImmutableMap.of(99f, 0.3f, 101f, 0.4f) ), 0.4f}, // Auto brightness - off, config for both - {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of( - BrightnessLimitMapType.DEFAULT, - ImmutableMap.of(99f, 0.1f, 101f, 0.2f), - BrightnessLimitMapType.ADAPTIVE, - ImmutableMap.of(99f, 0.3f, 101f, 0.4f) - ), 0.2f}, + // {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of( + // BrightnessLimitMapType.DEFAULT, + // ImmutableMap.of(99f, 0.1f, 101f, 0.2f), + // BrightnessLimitMapType.ADAPTIVE, + // ImmutableMap.of(99f, 0.3f, 101f, 0.4f) + // ), 0.2f}, // Auto brightness - on, config for both, ambient high {1000, AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, ImmutableMap.of( BrightnessLimitMapType.DEFAULT, diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java index 87fc7a484c5c..39ffe5be5882 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java @@ -33,6 +33,7 @@ import android.os.PowerManager; import androidx.test.filters.SmallTest; +import com.android.server.display.AutomaticBrightnessController; import com.android.server.display.config.HdrBrightnessData; import com.android.server.testutils.OffsettableClock; import com.android.server.testutils.TestHandler; @@ -230,6 +231,11 @@ public class HdrClamperTest { } private void configureClamper() { + // AutoBrightnessController sends ambientLux values *only* when auto brightness enabled. + // HdrClamper is temporary disabled if auto brightness is off. + // Temporary setting AutoBrightnessState to enabled for this test + // The issue is tracked here: b/322445088 + mHdrClamper.setAutoBrightnessState(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED); mHdrClamper.resetHdrConfig(TEST_HDR_DATA, WIDTH, HEIGHT, MIN_HDR_PERCENT, mMockBinder); mHdrChangeListener.onHdrVisible(true); } diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java index a918be2292af..8bdfc500fdc2 100644 --- a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java +++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioManagerRouteControllerTest.java @@ -69,6 +69,13 @@ import java.util.Set; public class AudioManagerRouteControllerTest { private static final String FAKE_ROUTE_NAME = "fake name"; + + /** + * The number of milliseconds to wait for an asynchronous operation before failing an associated + * assertion. + */ + private static final int ASYNC_CALL_TIMEOUTS_MS = 1000; + private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER = createAudioDeviceInfo( AudioSystem.DEVICE_OUT_SPEAKER, "name_builtin", /* address= */ null); @@ -231,7 +238,7 @@ public class AudioManagerRouteControllerTest { MediaRoute2Info builtInSpeakerRoute = getAvailableRouteWithType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); mControllerUnderTest.transferTo(builtInSpeakerRoute.getId()); - verify(mMockAudioManager) + verify(mMockAudioManager, Mockito.timeout(ASYNC_CALL_TIMEOUTS_MS)) .setPreferredDeviceForStrategy( mMediaAudioProductStrategy, createAudioDeviceAttribute(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)); @@ -239,7 +246,7 @@ public class AudioManagerRouteControllerTest { MediaRoute2Info wiredHeadsetRoute = getAvailableRouteWithType(MediaRoute2Info.TYPE_WIRED_HEADSET); mControllerUnderTest.transferTo(wiredHeadsetRoute.getId()); - verify(mMockAudioManager) + verify(mMockAudioManager, Mockito.timeout(ASYNC_CALL_TIMEOUTS_MS)) .setPreferredDeviceForStrategy( mMediaAudioProductStrategy, createAudioDeviceAttribute(AudioDeviceInfo.TYPE_WIRED_HEADSET)); diff --git a/services/tests/mockingservicestests/src/com/android/server/OWNERS b/services/tests/mockingservicestests/src/com/android/server/OWNERS index f80156021408..0eb8639bd005 100644 --- a/services/tests/mockingservicestests/src/com/android/server/OWNERS +++ b/services/tests/mockingservicestests/src/com/android/server/OWNERS @@ -1,5 +1,5 @@ per-file *Alarm* = file:/apex/jobscheduler/OWNERS per-file *AppStateTracker* = file:/apex/jobscheduler/OWNERS per-file *DeviceIdleController* = file:/apex/jobscheduler/OWNERS -per-file SensitiveContentProtectionManagerServiceTest.java = file:/core/java/android/permission/OWNERS +per-file SensitiveContentProtectionManagerService* = file:/core/java/android/permission/OWNERS per-file RescuePartyTest.java = file:/packages/CrashRecovery/OWNERS diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java index 682569f1d9ab..697548cbe2b4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java @@ -1111,16 +1111,9 @@ public class RescuePartyTest { // mock properties in BootThreshold 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( + 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/SensitiveContentProtectionManagerServiceNotificationTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java index 506514469338..edee8cd217d0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java @@ -25,7 +25,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -34,6 +33,7 @@ import static org.mockito.Mockito.when; import android.content.pm.PackageManagerInternal; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; +import android.os.Process; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -79,7 +79,8 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { private static final String NOTIFICATION_PKG_1 = "com.android.server.notification.one"; private static final String NOTIFICATION_PKG_2 = "com.android.server.notification.two"; - private static final String EXEMPTED_SCREEN_RECORDER_PACKAGE = "test.screen.recorder.package"; + private static final String SCREEN_RECORDER_PACKAGE = "test.screen.recorder.package"; + private static final String EXEMPTED_SCREEN_RECORDER_PACKAGE = "exempt.screen.recorder.package"; private static final int NOTIFICATION_UID_1 = 5; private static final int NOTIFICATION_UID_2 = 6; @@ -281,10 +282,18 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { .getActiveNotifications(); } + private MediaProjectionInfo createMediaProjectionInfo() { + return new MediaProjectionInfo(SCREEN_RECORDER_PACKAGE, Process.myUserHandle(), null); + } + + private MediaProjectionInfo createExemptMediaProjectionInfo() { + return new MediaProjectionInfo( + EXEMPTED_SCREEN_RECORDER_PACKAGE, Process.myUserHandle(), null); + } + @Test public void mediaProjectionOnStart_verifyExemptedRecorderPackage() { - MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class); - when(mediaProjectionInfo.getPackageName()).thenReturn(EXEMPTED_SCREEN_RECORDER_PACKAGE); + MediaProjectionInfo mediaProjectionInfo = createExemptMediaProjectionInfo(); mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); @@ -295,7 +304,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() { ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @@ -304,18 +313,18 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { public void mediaProjectionOnStart_noSensitiveNotifications_noBlockedPackages() { setupNoSensitiveNotifications(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test public void mediaProjectionOnStart_noNotifications_noBlockedPackages() { setupNoNotifications(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -323,7 +332,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromSamePackageAndUid(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @@ -333,7 +342,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromDifferentPackage(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @@ -343,7 +352,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromDifferentUid(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @@ -352,7 +361,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { public void mediaProjectionOnStop_onProjectionEnd_clearWmBlockedPackages() { setupSensitiveNotification(); - MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class); + MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo(); mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); Mockito.reset(mWindowManager); @@ -365,7 +374,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { public void mediaProjectionOnStart_afterOnStop_onProjectionStart_setWmBlockedPackages() { ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); - MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class); + MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo(); mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo); Mockito.reset(mWindowManager); @@ -381,9 +390,9 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { .when(mSensitiveContentProtectionManagerService.mNotificationListener) .getActiveNotifications(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -392,9 +401,9 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { .when(mSensitiveContentProtectionManagerService.mNotificationListener) .getCurrentRanking(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -403,9 +412,9 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { .when(mSensitiveContentProtectionManagerService.mNotificationListener) .getCurrentRanking(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -416,9 +425,9 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { .when(mSensitiveContentProtectionManagerService.mNotificationListener) .getCurrentRanking(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -426,7 +435,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mockDisabledViaDevelopOption(); setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); verifyZeroInteractions(mWindowManager); } @@ -447,8 +456,9 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo(); + mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); + mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); @@ -461,7 +471,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); @@ -472,23 +482,23 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { @Test public void nlsOnListenerConnected_noSensitiveNotifications_noBlockedPackages() { setupNoSensitiveNotifications(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test public void nlsOnListenerConnected_noNotifications_noBlockedPackages() { setupNoNotifications(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -496,7 +506,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); doReturn(null) .when(mSensitiveContentProtectionManagerService.mNotificationListener) @@ -504,7 +514,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -512,7 +522,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); doReturn(mRankingMap) @@ -521,7 +531,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -530,7 +540,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); verifyZeroInteractions(mWindowManager); @@ -553,8 +563,9 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo(); + mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); + mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener @@ -568,7 +579,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener @@ -580,25 +591,25 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { @Test public void nlsOnNotificationRankingUpdate_noSensitiveNotifications_noBlockedPackages() { setupNoSensitiveNotifications(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener .onNotificationRankingUpdate(mRankingMap); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test public void nlsOnNotificationRankingUpdate_noNotifications_noBlockedPackages() { setupNoNotifications(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener .onNotificationRankingUpdate(mRankingMap); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -606,13 +617,13 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener .onNotificationRankingUpdate(null); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -620,7 +631,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); doReturn(mRankingMap) @@ -630,7 +641,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mSensitiveContentProtectionManagerService.mNotificationListener .onNotificationRankingUpdate(mRankingMap); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -638,7 +649,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); doThrow(SecurityException.class) @@ -648,7 +659,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mSensitiveContentProtectionManagerService.mNotificationListener .onNotificationRankingUpdate(mRankingMap); - verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + verifyZeroInteractions(mWindowManager); } @Test @@ -657,7 +668,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); mSensitiveContentProtectionManagerService.mNotificationListener .onNotificationRankingUpdate(mRankingMap); @@ -681,8 +692,9 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + MediaProjectionInfo mediaProjectionInfo = createMediaProjectionInfo(); + mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); + mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener @@ -696,7 +708,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener @@ -712,7 +724,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener @@ -726,7 +738,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener @@ -740,7 +752,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); mSensitiveContentProtectionManagerService.mNotificationListener @@ -754,7 +766,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); Mockito.reset(mWindowManager); when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); @@ -770,7 +782,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 // as non-sensitive setupSensitiveNotification(); - mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); mSensitiveContentProtectionManagerService.mNotificationListener .onNotificationPosted(mNotification1, mRankingMap); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index a7430e563904..419bcb8650a7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -38,6 +38,7 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.am.ActivityManagerInternalTest.CustomThread; import static com.android.server.am.ActivityManagerService.Injector; @@ -692,6 +693,31 @@ public class ActivityManagerServiceTest { assertEquals(uid, -1); } + @SuppressWarnings("GuardedBy") + @Test + public void testFifoSwitch() { + addUidRecord(TEST_UID, TEST_PACKAGE); + final ProcessRecord fifoProc = mAms.getProcessRecordLocked(TEST_PACKAGE, TEST_UID); + final var wpc = fifoProc.getWindowProcessController(); + spyOn(wpc); + doReturn(true).when(wpc).useFifoUiScheduling(); + fifoProc.makeActive(fifoProc.getThread(), mAms.mProcessStats); + assertTrue(fifoProc.useFifoUiScheduling()); + assertTrue(mAms.mSpecifiedFifoProcesses.contains(fifoProc)); + + // If there is a request to use more CPU resource (e.g. camera), the current fifo process + // should switch the capability of using fifo. + final UidRecord uidRecord = addUidRecord(TEST_UID + 1, TEST_PACKAGE + 1); + uidRecord.setCurProcState(PROCESS_STATE_TOP); + mAms.adjustFifoProcessesIfNeeded(uidRecord.getUid(), false /* allowSpecifiedFifo */); + assertFalse(fifoProc.useFifoUiScheduling()); + mAms.adjustFifoProcessesIfNeeded(uidRecord.getUid(), true /* allowSpecifiedFifo */); + assertTrue(fifoProc.useFifoUiScheduling()); + + fifoProc.makeInactive(mAms.mProcessStats); + assertFalse(mAms.mSpecifiedFifoProcesses.contains(fifoProc)); + } + @Test public void testGlobalIsolatedUidAllocator() { final IsolatedUidRange globalUidRange = mAms.mProcessList.mGlobalIsolatedUids; diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java index b41568298dbc..0532e04257d4 100644 --- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java @@ -55,6 +55,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -63,8 +64,7 @@ import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; -import android.security.Authorization; -import android.security.authorization.IKeystoreAuthorization; +import android.security.KeyStoreAuthorization; import android.service.trust.TrustAgentService; import android.testing.TestableContext; import android.view.IWindowManager; @@ -96,7 +96,6 @@ public class TrustManagerServiceTest { @Rule public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) .spyStatic(ActivityManager.class) - .spyStatic(Authorization.class) .mockStatic(ServiceManager.class) .mockStatic(WindowManagerGlobal.class) .build(); @@ -126,14 +125,13 @@ public class TrustManagerServiceTest { private @Mock DevicePolicyManager mDevicePolicyManager; private @Mock FaceManager mFaceManager; private @Mock FingerprintManager mFingerprintManager; - private @Mock IKeystoreAuthorization mKeystoreAuthorization; + private @Mock KeyStoreAuthorization mKeyStoreAuthorization; private @Mock LockPatternUtils mLockPatternUtils; private @Mock PackageManager mPackageManager; private @Mock UserManager mUserManager; private @Mock IWindowManager mWindowManager; private HandlerThread mHandlerThread; - private TrustManagerService.Injector mInjector; private TrustManagerService mService; private ITrustManager mTrustManager; @@ -145,8 +143,6 @@ public class TrustManagerServiceTest { when(mFaceManager.getSensorProperties()).thenReturn(List.of()); when(mFingerprintManager.getSensorProperties()).thenReturn(List.of()); - doReturn(mKeystoreAuthorization).when(() -> Authorization.getService()); - when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager); when(mLockPatternUtils.isSecure(TEST_USER_ID)).thenReturn(true); when(mLockPatternUtils.getKnownTrustAgents(TEST_USER_ID)).thenReturn(mKnownTrustAgents); @@ -193,8 +189,7 @@ public class TrustManagerServiceTest { mHandlerThread = new HandlerThread("handler"); mHandlerThread.start(); - mInjector = new TrustManagerService.Injector(mLockPatternUtils, mHandlerThread.getLooper()); - mService = new TrustManagerService(mMockContext, mInjector); + mService = new TrustManagerService(mMockContext, new MockInjector(mMockContext)); // Get the ITrustManager from the new TrustManagerService. mService.onStart(); @@ -204,6 +199,27 @@ public class TrustManagerServiceTest { mTrustManager = ITrustManager.Stub.asInterface(binderArgumentCaptor.getValue()); } + private class MockInjector extends TrustManagerService.Injector { + MockInjector(Context context) { + super(context); + } + + @Override + LockPatternUtils getLockPatternUtils() { + return mLockPatternUtils; + } + + @Override + KeyStoreAuthorization getKeyStoreAuthorization() { + return mKeyStoreAuthorization; + } + + @Override + Looper getLooper() { + return mHandlerThread.getLooper(); + } + } + @After public void tearDown() { LocalServices.removeServiceForTest(SystemServiceManager.class); @@ -371,14 +387,14 @@ public class TrustManagerServiceTest { when(mWindowManager.isKeyguardLocked()).thenReturn(false); mTrustManager.reportKeyguardShowingChanged(); - verify(mKeystoreAuthorization).onDeviceUnlocked(PARENT_USER_ID, null); - verify(mKeystoreAuthorization).onDeviceUnlocked(PROFILE_USER_ID, null); + verify(mKeyStoreAuthorization).onDeviceUnlocked(PARENT_USER_ID, null); + verify(mKeyStoreAuthorization).onDeviceUnlocked(PROFILE_USER_ID, null); when(mWindowManager.isKeyguardLocked()).thenReturn(true); mTrustManager.reportKeyguardShowingChanged(); - verify(mKeystoreAuthorization) + verify(mKeyStoreAuthorization) .onDeviceLocked(eq(PARENT_USER_ID), eq(PARENT_BIOMETRIC_SIDS), eq(false)); - verify(mKeystoreAuthorization) + verify(mKeyStoreAuthorization) .onDeviceLocked(eq(PROFILE_USER_ID), eq(PARENT_BIOMETRIC_SIDS), eq(false)); } @@ -392,10 +408,10 @@ public class TrustManagerServiceTest { setupMocksForProfile(/* unifiedChallenge= */ false); mTrustManager.setDeviceLockedForUser(PROFILE_USER_ID, false); - verify(mKeystoreAuthorization).onDeviceUnlocked(PROFILE_USER_ID, null); + verify(mKeyStoreAuthorization).onDeviceUnlocked(PROFILE_USER_ID, null); mTrustManager.setDeviceLockedForUser(PROFILE_USER_ID, true); - verify(mKeystoreAuthorization) + verify(mKeyStoreAuthorization) .onDeviceLocked(eq(PROFILE_USER_ID), eq(PROFILE_BIOMETRIC_SIDS), eq(false)); } @@ -572,11 +588,11 @@ public class TrustManagerServiceTest { private void verifyWeakUnlockValue(boolean expectedWeakUnlockEnabled) throws Exception { when(mWindowManager.isKeyguardLocked()).thenReturn(false); mTrustManager.reportKeyguardShowingChanged(); - verify(mKeystoreAuthorization).onDeviceUnlocked(TEST_USER_ID, null); + verify(mKeyStoreAuthorization).onDeviceUnlocked(TEST_USER_ID, null); when(mWindowManager.isKeyguardLocked()).thenReturn(true); mTrustManager.reportKeyguardShowingChanged(); - verify(mKeystoreAuthorization).onDeviceLocked(eq(TEST_USER_ID), any(), + verify(mKeyStoreAuthorization).onDeviceLocked(eq(TEST_USER_ID), any(), eq(expectedWeakUnlockEnabled)); } diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java index 7ecc7fd1b94b..29f3720a1828 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java @@ -235,12 +235,11 @@ public class WallpaperCropperTest { int expectedWidth = (int) (displaySize.x * (1 + WallpaperCropper.MAX_PARALLAX)); Point expectedCropSize = new Point(expectedWidth, 1000); for (int mode: ALL_MODES) { - assertThat(WallpaperCropper.getAdjustedCrop( - crop, bitmapSize, displaySize, true, false, mode)) - .isEqualTo(leftOf(crop, expectedCropSize)); - assertThat(WallpaperCropper.getAdjustedCrop( - crop, bitmapSize, displaySize, true, true, mode)) - .isEqualTo(rightOf(crop, expectedCropSize)); + for (boolean rtl: List.of(false, true)) { + assertThat(WallpaperCropper.getAdjustedCrop( + crop, bitmapSize, displaySize, true, rtl, mode)) + .isEqualTo(centerOf(crop, expectedCropSize)); + } } } @@ -362,11 +361,13 @@ public class WallpaperCropperTest { } /** - * Test that {@link WallpaperCropper#getCrop} follows a simple centre-align strategy when - * no suggested crops are provided. + * Test that {@link WallpaperCropper#getCrop} uses the full image when no crops are provided. + * If the image has more width/height ratio than the screen, keep that width for parallax up + * to {@link WallpaperCropper#MAX_PARALLAX}. If the crop has less width/height ratio, remove the + * surplus height, on both sides to keep the wallpaper centered. */ @Test - public void testGetCrop_noSuggestedCrops_centersWallpaper() { + public void testGetCrop_noSuggestedCrops() { setUpWithDisplays(STANDARD_DISPLAY); Point bitmapSize = new Point(800, 1000); Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y); @@ -374,9 +375,11 @@ public class WallpaperCropperTest { List<Point> displaySizes = List.of( new Point(500, 1000), + new Point(200, 1000), new Point(1000, 500)); List<Point> expectedCropSizes = List.of( - new Point(500, 1000), + new Point(Math.min(800, (int) (500 * (1 + WallpaperCropper.MAX_PARALLAX))), 1000), + new Point(Math.min(800, (int) (200 * (1 + WallpaperCropper.MAX_PARALLAX))), 1000), new Point(800, 400)); for (int i = 0; i < displaySizes.size(); i++) { @@ -450,7 +453,8 @@ public class WallpaperCropperTest { /** * Test that {@link WallpaperCropper#getCrop}, when asked for a folded crop with a suggested * crop only for the relative unfolded orientation, creates the folded crop at the center of the - * unfolded crop, by removing content on two sides to match the folded screen dimensions. + * unfolded crop, by removing content on two sides to match the folded screen dimensions, and + * then adds some width for parallax. * <p> * To simplify, in this test case all crops have the same size as the display (no zoom) * and are at the center of the image. @@ -468,6 +472,7 @@ public class WallpaperCropperTest { int unfoldedTwo = getRotatedOrientation(unfoldedOne); Rect unfoldedCropOne = centerOf(bitmapRect, mDisplaySizes.get(unfoldedOne)); Rect unfoldedCropTwo = centerOf(bitmapRect, mDisplaySizes.get(unfoldedTwo)); + List<Rect> unfoldedCrops = List.of(unfoldedCropOne, unfoldedCropTwo); SparseArray<Rect> suggestedCrops = new SparseArray<>(); suggestedCrops.put(unfoldedOne, unfoldedCropOne); suggestedCrops.put(unfoldedTwo, unfoldedCropTwo); @@ -476,15 +481,28 @@ public class WallpaperCropperTest { int foldedTwo = getFoldedOrientation(unfoldedTwo); Point foldedDisplayOne = mDisplaySizes.get(foldedOne); Point foldedDisplayTwo = mDisplaySizes.get(foldedTwo); + List<Point> foldedDisplays = List.of(foldedDisplayOne, foldedDisplayTwo); for (boolean rtl : List.of(false, true)) { - assertThat(mWallpaperCropper.getCrop( - foldedDisplayOne, bitmapSize, suggestedCrops, rtl)) - .isEqualTo(centerOf(unfoldedCropOne, foldedDisplayOne)); - - assertThat(mWallpaperCropper.getCrop( - foldedDisplayTwo, bitmapSize, suggestedCrops, rtl)) - .isEqualTo(centerOf(unfoldedCropTwo, foldedDisplayTwo)); + for (int i = 0; i < 2; i++) { + Rect unfoldedCrop = unfoldedCrops.get(i); + Point foldedDisplay = foldedDisplays.get(i); + Rect expectedCrop = centerOf(unfoldedCrop, foldedDisplay); + int maxParallax = (int) (WallpaperCropper.MAX_PARALLAX * unfoldedCrop.width()); + + // the expected behaviour is that we add width for parallax until we reach + // either MAX_PARALLAX or the edge of the crop for the unfolded screen. + if (rtl) { + expectedCrop.left = Math.max( + unfoldedCrop.left, expectedCrop.left - maxParallax); + } else { + expectedCrop.right = Math.min( + unfoldedCrop.right, unfoldedCrop.right + maxParallax); + } + assertThat(mWallpaperCropper.getCrop( + foldedDisplay, bitmapSize, suggestedCrops, rtl)) + .isEqualTo(expectedCrop); + } } } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java index 05d8a005d21e..c4561b16d73c 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java @@ -87,7 +87,7 @@ public class BatteryStatsUserLifecycleTests { final boolean[] userStopped = new boolean[1]; CountDownLatch stopUserLatch = new CountDownLatch(1); - mIam.stopUser(mTestUserId, true, new IStopUserCallback.Stub() { + mIam.stopUserWithCallback(mTestUserId, new IStopUserCallback.Stub() { @Override public void userStopped(int userId) throws RemoteException { userStopped[0] = true; diff --git a/services/tests/selinux/AndroidTest.xml b/services/tests/selinux/AndroidTest.xml new file mode 100644 index 000000000000..16d8e0709345 --- /dev/null +++ b/services/tests/selinux/AndroidTest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs Selinux Frameworks Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="SelinuxFrameworksTests.apk" /> + </target_preparer> + + <option name="test-tag" value="SelinuxFrameworksTests" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.selinuxtests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java index e0a99b016da9..e18909879934 100644 --- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java @@ -3156,6 +3156,39 @@ public class AccountManagerServiceTest extends AndroidTestCase { } @SmallTest + public void testAccountRemovedBroadcastMarkedAccountAsVisibleTwice() throws Exception { + unlockSystemUser(); + + HashMap<String, Integer> visibility = new HashMap<>(); + visibility.put("testpackage1", AccountManager.VISIBILITY_USER_MANAGED_VISIBLE); + + addAccountRemovedReceiver("testpackage1"); + mAms.registerAccountListener( + new String [] {AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1}, + "testpackage1"); + mAms.addAccountExplicitlyWithVisibility( + AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS, + /* password= */ "p11", + /* extras= */ null, + visibility, + /* callerPackage= */ null); + + updateBroadcastCounters(2); + assertEquals(mVisibleAccountsChangedBroadcasts, 1); + assertEquals(mLoginAccountsChangedBroadcasts, 1); + assertEquals(mAccountRemovedBroadcasts, 0); + + mAms.setAccountVisibility(AccountManagerServiceTestFixtures.ACCOUNT_SUCCESS, + "testpackage1", + AccountManager.VISIBILITY_VISIBLE); + + updateBroadcastCounters(3); + assertEquals(mVisibleAccountsChangedBroadcasts, 1); // visibility was not changed + assertEquals(mLoginAccountsChangedBroadcasts, 2); + assertEquals(mAccountRemovedBroadcasts, 0); + } + + @SmallTest public void testRegisterAccountListenerCredentialsUpdate() throws Exception { unlockSystemUser(); mAms.registerAccountListener( @@ -3493,6 +3526,12 @@ public class AccountManagerServiceTest extends AndroidTestCase { } @Override + public boolean bindServiceAsUser(Intent service, ServiceConnection conn, + Context.BindServiceFlags flags, UserHandle user) { + return mTestContext.bindServiceAsUser(service, conn, flags, user); + } + + @Override public void unbindService(ServiceConnection conn) { mTestContext.unbindService(conn); } diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 0a2a855e9317..7c0dbf4889da 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -888,7 +888,7 @@ public class UserControllerTest { int userId = -1; assertThrows(IllegalArgumentException.class, - () -> mUserController.stopUser(userId, /* force= */ true, + () -> mUserController.stopUser(userId, /* allowDelayedLocking= */ true, /* stopUserCallback= */ null, /* keyEvictedCallback= */ null)); } @@ -897,7 +897,7 @@ public class UserControllerTest { public void testStopUser_systemUser() { int userId = UserHandle.USER_SYSTEM; - int r = mUserController.stopUser(userId, /* force= */ true, + int r = mUserController.stopUser(userId, /* allowDelayedLocking= */ true, /* stopUserCallback= */ null, /* keyEvictedCallback= */ null); @@ -909,7 +909,7 @@ public class UserControllerTest { setUpUser(TEST_USER_ID1, /* flags= */ 0); mUserController.startUser(TEST_USER_ID1, USER_START_MODE_FOREGROUND); - int r = mUserController.stopUser(TEST_USER_ID1, /* force= */ true, + int r = mUserController.stopUser(TEST_USER_ID1, /* allowDelayedLocking= */ true, /* stopUserCallback= */ null, /* keyEvictedCallback= */ null); @@ -1338,7 +1338,7 @@ public class UserControllerTest { private void assertUserLockedOrUnlockedAfterStopping(int userId, boolean allowDelayedLocking, KeyEvictedCallback keyEvictedCallback, boolean expectLocking) throws Exception { - int r = mUserController.stopUser(userId, /* force= */ true, /* allowDelayedLocking= */ + int r = mUserController.stopUser(userId, /* allowDelayedLocking= */ allowDelayedLocking, null, keyEvictedCallback); assertThat(r).isEqualTo(ActivityManager.USER_OP_SUCCESS); assertUserLockedOrUnlockedState(userId, allowDelayedLocking, expectLocking); diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java index f1c1dc365b90..59f4d56b44f1 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; @@ -315,7 +316,7 @@ public class AudioDeviceBrokerTest { mFakeBtDevice.getAddress())); verify(mMockAudioService, timeout(MAX_MESSAGE_HANDLING_DELAY_MS).times(0)).onUpdatedAdiDeviceState( - eq(devState)); + eq(devState), anyBoolean()); } // metadata set @@ -326,7 +327,7 @@ public class AudioDeviceBrokerTest { mFakeBtDevice.getAddress())); verify(mMockAudioService, timeout(MAX_MESSAGE_HANDLING_DELAY_MS)).onUpdatedAdiDeviceState( - any()); + any(), anyBoolean()); } } finally { // reset the metadata device type @@ -354,7 +355,7 @@ public class AudioDeviceBrokerTest { verify(mMockAudioService, timeout(MAX_MESSAGE_HANDLING_DELAY_MS).atLeast(1)).onUpdatedAdiDeviceState( ArgumentMatchers.argThat(devState -> devState.getAudioDeviceCategory() - == AudioManager.AUDIO_DEVICE_CATEGORY_OTHER)); + == AudioManager.AUDIO_DEVICE_CATEGORY_OTHER), anyBoolean()); } private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 74eb79d7554c..34092b6855b1 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -68,7 +68,7 @@ import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; -import android.security.KeyStore; +import android.security.KeyStoreAuthorization; import androidx.test.filters.SmallTest; @@ -105,7 +105,7 @@ public class AuthSessionTest { @Mock private IBiometricServiceReceiver mClientReceiver; @Mock private IStatusBarService mStatusBarService; @Mock private IBiometricSysuiReceiver mSysuiReceiver; - @Mock private KeyStore mKeyStore; + @Mock private KeyStoreAuthorization mKeyStoreAuthorization; @Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver; @Mock private BiometricFrameworkStatsLogger mBiometricFrameworkStatsLogger; @Mock private BiometricCameraManager mBiometricCameraManager; @@ -665,9 +665,10 @@ public class AuthSessionTest { final PreAuthInfo preAuthInfo = createPreAuthInfo(sensors, userId, promptInfo, checkDevicePolicyManager); return new AuthSession(mContext, mBiometricContext, mStatusBarService, mSysuiReceiver, - mKeyStore, mRandom, mClientDeathReceiver, preAuthInfo, mToken, requestId, - operationId, userId, mSensorReceiver, mClientReceiver, TEST_PACKAGE, promptInfo, - false /* debugEnabled */, mFingerprintSensorProps, mBiometricFrameworkStatsLogger); + mKeyStoreAuthorization, mRandom, mClientDeathReceiver, preAuthInfo, mToken, + requestId, operationId, userId, mSensorReceiver, mClientReceiver, TEST_PACKAGE, + promptInfo, false /* debugEnabled */, mFingerprintSensorProps, + mBiometricFrameworkStatsLogger); } private PromptInfo createPromptInfo(@Authenticators.Types int authenticators) { 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 a852677c2ed1..5fd29c2b0cb9 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -82,8 +82,7 @@ import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.security.GateKeeper; -import android.security.KeyStore; -import android.security.authorization.IKeystoreAuthorization; +import android.security.KeyStoreAuthorization; import android.service.gatekeeper.IGateKeeperService; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -182,7 +181,7 @@ public class BiometricServiceTest { private BiometricHandlerProvider mBiometricHandlerProvider; @Mock - private IKeystoreAuthorization mKeystoreAuthService; + private KeyStoreAuthorization mKeyStoreAuthorization; @Mock private IGateKeeperService mGateKeeperService; @@ -207,7 +206,7 @@ public class BiometricServiceTest { when(mInjector.getStatusBarService()).thenReturn(mock(IStatusBarService.class)); when(mInjector.getSettingObserver(any(), any(), any())) .thenReturn(mock(BiometricService.SettingObserver.class)); - when(mInjector.getKeyStore()).thenReturn(mock(KeyStore.class)); + when(mInjector.getKeyStoreAuthorization()).thenReturn(mock(KeyStoreAuthorization.class)); when(mInjector.isDebugEnabled(any(), anyInt())).thenReturn(false); when(mInjector.getBiometricStrengthController(any())) .thenReturn(mock(BiometricStrengthController.class)); @@ -243,7 +242,7 @@ public class BiometricServiceTest { mStatusBarService, null /* handler */, mAuthSessionCoordinator); when(mInjector.getBiometricContext(any())).thenReturn(mBiometricContextProvider); - when(mInjector.getKeystoreAuthorizationService()).thenReturn(mKeystoreAuthService); + when(mInjector.getKeyStoreAuthorization()).thenReturn(mKeyStoreAuthorization); when(mInjector.getGateKeeperService()).thenReturn(mGateKeeperService); when(mInjector.getNotificationLogger()).thenReturn(mNotificationLogger); when(mGateKeeperService.getSecureUserId(anyInt())).thenReturn(42L); @@ -682,9 +681,9 @@ public class BiometricServiceTest { waitForIdle(); // HAT sent to keystore if (isStrongBiometric) { - verify(mBiometricService.mKeyStore).addAuthToken(AdditionalMatchers.aryEq(HAT)); + verify(mKeyStoreAuthorization).addAuthToken(AdditionalMatchers.aryEq(HAT)); } else { - verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); + verify(mKeyStoreAuthorization, never()).addAuthToken(any(byte[].class)); } // Send onAuthenticated to client verify(mReceiver1).onAuthenticationSucceeded( @@ -747,7 +746,7 @@ public class BiometricServiceTest { waitForIdle(); // Waiting for SystemUI to send confirmation callback assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mAuthSession.getState()); - verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); + verify(mKeyStoreAuthorization, never()).addAuthToken(any(byte[].class)); // SystemUI sends confirm, HAT is sent to keystore and client is notified. mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( @@ -755,9 +754,9 @@ public class BiometricServiceTest { null /* credentialAttestation */); waitForIdle(); if (isStrongBiometric) { - verify(mBiometricService.mKeyStore).addAuthToken(AdditionalMatchers.aryEq(HAT)); + verify(mKeyStoreAuthorization).addAuthToken(AdditionalMatchers.aryEq(HAT)); } else { - verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); + verify(mKeyStoreAuthorization, never()).addAuthToken(any(byte[].class)); } verify(mReceiver1).onAuthenticationSucceeded( BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC); @@ -1313,7 +1312,7 @@ public class BiometricServiceTest { eq(TYPE_FACE), eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(0 /* vendorCode */)); - verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); + verify(mKeyStoreAuthorization, never()).addAuthToken(any(byte[].class)); assertNull(mBiometricService.mAuthSession); } @@ -1839,7 +1838,7 @@ public class BiometricServiceTest { final long expectedResult = 31337L; - when(mKeystoreAuthService.getLastAuthTime(eq(secureUserId), eq(hardwareAuthenticators))) + when(mKeyStoreAuthorization.getLastAuthTime(eq(secureUserId), eq(hardwareAuthenticators))) .thenReturn(expectedResult); mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider); @@ -1848,7 +1847,8 @@ public class BiometricServiceTest { Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL); assertEquals(expectedResult, result); - verify(mKeystoreAuthService).getLastAuthTime(eq(secureUserId), eq(hardwareAuthenticators)); + verify(mKeyStoreAuthorization).getLastAuthTime(eq(secureUserId), + eq(hardwareAuthenticators)); } // Helper methods diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java index fa892782f42e..44aa868716eb 100644 --- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java +++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java @@ -30,6 +30,9 @@ import android.graphics.fonts.SystemFonts; import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.system.Os; import android.text.FontConfig; import android.util.Xml; @@ -38,8 +41,11 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.text.flags.Flags; + import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.xmlpull.v1.XmlPullParser; @@ -69,6 +75,9 @@ public final class UpdatableFontDirTest { private static final String LEGACY_FONTS_XML = "/system/etc/fonts.xml"; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + /** * A {@link UpdatableFontDir.FontFileParser} for testing. Instead of using real font files, * this test uses fake font files. A fake font file has its PostScript naem and revision as the @@ -1097,6 +1106,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1116,6 +1126,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1); @@ -1135,6 +1146,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf and bar.ttf installTestFontFile(2 /* numFonts */, 1 /* version */); @@ -1154,6 +1166,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf and bar.ttf installTestFontFile(2 /* numFonts */, 1 /* version */); @@ -1173,6 +1186,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1192,6 +1206,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureAllMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1211,6 +1226,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureAllMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1230,6 +1246,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void signatureAllMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1249,6 +1266,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1268,6 +1286,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1); @@ -1287,6 +1306,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf and bar.ttf installTestFontFile(2 /* numFonts */, 1 /* version */); @@ -1306,6 +1326,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf and bar.ttf installTestFontFile(2 /* numFonts */, 1 /* version */); @@ -1325,6 +1346,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1344,6 +1366,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontAllMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1363,6 +1386,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontAllMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1382,6 +1406,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontAllMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1401,6 +1426,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontDirAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1420,6 +1446,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontDirAllMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1439,6 +1466,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontDirAllMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1458,6 +1486,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void fontDirAllMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1477,6 +1506,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void dirContentAllMissingCase_fontFamilyInstalled_fontFamilyInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1497,6 +1527,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void dirContentAllMissingCase_fontFamilyInstalled_fontInstallLater() { // Install font families, foo.ttf, bar.ttf. installTestFontFamilies(1 /* version */); @@ -1517,6 +1548,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void dirContentAllMissingCase_fontFileInstalled_fontFamilyInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); @@ -1537,6 +1569,7 @@ public final class UpdatableFontDirTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_FIX_FONT_UPDATE_FAILURE) public void dirContentAllMissingCase_fontFileInstalled_fontFileInstallLater() { // Install font file, foo.ttf installTestFontFile(1 /* numFonts */, 1 /* version */); diff --git a/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java b/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java index 43bf53714cba..72caae3694de 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserLifecycleStressTest.java @@ -242,7 +242,7 @@ public class UserLifecycleStressTest { private void stopUser(int userId) throws RemoteException, InterruptedException { runWithLatch("stop user", countDownLatch -> { ActivityManager.getService() - .stopUser(userId, /* force= */ true, new IStopUserCallback.Stub() { + .stopUserWithCallback(userId, new IStopUserCallback.Stub() { @Override public void userStopped(int userId) { countDownLatch.countDown(); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 52df010bd588..eac99298559e 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -85,7 +85,6 @@ import android.os.VibratorInfo; import android.os.test.TestLooper; import android.service.dreams.DreamManagerInternal; import android.telecom.TelecomManager; -import android.util.FeatureFlagUtils; import android.view.Display; import android.view.InputDevice; import android.view.KeyEvent; @@ -743,15 +742,8 @@ class TestPhoneWindowManager { void assertSwitchKeyboardLayout(int direction, int displayId) { mTestLooper.dispatchAll(); - if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI)) { - verify(mInputMethodManagerInternal).onSwitchKeyboardLayoutShortcut(eq(direction), - eq(displayId), eq(mImeTargetWindowToken)); - verify(mWindowManagerFuncsImpl, never()).switchKeyboardLayout(anyInt(), anyInt()); - } else { - verify(mWindowManagerFuncsImpl).switchKeyboardLayout(anyInt(), eq(direction)); - verify(mInputMethodManagerInternal, never()) - .onSwitchKeyboardLayoutShortcut(anyInt(), anyInt(), any()); - } + verify(mInputMethodManagerInternal).onSwitchKeyboardLayoutShortcut(eq(direction), + eq(displayId), eq(mImeTargetWindowToken)); } void assertTakeBugreport() { diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 13550923cf3d..10eae577f706 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -46,6 +46,7 @@ import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; +import static android.server.wm.ActivityManagerTestBase.isTablet; import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; @@ -75,6 +76,7 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -122,6 +124,7 @@ import com.android.server.pm.pkg.AndroidPackage; import com.android.server.wm.BackgroundActivityStartController.BalVerdict; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; import com.android.server.wm.utils.MockTracker; +import com.android.window.flags.Flags; import org.junit.After; import org.junit.Before; @@ -1295,6 +1298,12 @@ public class ActivityStarterTests extends WindowTestsBase { */ @Test public void testDeliverIntentToTopActivityOfNonTopDisplay() { + // TODO(b/330152508): Remove check once legacy multi-display behaviour can coexist with + // desktop windowing mode + // Ignore test if desktop windowing is enabled on tablets as legacy multi-display + // behaviour will not be respected + assumeFalse(Flags.enableDesktopWindowingMode() && isTablet()); + final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK, false /* mockGetRootTask */); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java index 5aa4ba3e1d42..695faa525dd6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java @@ -504,22 +504,37 @@ public class BackgroundActivityStartControllerTests { assertThat(balState.callerExplicitOptInOrOut()).isFalse(); assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isTrue(); assertThat(balState.realCallerExplicitOptInOrOut()).isFalse(); - assertThat(balState.toString()).isEqualTo( - "[callingPackage: package.app1; callingPackageTargetSdk: -1; callingUid: 10001; " - + "callingPid: 11001; appSwitchState: 0; callingUidHasAnyVisibleWindow: " - + "false; callingUidProcState: NONEXISTENT; " - + "isCallingUidPersistentSystemProcess: false; forcedBalByPiSender: BSP" - + ".NONE; intent: Intent { cmp=package.app3/someClass }; callerApp: " - + "mCallerApp; inVisibleTask: false; balAllowedByPiCreator: BSP" - + ".ALLOW_BAL; balAllowedByPiCreatorWithHardening: BSP.ALLOW_BAL; " - + "resultIfPiCreatorAllowsBal: null; hasRealCaller: true; " - + "isCallForResult: false; isPendingIntent: false; autoOptInReason: " - + "notPendingIntent; realCallingPackage: uid=1[debugOnly]; " - + "realCallingPackageTargetSdk: -1; realCallingUid: 1; realCallingPid: 1;" - + " realCallingUidHasAnyVisibleWindow: false; realCallingUidProcState: " - + "NONEXISTENT; isRealCallingUidPersistentSystemProcess: false; " - + "originatingPendingIntent: null; realCallerApp: null; " - + "balAllowedByPiSender: BSP.ALLOW_BAL; resultIfPiSenderAllowsBal: null]"); + assertThat(balState.toString()).contains( + "[callingPackage: package.app1; " + + "callingPackageTargetSdk: -1; " + + "callingUid: 10001; " + + "callingPid: 11001; " + + "appSwitchState: 0; " + + "callingUidHasAnyVisibleWindow: false; " + + "callingUidProcState: NONEXISTENT; " + + "isCallingUidPersistentSystemProcess: false; " + + "forcedBalByPiSender: BSP.NONE; " + + "intent: Intent { cmp=package.app3/someClass }; " + + "callerApp: mCallerApp; " + + "inVisibleTask: false; " + + "balAllowedByPiCreator: BSP.ALLOW_BAL; " + + "balAllowedByPiCreatorWithHardening: BSP.ALLOW_BAL; " + + "resultIfPiCreatorAllowsBal: null; " + + "hasRealCaller: true; " + + "isCallForResult: false; " + + "isPendingIntent: false; " + + "autoOptInReason: notPendingIntent; " + + "realCallingPackage: uid=1[debugOnly]; " + + "realCallingPackageTargetSdk: -1; " + + "realCallingUid: 1; " + + "realCallingPid: 1; " + + "realCallingUidHasAnyVisibleWindow: false; " + + "realCallingUidProcState: NONEXISTENT; " + + "isRealCallingUidPersistentSystemProcess: false; " + + "originatingPendingIntent: null; " + + "realCallerApp: null; " + + "balAllowedByPiSender: BSP.ALLOW_BAL; " + + "resultIfPiSenderAllowsBal: null"); } @Test @@ -588,21 +603,36 @@ public class BackgroundActivityStartControllerTests { assertThat(balState.callerExplicitOptInOrOut()).isFalse(); assertThat(balState.realCallerExplicitOptInOrAutoOptIn()).isFalse(); assertThat(balState.realCallerExplicitOptInOrOut()).isFalse(); - assertThat(balState.toString()).isEqualTo( - "[callingPackage: package.app1; callingPackageTargetSdk: -1; callingUid: 10001; " - + "callingPid: 11001; appSwitchState: 0; callingUidHasAnyVisibleWindow: " - + "false; callingUidProcState: NONEXISTENT; " - + "isCallingUidPersistentSystemProcess: false; forcedBalByPiSender: BSP" - + ".NONE; intent: Intent { cmp=package.app3/someClass }; callerApp: " - + "mCallerApp; inVisibleTask: false; balAllowedByPiCreator: BSP" - + ".NONE; balAllowedByPiCreatorWithHardening: BSP.NONE; " - + "resultIfPiCreatorAllowsBal: null; hasRealCaller: true; " - + "isCallForResult: false; isPendingIntent: true; autoOptInReason: " - + "null; realCallingPackage: uid=1[debugOnly]; " - + "realCallingPackageTargetSdk: -1; realCallingUid: 1; realCallingPid: 1;" - + " realCallingUidHasAnyVisibleWindow: false; realCallingUidProcState: " - + "NONEXISTENT; isRealCallingUidPersistentSystemProcess: false; " - + "originatingPendingIntent: PendingIntentRecord; realCallerApp: null; " - + "balAllowedByPiSender: BSP.ALLOW_FGS; resultIfPiSenderAllowsBal: null]"); + assertThat(balState.toString()).contains( + "[callingPackage: package.app1; " + + "callingPackageTargetSdk: -1; " + + "callingUid: 10001; " + + "callingPid: 11001; " + + "appSwitchState: 0; " + + "callingUidHasAnyVisibleWindow: false; " + + "callingUidProcState: NONEXISTENT; " + + "isCallingUidPersistentSystemProcess: false; " + + "forcedBalByPiSender: BSP.NONE; " + + "intent: Intent { cmp=package.app3/someClass }; " + + "callerApp: mCallerApp; " + + "inVisibleTask: false; " + + "balAllowedByPiCreator: BSP.NONE; " + + "balAllowedByPiCreatorWithHardening: BSP.NONE; " + + "resultIfPiCreatorAllowsBal: null; " + + "hasRealCaller: true; " + + "isCallForResult: false; " + + "isPendingIntent: true; " + + "autoOptInReason: null; " + + "realCallingPackage: uid=1[debugOnly]; " + + "realCallingPackageTargetSdk: -1; " + + "realCallingUid: 1; " + + "realCallingPid: 1; " + + "realCallingUidHasAnyVisibleWindow: false; " + + "realCallingUidProcState: NONEXISTENT; " + + "isRealCallingUidPersistentSystemProcess: false; " + + "originatingPendingIntent: PendingIntentRecord; " + + "realCallerApp: null; " + + "balAllowedByPiSender: BSP.ALLOW_FGS; " + + "resultIfPiSenderAllowsBal: null"); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java index b11f9b2306df..073b55165c9f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; +import android.os.Message; import android.platform.test.annotations.Presubmit; import android.view.DisplayInfo; @@ -60,6 +61,8 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { private int mColorMode; private int mLogicalDensityDpi; + private final Message mScreenUnblocker = mock(Message.class); + @Override protected void onBeforeSystemServicesCreated() { // Set other flags to their default values @@ -73,12 +76,11 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { doReturn(true).when(mDisplayContent).getLastHasContent(); mockTransitionsController(/* enabled= */ true); mockRemoteDisplayChangeController(); + performInitialDisplayUpdate(); } @Test public void testUpdate_deferrableFieldChangedTransitionStarted_deferrableFieldUpdated() { - performInitialDisplayUpdate(); - mUniqueId = "old"; Runnable onUpdated = mock(Runnable.class); mDisplayContent.requestDisplayUpdate(onUpdated); @@ -107,8 +109,6 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { @Test public void testUpdate_nonDeferrableUpdateAndTransitionDeferred_nonDeferrableFieldUpdated() { - performInitialDisplayUpdate(); - // Update only color mode (non-deferrable field) and keep the same unique id mUniqueId = "initial_unique_id"; mColorMode = 123; @@ -121,8 +121,6 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { @Test public void testUpdate_nonDeferrableUpdateTwiceAndTransitionDeferred_fieldHasLatestValue() { - performInitialDisplayUpdate(); - // Update only color mode (non-deferrable field) and keep the same unique id mUniqueId = "initial_unique_id"; mColorMode = 123; @@ -163,7 +161,6 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { @Test public void testUpdate_deferrableFieldUpdatedTransitionPending_fieldNotUpdated() { - performInitialDisplayUpdate(); mUniqueId = "old"; Runnable onUpdated = mock(Runnable.class); mDisplayContent.requestDisplayUpdate(onUpdated); @@ -181,7 +178,6 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { @Test public void testTwoDisplayUpdates_transitionStarted_displayUpdated() { - performInitialDisplayUpdate(); mUniqueId = "old"; Runnable onUpdated = mock(Runnable.class); mDisplayContent.requestDisplayUpdate(onUpdated); @@ -212,6 +208,51 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { assertThat(mDisplayContent.getDisplayInfo().uniqueId).isEqualTo("new2"); } + @Test + public void testWaitForTransition_displaySwitching_waitsForTransitionToBeStarted() { + mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH); + mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ true); + boolean willWait = mDisplayContent.mDisplayUpdater.waitForTransition(mScreenUnblocker); + assertThat(willWait).isTrue(); + mUniqueId = "new"; + mDisplayContent.requestDisplayUpdate(mock(Runnable.class)); + when(mDisplayContent.mTransitionController.inTransition()).thenReturn(true); + captureStartTransitionCollection().getValue().onCollectStarted(/* deferred= */ true); + + // Verify that screen is not unblocked yet as the start transaction hasn't been presented + verify(mScreenUnblocker, never()).sendToTarget(); + + when(mDisplayContent.mTransitionController.inTransition()).thenReturn(false); + final Transition transition = captureRequestedTransition().getValue(); + makeTransitionTransactionCompleted(transition); + + // Verify that screen is unblocked as start transaction of the transition + // has been completed + verify(mScreenUnblocker).sendToTarget(); + } + + @Test + public void testWaitForTransition_displayNotSwitching_doesNotWait() { + mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH); + mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ false); + + boolean willWait = mDisplayContent.mDisplayUpdater.waitForTransition(mScreenUnblocker); + + assertThat(willWait).isFalse(); + verify(mScreenUnblocker, never()).sendToTarget(); + } + + @Test + public void testWaitForTransition_displayIsSwitchingButFlagDisabled_doesNotWait() { + mSetFlagsRule.disableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH); + mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ true); + + boolean willWait = mDisplayContent.mDisplayUpdater.waitForTransition(mScreenUnblocker); + + assertThat(willWait).isFalse(); + verify(mScreenUnblocker, never()).sendToTarget(); + } + private void mockTransitionsController(boolean enabled) { spyOn(mDisplayContent.mTransitionController); when(mDisplayContent.mTransitionController.isShellTransitionsEnabled()).thenReturn(enabled); @@ -233,6 +274,23 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { return callbackCaptor; } + private ArgumentCaptor<Transition> captureRequestedTransition() { + ArgumentCaptor<Transition> callbackCaptor = + ArgumentCaptor.forClass(Transition.class); + verify(mDisplayContent.mTransitionController, atLeast(1)) + .requestStartTransition(callbackCaptor.capture(), any(), any(), any()); + return callbackCaptor; + } + + private void makeTransitionTransactionCompleted(Transition transition) { + if (transition.mTransactionCompletedListeners != null) { + for (int i = 0; i < transition.mTransactionCompletedListeners.size(); i++) { + final Runnable listener = transition.mTransactionCompletedListeners.get(i); + listener.run(); + } + } + } + private void performInitialDisplayUpdate() { mUniqueId = "initial_unique_id"; mColorMode = 0; diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java index 1233686a4b48..00a8842c358e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java @@ -167,6 +167,10 @@ class TestWindowManagerPolicy implements WindowManagerPolicy { } @Override + public void onDisplaySwitchStart(int displayId) { + } + + @Override public boolean okToAnimate(boolean ignoreScreenOn) { return mOkToAnimate; } diff --git a/services/usb/Android.bp b/services/usb/Android.bp index e8ffe541d641..e00627e4f0b5 100644 --- a/services/usb/Android.bp +++ b/services/usb/Android.bp @@ -44,6 +44,7 @@ java_library_static { aconfig_declarations { name: "usb_flags", package: "com.android.server.usb.flags", + container: "system", srcs: ["**/usb_flags.aconfig"], } diff --git a/services/usb/java/com/android/server/usb/UsbService.java b/services/usb/java/com/android/server/usb/UsbService.java index 9b5612eacc13..9470c0a944c2 100644 --- a/services/usb/java/com/android/server/usb/UsbService.java +++ b/services/usb/java/com/android/server/usb/UsbService.java @@ -60,6 +60,7 @@ import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.os.BackgroundThread; import com.android.internal.util.DumpUtils; @@ -222,6 +223,21 @@ public class UsbService extends IUsbManager.Stub { mContext.registerReceiverAsUser(receiver, UserHandle.ALL, filter, null, null); } + // Ideally we should use the injector pattern so we wouldn't need this constructor for test + @VisibleForTesting + UsbService(Context context, + UsbPortManager usbPortManager, + UsbAlsaManager usbAlsaManager, + UserManager userManager, + UsbSettingsManager usbSettingsManager) { + mContext = context; + mPortManager = usbPortManager; + mAlsaManager = usbAlsaManager; + mUserManager = userManager; + mSettingsManager = usbSettingsManager; + mPermissionManager = new UsbPermissionManager(context, this); + } + /** * Set new {@link #mCurrentUserId} and propagate it to other modules. * @@ -886,7 +902,16 @@ public class UsbService extends IUsbManager.Stub { @Override public boolean enableUsbData(String portId, boolean enable, int operationId, - IUsbOperationInternal callback) { + IUsbOperationInternal callback) { + return enableUsbDataInternal(portId, enable, operationId, callback, Binder.getCallingUid()); + } + + /** + * Internal function abstracted for testing with callerUid + */ + @VisibleForTesting + boolean enableUsbDataInternal(String portId, boolean enable, int operationId, + IUsbOperationInternal callback, int callerUid) { Objects.requireNonNull(portId, "enableUsbData: portId must not be null. opId:" + operationId); Objects.requireNonNull(callback, "enableUsbData: callback must not be null. opId:" @@ -894,7 +919,7 @@ public class UsbService extends IUsbManager.Stub { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null); if (android.hardware.usb.flags.Flags.enableUsbDataSignalStaking()) { - if (!shouldUpdateUsbSignaling(portId, enable, Binder.getCallingUid())) { + if (!shouldUpdateUsbSignaling(portId, enable, callerUid)) { try { callback.onOperationComplete(USB_OPERATION_ERROR_INTERNAL); } catch (RemoteException e) { @@ -950,7 +975,16 @@ public class UsbService extends IUsbManager.Stub { @Override public void enableUsbDataWhileDocked(String portId, int operationId, - IUsbOperationInternal callback) { + IUsbOperationInternal callback) { + enableUsbDataWhileDockedInternal(portId, operationId, callback, Binder.getCallingUid()); + } + + /** + * Internal function abstracted for testing with callerUid + */ + @VisibleForTesting + void enableUsbDataWhileDockedInternal(String portId, int operationId, + IUsbOperationInternal callback, int callerUid) { Objects.requireNonNull(portId, "enableUsbDataWhileDocked: portId must not be null. opId:" + operationId); Objects.requireNonNull(callback, @@ -959,7 +993,7 @@ public class UsbService extends IUsbManager.Stub { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null); if (android.hardware.usb.flags.Flags.enableUsbDataSignalStaking()) { - if (!shouldUpdateUsbSignaling(portId, true, Binder.getCallingUid())) { + if (!shouldUpdateUsbSignaling(portId, true, callerUid)) { try { callback.onOperationComplete(USB_OPERATION_ERROR_INTERNAL); } catch (RemoteException e) { diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig index ea6e50221cd0..a7c5ddb115b8 100644 --- a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig +++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.server.usb.flags" +container: "system" flag { name: "allow_restriction_of_overlay_activities" diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 10c17c1be6e6..7db41804067b 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -9898,6 +9898,50 @@ public class CarrierConfigManager { */ public static final String KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING = "satellite_information_redirect_url_string"; + /** + * Indicate whether a carrier supports emergency messaging. When this config is {@code false}, + * emergency call to satellite T911 handover will be disabled. + * + * This will need agreement with carriers before enabling this flag. + * + * The default value is false. + * + * @hide + */ + public static final String KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL = + "emergency_messaging_supported_bool"; + + /** + * An integer key holds the timeout duration in milliseconds used to determine whether to hand + * over an emergency call to satellite T911. + * + * The timer is started when there is an ongoing emergency call, and the IMS is not registered, + * and cellular service is not available. When the timer expires, + * {@link com.android.internal.telephony.satellite.SatelliteSOSMessageRecommender} will send the + * event {@link TelephonyManager#EVENT_DISPLAY_EMERGENCY_MESSAGE} to Dialer, which will then + * prompt user to switch to using satellite emergency messaging. + * + * The default value is 30 seconds. + * + * @hide + */ + public static final String KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT = + "emergency_call_to_satellite_t911_handover_timeout_millis_int"; + + /** + * An int array that contains default capabilities for carrier enabled satellite roaming. + * If any PLMN is provided from the entitlement server, and it is not listed in + * {@link #KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE}, default capabilities + * will be used instead. + * <p> + * The default capabilities are + * {@link NetworkRegistrationInfo#SERVICE_TYPE_SMS}, and + * {@link NetworkRegistrationInfo#SERVICE_TYPE_MMS} + * + * @hide + */ + public static final String KEY_CARRIER_ROAMING_SATELLITE_DEFAULT_SERVICES_INT_ARRAY = + "carrier_roaming_satellite_default_services_int_array"; /** * Indicating whether DUN APN should be disabled when the device is roaming. In that case, @@ -11045,7 +11089,15 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false); sDefaults.putString(KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING, "androidSatmode"); sDefaults.putString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING, ""); + sDefaults.putIntArray(KEY_CARRIER_ROAMING_SATELLITE_DEFAULT_SERVICES_INT_ARRAY, + new int[] { + NetworkRegistrationInfo.SERVICE_TYPE_SMS, + NetworkRegistrationInfo.SERVICE_TYPE_MMS + }); sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false); + sDefaults.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, false); + sDefaults.putInt(KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT, + (int) TimeUnit.SECONDS.toMillis(30)); sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, ""); sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false); sDefaults.putBoolean(KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL, false); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index c5f2d42389e5..ba7ba53272f1 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -3137,7 +3137,7 @@ public class SubscriptionManager { if (useRootLocale) { configurationKey.setLocale(Locale.ROOT); } - cacheKey = Pair.create(context.getPackageName(), configurationKey); + cacheKey = Pair.create(context.getPackageName() + ", subid=" + subId, configurationKey); synchronized (sResourcesCache) { Resources cached = sResourcesCache.get(cacheKey); if (cached != null) { diff --git a/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl index 06fc3c631eba..579fda320e9a 100644 --- a/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl +++ b/telephony/java/android/telephony/satellite/ISatelliteTransmissionUpdateCallback.aidl @@ -26,11 +26,13 @@ oneway interface ISatelliteTransmissionUpdateCallback { /** * Called when satellite datagram send state changed. * + * @param datagramType The datagram type of currently being sent. * @param state The new send datagram transfer state. * @param sendPendingCount The number of datagrams that are currently being sent. * @param errorCode If datagram transfer failed, the reason for failure. */ - void onSendDatagramStateChanged(in int state, in int sendPendingCount, in int errorCode); + void onSendDatagramStateChanged(int datagramType, int state, int sendPendingCount, + int errorCode); /** * Called when satellite datagram receive state changed. @@ -39,7 +41,7 @@ oneway interface ISatelliteTransmissionUpdateCallback { * @param receivePendingCount The number of datagrams that are currently pending to be received. * @param errorCode If datagram transfer failed, the reason for failure. */ - void onReceiveDatagramStateChanged(in int state, in int receivePendingCount, in int errorCode); + void onReceiveDatagramStateChanged(int state, int receivePendingCount, int errorCode); /** * Called when the satellite position changed. diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 20b24b9db6f4..87bb0f05c742 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -992,12 +992,19 @@ public final class SatelliteManager { */ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2; + /** + * This type of datagram is used to keep the device in satellite connected state or check if + * there is any incoming message. + * @hide + */ + public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3; /** @hide */ @IntDef(prefix = "DATAGRAM_TYPE_", value = { DATAGRAM_TYPE_UNKNOWN, DATAGRAM_TYPE_SOS_MESSAGE, - DATAGRAM_TYPE_LOCATION_SHARING + DATAGRAM_TYPE_LOCATION_SHARING, + DATAGRAM_TYPE_KEEP_ALIVE }) @Retention(RetentionPolicy.SOURCE) public @interface DatagramType {} @@ -1077,8 +1084,13 @@ public final class SatelliteManager { } @Override - public void onSendDatagramStateChanged(int state, int sendPendingCount, - int errorCode) { + public void onSendDatagramStateChanged(int datagramType, int state, + int sendPendingCount, int errorCode) { + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> callback.onSendDatagramStateChanged(datagramType, + state, sendPendingCount, errorCode))); + + // For backward compatibility executor.execute(() -> Binder.withCleanCallingIdentity( () -> callback.onSendDatagramStateChanged( state, sendPendingCount, errorCode))); diff --git a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java index e02097019c4c..d8bd66284fb0 100644 --- a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java @@ -52,6 +52,19 @@ public interface SatelliteTransmissionUpdateCallback { @SatelliteManager.SatelliteResult int errorCode); /** + * Called when satellite datagram send state changed. + * + * @param datagramType The datagram type of currently being sent. + * @param state The new send datagram transfer state. + * @param sendPendingCount The number of datagrams that are currently being sent. + * @param errorCode If datagram transfer failed, the reason for failure. + * + * @hide + */ + void onSendDatagramStateChanged(@SatelliteManager.DatagramType int datagramType, + @SatelliteManager.SatelliteDatagramTransferState int state, int sendPendingCount, + @SatelliteManager.SatelliteResult int errorCode); + /** * Called when satellite datagram receive state changed. * * @param state The new receive datagram transfer state. diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl index 36485c6b6fb5..16983a0dbca1 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl @@ -83,6 +83,9 @@ oneway interface ISatellite { * * @param enableSatellite True to enable the satellite modem and false to disable. * @param enableDemoMode True to enable demo mode and false to disable. + * @param isEmergency To specify the satellite is enabled for emergency session and false for + * non emergency session. Note: it is possible that a emergency session started get converted + * to a non emergency session and vice versa. * @param resultCallback The callback to receive the error code result of the operation. * * Valid result codes returned: @@ -96,7 +99,7 @@ oneway interface ISatellite { * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES */ void requestSatelliteEnabled(in boolean enableSatellite, in boolean enableDemoMode, - in IIntegerConsumer resultCallback); + in boolean isEmergency, in IIntegerConsumer resultCallback); /** * Request to get whether the satellite modem is enabled. diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java index b7dc79ff7283..a62363335fb9 100644 --- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java +++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java @@ -90,11 +90,11 @@ public class SatelliteImplBase extends SatelliteService { @Override public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode, - IIntegerConsumer resultCallback) throws RemoteException { + boolean isEmergency, IIntegerConsumer resultCallback) throws RemoteException { executeMethodAsync( () -> SatelliteImplBase.this .requestSatelliteEnabled( - enableSatellite, enableDemoMode, resultCallback), + enableSatellite, enableDemoMode, isEmergency, resultCallback), "requestSatelliteEnabled"); } @@ -337,6 +337,9 @@ public class SatelliteImplBase extends SatelliteService { * * @param enableSatellite True to enable the satellite modem and false to disable. * @param enableDemoMode True to enable demo mode and false to disable. + * @param isEmergency To specify the satellite is enabled for emergency session and false for + * non emergency session. Note: it is possible that a emergency session started get converted + * to a non emergency session and vice versa. * @param resultCallback The callback to receive the error code result of the operation. * * Valid result codes returned: @@ -350,7 +353,7 @@ public class SatelliteImplBase extends SatelliteService { * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES */ public void requestSatelliteEnabled(boolean enableSatellite, boolean enableDemoMode, - @NonNull IIntegerConsumer resultCallback) { + boolean isEmergency, @NonNull IIntegerConsumer resultCallback) { // stub implementation } diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt index 511c94849681..13902184ac6e 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt @@ -17,8 +17,12 @@ package com.android.server.wm.flicker.activityembedding.rotation import android.platform.test.annotations.Presubmit +import android.tools.Position +import android.tools.datatypes.Rect import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.traces.Condition +import android.tools.traces.DeviceStateDump import android.tools.traces.component.ComponentNameMatcher import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase import com.android.server.wm.flicker.helpers.setRotation @@ -30,7 +34,14 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : ActivityEmbeddin override val transition: FlickerBuilder.() -> Unit = { setup { this.setRotation(flicker.scenario.startRotation) } teardown { testApp.exit(wmHelper) } - transitions { this.setRotation(flicker.scenario.endRotation) } + transitions { + this.setRotation(flicker.scenario.endRotation) + if (!flicker.scenario.isTablet) { + wmHelper.StateSyncBuilder() + .add(navBarInPosition(flicker.scenario.isGesturalNavigation)) + .waitForAndVerify() + } + } } /** {@inheritDoc} */ @@ -76,4 +87,37 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : ActivityEmbeddin appLayerRotates_StartingPos() appLayerRotates_EndingPos() } + + private fun navBarInPosition(isGesturalNavigation: Boolean): Condition<DeviceStateDump> { + return Condition("navBarPosition") { dump -> + val display = + dump.layerState.displays.filterNot { it.isOff }.minByOrNull { it.id } + ?: error("There is no display!") + val displayArea = display.layerStackSpace + val navBarPosition = display.navBarPosition(isGesturalNavigation) + val navBarRegion = dump.layerState + .getLayerWithBuffer(ComponentNameMatcher.NAV_BAR) + ?.visibleRegion?.bounds ?: Rect.EMPTY + + when (navBarPosition) { + Position.TOP -> + navBarRegion.top == displayArea.top && + navBarRegion.left == displayArea.left && + navBarRegion.right == displayArea.right + Position.BOTTOM -> + navBarRegion.bottom == displayArea.bottom && + navBarRegion.left == displayArea.left && + navBarRegion.right == displayArea.right + Position.LEFT -> + navBarRegion.left == displayArea.left && + navBarRegion.top == displayArea.top && + navBarRegion.bottom == displayArea.bottom + Position.RIGHT -> + navBarRegion.right == displayArea.right && + navBarRegion.top == displayArea.top && + navBarRegion.bottom == displayArea.bottom + else -> error("Unknown position $navBarPosition") + } + } + } } diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt index 8e210d455591..f1df8a68fb63 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt @@ -123,7 +123,9 @@ class OpenAppFromLockscreenNotificationWithOverlayAppTest(flicker: LegacyFlicker @Test override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() - @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + @FlakyTest(bugId = 227143265) + @Test + override fun entireScreenCovered() = super.entireScreenCovered() @FlakyTest(bugId = 278227468) @Test diff --git a/tests/FlickerTests/README.md b/tests/FlickerTests/README.md index 6b28fdf8a8ef..7429250f5cc0 100644 --- a/tests/FlickerTests/README.md +++ b/tests/FlickerTests/README.md @@ -7,82 +7,17 @@ The tests are organized in packages according to the transitions they test (e.g. ## Adding a Test -By default tests should inherit from `RotationTestBase` or `NonRotationTestBase` and must override the variable `transitionToRun` (Kotlin) or the function `getTransitionToRun()` (Java). -Only tests that are not supported by these classes should inherit directly from the `FlickerTestBase` class. +By default, tests should inherit from `TestBase` and override the variable `transition` (Kotlin) or the function `getTransition()` (Java). -### Rotation animations and transitions +Inheriting from this class ensures the common assertions will be executed, namely: -Tests that rotate the device should inherit from `RotationTestBase`. -Tests that inherit from the class automatically receive start and end rotation values. -Moreover, these tests inherit the following checks: * all regions on the screen are covered * status bar is always visible -* status bar rotates +* status bar is at the correct position at the start and end of the transition * nav bar is always visible -* nav bar is rotates +* nav bar is at the correct position at the start and end of the transition The default tests can be disabled by overriding the respective methods and including an `@Ignore` annotation. -### Non-Rotation animations and transitions +For more examples of how a test looks like check `ChangeAppRotationTest` within the `Rotation` subdirectory. -`NonRotationTestBase` was created to make it easier to write tests that do not involve rotation (e.g., `Pip`, `split screen` or `IME`). -Tests that inherit from the class are automatically executed twice: once in portrait and once in landscape mode and the assertions are checked independently. -Moreover, these tests inherit the following checks: -* all regions on the screen are covered -* status bar is always visible -* nav bar is always visible - -The default tests can be disabled by overriding the respective methods and including an `@Ignore` annotation. - -### Exceptional cases - -Tests that rotate the device should inherit from `RotationTestBase`. -This class allows the test to be freely configured and does not provide any assertions. - - -### Example - -Start by defining common or error prone transitions using `TransitionRunner`. -```kotlin -@LargeTest -@RunWith(Parameterized::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class MyTest( - beginRotationName: String, - beginRotation: Int -) : NonRotationTestBase(beginRotationName, beginRotation) { - init { - mTestApp = MyAppHelper(InstrumentationRegistry.getInstrumentation()) - } - - override val transitionToRun: TransitionRunner - get() = TransitionRunner.newBuilder() - .withTag("myTest") - .recordAllRuns() - .runBefore { device.pressHome() } - .runBefore { device.waitForIdle() } - .run { testApp.open() } - .runAfter{ testApp.exit() } - .repeat(2) - .includeJankyRuns() - .build() - - @Test - fun myWMTest() { - checkResults { - WmTraceSubject.assertThat(it) - .showsAppWindow(MyTestApp) - .forAllEntries() - } - } - - @Test - fun mySFTest() { - checkResults { - LayersTraceSubject.assertThat(it) - .showsLayer(MyTestApp) - .forAllEntries() - } - } -} -``` diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt index e60764f137af..80282c309320 100644 --- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -33,7 +33,6 @@ import android.icu.util.ULocale import android.os.Bundle import android.os.test.TestLooper import android.platform.test.annotations.Presubmit -import android.provider.Settings import android.util.proto.ProtoOutputStream import android.view.InputDevice import android.view.inputmethod.InputMethodInfo @@ -47,9 +46,7 @@ import com.android.test.input.R import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNull import org.junit.Assert.assertTrue -import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Rule import org.junit.Test @@ -234,631 +231,326 @@ class KeyboardLayoutManagerTests { } @Test - fun testDefaultUi_getKeyboardLayouts() { - NewSettingsApiFlag(false).use { - val keyboardLayouts = keyboardLayoutManager.keyboardLayouts - assertNotEquals( - "Default UI: Keyboard layout API should not return empty array", - 0, - keyboardLayouts.size - ) - assertTrue( - "Default UI: Keyboard layout API should provide English(US) layout", - hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) - ) - } + fun testGetKeyboardLayouts() { + val keyboardLayouts = keyboardLayoutManager.keyboardLayouts + assertNotEquals( + "Keyboard layout API should not return empty array", + 0, + keyboardLayouts.size + ) + assertTrue( + "Keyboard layout API should provide English(US) layout", + hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) } @Test - fun testNewUi_getKeyboardLayouts() { - NewSettingsApiFlag(true).use { - val keyboardLayouts = keyboardLayoutManager.keyboardLayouts - assertNotEquals( - "New UI: Keyboard layout API should not return empty array", - 0, - keyboardLayouts.size - ) - assertTrue( - "New UI: Keyboard layout API should provide English(US) layout", - hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) - ) - } + fun testGetKeyboardLayout() { + val keyboardLayout = + keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) + assertEquals("getKeyboardLayout API should return correct Layout from " + + "available layouts", + ENGLISH_US_LAYOUT_DESCRIPTOR, + keyboardLayout!!.descriptor + ) } @Test - fun testDefaultUi_getKeyboardLayoutsForInputDevice() { - NewSettingsApiFlag(false).use { - val keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier) - assertNotEquals( - "Default UI: getKeyboardLayoutsForInputDevice API should not return empty array", - 0, - keyboardLayouts.size - ) - assertTrue( - "Default UI: getKeyboardLayoutsForInputDevice API should provide English(US) " + - "layout", - hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) - ) + fun testGetSetKeyboardLayoutForInputDevice_withImeInfo() { + val imeSubtype = createImeSubtype() - val vendorSpecificKeyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutsForInputDevice( - vendorSpecificKeyboardDevice.identifier - ) - assertEquals( - "Default UI: getKeyboardLayoutsForInputDevice API should return only vendor " + - "specific layout", - 1, - vendorSpecificKeyboardLayouts.size - ) - assertEquals( - "Default UI: getKeyboardLayoutsForInputDevice API should return vendor specific " + - "layout", - VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR, - vendorSpecificKeyboardLayouts[0].descriptor + keyboardLayoutManager.setKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, + ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + var result = + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype ) - } - } + assertEquals( + "getKeyboardLayoutForInputDevice API should return the set layout", + ENGLISH_UK_LAYOUT_DESCRIPTOR, + result.layoutDescriptor + ) - @Test - fun testNewUi_getKeyboardLayoutsForInputDevice() { - NewSettingsApiFlag(true).use { - val keyboardLayouts = keyboardLayoutManager.keyboardLayouts - assertNotEquals( - "New UI: getKeyboardLayoutsForInputDevice API should not return empty array", - 0, - keyboardLayouts.size - ) - assertTrue( - "New UI: getKeyboardLayoutsForInputDevice API should provide English(US) " + - "layout", - hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + // This should replace previously set layout + keyboardLayoutManager.setKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + result = + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype ) - } + assertEquals( + "getKeyboardLayoutForInputDevice API should return the last set layout", + ENGLISH_US_LAYOUT_DESCRIPTOR, + result.layoutDescriptor + ) } @Test - fun testDefaultUi_getSetCurrentKeyboardLayoutForInputDevice() { - NewSettingsApiFlag(false).use { - assertNull( - "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null if " + - "nothing was set", - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - ) - - keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier, - ENGLISH_US_LAYOUT_DESCRIPTOR - ) - val keyboardLayout = - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - assertEquals( - "Default UI: getCurrentKeyboardLayoutForInputDevice API should return the set " + - "layout", - ENGLISH_US_LAYOUT_DESCRIPTOR, - keyboardLayout + fun testGetKeyboardLayoutListForInputDevice() { + // Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts + var keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("hi-Latn") + ) + assertNotEquals( + "getKeyboardLayoutListForInputDevice API should return the list of " + + "supported layouts with matching script code", + 0, + keyboardLayouts.size + ) + assertTrue("getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(US) layout for hi-Latn", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + assertTrue("getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(No script code) layout for hi-Latn", + containsLayout( + keyboardLayouts, + createLayoutDescriptor("keyboard_layout_english_without_script_code") ) - } - } + ) - @Test - fun testNewUi_getSetCurrentKeyboardLayoutForInputDevice() { - NewSettingsApiFlag(true).use { - keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier, - ENGLISH_US_LAYOUT_DESCRIPTOR + // Check Layouts for "hi" which by default uses 'Deva' script. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("hi") ) - assertNull( - "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null " + - "even after setCurrentKeyboardLayoutForInputDevice", - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - ) - } - } + assertEquals("getKeyboardLayoutListForInputDevice API should return empty " + + "list if no supported layouts available", + 0, + keyboardLayouts.size + ) - @Test - fun testDefaultUi_getEnabledKeyboardLayoutsForInputDevice() { - NewSettingsApiFlag(false).use { - keyboardLayoutManager.addKeyboardLayoutForInputDevice( - keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR + // If user manually selected some layout, always provide it in the layout list + val imeSubtype = createImeSubtypeForLanguageTag("hi") + keyboardLayoutManager.setKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + imeSubtype ) - - val keyboardLayouts = - keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( - keyboardDevice.identifier - ) - assertEquals( - "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return added " + - "layout", + assertEquals("getKeyboardLayoutListForInputDevice API should return user " + + "selected layout even if the script is incompatible with IME", 1, - keyboardLayouts.size - ) - assertEquals( - "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return " + - "English(US) layout", - ENGLISH_US_LAYOUT_DESCRIPTOR, - keyboardLayouts[0] - ) - assertEquals( - "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + - "English(US) layout (Auto select the first enabled layout)", - ENGLISH_US_LAYOUT_DESCRIPTOR, - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - ) - - keyboardLayoutManager.removeKeyboardLayoutForInputDevice( - keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR - ) - assertEquals( - "Default UI: getKeyboardLayoutsForInputDevice API should return 0 layouts", - 0, - keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( - keyboardDevice.identifier - ).size - ) - assertNull( - "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null after " + - "the enabled layout is removed", - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - ) - } - } - - @Test - fun testNewUi_getEnabledKeyboardLayoutsForInputDevice() { - NewSettingsApiFlag(true).use { - keyboardLayoutManager.addKeyboardLayoutForInputDevice( - keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR - ) - - assertEquals( - "New UI: getEnabledKeyboardLayoutsForInputDevice API should return always return " + - "an empty array", - 0, - keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( - keyboardDevice.identifier - ).size - ) - assertNull( - "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null", - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - ) - } - } - - @Test - fun testDefaultUi_switchKeyboardLayout() { - NewSettingsApiFlag(false).use { - keyboardLayoutManager.addKeyboardLayoutForInputDevice( - keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR - ) - keyboardLayoutManager.addKeyboardLayoutForInputDevice( - keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR - ) - assertEquals( - "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + - "English(US) layout", - ENGLISH_US_LAYOUT_DESCRIPTOR, - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - ) - - keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1) - - // Throws null pointer because trying to show toast using TestLooper - assertThrows(NullPointerException::class.java) { testLooper.dispatchAll() } - assertEquals("Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + - "English(UK) layout", - ENGLISH_UK_LAYOUT_DESCRIPTOR, - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - ) - } - } - - @Test - fun testNewUi_switchKeyboardLayout() { - NewSettingsApiFlag(true).use { - keyboardLayoutManager.addKeyboardLayoutForInputDevice( - keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR - ) - keyboardLayoutManager.addKeyboardLayoutForInputDevice( - keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR - ) - - keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1) - testLooper.dispatchAll() - - assertNull("New UI: getCurrentKeyboardLayoutForInputDevice API should always return " + - "null", - keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( - keyboardDevice.identifier - ) - ) - } - } - - @Test - fun testDefaultUi_getKeyboardLayout() { - NewSettingsApiFlag(false).use { - val keyboardLayout = - keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) - assertEquals("Default UI: getKeyboardLayout API should return correct Layout from " + - "available layouts", - ENGLISH_US_LAYOUT_DESCRIPTOR, - keyboardLayout!!.descriptor - ) - } - } - - @Test - fun testNewUi_getKeyboardLayout() { - NewSettingsApiFlag(true).use { - val keyboardLayout = - keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) - assertEquals("New UI: getKeyboardLayout API should return correct Layout from " + - "available layouts", - ENGLISH_US_LAYOUT_DESCRIPTOR, - keyboardLayout!!.descriptor - ) - } - } - - @Test - fun testDefaultUi_getSetKeyboardLayoutForInputDevice_WithImeInfo() { - NewSettingsApiFlag(false).use { - val imeSubtype = createImeSubtype() - keyboardLayoutManager.setKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, - ENGLISH_UK_LAYOUT_DESCRIPTOR - ) - assertEquals( - "Default UI: getKeyboardLayoutForInputDevice API should always return " + - "KeyboardLayoutSelectionResult.FAILED", - KeyboardLayoutSelectionResult.FAILED, - keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype - ) - ) - } - } - - @Test - fun testNewUi_getSetKeyboardLayoutForInputDevice_withImeInfo() { - NewSettingsApiFlag(true).use { - val imeSubtype = createImeSubtype() - - keyboardLayoutManager.setKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, - ENGLISH_UK_LAYOUT_DESCRIPTOR - ) - var result = - keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype - ) - assertEquals( - "New UI: getKeyboardLayoutForInputDevice API should return the set layout", - ENGLISH_UK_LAYOUT_DESCRIPTOR, - result.layoutDescriptor - ) - - // This should replace previously set layout - keyboardLayoutManager.setKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, - ENGLISH_US_LAYOUT_DESCRIPTOR - ) - result = - keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype - ) - assertEquals( - "New UI: getKeyboardLayoutForInputDevice API should return the last set layout", - ENGLISH_US_LAYOUT_DESCRIPTOR, - result.layoutDescriptor - ) - } - } - - @Test - fun testDefaultUi_getKeyboardLayoutListForInputDevice() { - NewSettingsApiFlag(false).use { - val keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtype() - ) - assertEquals("Default UI: getKeyboardLayoutListForInputDevice API should always " + - "return empty array", - 0, - keyboardLayouts.size - ) - } - } + keyboardLayouts.size + ) - @Test - fun testNewUi_getKeyboardLayoutListForInputDevice() { - NewSettingsApiFlag(true).use { - // Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts - var keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + // Special case Japanese: UScript ignores provided script code for certain language tags + // Should manually match provided script codes and then rely on Uscript to derive + // script from language tags and match those. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("hi-Latn") - ) - assertNotEquals( - "New UI: getKeyboardLayoutListForInputDevice API should return the list of " + - "supported layouts with matching script code", - 0, - keyboardLayouts.size - ) - assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + - "containing English(US) layout for hi-Latn", - containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + createImeSubtypeForLanguageTag("ja-Latn-JP") ) - assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + - "containing English(No script code) layout for hi-Latn", - containsLayout( - keyboardLayouts, - createLayoutDescriptor("keyboard_layout_english_without_script_code") - ) + assertNotEquals( + "getKeyboardLayoutListForInputDevice API should return the list of " + + "supported layouts with matching script code for ja-Latn-JP", + 0, + keyboardLayouts.size + ) + assertTrue("getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(US) layout for ja-Latn-JP", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + assertTrue("getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(No script code) layout for ja-Latn-JP", + containsLayout( + keyboardLayouts, + createLayoutDescriptor("keyboard_layout_english_without_script_code") ) + ) - // Check Layouts for "hi" which by default uses 'Deva' script. - keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + // If script code not explicitly provided for Japanese should rely on Uscript to find + // derived script code and hence no suitable layout will be found. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("hi") - ) - assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return empty " + - "list if no supported layouts available", - 0, - keyboardLayouts.size - ) - - // If user manually selected some layout, always provide it in the layout list - val imeSubtype = createImeSubtypeForLanguageTag("hi") - keyboardLayoutManager.setKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, - ENGLISH_US_LAYOUT_DESCRIPTOR - ) - keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - imeSubtype - ) - assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return user " + - "selected layout even if the script is incompatible with IME", - 1, - keyboardLayouts.size - ) - - // Special case Japanese: UScript ignores provided script code for certain language tags - // Should manually match provided script codes and then rely on Uscript to derive - // script from language tags and match those. - keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("ja-Latn-JP") - ) - assertNotEquals( - "New UI: getKeyboardLayoutListForInputDevice API should return the list of " + - "supported layouts with matching script code for ja-Latn-JP", - 0, - keyboardLayouts.size - ) - assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + - "containing English(US) layout for ja-Latn-JP", - containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) - ) - assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + - "containing English(No script code) layout for ja-Latn-JP", - containsLayout( - keyboardLayouts, - createLayoutDescriptor("keyboard_layout_english_without_script_code") - ) - ) - - // If script code not explicitly provided for Japanese should rely on Uscript to find - // derived script code and hence no suitable layout will be found. - keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("ja-JP") - ) - assertEquals( - "New UI: getKeyboardLayoutListForInputDevice API should return empty list of " + - "supported layouts with matching script code for ja-JP", - 0, - keyboardLayouts.size - ) - - // If IME doesn't have a corresponding language tag, then should show all available - // layouts no matter the script code. - keyboardLayouts = - keyboardLayoutManager.getKeyboardLayoutListForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, null - ) - assertNotEquals( - "New UI: getKeyboardLayoutListForInputDevice API should return all layouts if" + - "language tag or subtype not provided", - 0, - keyboardLayouts.size - ) - assertTrue("New UI: getKeyboardLayoutListForInputDevice API should contain Latin " + - "layouts if language tag or subtype not provided", - containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + createImeSubtypeForLanguageTag("ja-JP") ) - assertTrue("New UI: getKeyboardLayoutListForInputDevice API should contain Cyrillic " + - "layouts if language tag or subtype not provided", - containsLayout( - keyboardLayouts, - createLayoutDescriptor("keyboard_layout_russian") - ) - ) - } - } + assertEquals( + "getKeyboardLayoutListForInputDevice API should return empty list of " + + "supported layouts with matching script code for ja-JP", + 0, + keyboardLayouts.size + ) - @Test - fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTag() { - NewSettingsApiFlag(true).use { - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTag("en-US"), - ENGLISH_US_LAYOUT_DESCRIPTOR - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTag("en-GB"), - ENGLISH_UK_LAYOUT_DESCRIPTOR - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTag("de"), - GERMAN_LAYOUT_DESCRIPTOR - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTag("fr-FR"), - createLayoutDescriptor("keyboard_layout_french") - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTag("ru"), + // If IME doesn't have a corresponding language tag, then should show all available + // layouts no matter the script code. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, null + ) + assertNotEquals( + "getKeyboardLayoutListForInputDevice API should return all layouts if" + + "language tag or subtype not provided", + 0, + keyboardLayouts.size + ) + assertTrue("getKeyboardLayoutListForInputDevice API should contain Latin " + + "layouts if language tag or subtype not provided", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + assertTrue("getKeyboardLayoutListForInputDevice API should contain Cyrillic " + + "layouts if language tag or subtype not provided", + containsLayout( + keyboardLayouts, createLayoutDescriptor("keyboard_layout_russian") ) - assertEquals( - "New UI: getDefaultKeyboardLayoutForInputDevice should return " + - "KeyboardLayoutSelectionResult.FAILED when no layout available", - KeyboardLayoutSelectionResult.FAILED, - keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("it") - ) - ) - assertEquals( - "New UI: getDefaultKeyboardLayoutForInputDevice should return " + - "KeyboardLayoutSelectionResult.FAILED when no layout for script code is" + - "available", - KeyboardLayoutSelectionResult.FAILED, - keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTag("en-Deva") - ) - ) - } + ) } @Test - fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTagAndLayoutType() { - NewSettingsApiFlag(true).use { - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("en-US", "qwerty"), - ENGLISH_US_LAYOUT_DESCRIPTOR - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("en-US", "dvorak"), - createLayoutDescriptor("keyboard_layout_english_us_dvorak") - ) - // Try to match layout type even if country doesn't match - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("en-GB", "dvorak"), - createLayoutDescriptor("keyboard_layout_english_us_dvorak") - ) - // Choose layout based on layout type priority, if layout type is not provided by IME - // (Qwerty > Dvorak > Extended) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("en-US", ""), - ENGLISH_US_LAYOUT_DESCRIPTOR - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("en-GB", "qwerty"), - ENGLISH_UK_LAYOUT_DESCRIPTOR - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("de", "qwertz"), - GERMAN_LAYOUT_DESCRIPTOR - ) - // Wrong layout type should match with language if provided layout type not available - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("de", "qwerty"), - GERMAN_LAYOUT_DESCRIPTOR - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("fr-FR", "azerty"), - createLayoutDescriptor("keyboard_layout_french") - ) - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("ru", "qwerty"), - createLayoutDescriptor("keyboard_layout_russian_qwerty") - ) - // If layout type is empty then prioritize KCM with empty layout type - assertCorrectLayout( - keyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("ru", ""), - createLayoutDescriptor("keyboard_layout_russian") + fun testGetDefaultKeyboardLayoutForInputDevice_withImeLanguageTag() { + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("en-US"), + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("en-GB"), + ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("de"), + GERMAN_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("fr-FR"), + createLayoutDescriptor("keyboard_layout_french") + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("ru"), + createLayoutDescriptor("keyboard_layout_russian") + ) + assertEquals( + "getDefaultKeyboardLayoutForInputDevice should return " + + "KeyboardLayoutSelectionResult.FAILED when no layout available", + KeyboardLayoutSelectionResult.FAILED, + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("it") ) - assertEquals("New UI: getDefaultKeyboardLayoutForInputDevice should return " + + ) + assertEquals( + "getDefaultKeyboardLayoutForInputDevice should return " + "KeyboardLayoutSelectionResult.FAILED when no layout for script code is" + "available", - KeyboardLayoutSelectionResult.FAILED, - keyboardLayoutManager.getKeyboardLayoutForInputDevice( - keyboardDevice.identifier, USER_ID, imeInfo, - createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "") - ) + KeyboardLayoutSelectionResult.FAILED, + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("en-Deva") ) - } + ) } @Test - fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withHwLanguageTagAndLayoutType() { - NewSettingsApiFlag(true).use { - val frenchSubtype = createImeSubtypeForLanguageTagAndLayoutType("fr", "azerty") - // Should return English dvorak even if IME current layout is French, since HW says the - // keyboard is a Dvorak keyboard - assertCorrectLayout( - englishDvorakKeyboardDevice, - frenchSubtype, - createLayoutDescriptor("keyboard_layout_english_us_dvorak") + fun testGetDefaultKeyboardLayoutForInputDevice_withImeLanguageTagAndLayoutType() { + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-US", "qwerty"), + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-US", "dvorak"), + createLayoutDescriptor("keyboard_layout_english_us_dvorak") + ) + // Try to match layout type even if country doesn't match + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-GB", "dvorak"), + createLayoutDescriptor("keyboard_layout_english_us_dvorak") + ) + // Choose layout based on layout type priority, if layout type is not provided by IME + // (Qwerty > Dvorak > Extended) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-US", ""), + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-GB", "qwerty"), + ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("de", "qwertz"), + GERMAN_LAYOUT_DESCRIPTOR + ) + // Wrong layout type should match with language if provided layout type not available + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("de", "qwerty"), + GERMAN_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("fr-FR", "azerty"), + createLayoutDescriptor("keyboard_layout_french") + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("ru", "qwerty"), + createLayoutDescriptor("keyboard_layout_russian_qwerty") + ) + // If layout type is empty then prioritize KCM with empty layout type + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("ru", ""), + createLayoutDescriptor("keyboard_layout_russian") + ) + assertEquals("getDefaultKeyboardLayoutForInputDevice should return " + + "KeyboardLayoutSelectionResult.FAILED when no layout for script code is" + + "available", + KeyboardLayoutSelectionResult.FAILED, + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "") ) + ) + } - // Back to back changing HW keyboards with same product and vendor ID but different - // language and layout type should configure the layouts correctly. - assertCorrectLayout( - englishQwertyKeyboardDevice, - frenchSubtype, - createLayoutDescriptor("keyboard_layout_english_us") - ) + @Test + fun testGetDefaultKeyboardLayoutForInputDevice_withHwLanguageTagAndLayoutType() { + val frenchSubtype = createImeSubtypeForLanguageTagAndLayoutType("fr", "azerty") + // Should return English dvorak even if IME current layout is French, since HW says the + // keyboard is a Dvorak keyboard + assertCorrectLayout( + englishDvorakKeyboardDevice, + frenchSubtype, + createLayoutDescriptor("keyboard_layout_english_us_dvorak") + ) - // Fallback to IME information if the HW provided layout script is incompatible with the - // provided IME subtype - assertCorrectLayout( - englishDvorakKeyboardDevice, - createImeSubtypeForLanguageTagAndLayoutType("ru", ""), - createLayoutDescriptor("keyboard_layout_russian") - ) - } + // Back to back changing HW keyboards with same product and vendor ID but different + // language and layout type should configure the layouts correctly. + assertCorrectLayout( + englishQwertyKeyboardDevice, + frenchSubtype, + createLayoutDescriptor("keyboard_layout_english_us") + ) + + // Fallback to IME information if the HW provided layout script is incompatible with the + // provided IME subtype + assertCorrectLayout( + englishDvorakKeyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("ru", ""), + createLayoutDescriptor("keyboard_layout_russian") + ) } @Test @@ -867,27 +559,25 @@ class KeyboardLayoutManagerTests { KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtypeForLanguageTagAndLayoutType("de-Latn", "qwertz"))) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping - NewSettingsApiFlag(true).use { - keyboardLayoutManager.onInputDeviceAdded(keyboardDevice.id) - ExtendedMockito.verify { - FrameworkStatsLog.write( - ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.eq(keyboardDevice.vendorId), - ArgumentMatchers.eq(keyboardDevice.productId), - ArgumentMatchers.eq( - createByteArray( - KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, - LAYOUT_TYPE_DEFAULT, - GERMAN_LAYOUT_NAME, - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD, - "de-Latn", - LAYOUT_TYPE_QWERTZ - ), + keyboardLayoutManager.onInputDeviceAdded(keyboardDevice.id) + ExtendedMockito.verify { + FrameworkStatsLog.write( + ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.eq(keyboardDevice.vendorId), + ArgumentMatchers.eq(keyboardDevice.productId), + ArgumentMatchers.eq( + createByteArray( + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + LAYOUT_TYPE_DEFAULT, + GERMAN_LAYOUT_NAME, + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD, + "de-Latn", + LAYOUT_TYPE_QWERTZ ), - ArgumentMatchers.eq(keyboardDevice.deviceBus), - ) - } + ), + ArgumentMatchers.eq(keyboardDevice.deviceBus), + ) } } @@ -897,27 +587,25 @@ class KeyboardLayoutManagerTests { KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtypeForLanguageTagAndLayoutType("de-Latn", "qwertz"))) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping - NewSettingsApiFlag(true).use { - keyboardLayoutManager.onInputDeviceAdded(englishQwertyKeyboardDevice.id) - ExtendedMockito.verify { - FrameworkStatsLog.write( - ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.eq(englishQwertyKeyboardDevice.vendorId), - ArgumentMatchers.eq(englishQwertyKeyboardDevice.productId), - ArgumentMatchers.eq( - createByteArray( - "en", - LAYOUT_TYPE_QWERTY, - ENGLISH_US_LAYOUT_NAME, - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE, - "de-Latn", - LAYOUT_TYPE_QWERTZ - ) - ), - ArgumentMatchers.eq(keyboardDevice.deviceBus), - ) - } + keyboardLayoutManager.onInputDeviceAdded(englishQwertyKeyboardDevice.id) + ExtendedMockito.verify { + FrameworkStatsLog.write( + ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.eq(englishQwertyKeyboardDevice.vendorId), + ArgumentMatchers.eq(englishQwertyKeyboardDevice.productId), + ArgumentMatchers.eq( + createByteArray( + "en", + LAYOUT_TYPE_QWERTY, + ENGLISH_US_LAYOUT_NAME, + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE, + "de-Latn", + LAYOUT_TYPE_QWERTZ + ) + ), + ArgumentMatchers.eq(keyboardDevice.deviceBus), + ) } } @@ -925,27 +613,25 @@ class KeyboardLayoutManagerTests { fun testConfigurationLogged_onInputDeviceAdded_DefaultSelection() { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping - NewSettingsApiFlag(true).use { - keyboardLayoutManager.onInputDeviceAdded(keyboardDevice.id) - ExtendedMockito.verify { - FrameworkStatsLog.write( - ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.eq(keyboardDevice.vendorId), - ArgumentMatchers.eq(keyboardDevice.productId), - ArgumentMatchers.eq( - createByteArray( - KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, - LAYOUT_TYPE_DEFAULT, - "Default", - KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT, - KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, - LAYOUT_TYPE_DEFAULT - ), + keyboardLayoutManager.onInputDeviceAdded(keyboardDevice.id) + ExtendedMockito.verify { + FrameworkStatsLog.write( + ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.eq(keyboardDevice.vendorId), + ArgumentMatchers.eq(keyboardDevice.productId), + ArgumentMatchers.eq( + createByteArray( + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + LAYOUT_TYPE_DEFAULT, + "Default", + KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT, + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + LAYOUT_TYPE_DEFAULT ), - ArgumentMatchers.eq(keyboardDevice.deviceBus), - ) - } + ), + ArgumentMatchers.eq(keyboardDevice.deviceBus), + ) } } @@ -953,19 +639,17 @@ class KeyboardLayoutManagerTests { fun testConfigurationNotLogged_onInputDeviceChanged() { val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping - NewSettingsApiFlag(true).use { - keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) - ExtendedMockito.verify({ - FrameworkStatsLog.write( - ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any(ByteArray::class.java), - ArgumentMatchers.anyInt(), - ) - }, Mockito.times(0)) - } + keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) + ExtendedMockito.verify({ + FrameworkStatsLog.write( + ArgumentMatchers.eq(FrameworkStatsLog.KEYBOARD_CONFIGURED), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(ByteArray::class.java), + ArgumentMatchers.anyInt(), + ) + }, Mockito.times(0)) } @Test @@ -975,18 +659,16 @@ class KeyboardLayoutManagerTests { Mockito.doReturn(false).`when`(keyboardLayoutManager).isVirtualDevice( ArgumentMatchers.eq(keyboardDevice.id) ) - NewSettingsApiFlag(true).use { - keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) - ExtendedMockito.verify( - notificationManager, - Mockito.times(1) - ).notifyAsUser( - ArgumentMatchers.isNull(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any(), - ArgumentMatchers.any() - ) - } + keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) + ExtendedMockito.verify( + notificationManager, + Mockito.times(1) + ).notifyAsUser( + ArgumentMatchers.isNull(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) } @Test @@ -996,18 +678,16 @@ class KeyboardLayoutManagerTests { Mockito.doReturn(true).`when`(keyboardLayoutManager).isVirtualDevice( ArgumentMatchers.eq(keyboardDevice.id) ) - NewSettingsApiFlag(true).use { - keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) - ExtendedMockito.verify( - notificationManager, - Mockito.never() - ).notifyAsUser( - ArgumentMatchers.isNull(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any(), - ArgumentMatchers.any() - ) - } + keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) + ExtendedMockito.verify( + notificationManager, + Mockito.never() + ).notifyAsUser( + ArgumentMatchers.isNull(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) } private fun assertCorrectLayout( @@ -1019,7 +699,7 @@ class KeyboardLayoutManagerTests { device.identifier, USER_ID, imeInfo, imeSubtype ) assertEquals( - "New UI: getDefaultKeyboardLayoutForInputDevice should return $expectedLayout", + "getDefaultKeyboardLayoutForInputDevice should return $expectedLayout", expectedLayout, result.layoutDescriptor ) @@ -1123,21 +803,4 @@ class KeyboardLayoutManagerTests { info.serviceInfo.name = RECEIVER_NAME return info } - - private inner class NewSettingsApiFlag constructor(enabled: Boolean) : AutoCloseable { - init { - Settings.Global.putString( - context.contentResolver, - "settings_new_keyboard_ui", enabled.toString() - ) - } - - override fun close() { - Settings.Global.putString( - context.contentResolver, - "settings_new_keyboard_ui", - "" - ) - } - } } diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java index d9a4c261ee15..5cdfb2858b33 100644 --- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java @@ -90,7 +90,7 @@ public class LegacyProtoLogImplTest { //noinspection ResultOfMethodCallIgnored mFile.delete(); mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename, - 1024 * 1024, mReader, 1024, new TreeMap<>()); + 1024 * 1024, mReader, 1024, new TreeMap<>(), () -> {}); } @After diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java index a96389046d6a..001a09a0225a 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoDataSourceTest.java @@ -67,7 +67,7 @@ public class PerfettoDataSourceTest { @Test public void allEnabledTraceMode() { - final ProtoLogDataSource ds = new ProtoLogDataSource(() -> {}, () -> {}, () -> {}); + final ProtoLogDataSource ds = new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {}); final ProtoLogDataSource.TlsState tlsState = createTlsState( DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig( @@ -154,7 +154,7 @@ public class PerfettoDataSourceTest { private ProtoLogDataSource.TlsState createTlsState( DataSourceConfigOuterClass.DataSourceConfig config) { final ProtoLogDataSource ds = - Mockito.spy(new ProtoLogDataSource(() -> {}, () -> {}, () -> {})); + Mockito.spy(new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {})); ProtoInputStream configStream = new ProtoInputStream(config.toByteArray()); final ProtoLogDataSource.Instance dsInstance = Mockito.spy( diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java index 548adeff07b2..f6ac080ebf73 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java @@ -65,6 +65,7 @@ import java.io.IOException; import java.util.List; import java.util.Random; import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicInteger; import perfetto.protos.Protolog; import perfetto.protos.ProtologCommon; @@ -95,6 +96,7 @@ public class PerfettoProtoLogImplTest { private PerfettoProtoLogImpl mProtoLog; private Protolog.ProtoLogViewerConfig.Builder mViewerConfigBuilder; private File mFile; + private Runnable mCacheUpdater; private ProtoLogViewerConfigReader mReader; @@ -152,9 +154,11 @@ public class PerfettoProtoLogImplTest { Mockito.when(viewerConfigInputStreamProvider.getInputStream()) .thenAnswer(it -> new ProtoInputStream(mViewerConfigBuilder.build().toByteArray())); + mCacheUpdater = () -> {}; mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); - mProtoLog = - new PerfettoProtoLogImpl(viewerConfigInputStreamProvider, mReader, new TreeMap<>()); + mProtoLog = new PerfettoProtoLogImpl( + viewerConfigInputStreamProvider, mReader, new TreeMap<>(), + () -> mCacheUpdater.run()); } @After @@ -500,7 +504,8 @@ public class PerfettoProtoLogImplTest { PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder().enableProtoLog(true, List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( - TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, true))) + TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, + true))) .build(); try { traceMonitor.start(); @@ -526,6 +531,142 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(stacktrace).contains("stackTraceTrimmed"); } + @Test + public void cacheIsUpdatedWhenTracesStartAndStop() { + final AtomicInteger cacheUpdateCallCount = new AtomicInteger(0); + mCacheUpdater = cacheUpdateCallCount::incrementAndGet; + + PerfettoTraceMonitor traceMonitor1 = + PerfettoTraceMonitor.newBuilder().enableProtoLog(true, + List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( + TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, + false))) + .build(); + + PerfettoTraceMonitor traceMonitor2 = + PerfettoTraceMonitor.newBuilder().enableProtoLog(true, + List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( + TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, + false))) + .build(); + + Truth.assertThat(cacheUpdateCallCount.get()).isEqualTo(0); + + try { + traceMonitor1.start(); + + Truth.assertThat(cacheUpdateCallCount.get()).isEqualTo(1); + + try { + traceMonitor2.start(); + + Truth.assertThat(cacheUpdateCallCount.get()).isEqualTo(2); + } finally { + traceMonitor2.stop(mWriter); + } + + Truth.assertThat(cacheUpdateCallCount.get()).isEqualTo(3); + + } finally { + traceMonitor1.stop(mWriter); + } + + Truth.assertThat(cacheUpdateCallCount.get()).isEqualTo(4); + } + + @Test + public void isEnabledUpdatesBasedOnRunningTraces() { + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isTrue(); + + PerfettoTraceMonitor traceMonitor1 = + PerfettoTraceMonitor.newBuilder().enableProtoLog(true, + List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( + TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, + false))) + .build(); + + PerfettoTraceMonitor traceMonitor2 = + PerfettoTraceMonitor.newBuilder().enableProtoLog(true, + List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( + TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, + false))) + .build(); + + try { + traceMonitor1.start(); + + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + .isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + .isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + .isTrue(); + + try { + traceMonitor2.start(); + + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + .isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, + LogLevel.VERBOSE)).isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + .isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + .isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + .isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + .isTrue(); + } finally { + traceMonitor2.stop(mWriter); + } + + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + .isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + .isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + .isTrue(); + } finally { + traceMonitor1.stop(mWriter); + } + + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + .isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + .isTrue(); + } + private enum TestProtoLogGroup implements IProtoLogGroup { TEST_GROUP(true, true, false, "TEST_TAG"); diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java index 081da11f2aa8..489ef4444e1d 100644 --- a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java +++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java @@ -66,6 +66,7 @@ import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; @@ -220,43 +221,36 @@ public class CrashRecoveryTest { 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; - } + watchdog.noteBoot(); + 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(); - } + 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(); - } + 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(); - } + 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(); - } + watchdog.noteBoot(); + verify(rescuePartyObserver).executeBootLoopMitigation(6); verify(rescuePartyObserver, never()).executeBootLoopMitigation(7); } @@ -268,11 +262,11 @@ public class CrashRecoveryTest { 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); @@ -280,19 +274,16 @@ public class CrashRecoveryTest { 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(); - } + 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(); - } + watchdog.noteBoot(); + verify(rollbackObserver, never()).executeBootLoopMitigation(3); } @@ -305,27 +296,21 @@ public class CrashRecoveryTest { 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; - } + watchdog.noteBoot(); + 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; - } + watchdog.noteBoot(); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); verify(rollbackObserver).executeBootLoopMitigation(1); verify(rollbackObserver, never()).executeBootLoopMitigation(2); @@ -333,43 +318,46 @@ public class CrashRecoveryTest { 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(); - } + 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(); - } + 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(); - } + 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(); - } + 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(); - } + watchdog.noteBoot(); + verify(rescuePartyObserver).executeBootLoopMitigation(6); verify(rescuePartyObserver, never()).executeBootLoopMitigation(7); verify(rollbackObserver, never()).executeBootLoopMitigation(3); + + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1); + Mockito.reset(rescuePartyObserver); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(1); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); } RollbackPackageHealthObserver setUpRollbackPackageHealthObserver(PackageWatchdog watchdog) { @@ -506,16 +494,9 @@ public class CrashRecoveryTest { } 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( + 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 @@ -640,5 +621,16 @@ public class CrashRecoveryTest { public long uptimeMillis() { return mUpTimeMillis; } + public void moveTimeForward(long milliSeconds) { + mUpTimeMillis += milliSeconds; + } + } + + private void moveTimeForwardAndDispatch(long milliSeconds) { + // Exhaust all due runnables now which shouldn't be executed after time-leap + mTestLooper.dispatchAll(); + mTestClock.moveTimeForward(milliSeconds); + mTestLooper.moveTimeForward(milliSeconds); + mTestLooper.dispatchAll(); } } diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index 4f27e06083ba..093923f3ed53 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -45,13 +45,13 @@ import android.os.test.TestLooper; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.util.AtomicFile; +import android.util.LongArrayQueue; import android.util.Xml; -import android.utils.LongArrayQueue; -import android.utils.XmlUtils; import androidx.test.InstrumentationRegistry; import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.PackageWatchdog.HealthCheckState; @@ -1224,7 +1224,7 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); watchdog.registerHealthObserver(bootObserver); - for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) { + for (int i = 0; i < 15; i++) { watchdog.noteBoot(); } assertThat(bootObserver.mitigatedBootLoop()).isTrue(); @@ -1262,22 +1262,6 @@ 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_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 @@ -1306,7 +1290,7 @@ public class PackageWatchdogTest { bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); watchdog.registerHealthObserver(bootObserver1); watchdog.registerHealthObserver(bootObserver2); - for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) { + for (int i = 0; i < 15; i++) { watchdog.noteBoot(); } assertThat(bootObserver1.mitigatedBootLoop()).isTrue(); @@ -1349,9 +1333,7 @@ public class PackageWatchdogTest { 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); @@ -1360,38 +1342,7 @@ public class PackageWatchdogTest { 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)); @@ -1642,8 +1593,7 @@ public class PackageWatchdogTest { mSpyBootThreshold = spy(watchdog.new BootThreshold( PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, - PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); watchdog.saveAllObserversBootMitigationCountToMetadata(filePath); @@ -1798,16 +1748,9 @@ public class PackageWatchdogTest { mCrashRecoveryPropertiesMap = new HashMap<>(); 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( + 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/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp index a16a7eafc8e8..f0bea3f3c28a 100644 --- a/tests/UsbManagerTests/Android.bp +++ b/tests/UsbManagerTests/Android.bp @@ -21,6 +21,7 @@ package { // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_android_usb", } android_test { diff --git a/tests/UsbTests/Android.bp b/tests/UsbTests/Android.bp index 92c271165ad7..c012cce494e2 100644 --- a/tests/UsbTests/Android.bp +++ b/tests/UsbTests/Android.bp @@ -21,6 +21,7 @@ package { // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_android_usb", } android_test { @@ -36,6 +37,8 @@ android_test { "services.usb", "truth", "UsbManagerTestLib", + "android.hardware.usb.flags-aconfig-java", + "flag-junit", ], jni_libs: [ // Required for ExtendedMockito diff --git a/tests/UsbTests/TEST_MAPPING b/tests/UsbTests/TEST_MAPPING new file mode 100644 index 000000000000..70134b818722 --- /dev/null +++ b/tests/UsbTests/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "UsbTests" + } + ] +}
\ No newline at end of file diff --git a/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java b/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java new file mode 100644 index 000000000000..b506d74d6500 --- /dev/null +++ b/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.usb; + +import static android.hardware.usb.UsbOperationInternal.USB_OPERATION_ERROR_INTERNAL; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.hardware.usb.IUsbOperationInternal; +import android.hardware.usb.flags.Flags; +import android.os.RemoteException; +import android.os.UserManager; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import androidx.test.runner.AndroidJUnit4; + +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; + +/** + * Tests for {@link com.android.server.usb.UsbService} + */ +@RunWith(AndroidJUnit4.class) +public class UsbServiceTest { + + @Mock + private Context mContext; + @Mock + private UsbPortManager mUsbPortManager; + @Mock + private UsbAlsaManager mUsbAlsaManager; + @Mock + private UserManager mUserManager; + @Mock + private UsbSettingsManager mUsbSettingsManager; + @Mock + private IUsbOperationInternal mIUsbOperationInternal; + + private static final String TEST_PORT_ID = "123"; + + private static final int TEST_TRANSACTION_ID = 1; + + private static final int TEST_FIRST_CALLER_ID = 1000; + + private static final int TEST_SECOND_CALLER_ID = 2000; + + private UsbService mUsbService; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mUsbService = new UsbService(mContext, mUsbPortManager, mUsbAlsaManager, mUserManager, + mUsbSettingsManager); + } + + /** + * Verify enableUsbData successfully disables USB port without error + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING) + public void usbPort_SuccessfullyDisabled() { + boolean enableState = false; + when(mUsbPortManager.enableUsbData(TEST_PORT_ID, enableState, TEST_TRANSACTION_ID, + mIUsbOperationInternal, null)).thenReturn(true); + + assertTrue(mUsbService.enableUsbDataInternal(TEST_PORT_ID, enableState, + TEST_TRANSACTION_ID, mIUsbOperationInternal, TEST_FIRST_CALLER_ID)); + + verify(mUsbPortManager, times(1)).enableUsbData(TEST_PORT_ID, + enableState, TEST_TRANSACTION_ID, mIUsbOperationInternal, null); + verifyZeroInteractions(mIUsbOperationInternal); + } + + /** + * Verify enableUsbData successfully enables USB port without error given no other stakers + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING) + public void usbPortWhenNoOtherStakers_SuccessfullyEnabledUsb() { + boolean enableState = true; + when(mUsbPortManager.enableUsbData(TEST_PORT_ID, enableState, TEST_TRANSACTION_ID, + mIUsbOperationInternal, null)) + .thenReturn(true); + + assertTrue(mUsbService.enableUsbDataInternal(TEST_PORT_ID, enableState, + TEST_TRANSACTION_ID, mIUsbOperationInternal, TEST_FIRST_CALLER_ID)); + verifyZeroInteractions(mIUsbOperationInternal); + } + + /** + * Verify enableUsbData does not enable USB port if other stakers are present + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING) + public void usbPortWithOtherStakers_DoesNotToEnableUsb() throws RemoteException { + mUsbService.enableUsbDataInternal(TEST_PORT_ID, false, TEST_TRANSACTION_ID, + mIUsbOperationInternal, TEST_FIRST_CALLER_ID); + clearInvocations(mUsbPortManager); + + assertFalse(mUsbService.enableUsbDataInternal(TEST_PORT_ID, true, + TEST_TRANSACTION_ID, mIUsbOperationInternal, TEST_SECOND_CALLER_ID)); + + verifyZeroInteractions(mUsbPortManager); + verify(mIUsbOperationInternal).onOperationComplete(USB_OPERATION_ERROR_INTERNAL); + } + + /** + * Verify enableUsbDataWhileDockedInternal does not enable USB port if other stakers are present + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING) + public void enableUsbWhileDockedWhenThereAreOtherStakers_DoesNotEnableUsb() + throws RemoteException { + mUsbService.enableUsbDataInternal(TEST_PORT_ID, false, TEST_TRANSACTION_ID, + mIUsbOperationInternal, TEST_FIRST_CALLER_ID); + + mUsbService.enableUsbDataWhileDockedInternal(TEST_PORT_ID, 0, + mIUsbOperationInternal, TEST_SECOND_CALLER_ID); + + verify(mUsbPortManager, never()).enableUsbDataWhileDocked(any(), + anyLong(), any(), any()); + verify(mIUsbOperationInternal).onOperationComplete(USB_OPERATION_ERROR_INTERNAL); + } + + /** + * Verify enableUsbDataWhileDockedInternal does enable USB port if other stakers are + * not present + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING) + public void enableUsbWhileDockedWhenThereAreNoStakers_SuccessfullyEnableUsb() + throws RemoteException { + mUsbService.enableUsbDataWhileDockedInternal(TEST_PORT_ID, TEST_TRANSACTION_ID, + mIUsbOperationInternal, TEST_SECOND_CALLER_ID); + + verify(mUsbPortManager, times(1)) + .enableUsbDataWhileDocked(TEST_PORT_ID, TEST_TRANSACTION_ID, + mIUsbOperationInternal, null); + verifyZeroInteractions(mIUsbOperationInternal); + } +} diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp index a012dca19904..dced50d7ee3b 100644 --- a/tools/app_metadata_bundles/Android.bp +++ b/tools/app_metadata_bundles/Android.bp @@ -13,6 +13,9 @@ java_library_host { srcs: [ "src/lib/java/**/*.java", ], + static_libs: [ + "guava", + ], } java_binary_host { diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java index 9dd55314e844..c1c520e99cac 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java @@ -16,7 +16,10 @@ package com.android.asllib; +import com.android.asllib.marshallable.AndroidSafetyLabel; +import com.android.asllib.marshallable.AndroidSafetyLabelFactory; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; 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 deleted file mode 100644 index a0a75377e988..000000000000 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java +++ /dev/null @@ -1,156 +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.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/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java index cdb559b52c0e..112b92c9aebb 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; 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/marshallable/AndroidSafetyLabelFactory.java index 3dc725b5452b..b69c30f7f522 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java index f94b6591cd10..3f1ddebefe99 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfo.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java index 26d94c16c7f0..59a437d7ece5 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AppInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; -import java.util.Arrays; import java.util.List; public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { @@ -37,31 +37,22 @@ public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { String title = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_TITLE); String description = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_DESCRIPTION); - Boolean containsAds = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_CONTAINS_ADS); - Boolean obeyAps = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_OBEY_APS); + Boolean containsAds = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_CONTAINS_ADS, true); + Boolean obeyAps = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_OBEY_APS, true); Boolean adsFingerprinting = - XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_ADS_FINGERPRINTING); + XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_ADS_FINGERPRINTING, true); Boolean securityFingerprinting = - XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_SECURITY_FINGERPRINTING); + XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_SECURITY_FINGERPRINTING, true); String privacyPolicy = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_PRIVACY_POLICY); List<String> securityEndpoints = - Arrays.stream( - appInfoEle - .getAttribute(XmlUtils.HR_ATTR_SECURITY_ENDPOINTS) - .split("\\|")) - .toList(); + XmlUtils.getPipelineSplitAttr( + appInfoEle, XmlUtils.HR_ATTR_SECURITY_ENDPOINTS, true); List<String> firstPartyEndpoints = - Arrays.stream( - appInfoEle - .getAttribute(XmlUtils.HR_ATTR_FIRST_PARTY_ENDPOINTS) - .split("\\|")) - .toList(); + XmlUtils.getPipelineSplitAttr( + appInfoEle, XmlUtils.HR_ATTR_FIRST_PARTY_ENDPOINTS, true); List<String> serviceProviderEndpoints = - Arrays.stream( - appInfoEle - .getAttribute(XmlUtils.HR_ATTR_SERVICE_PROVIDER_ENDPOINTS) - .split("\\|")) - .toList(); + XmlUtils.getPipelineSplitAttr( + appInfoEle, XmlUtils.HR_ATTR_SERVICE_PROVIDER_ENDPOINTS, true); String category = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_CATEGORY); String email = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_EMAIL); String website = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_WEBSITE, false); 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/marshallable/AslMarshallable.java index 4e64ab0c53c1..48747ccbcff6 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallable.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import org.w3c.dom.Document; import org.w3c.dom.Element; 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/marshallable/AslMarshallableFactory.java index b8f9f0ef6235..a49b3e77155b 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AslMarshallableFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import com.android.asllib.util.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/marshallable/DataCategory.java index b9e06fbdfc7e..4d67162b442d 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java @@ -14,7 +14,11 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.DataCategoryConstants; +import com.android.asllib.util.DataTypeConstants; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; 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/marshallable/DataCategoryFactory.java index d2b671271561..37d99e7ef85e 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategoryFactory.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; +import com.android.asllib.util.DataTypeConstants; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; @@ -30,11 +32,17 @@ public class DataCategoryFactory implements AslMarshallableFactory<DataCategory> String categoryName = null; Map<String, DataType> dataTypeMap = new LinkedHashMap<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)) { + categoryName = XmlUtils.getStringAttr(ele, XmlUtils.HR_ATTR_DATA_CATEGORY, true); + String dataTypeName = XmlUtils.getStringAttr(ele, XmlUtils.HR_ATTR_DATA_TYPE, true); + if (!DataTypeConstants.getValidDataTypes().containsKey(categoryName)) { throw new MalformedXmlException( - String.format("Unrecognized data type name: %s", dataTypeName)); + String.format("Unrecognized data category %s", categoryName)); + } + if (!DataTypeConstants.getValidDataTypes().get(categoryName).contains(dataTypeName)) { + throw new MalformedXmlException( + String.format( + "Unrecognized data type name %s for category %s", + dataTypeName, categoryName)); } dataTypeMap.put( dataTypeName, new DataTypeFactory().createFromHrElements(XmlUtils.listOf(ele))); 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/marshallable/DataLabels.java index 96ec93c28c87..7516faf9f77a 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java @@ -14,7 +14,10 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.DataCategoryConstants; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -41,24 +44,23 @@ public class DataLabels implements AslMarshallable { } /** - * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to - * {@link DataCategory} + * Returns the data accessed {@link Map} of {@link 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} + * Returns the data collected {@link Map} of {@link 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} + * Returns the data shared {@link Map} of {@link DataCategoryConstants} to {@link DataCategory} */ public Map<String, DataCategory> getDataShared() { return mDataShared; @@ -70,7 +72,7 @@ public class DataLabels implements AslMarshallable { Element dataLabelsEle = XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS); - maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED); + maybeAppendDataUsages(doc, dataLabelsEle, mDataAccessed, XmlUtils.OD_NAME_DATA_ACCESSED); maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED); maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED); 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/marshallable/DataLabelsFactory.java index 79edab7631f0..dc77fd08aa53 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabelsFactory.java @@ -14,10 +14,12 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import com.android.asllib.util.AslgenUtil; +import com.android.asllib.util.DataCategoryConstants; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; import org.w3c.dom.NodeList; 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/marshallable/DataType.java index d011cfeef363..347136237966 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; 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/marshallable/DataTypeFactory.java index 27c1b599fec7..ed434cda0823 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataTypeFactory.java @@ -14,30 +14,40 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; -import java.util.Arrays; +import java.util.HashSet; 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) { + public DataType createFromHrElements(List<Element> elements) throws MalformedXmlException { Element hrDataTypeEle = XmlUtils.getSingleElement(elements); String dataTypeName = hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE); List<DataType.Purpose> purposes = - Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|")) + XmlUtils.getPipelineSplitAttr(hrDataTypeEle, XmlUtils.HR_ATTR_PURPOSES, true) + .stream() .map(DataType.Purpose::forString) .collect(Collectors.toList()); + if (purposes.isEmpty()) { + throw new MalformedXmlException(String.format("Found no purpose in: %s", dataTypeName)); + } + if (new HashSet<>(purposes).size() != purposes.size()) { + throw new MalformedXmlException( + String.format("Found non-unique purposes in: %s", dataTypeName)); + } Boolean isCollectionOptional = - XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL); + XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL, false); Boolean isSharingOptional = - XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL); - Boolean ephemeral = XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_EPHEMERAL); + XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL, false); + Boolean ephemeral = XmlUtils.getBoolAttr(hrDataTypeEle, XmlUtils.HR_ATTR_EPHEMERAL, false); return new DataType( dataTypeName, purposes, isCollectionOptional, isSharingOptional, ephemeral); } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java index 44a5b129e428..382a1f0d0eca 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfo.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java index 4961892b10c3..b5310bac232a 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DeveloperInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; 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/marshallable/SafetyLabels.java index 40ef48dc5334..22c3fd8f2a1c 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; 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/marshallable/SafetyLabelsFactory.java index ab81b1d56033..6bf8ef3df32d 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; @@ -40,7 +41,9 @@ public class SafetyLabelsFactory implements AslMarshallableFactory<SafetyLabels> .createFromHrElements( XmlUtils.listOf( XmlUtils.getSingleChildElement( - safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS))); + safetyLabelsEle, + XmlUtils.HR_TAG_DATA_LABELS, + false))); return new SafetyLabels(version, dataLabels); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java index 93d9c2b080c5..595d748b59af 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabel.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java index c8c1c7beba24..f99955993d6c 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SystemAppSafetyLabelFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java index 88717b9568b8..ddd3557616ca 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfo.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; + +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java index 13a7eb62fedd..d9c2af41fcac 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/TransparencyInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.marshallable; import com.android.asllib.util.AslgenUtil; import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; import org.w3c.dom.Element; 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/util/DataCategoryConstants.java index b364c8b37194..b5ae54c5bc00 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataCategoryConstants.java @@ -14,8 +14,11 @@ * limitations under the License. */ -package com.android.asllib; +package com.android.asllib.util; +import com.android.asllib.marshallable.DataCategory; +import com.android.asllib.marshallable.DataType; +import com.android.asllib.marshallable.SafetyLabels; import java.util.Arrays; import java.util.Collections; diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataTypeConstants.java new file mode 100644 index 000000000000..358d575c1d56 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/DataTypeConstants.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import com.android.asllib.marshallable.DataCategory; +import com.android.asllib.marshallable.DataType; +import com.android.asllib.marshallable.SafetyLabels; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import java.util.Map; +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_PHYSICAL_ADDRESS = "physical_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 */ + private static final Map<String, Set<String>> VALID_DATA_TYPES = + new ImmutableMap.Builder<String, Set<String>>() + .put( + DataCategoryConstants.CATEGORY_PERSONAL, + ImmutableSet.of( + DataTypeConstants.TYPE_NAME, + DataTypeConstants.TYPE_EMAIL_ADDRESS, + DataTypeConstants.TYPE_PHYSICAL_ADDRESS, + DataTypeConstants.TYPE_PHONE_NUMBER, + DataTypeConstants.TYPE_RACE_ETHNICITY, + DataTypeConstants.TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS, + DataTypeConstants.TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY, + DataTypeConstants.TYPE_PERSONAL_IDENTIFIERS, + DataTypeConstants.TYPE_OTHER)) + .put( + DataCategoryConstants.CATEGORY_FINANCIAL, + ImmutableSet.of( + DataTypeConstants.TYPE_CARD_BANK_ACCOUNT, + DataTypeConstants.TYPE_PURCHASE_HISTORY, + DataTypeConstants.TYPE_CREDIT_SCORE, + DataTypeConstants.TYPE_OTHER)) + .put( + DataCategoryConstants.CATEGORY_LOCATION, + ImmutableSet.of( + DataTypeConstants.TYPE_APPROX_LOCATION, + DataTypeConstants.TYPE_PRECISE_LOCATION)) + .put( + DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE, + ImmutableSet.of( + DataTypeConstants.TYPE_EMAILS, + DataTypeConstants.TYPE_TEXT_MESSAGES, + DataTypeConstants.TYPE_OTHER)) + .put( + DataCategoryConstants.CATEGORY_PHOTO_VIDEO, + ImmutableSet.of( + DataTypeConstants.TYPE_PHOTOS, DataTypeConstants.TYPE_VIDEOS)) + .put( + DataCategoryConstants.CATEGORY_AUDIO, + ImmutableSet.of( + DataTypeConstants.TYPE_SOUND_RECORDINGS, + DataTypeConstants.TYPE_MUSIC_FILES, + DataTypeConstants.TYPE_OTHER)) + .put( + DataCategoryConstants.CATEGORY_STORAGE, + ImmutableSet.of(DataTypeConstants.TYPE_FILES_DOCS)) + .put( + DataCategoryConstants.CATEGORY_HEALTH_FITNESS, + ImmutableSet.of( + DataTypeConstants.TYPE_HEALTH, DataTypeConstants.TYPE_FITNESS)) + .put( + DataCategoryConstants.CATEGORY_CONTACTS, + ImmutableSet.of(DataTypeConstants.TYPE_CONTACTS)) + .put( + DataCategoryConstants.CATEGORY_CALENDAR, + ImmutableSet.of(DataTypeConstants.TYPE_CALENDAR)) + .put( + DataCategoryConstants.CATEGORY_IDENTIFIERS, + ImmutableSet.of(DataTypeConstants.TYPE_OTHER)) + .put( + DataCategoryConstants.CATEGORY_APP_PERFORMANCE, + ImmutableSet.of( + DataTypeConstants.TYPE_CRASH_LOGS, + DataTypeConstants.TYPE_PERFORMANCE_DIAGNOSTICS, + DataTypeConstants.TYPE_OTHER)) + .put( + DataCategoryConstants.CATEGORY_ACTIONS_IN_APP, + ImmutableSet.of( + DataTypeConstants.TYPE_USER_INTERACTION, + DataTypeConstants.TYPE_IN_APP_SEARCH_HISTORY, + DataTypeConstants.TYPE_INSTALLED_APPS, + DataTypeConstants.TYPE_USER_GENERATED_CONTENT, + DataTypeConstants.TYPE_OTHER)) + .put( + DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING, + ImmutableSet.of(DataTypeConstants.TYPE_WEB_BROWSING_HISTORY)) + .buildOrThrow(); + + /** Returns {@link Set} of valid {@link String} category keys */ + public static Map<String, Set<String>> getValidDataTypes() { + return VALID_DATA_TYPES; + } + + private DataTypeConstants() { + /* do nothing - hide constructor */ + } +} 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/util/XmlUtils.java index cc8fe79cb579..691f92fca54b 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package com.android.asllib; - -import com.android.asllib.util.MalformedXmlException; +package com.android.asllib.util; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -259,21 +257,42 @@ public class XmlUtils { } /** Tries getting required version attribute and throws exception if it doesn't exist */ - public static Long tryGetVersion(Element ele) { + public static Long tryGetVersion(Element ele) throws MalformedXmlException { long version; try { version = Long.parseLong(ele.getAttribute(XmlUtils.HR_ATTR_VERSION)); } catch (Exception e) { - throw new IllegalArgumentException( + throw new MalformedXmlException( String.format( "Malformed or missing required version in: %s", ele.getTagName())); } return version; } - /** Gets an optional Boolean attribute. */ - public static Boolean getBoolAttr(Element ele, String attrName) { - return XmlUtils.fromString(ele.getAttribute(attrName)); + /** Gets a pipeline-split attribute. */ + public static List<String> getPipelineSplitAttr(Element ele, String attrName, boolean required) + throws MalformedXmlException { + List<String> list = Arrays.stream(ele.getAttribute(attrName).split("\\|")).toList(); + if ((list.isEmpty() || list.get(0).isEmpty()) && required) { + throw new MalformedXmlException( + String.format( + "Delimited string %s was required but missing, in %s.", + attrName, ele.getTagName())); + } + return list; + } + + /** Gets a Boolean attribute. */ + public static Boolean getBoolAttr(Element ele, String attrName, boolean required) + throws MalformedXmlException { + Boolean b = XmlUtils.fromString(ele.getAttribute(attrName)); + if (b == null && required) { + throw new MalformedXmlException( + String.format( + "Boolean %s was required but missing, in %s.", + attrName, ele.getTagName())); + } + return b; } /** Gets a required String attribute. */ diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java new file mode 100644 index 000000000000..03e8ac6d11c0 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AllTests.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable.AndroidSafetyLabelTest; +import com.android.asllib.marshallable.DataCategoryTest; +import com.android.asllib.marshallable.DataLabelsTest; +import com.android.asllib.marshallable.DeveloperInfoTest; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + AslgenTests.class, + AndroidSafetyLabelTest.class, + DeveloperInfoTest.class, + DataCategoryTest.class, + DataLabelsTest.class, +}) +public class AllTests {} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AslgenTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java index 3026f8bec2ed..5f43008d3dc6 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/aslgen/AslgenTests.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java @@ -14,40 +14,26 @@ * limitations under the License. */ -package com.android.aslgen; +package com.android.asllib; import static org.junit.Assert.assertEquals; -import com.android.asllib.AndroidSafetyLabel; -import com.android.asllib.AslConverter; +import com.android.asllib.marshallable.AndroidSafetyLabel; +import com.android.asllib.testutils.TestUtils; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; -import org.xml.sax.SAXException; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; 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; - @RunWith(JUnit4.class) public class AslgenTests { - private static final String VALID_MAPPINGS_PATH = "com/android/aslgen/validmappings"; + private static final String VALID_MAPPINGS_PATH = "com/android/asllib/validmappings"; private static final List<String> VALID_MAPPINGS_SUBDIRS = List.of("location", "contacts"); private static final String HR_XML_FILENAME = "hr.xml"; private static final String OD_XML_FILENAME = "od.xml"; @@ -78,28 +64,9 @@ public class AslgenTests { String out = AslConverter.getXmlAsString(asl, AslConverter.Format.ON_DEVICE); System.out.println("out: " + out); - assertEquals(getFormattedXml(out), getFormattedXml(odContents)); + assertEquals( + TestUtils.getFormattedXml(out, false), + TestUtils.getFormattedXml(odContents, false)); } } - - private static String getFormattedXml(String xmlStr) - throws ParserConfigurationException, IOException, SAXException, TransformerException { - InputStream stream = new ByteArrayInputStream(xmlStr.getBytes(StandardCharsets.UTF_8)); - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - Document document = factory.newDocumentBuilder().parse(stream); - - 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, "no"); - - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - StreamResult streamResult = new StreamResult(outStream); // out - DOMSource domSource = new DOMSource(document); - transformer.transform(domSource, streamResult); - - return outStream.toString(StandardCharsets.UTF_8); - } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java new file mode 100644 index 000000000000..013700728e50 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable; + +import com.android.asllib.testutils.TestUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Document; + +@RunWith(JUnit4.class) +public class AndroidSafetyLabelTest { + private static final String ANDROID_SAFETY_LABEL_HR_PATH = + "com/android/asllib/androidsafetylabel/hr"; + private static final String ANDROID_SAFETY_LABEL_OD_PATH = + "com/android/asllib/androidsafetylabel/od"; + + private static final String MISSING_VERSION_FILE_NAME = "missing-version.xml"; + private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml"; + private static final String WITH_SAFETY_LABELS_FILE_NAME = "with-safety-labels.xml"; + private static final String WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME = + "with-system-app-safety-label.xml"; + private static final String WITH_TRANSPARENCY_INFO_FILE_NAME = "with-transparency-info.xml"; + + private Document mDoc = null; + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + mDoc = TestUtils.document(); + } + + /** Test for android safety label missing version. */ + @Test + public void testAndroidSafetyLabelMissingVersion() throws Exception { + System.out.println("starting testAndroidSafetyLabelMissingVersion."); + hrToOdExpectException(MISSING_VERSION_FILE_NAME); + } + + /** Test for android safety label valid empty. */ + @Test + public void testAndroidSafetyLabelValidEmptyFile() throws Exception { + System.out.println("starting testAndroidSafetyLabelValidEmptyFile."); + testHrToOdAndroidSafetyLabel(VALID_EMPTY_FILE_NAME); + } + + /** Test for android safety label with safety labels. */ + @Test + public void testAndroidSafetyLabelWithSafetyLabels() throws Exception { + System.out.println("starting testAndroidSafetyLabelWithSafetyLabels."); + testHrToOdAndroidSafetyLabel(WITH_SAFETY_LABELS_FILE_NAME); + } + + /** Test for android safety label with system app safety label. */ + @Test + public void testAndroidSafetyLabelWithSystemAppSafetyLabel() throws Exception { + System.out.println("starting testAndroidSafetyLabelWithSystemAppSafetyLabel."); + testHrToOdAndroidSafetyLabel(WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME); + } + + /** Test for android safety label with transparency info. */ + @Test + public void testAndroidSafetyLabelWithTransparencyInfo() throws Exception { + System.out.println("starting testAndroidSafetyLabelWithTransparencyInfo."); + testHrToOdAndroidSafetyLabel(WITH_TRANSPARENCY_INFO_FILE_NAME); + } + + private void hrToOdExpectException(String fileName) { + TestUtils.hrToOdExpectException( + new AndroidSafetyLabelFactory(), ANDROID_SAFETY_LABEL_HR_PATH, fileName); + } + + private void testHrToOdAndroidSafetyLabel(String fileName) throws Exception { + TestUtils.testHrToOd( + mDoc, + new AndroidSafetyLabelFactory(), + ANDROID_SAFETY_LABEL_HR_PATH, + ANDROID_SAFETY_LABEL_OD_PATH, + fileName); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java new file mode 100644 index 000000000000..a015e2eacac5 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable; + +import static org.junit.Assert.assertThrows; + +import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Document; + +import java.nio.file.Paths; +import java.util.List; + +@RunWith(JUnit4.class) +public class AppInfoTest { + private static final String APP_INFO_HR_PATH = "com/android/asllib/appinfo/hr"; + private static final String APP_INFO_OD_PATH = "com/android/asllib/appinfo/od"; + public static final List<String> REQUIRED_FIELD_NAMES = + List.of( + "title", + "description", + "containsAds", + "obeyAps", + "adsFingerprinting", + "securityFingerprinting", + "privacyPolicy", + "securityEndpoints", + "firstPartyEndpoints", + "serviceProviderEndpoints", + "category", + "email"); + public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website"); + + private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; + + private Document mDoc = null; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + mDoc = TestUtils.document(); + } + + /** Test for all fields valid. */ + @Test + public void testAllFieldsValid() throws Exception { + System.out.println("starting testAllFieldsValid."); + testHrToOdAppInfo(ALL_FIELDS_VALID_FILE_NAME); + } + + /** Tests missing required fields fails. */ + @Test + public void testMissingRequiredFields() throws Exception { + System.out.println("Starting testMissingRequiredFields"); + for (String reqField : REQUIRED_FIELD_NAMES) { + System.out.println("testing missing required field: " + reqField); + var appInfoEle = + TestUtils.getElementsFromResource( + Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + appInfoEle.get(0).removeAttribute(reqField); + + assertThrows( + MalformedXmlException.class, + () -> new AppInfoFactory().createFromHrElements(appInfoEle)); + } + } + + /** Tests missing optional fields passes. */ + @Test + public void testMissingOptionalFields() throws Exception { + for (String optField : OPTIONAL_FIELD_NAMES) { + var ele = + TestUtils.getElementsFromResource( + Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + ele.get(0).removeAttribute(optField); + AppInfo appInfo = new AppInfoFactory().createFromHrElements(ele); + appInfo.toOdDomElements(mDoc); + } + } + + private void testHrToOdAppInfo(String fileName) throws Exception { + TestUtils.testHrToOd( + mDoc, new AppInfoFactory(), APP_INFO_HR_PATH, APP_INFO_OD_PATH, fileName); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java new file mode 100644 index 000000000000..822f1753f662 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable; + +import com.android.asllib.testutils.TestUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Document; + +@RunWith(JUnit4.class) +public class DataCategoryTest { + private static final String DATA_CATEGORY_HR_PATH = "com/android/asllib/datacategory/hr"; + private static final String DATA_CATEGORY_OD_PATH = "com/android/asllib/datacategory/od"; + + private static final String VALID_PERSONAL_FILE_NAME = "data-category-personal.xml"; + private static final String VALID_PARTIAL_PERSONAL_FILE_NAME = + "data-category-personal-partial.xml"; + private static final String VALID_FINANCIAL_FILE_NAME = "data-category-financial.xml"; + private static final String VALID_LOCATION_FILE_NAME = "data-category-location.xml"; + private static final String VALID_EMAIL_TEXT_MESSAGE_FILE_NAME = + "data-category-email-text-message.xml"; + private static final String VALID_PHOTO_VIDEO_FILE_NAME = "data-category-photo-video.xml"; + private static final String VALID_AUDIO_FILE_NAME = "data-category-audio.xml"; + private static final String VALID_STORAGE_FILE_NAME = "data-category-storage.xml"; + private static final String VALID_HEALTH_FITNESS_FILE_NAME = "data-category-health-fitness.xml"; + private static final String VALID_CONTACTS_FILE_NAME = "data-category-contacts.xml"; + private static final String VALID_CALENDAR_FILE_NAME = "data-category-calendar.xml"; + private static final String VALID_IDENTIFIERS_FILE_NAME = "data-category-identifiers.xml"; + private static final String VALID_APP_PERFORMANCE_FILE_NAME = + "data-category-app-performance.xml"; + private static final String VALID_ACTIONS_IN_APP_FILE_NAME = "data-category-actions-in-app.xml"; + private static final String VALID_SEARCH_AND_BROWSING_FILE_NAME = + "data-category-search-and-browsing.xml"; + + private static final String EMPTY_PURPOSE_PERSONAL_FILE_NAME = + "data-category-personal-empty-purpose.xml"; + private static final String MISSING_PURPOSE_PERSONAL_FILE_NAME = + "data-category-personal-missing-purpose.xml"; + private static final String UNRECOGNIZED_TYPE_PERSONAL_FILE_NAME = + "data-category-personal-unrecognized-type.xml"; + private static final String UNRECOGNIZED_CATEGORY_FILE_NAME = "data-category-unrecognized.xml"; + + private Document mDoc = null; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + mDoc = TestUtils.document(); + } + + /** Test for data category personal. */ + @Test + public void testDataCategoryPersonal() throws Exception { + System.out.println("starting testDataCategoryPersonal."); + testHrToOdDataCategory(VALID_PERSONAL_FILE_NAME); + } + + /** Test for data category financial. */ + @Test + public void testDataCategoryFinancial() throws Exception { + System.out.println("starting testDataCategoryFinancial."); + testHrToOdDataCategory(VALID_FINANCIAL_FILE_NAME); + } + + /** Test for data category location. */ + @Test + public void testDataCategoryLocation() throws Exception { + System.out.println("starting testDataCategoryLocation."); + testHrToOdDataCategory(VALID_LOCATION_FILE_NAME); + } + + /** Test for data category email text message. */ + @Test + public void testDataCategoryEmailTextMessage() throws Exception { + System.out.println("starting testDataCategoryEmailTextMessage."); + testHrToOdDataCategory(VALID_EMAIL_TEXT_MESSAGE_FILE_NAME); + } + + /** Test for data category photo video. */ + @Test + public void testDataCategoryPhotoVideo() throws Exception { + System.out.println("starting testDataCategoryPhotoVideo."); + testHrToOdDataCategory(VALID_PHOTO_VIDEO_FILE_NAME); + } + + /** Test for data category audio. */ + @Test + public void testDataCategoryAudio() throws Exception { + System.out.println("starting testDataCategoryAudio."); + testHrToOdDataCategory(VALID_AUDIO_FILE_NAME); + } + + /** Test for data category storage. */ + @Test + public void testDataCategoryStorage() throws Exception { + System.out.println("starting testDataCategoryStorage."); + testHrToOdDataCategory(VALID_STORAGE_FILE_NAME); + } + + /** Test for data category health fitness. */ + @Test + public void testDataCategoryHealthFitness() throws Exception { + System.out.println("starting testDataCategoryHealthFitness."); + testHrToOdDataCategory(VALID_HEALTH_FITNESS_FILE_NAME); + } + + /** Test for data category contacts. */ + @Test + public void testDataCategoryContacts() throws Exception { + System.out.println("starting testDataCategoryContacts."); + testHrToOdDataCategory(VALID_CONTACTS_FILE_NAME); + } + + /** Test for data category calendar. */ + @Test + public void testDataCategoryCalendar() throws Exception { + System.out.println("starting testDataCategoryCalendar."); + testHrToOdDataCategory(VALID_CALENDAR_FILE_NAME); + } + + /** Test for data category identifiers. */ + @Test + public void testDataCategoryIdentifiers() throws Exception { + System.out.println("starting testDataCategoryIdentifiers."); + testHrToOdDataCategory(VALID_IDENTIFIERS_FILE_NAME); + } + + /** Test for data category app performance. */ + @Test + public void testDataCategoryAppPerformance() throws Exception { + System.out.println("starting testDataCategoryAppPerformance."); + testHrToOdDataCategory(VALID_APP_PERFORMANCE_FILE_NAME); + } + + /** Test for data category actions in app. */ + @Test + public void testDataCategoryActionsInApp() throws Exception { + System.out.println("starting testDataCategoryActionsInApp."); + testHrToOdDataCategory(VALID_ACTIONS_IN_APP_FILE_NAME); + } + + /** Test for data category search and browsing. */ + @Test + public void testDataCategorySearchAndBrowsing() throws Exception { + System.out.println("starting testDataCategorySearchAndBrowsing."); + testHrToOdDataCategory(VALID_SEARCH_AND_BROWSING_FILE_NAME); + } + + /** Test for data category search and browsing. */ + @Test + public void testMissingOptionalsAllowed() throws Exception { + System.out.println("starting testMissingOptionalsAllowed."); + testHrToOdDataCategory(VALID_PARTIAL_PERSONAL_FILE_NAME); + } + + /** Test for empty purposes. */ + @Test + public void testEmptyPurposesNotAllowed() throws Exception { + System.out.println("starting testEmptyPurposesNotAllowed."); + hrToOdExpectException(EMPTY_PURPOSE_PERSONAL_FILE_NAME); + } + + /** Test for missing purposes. */ + @Test + public void testMissingPurposesNotAllowed() throws Exception { + System.out.println("starting testMissingPurposesNotAllowed."); + hrToOdExpectException(MISSING_PURPOSE_PERSONAL_FILE_NAME); + } + + /** Test for unrecognized type. */ + @Test + public void testUnrecognizedTypeNotAllowed() throws Exception { + System.out.println("starting testUnrecognizedTypeNotAllowed."); + hrToOdExpectException(UNRECOGNIZED_TYPE_PERSONAL_FILE_NAME); + } + + /** Test for unrecognized category. */ + @Test + public void testUnrecognizedCategoryNotAllowed() throws Exception { + System.out.println("starting testUnrecognizedCategoryNotAllowed."); + hrToOdExpectException(UNRECOGNIZED_CATEGORY_FILE_NAME); + } + + private void hrToOdExpectException(String fileName) { + TestUtils.hrToOdExpectException(new DataCategoryFactory(), DATA_CATEGORY_HR_PATH, fileName); + } + + private void testHrToOdDataCategory(String fileName) throws Exception { + TestUtils.testHrToOd( + mDoc, + new DataCategoryFactory(), + DATA_CATEGORY_HR_PATH, + DATA_CATEGORY_OD_PATH, + fileName); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java new file mode 100644 index 000000000000..2be447e182b2 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable; + +import com.android.asllib.testutils.TestUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Document; + +@RunWith(JUnit4.class) +public class DataLabelsTest { + private static final String DATA_LABELS_HR_PATH = "com/android/asllib/datalabels/hr"; + private static final String DATA_LABELS_OD_PATH = "com/android/asllib/datalabels/od"; + + private static final String ACCESSED_VALID_BOOL_FILE_NAME = + "data-labels-accessed-valid-bool.xml"; + private static final String ACCESSED_INVALID_BOOL_FILE_NAME = + "data-labels-accessed-invalid-bool.xml"; + private static final String COLLECTED_VALID_BOOL_FILE_NAME = + "data-labels-collected-valid-bool.xml"; + private static final String COLLECTED_INVALID_BOOL_FILE_NAME = + "data-labels-collected-invalid-bool.xml"; + private static final String SHARED_VALID_BOOL_FILE_NAME = "data-labels-shared-valid-bool.xml"; + private static final String SHARED_INVALID_BOOL_FILE_NAME = + "data-labels-shared-invalid-bool.xml"; + + private Document mDoc = null; + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + mDoc = TestUtils.document(); + } + + /** Test for data labels accessed valid bool. */ + @Test + public void testDataLabelsAccessedValidBool() throws Exception { + System.out.println("starting testDataLabelsAccessedValidBool."); + testHrToOdDataLabels(ACCESSED_VALID_BOOL_FILE_NAME); + } + + /** Test for data labels accessed invalid bool. */ + @Test + public void testDataLabelsAccessedInvalidBool() throws Exception { + System.out.println("starting testDataLabelsAccessedInvalidBool."); + hrToOdExpectException(ACCESSED_INVALID_BOOL_FILE_NAME); + } + + /** Test for data labels collected valid bool. */ + @Test + public void testDataLabelsCollectedValidBool() throws Exception { + System.out.println("starting testDataLabelsCollectedValidBool."); + testHrToOdDataLabels(COLLECTED_VALID_BOOL_FILE_NAME); + } + + /** Test for data labels collected invalid bool. */ + @Test + public void testDataLabelsCollectedInvalidBool() throws Exception { + System.out.println("starting testDataLabelsCollectedInvalidBool."); + hrToOdExpectException(COLLECTED_INVALID_BOOL_FILE_NAME); + } + + /** Test for data labels shared valid bool. */ + @Test + public void testDataLabelsSharedValidBool() throws Exception { + System.out.println("starting testDataLabelsSharedValidBool."); + testHrToOdDataLabels(SHARED_VALID_BOOL_FILE_NAME); + } + + /** Test for data labels shared invalid bool. */ + @Test + public void testDataLabelsSharedInvalidBool() throws Exception { + System.out.println("starting testDataLabelsSharedInvalidBool."); + hrToOdExpectException(SHARED_INVALID_BOOL_FILE_NAME); + } + + private void hrToOdExpectException(String fileName) { + TestUtils.hrToOdExpectException(new DataLabelsFactory(), DATA_LABELS_HR_PATH, fileName); + } + + private void testHrToOdDataLabels(String fileName) throws Exception { + TestUtils.testHrToOd( + mDoc, new DataLabelsFactory(), DATA_LABELS_HR_PATH, DATA_LABELS_OD_PATH, fileName); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java new file mode 100644 index 000000000000..ff8346a526ad --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable; + +import static org.junit.Assert.assertThrows; + +import com.android.asllib.testutils.TestUtils; +import com.android.asllib.util.MalformedXmlException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Document; + +import java.nio.file.Paths; +import java.util.List; + +@RunWith(JUnit4.class) +public class DeveloperInfoTest { + private static final String DEVELOPER_INFO_HR_PATH = "com/android/asllib/developerinfo/hr"; + private static final String DEVELOPER_INFO_OD_PATH = "com/android/asllib/developerinfo/od"; + public static final List<String> REQUIRED_FIELD_NAMES = + List.of("address", "countryRegion", "email", "name", "relationship"); + public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website", "registryId"); + + private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; + + private Document mDoc = null; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + mDoc = TestUtils.document(); + } + + /** Test for all fields valid. */ + @Test + public void testAllFieldsValid() throws Exception { + System.out.println("starting testAllFieldsValid."); + testHrToOdDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME); + } + + /** Tests missing required fields fails. */ + @Test + public void testMissingRequiredFields() throws Exception { + System.out.println("Starting testMissingRequiredFields"); + for (String reqField : REQUIRED_FIELD_NAMES) { + System.out.println("testing missing required field: " + reqField); + var developerInfoEle = + TestUtils.getElementsFromResource( + Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + developerInfoEle.get(0).removeAttribute(reqField); + + assertThrows( + MalformedXmlException.class, + () -> new DeveloperInfoFactory().createFromHrElements(developerInfoEle)); + } + } + + /** Tests missing optional fields passes. */ + @Test + public void testMissingOptionalFields() throws Exception { + for (String optField : OPTIONAL_FIELD_NAMES) { + var developerInfoEle = + TestUtils.getElementsFromResource( + Paths.get(DEVELOPER_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); + developerInfoEle.get(0).removeAttribute(optField); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromHrElements(developerInfoEle); + developerInfo.toOdDomElements(mDoc); + } + } + + private void testHrToOdDeveloperInfo(String fileName) throws Exception { + TestUtils.testHrToOd( + mDoc, + new DeveloperInfoFactory(), + DEVELOPER_INFO_HR_PATH, + DEVELOPER_INFO_OD_PATH, + fileName); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java new file mode 100644 index 000000000000..b62620ef417e --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable; + +import com.android.asllib.testutils.TestUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Document; + +@RunWith(JUnit4.class) +public class SafetyLabelsTest { + private static final String SAFETY_LABELS_HR_PATH = "com/android/asllib/safetylabels/hr"; + private static final String SAFETY_LABELS_OD_PATH = "com/android/asllib/safetylabels/od"; + + private static final String MISSING_VERSION_FILE_NAME = "missing-version.xml"; + private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml"; + private static final String WITH_DATA_LABELS_FILE_NAME = "with-data-labels.xml"; + + private Document mDoc = null; + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + mDoc = TestUtils.document(); + } + + /** Test for safety labels missing version. */ + @Test + public void testSafetyLabelsMissingVersion() throws Exception { + System.out.println("starting testSafetyLabelsMissingVersion."); + hrToOdExpectException(MISSING_VERSION_FILE_NAME); + } + + /** Test for safety labels valid empty. */ + @Test + public void testSafetyLabelsValidEmptyFile() throws Exception { + System.out.println("starting testSafetyLabelsValidEmptyFile."); + testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME); + } + + /** Test for safety labels with data labels. */ + @Test + public void testSafetyLabelsWithDataLabels() throws Exception { + System.out.println("starting testSafetyLabelsWithDataLabels."); + testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME); + } + + private void hrToOdExpectException(String fileName) { + TestUtils.hrToOdExpectException(new SafetyLabelsFactory(), SAFETY_LABELS_HR_PATH, fileName); + } + + private void testHrToOdSafetyLabels(String fileName) throws Exception { + TestUtils.testHrToOd( + mDoc, + new SafetyLabelsFactory(), + SAFETY_LABELS_HR_PATH, + SAFETY_LABELS_OD_PATH, + fileName); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java new file mode 100644 index 000000000000..191091a9e187 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable; + +import com.android.asllib.testutils.TestUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Document; + +@RunWith(JUnit4.class) +public class SystemAppSafetyLabelTest { + private static final String SYSTEM_APP_SAFETY_LABEL_HR_PATH = + "com/android/asllib/systemappsafetylabel/hr"; + private static final String SYSTEM_APP_SAFETY_LABEL_OD_PATH = + "com/android/asllib/systemappsafetylabel/od"; + + private static final String VALID_FILE_NAME = "valid.xml"; + private static final String MISSING_URL_FILE_NAME = "missing-url.xml"; + + private Document mDoc = null; + + /** Logic for setting up tests (empty if not yet needed). */ + public static void main(String[] params) throws Exception {} + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + mDoc = TestUtils.document(); + } + + /** Test for valid. */ + @Test + public void testValid() throws Exception { + System.out.println("starting testValid."); + testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME); + } + + /** Tests missing url. */ + @Test + public void testMissingUrl() throws Exception { + System.out.println("starting testMissingUrl."); + hrToOdExpectException(MISSING_URL_FILE_NAME); + } + + private void hrToOdExpectException(String fileName) { + TestUtils.hrToOdExpectException( + new SystemAppSafetyLabelFactory(), SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName); + } + + private void testHrToOdSystemAppSafetyLabel(String fileName) throws Exception { + TestUtils.testHrToOd( + mDoc, + new SystemAppSafetyLabelFactory(), + SYSTEM_APP_SAFETY_LABEL_HR_PATH, + SYSTEM_APP_SAFETY_LABEL_OD_PATH, + fileName); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java new file mode 100644 index 000000000000..56503f7d6c6b --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.marshallable; + +import com.android.asllib.testutils.TestUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.w3c.dom.Document; + +@RunWith(JUnit4.class) +public class TransparencyInfoTest { + private static final String TRANSPARENCY_INFO_HR_PATH = + "com/android/asllib/transparencyinfo/hr"; + private static final String TRANSPARENCY_INFO_OD_PATH = + "com/android/asllib/transparencyinfo/od"; + + private static final String VALID_EMPTY_FILE_NAME = "valid-empty.xml"; + private static final String WITH_DEVELOPER_INFO_FILE_NAME = "with-developer-info.xml"; + private static final String WITH_APP_INFO_FILE_NAME = "with-app-info.xml"; + + private Document mDoc = null; + + @Before + public void setUp() throws Exception { + System.out.println("set up."); + mDoc = TestUtils.document(); + } + + /** Test for transparency info valid empty. */ + @Test + public void testTransparencyInfoValidEmptyFile() throws Exception { + System.out.println("starting testTransparencyInfoValidEmptyFile."); + testHrToOdTransparencyInfo(VALID_EMPTY_FILE_NAME); + } + + /** Test for transparency info with developer info. */ + @Test + public void testTransparencyInfoWithDeveloperInfo() throws Exception { + System.out.println("starting testTransparencyInfoWithDeveloperInfo."); + testHrToOdTransparencyInfo(WITH_DEVELOPER_INFO_FILE_NAME); + } + + /** Test for transparency info with app info. */ + @Test + public void testTransparencyInfoWithAppInfo() throws Exception { + System.out.println("starting testTransparencyInfoWithAppInfo."); + testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME); + } + + private void testHrToOdTransparencyInfo(String fileName) throws Exception { + TestUtils.testHrToOd( + mDoc, + new TransparencyInfoFactory(), + TRANSPARENCY_INFO_HR_PATH, + TRANSPARENCY_INFO_OD_PATH, + fileName); + } +} diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java new file mode 100644 index 000000000000..faea340ae7bd --- /dev/null +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.testutils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.android.asllib.marshallable.AslMarshallable; +import com.android.asllib.marshallable.AslMarshallableFactory; +import com.android.asllib.util.MalformedXmlException; +import com.android.asllib.util.XmlUtils; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +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 TestUtils { + public static final String HOLDER_TAG_NAME = "holder_of_flattened_for_testing"; + + /** Reads a Resource file into a String. */ + public static String readStrFromResource(Path filePath) throws IOException { + InputStream hrStream = + TestUtils.class.getClassLoader().getResourceAsStream(filePath.toString()); + return new String(hrStream.readAllBytes(), StandardCharsets.UTF_8); + } + + /** Gets List of Element from a path to an existing Resource. */ + public static List<Element> getElementsFromResource(Path filePath) + throws ParserConfigurationException, IOException, SAXException { + String str = readStrFromResource(filePath); + InputStream stream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document document = factory.newDocumentBuilder().parse(stream); + Element root = document.getDocumentElement(); + if (root.getTagName().equals(HOLDER_TAG_NAME)) { + String tagName = + XmlUtils.asElementList(root.getChildNodes()).stream() + .findFirst() + .get() + .getTagName(); + return XmlUtils.asElementList(root.getElementsByTagName(tagName)); + } else { + return List.of(root); + } + } + + /** Reads a Document into a String. */ + public static String docToStr(Document doc, boolean omitXmlDeclaration) + throws TransformerException { + 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, omitXmlDeclaration ? "yes" : "no"); + + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + StreamResult streamResult = new StreamResult(outStream); // out + DOMSource domSource = new DOMSource(doc); + transformer.transform(domSource, streamResult); + + return outStream.toString(StandardCharsets.UTF_8); + } + + /** + * Gets formatted XML for slightly more robust comparison checking than naive string comparison. + */ + public static String getFormattedXml(String xmlStr, boolean omitXmlDeclaration) + throws ParserConfigurationException, IOException, SAXException, TransformerException { + InputStream stream = new ByteArrayInputStream(xmlStr.getBytes(StandardCharsets.UTF_8)); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document document = factory.newDocumentBuilder().parse(stream); + + return docToStr(document, omitXmlDeclaration); + } + + /** Helper for getting a new Document */ + public static Document document() throws ParserConfigurationException { + return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + } + + /** Helper for testing human-readable to on-device conversion expecting exception */ + public static <T extends AslMarshallable> void hrToOdExpectException( + AslMarshallableFactory<T> factory, String hrFolderPath, String fileName) { + assertThrows( + MalformedXmlException.class, + () -> { + factory.createFromHrElements( + TestUtils.getElementsFromResource(Paths.get(hrFolderPath, fileName))); + }); + } + + /** Helper for testing human-readable to on-device conversion */ + public static <T extends AslMarshallable> void testHrToOd( + Document doc, + AslMarshallableFactory<T> factory, + String hrFolderPath, + String odFolderPath, + String fileName) + throws Exception { + AslMarshallable marshallable = + factory.createFromHrElements( + TestUtils.getElementsFromResource(Paths.get(hrFolderPath, fileName))); + + for (var child : marshallable.toOdDomElements(doc)) { + doc.appendChild(child); + } + String converted = TestUtils.docToStr(doc, true); + System.out.println("converted: " + converted); + + String expectedOdContents = + TestUtils.readStrFromResource(Paths.get(odFolderPath, fileName)); + assertEquals( + TestUtils.getFormattedXml(expectedOdContents, true), + TestUtils.getFormattedXml(converted, true)); + } +} diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml new file mode 100644 index 000000000000..ec0cd702fd43 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/missing-version.xml @@ -0,0 +1,3 @@ +<app-metadata-bundles> + +</app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml new file mode 100644 index 000000000000..19bfd826f770 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/valid-empty.xml @@ -0,0 +1 @@ +<app-metadata-bundles version="123456"></app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml new file mode 100644 index 000000000000..53794a1d1c80 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-safety-labels.xml @@ -0,0 +1,4 @@ +<app-metadata-bundles version="123456"> + <safety-labels version="12345"> + </safety-labels> +</app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml new file mode 100644 index 000000000000..7bcde4547933 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-system-app-safety-label.xml @@ -0,0 +1,4 @@ +<app-metadata-bundles version="123456"> +<system-app-safety-label url="www.example.com"> +</system-app-safety-label> +</app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml new file mode 100644 index 000000000000..00bcfa80e9b1 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/hr/with-transparency-info.xml @@ -0,0 +1,4 @@ +<app-metadata-bundles version="123456"> +<transparency-info> +</transparency-info> +</app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml new file mode 100644 index 000000000000..37bdfad4065f --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/valid-empty.xml @@ -0,0 +1,3 @@ +<bundle> + <long name="version" value="123456"/> +</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml new file mode 100644 index 000000000000..74644ed0413c --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-safety-labels.xml @@ -0,0 +1,6 @@ +<bundle> + <long name="version" value="123456"/> + <pbundle_as_map name="safety_labels"> + <long name="version" value="12345"/> + </pbundle_as_map> +</bundle> diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml new file mode 100644 index 000000000000..ef0f549fc46b --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-system-app-safety-label.xml @@ -0,0 +1,6 @@ +<bundle> + <long name="version" value="123456"/> + <pbundle_as_map name="system_app_safety_label"> + <string name="url" value="www.example.com"/> + </pbundle_as_map> +</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml new file mode 100644 index 000000000000..63c5094333cc --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/with-transparency-info.xml @@ -0,0 +1,4 @@ +<bundle> + <long name="version" value="123456"/> + <pbundle_as_map name="transparency_info"/> +</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml new file mode 100644 index 000000000000..883170a2d36f --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/hr/all-fields-valid.xml @@ -0,0 +1,14 @@ +<app-info + title="beervision" + description="a beer app" + containsAds="true" + obeyAps="false" + adsFingerprinting="false" + securityFingerprinting="false" + privacyPolicy="www.example.com" + securityEndpoints="url1|url2|url3" + firstPartyEndpoints="url1" + serviceProviderEndpoints="url55|url56" + category="Food and drink" + email="max@maxloh.com" + website="www.example.com" />
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml new file mode 100644 index 000000000000..6e976a3278de --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/appinfo/od/all-fields-valid.xml @@ -0,0 +1,25 @@ + +<pbundle_as_map name="app_info"> + <string name="title" value="beervision"/> + <string name="description" value="a beer app"/> + <boolean name="contains_ads" value="true"/> + <boolean name="obey_aps" value="false"/> + <boolean name="ads_fingerprinting" value="false"/> + <boolean name="security_fingerprinting" value="false"/> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="security_endpoint" num="3"> + <item value="url1"/> + <item value="url2"/> + <item value="url3"/> + </string-array> + <string-array name="first_party_endpoint" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoint" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <string name="category" value="Food and drink"/> + <string name="email" value="max@maxloh.com"/> + <string name="website" value="www.example.com"/> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-actions-in-app.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-actions-in-app.xml new file mode 100644 index 000000000000..520e525679b8 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-actions-in-app.xml @@ -0,0 +1,17 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="actions_in_app" + dataType="user_interaction" + purposes="analytics" /> + <data-shared dataCategory="actions_in_app" + dataType="in_app_search_history" + purposes="analytics" /> + <data-shared dataCategory="actions_in_app" + dataType="installed_apps" + purposes="analytics" /> + <data-shared dataCategory="actions_in_app" + dataType="user_generated_content" + purposes="analytics" /> + <data-shared dataCategory="actions_in_app" + dataType="other" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-app-performance.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-app-performance.xml new file mode 100644 index 000000000000..0d08e5b5ae4d --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-app-performance.xml @@ -0,0 +1,11 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="app_performance" + dataType="crash_logs" + purposes="analytics" /> + <data-shared dataCategory="app_performance" + dataType="performance_diagnostics" + purposes="analytics" /> + <data-shared dataCategory="app_performance" + dataType="other" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-audio.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-audio.xml new file mode 100644 index 000000000000..b1cf3b44fd4a --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-audio.xml @@ -0,0 +1,11 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="audio" + dataType="sound_recordings" + purposes="analytics" /> + <data-shared dataCategory="audio" + dataType="music_files" + purposes="analytics" /> + <data-shared dataCategory="audio" + dataType="other" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-calendar.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-calendar.xml new file mode 100644 index 000000000000..a723070c43b7 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-calendar.xml @@ -0,0 +1,5 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="calendar" + dataType="calendar" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-contacts.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-contacts.xml new file mode 100644 index 000000000000..2fe28ffe4940 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-contacts.xml @@ -0,0 +1,5 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="contacts" + dataType="contacts" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-email-text-message.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-email-text-message.xml new file mode 100644 index 000000000000..49a326fc43e5 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-email-text-message.xml @@ -0,0 +1,11 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="email_text_message" + dataType="emails" + purposes="analytics" /> + <data-shared dataCategory="email_text_message" + dataType="text_messages" + purposes="analytics" /> + <data-shared dataCategory="email_text_message" + dataType="other" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-financial.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-financial.xml new file mode 100644 index 000000000000..f5de3707ef8a --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-financial.xml @@ -0,0 +1,14 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="financial" + dataType="card_bank_account" + purposes="analytics" /> + <data-shared dataCategory="financial" + dataType="purchase_history" + purposes="analytics" /> + <data-shared dataCategory="financial" + dataType="credit_score" + purposes="analytics" /> + <data-shared dataCategory="financial" + dataType="other" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-health-fitness.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-health-fitness.xml new file mode 100644 index 000000000000..9891f8170bd7 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-health-fitness.xml @@ -0,0 +1,8 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="health_fitness" + dataType="health" + purposes="analytics" /> + <data-shared dataCategory="health_fitness" + dataType="fitness" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-identifiers.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-identifiers.xml new file mode 100644 index 000000000000..3e74da1ad527 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-identifiers.xml @@ -0,0 +1,5 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="identifiers" + dataType="other" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-location.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-location.xml new file mode 100644 index 000000000000..4762f16d64db --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-location.xml @@ -0,0 +1,8 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="location" + dataType="approx_location" + purposes="analytics" /> + <data-shared dataCategory="location" + dataType="precise_location" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-empty-purpose.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-empty-purpose.xml new file mode 100644 index 000000000000..964e178e4dbd --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-empty-purpose.xml @@ -0,0 +1,5 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="personal" + dataType="email_address" + purposes="" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-missing-purpose.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-missing-purpose.xml new file mode 100644 index 000000000000..3ce1288fb2c3 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-missing-purpose.xml @@ -0,0 +1,4 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="personal" + dataType="email_address" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-partial.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-partial.xml new file mode 100644 index 000000000000..68baae30ef4f --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-partial.xml @@ -0,0 +1,8 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="personal" + dataType="name" + purposes="analytics|developer_communications" /> + <data-shared dataCategory="personal" + dataType="email_address" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-unrecognized-type.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-unrecognized-type.xml new file mode 100644 index 000000000000..921a90a3fde7 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal-unrecognized-type.xml @@ -0,0 +1,5 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="personal" + dataType="unrecognized" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal.xml new file mode 100644 index 000000000000..4533773ec9c1 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-personal.xml @@ -0,0 +1,31 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="personal" + dataType="name" + ephemeral="true" + isCollectionOptional="true" + purposes="analytics|developer_communications" /> + <data-shared dataCategory="personal" + dataType="email_address" + purposes="analytics" /> + <data-shared dataCategory="personal" + dataType="physical_address" + purposes="analytics" /> + <data-shared dataCategory="personal" + dataType="phone_number" + purposes="analytics" /> + <data-shared dataCategory="personal" + dataType="race_ethnicity" + purposes="analytics" /> + <data-shared dataCategory="personal" + dataType="political_or_religious_beliefs" + purposes="analytics" /> + <data-shared dataCategory="personal" + dataType="sexual_orientation_or_gender_identity" + purposes="analytics" /> + <data-shared dataCategory="personal" + dataType="personal_identifiers" + purposes="analytics" /> + <data-shared dataCategory="personal" + dataType="other" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-photo-video.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-photo-video.xml new file mode 100644 index 000000000000..234fb265ae55 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-photo-video.xml @@ -0,0 +1,8 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="photo_video" + dataType="photos" + purposes="analytics" /> + <data-shared dataCategory="photo_video" + dataType="videos" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-search-and-browsing.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-search-and-browsing.xml new file mode 100644 index 000000000000..db851633aec5 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-search-and-browsing.xml @@ -0,0 +1,5 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="search_and_browsing" + dataType="web_browsing_history" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-storage.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-storage.xml new file mode 100644 index 000000000000..9aad02de8877 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-storage.xml @@ -0,0 +1,5 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="storage" + dataType="files_docs" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-unrecognized.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-unrecognized.xml new file mode 100644 index 000000000000..64b9ea72e05b --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/hr/data-category-unrecognized.xml @@ -0,0 +1,5 @@ +<holder_of_flattened_for_testing> + <data-shared dataCategory="unrecognized" + dataType="email_address" + purposes="analytics" /> +</holder_of_flattened_for_testing>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-actions-in-app.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-actions-in-app.xml new file mode 100644 index 000000000000..5b99900b5a8a --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-actions-in-app.xml @@ -0,0 +1,27 @@ +<pbundle_as_map name="actions_in_app"> + <pbundle_as_map name="user_interaction"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="in_app_search_history"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="installed_apps"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="user_generated_content"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="other"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-app-performance.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-app-performance.xml new file mode 100644 index 000000000000..0fe102202be5 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-app-performance.xml @@ -0,0 +1,17 @@ +<pbundle_as_map name="app_performance"> + <pbundle_as_map name="crash_logs"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="performance_diagnostics"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="other"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-audio.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-audio.xml new file mode 100644 index 000000000000..51f1dfd6d823 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-audio.xml @@ -0,0 +1,17 @@ +<pbundle_as_map name="audio"> + <pbundle_as_map name="sound_recordings"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="music_files"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="other"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-calendar.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-calendar.xml new file mode 100644 index 000000000000..326da47a1cb9 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-calendar.xml @@ -0,0 +1,7 @@ +<pbundle_as_map name="calendar"> + <pbundle_as_map name="calendar"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-contacts.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-contacts.xml new file mode 100644 index 000000000000..5d4387d2f906 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-contacts.xml @@ -0,0 +1,7 @@ +<pbundle_as_map name="contacts"> + <pbundle_as_map name="contacts"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-email-text-message.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-email-text-message.xml new file mode 100644 index 000000000000..5ac98f56ace6 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-email-text-message.xml @@ -0,0 +1,17 @@ +<pbundle_as_map name="email_text_message"> + <pbundle_as_map name="emails"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="text_messages"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="other"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-financial.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-financial.xml new file mode 100644 index 000000000000..a66f1a44dcb3 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-financial.xml @@ -0,0 +1,22 @@ +<pbundle_as_map name="financial"> + <pbundle_as_map name="card_bank_account"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="purchase_history"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="credit_score"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="other"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-health-fitness.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-health-fitness.xml new file mode 100644 index 000000000000..8e697b47364c --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-health-fitness.xml @@ -0,0 +1,12 @@ +<pbundle_as_map name="health_fitness"> + <pbundle_as_map name="health"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="fitness"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-identifiers.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-identifiers.xml new file mode 100644 index 000000000000..34b4016e1364 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-identifiers.xml @@ -0,0 +1,7 @@ +<pbundle_as_map name="identifiers"> + <pbundle_as_map name="other"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-location.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-location.xml new file mode 100644 index 000000000000..db2e6965ff2a --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-location.xml @@ -0,0 +1,12 @@ +<pbundle_as_map name="location"> + <pbundle_as_map name="approx_location"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="precise_location"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal-partial.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal-partial.xml new file mode 100644 index 000000000000..839922a8bec6 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal-partial.xml @@ -0,0 +1,13 @@ +<pbundle_as_map name="personal"> + <pbundle_as_map name="name"> + <int-array name="purposes" num="2"> + <item value="2" /> + <item value="3" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="email_address"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal.xml new file mode 100644 index 000000000000..43650b6a77fe --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-personal.xml @@ -0,0 +1,50 @@ +<pbundle_as_map name="personal"> + <pbundle_as_map name="name"> + <int-array name="purposes" num="2"> + <item value="2" /> + <item value="3" /> + </int-array> + <boolean name="is_collection_optional" value="true" /> + <boolean name="ephemeral" value="true" /> + </pbundle_as_map> + <pbundle_as_map name="email_address"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="physical_address"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="phone_number"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="race_ethnicity"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="political_or_religious_beliefs"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="sexual_orientation_or_gender_identity"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="personal_identifiers"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="other"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-photo-video.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-photo-video.xml new file mode 100644 index 000000000000..2a3178005df3 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-photo-video.xml @@ -0,0 +1,12 @@ +<pbundle_as_map name="photo_video"> + <pbundle_as_map name="photos"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> + <pbundle_as_map name="videos"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-search-and-browsing.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-search-and-browsing.xml new file mode 100644 index 000000000000..9e654efcc5ab --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-search-and-browsing.xml @@ -0,0 +1,7 @@ +<pbundle_as_map name="search_and_browsing"> + <pbundle_as_map name="web_browsing_history"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-storage.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-storage.xml new file mode 100644 index 000000000000..9abc37fdb2b9 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datacategory/od/data-category-storage.xml @@ -0,0 +1,7 @@ +<pbundle_as_map name="storage"> + <pbundle_as_map name="files_docs"> + <int-array name="purposes" num="1"> + <item value="2" /> + </int-array> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-invalid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-invalid-bool.xml new file mode 100644 index 000000000000..bb45f426e083 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-invalid-bool.xml @@ -0,0 +1,7 @@ +<data-labels> + <data-accessed dataCategory="location" + dataType="approx_location" + ephemeral="false" + isSharingOptional="false" + purposes="app_functionality" /> +</data-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-valid-bool.xml new file mode 100644 index 000000000000..f927bba838fd --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-accessed-valid-bool.xml @@ -0,0 +1,6 @@ +<data-labels> + <data-accessed dataCategory="location" + dataType="approx_location" + ephemeral="false" + purposes="app_functionality" /> +</data-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-invalid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-invalid-bool.xml new file mode 100644 index 000000000000..ba11afbdce5f --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-invalid-bool.xml @@ -0,0 +1,7 @@ +<data-labels> + <data-collected dataCategory="location" + dataType="approx_location" + ephemeral="false" + isSharingOptional="false" + purposes="app_functionality" /> +</data-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-valid-bool.xml new file mode 100644 index 000000000000..4b6d39776aeb --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-collected-valid-bool.xml @@ -0,0 +1,7 @@ +<data-labels> + <data-collected dataCategory="location" + dataType="approx_location" + ephemeral="false" + isCollectionOptional="false" + purposes="app_functionality" /> +</data-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-invalid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-invalid-bool.xml new file mode 100644 index 000000000000..7840b9876ad8 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-invalid-bool.xml @@ -0,0 +1,7 @@ +<data-labels> + <data-shared dataCategory="location" + dataType="approx_location" + ephemeral="false" + isCollectionOptional="false" + purposes="app_functionality" /> +</data-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-valid-bool.xml new file mode 100644 index 000000000000..ccf77b0e03be --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/hr/data-labels-shared-valid-bool.xml @@ -0,0 +1,7 @@ +<data-labels> + <data-shared dataCategory="location" + dataType="approx_location" + ephemeral="false" + isSharingOptional="false" + purposes="app_functionality" /> +</data-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-accessed-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-accessed-valid-bool.xml new file mode 100644 index 000000000000..ddefc18f62e3 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-accessed-valid-bool.xml @@ -0,0 +1,12 @@ +<pbundle_as_map name="data_labels"> + <pbundle_as_map name="data_accessed"> + <pbundle_as_map name="location"> + <pbundle_as_map name="approx_location"> + <int-array name="purposes" num="1"> + <item value="1"/> + </int-array> + <boolean name="ephemeral" value="false"/> + </pbundle_as_map> + </pbundle_as_map> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-collected-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-collected-valid-bool.xml new file mode 100644 index 000000000000..252c7282da20 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-collected-valid-bool.xml @@ -0,0 +1,13 @@ +<pbundle_as_map name="data_labels"> + <pbundle_as_map name="data_collected"> + <pbundle_as_map name="location"> + <pbundle_as_map name="approx_location"> + <int-array name="purposes" num="1"> + <item value="1"/> + </int-array> + <boolean name="is_collection_optional" value="false"/> + <boolean name="ephemeral" value="false"/> + </pbundle_as_map> + </pbundle_as_map> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-shared-valid-bool.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-shared-valid-bool.xml new file mode 100644 index 000000000000..d1d4e33e855a --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/datalabels/od/data-labels-shared-valid-bool.xml @@ -0,0 +1,13 @@ +<pbundle_as_map name="data_labels"> + <pbundle_as_map name="data_shared"> + <pbundle_as_map name="location"> + <pbundle_as_map name="approx_location"> + <int-array name="purposes" num="1"> + <item value="1"/> + </int-array> + <boolean name="is_sharing_optional" value="false"/> + <boolean name="ephemeral" value="false"/> + </pbundle_as_map> + </pbundle_as_map> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/hr/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/hr/all-fields-valid.xml new file mode 100644 index 000000000000..908d8ea2f53e --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/hr/all-fields-valid.xml @@ -0,0 +1,8 @@ +<developer-info + name="max" + email="max@example.com" + address="111 blah lane" + countryRegion="US" + relationship="aosp" + website="example.com" + registryId="registry_id" />
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/od/all-fields-valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/od/all-fields-valid.xml new file mode 100644 index 000000000000..784ec6156c1d --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/developerinfo/od/all-fields-valid.xml @@ -0,0 +1,9 @@ +<pbundle_as_map name="developer_info"> + <string name="name" value="max"/> + <string name="email" value="max@example.com"/> + <string name="address" value="111 blah lane"/> + <string name="country_region" value="US"/> + <long name="relationship" value="5"/> + <string name="website" value="example.com"/> + <string name="app_developer_registry_id" value="registry_id"/> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/missing-version.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/missing-version.xml new file mode 100644 index 000000000000..762f3bdf7875 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/missing-version.xml @@ -0,0 +1,2 @@ +<safety-labels> +</safety-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/valid-empty.xml new file mode 100644 index 000000000000..7decfd4865b1 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/valid-empty.xml @@ -0,0 +1 @@ +<safety-labels version="12345"></safety-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/with-data-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/with-data-labels.xml new file mode 100644 index 000000000000..8997f4f30c33 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/hr/with-data-labels.xml @@ -0,0 +1,9 @@ +<safety-labels version="12345"> + <data-labels> + <data-shared dataCategory="location" + dataType="approx_location" + isSharingOptional="false" + ephemeral="false" + purposes="app_functionality" /> + </data-labels> +</safety-labels>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-empty.xml new file mode 100644 index 000000000000..4f03d88a3bd2 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/valid-empty.xml @@ -0,0 +1,3 @@ +<pbundle_as_map name="safety_labels"> + <long name="version" value="12345"/> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/with-data-labels.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/with-data-labels.xml new file mode 100644 index 000000000000..a966fdaf9fe0 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/with-data-labels.xml @@ -0,0 +1,16 @@ +<pbundle_as_map name="safety_labels"> + <long name="version" value="12345"/> + <pbundle_as_map name="data_labels"> + <pbundle_as_map name="data_shared"> + <pbundle_as_map name="location"> + <pbundle_as_map name="approx_location"> + <int-array name="purposes" num="1"> + <item value="1"/> + </int-array> + <boolean name="is_sharing_optional" value="false"/> + <boolean name="ephemeral" value="false"/> + </pbundle_as_map> + </pbundle_as_map> + </pbundle_as_map> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/missing-url.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/missing-url.xml new file mode 100644 index 000000000000..ff26c05abdb0 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/missing-url.xml @@ -0,0 +1 @@ +<system-app-safety-label></system-app-safety-label>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/valid.xml new file mode 100644 index 000000000000..6fe86c33523b --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/hr/valid.xml @@ -0,0 +1 @@ +<system-app-safety-label url="www.example.com"></system-app-safety-label>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid.xml new file mode 100644 index 000000000000..f96535b4b49b --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/valid.xml @@ -0,0 +1,3 @@ +<pbundle_as_map name="system_app_safety_label"> + <string name="url" value="www.example.com"/> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/valid-empty.xml new file mode 100644 index 000000000000..254a37fb99e8 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/valid-empty.xml @@ -0,0 +1,4 @@ + +<transparency-info> + +</transparency-info>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml new file mode 100644 index 000000000000..a7c48fc68cf1 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-app-info.xml @@ -0,0 +1,4 @@ + +<transparency-info> + <app-info title="beervision" description="a beer app" containsAds="true" obeyAps="false" adsFingerprinting="false" securityFingerprinting="false" privacyPolicy="www.example.com" securityEndpoints="url1|url2|url3" firstPartyEndpoints="url1" serviceProviderEndpoints="url55|url56" category="Food and drink" email="max@maxloh.com" /> +</transparency-info>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml new file mode 100644 index 000000000000..862bda465b25 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml @@ -0,0 +1,11 @@ + +<transparency-info> + <developer-info + name="max" + email="max@example.com" + address="111 blah lane" + countryRegion="US" + relationship="aosp" + website="example.com" + appDeveloperRegistryId="registry_id" /> +</transparency-info>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml new file mode 100644 index 000000000000..af574cf92b3a --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/valid-empty.xml @@ -0,0 +1 @@ +<pbundle_as_map name="transparency_info"/>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml new file mode 100644 index 000000000000..b813641f74f8 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-app-info.xml @@ -0,0 +1,26 @@ + +<pbundle_as_map name="transparency_info"> + <pbundle_as_map name="app_info"> + <string name="title" value="beervision"/> + <string name="description" value="a beer app"/> + <boolean name="contains_ads" value="true"/> + <boolean name="obey_aps" value="false"/> + <boolean name="ads_fingerprinting" value="false"/> + <boolean name="security_fingerprinting" value="false"/> + <string name="privacy_policy" value="www.example.com"/> + <string-array name="security_endpoint" num="3"> + <item value="url1"/> + <item value="url2"/> + <item value="url3"/> + </string-array> + <string-array name="first_party_endpoint" num="1"> + <item value="url1"/> + </string-array> + <string-array name="service_provider_endpoint" num="2"> + <item value="url55"/> + <item value="url56"/> + </string-array> + <string name="category" value="Food and drink"/> + <string name="email" value="max@maxloh.com"/> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml new file mode 100644 index 000000000000..101c98bd8e60 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml @@ -0,0 +1,11 @@ + +<pbundle_as_map name="transparency_info"> + <pbundle_as_map name="developer_info"> + <string name="name" value="max"/> + <string name="email" value="max@example.com"/> + <string name="address" value="111 blah lane"/> + <string name="country_region" value="US"/> + <long name="relationship" value="5"/> + <string name="website" value="example.com"/> + </pbundle_as_map> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/contacts/hr.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/contacts/hr.xml index b2ff4495a6d2..b2ff4495a6d2 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/contacts/hr.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/contacts/hr.xml diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/contacts/od.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/contacts/od.xml index 81277bf456a4..81277bf456a4 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/contacts/od.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/contacts/od.xml diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/location/hr.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/location/hr.xml index ac844b3b2767..ac844b3b2767 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/location/hr.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/location/hr.xml diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/location/od.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/location/od.xml index d0a3bfa7e64f..d0a3bfa7e64f 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/aslgen/validmappings/location/od.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/location/od.xml diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp index 30333da5e86c..682adbc86d06 100644 --- a/tools/hoststubgen/hoststubgen/Android.bp +++ b/tools/hoststubgen/hoststubgen/Android.bp @@ -82,13 +82,30 @@ java_library { jarjar_rules: "jarjar-rules.txt", } +// For sharing the code with other tools +java_library_host { + name: "hoststubgen-lib", + defaults: ["ravenwood-internal-only-visibility-java"], + srcs: ["src/**/*.kt"], + static_libs: [ + "hoststubgen-helper-runtime", + ], + libs: [ + "junit", + "ow2-asm", + "ow2-asm-analysis", + "ow2-asm-commons", + "ow2-asm-tree", + "ow2-asm-util", + ], +} + // Host-side stub generator tool. java_binary_host { name: "hoststubgen", - main_class: "com.android.hoststubgen.Main", - srcs: ["src/**/*.kt"], + main_class: "com.android.hoststubgen.HostStubGenMain", static_libs: [ - "hoststubgen-helper-runtime", + "hoststubgen-lib", "junit", "ow2-asm", "ow2-asm-analysis", diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 1089f82b6472..803dc283b8c7 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -32,7 +32,6 @@ import com.android.hoststubgen.visitors.PackageRedirectRemapper import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor import org.objectweb.asm.ClassWriter -import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.util.CheckClassAdapter import java.io.BufferedInputStream import java.io.FileOutputStream @@ -52,7 +51,7 @@ class HostStubGen(val options: HostStubGenOptions) { val stats = HostStubGenStats() // Load all classes. - val allClasses = loadClassStructures(options.inJar.get) + val allClasses = ClassNodes.loadClassStructures(options.inJar.get) // Dump the classes, if specified. options.inputJarDumpFile.ifSet { @@ -92,55 +91,6 @@ class HostStubGen(val options: HostStubGenOptions) { } /** - * Load all the classes, without code. - */ - private fun loadClassStructures(inJar: String): ClassNodes { - log.i("Reading class structure from $inJar ...") - val start = System.currentTimeMillis() - - val allClasses = ClassNodes() - - log.withIndent { - ZipFile(inJar).use { inZip -> - val inEntries = inZip.entries() - - while (inEntries.hasMoreElements()) { - val entry = inEntries.nextElement() - - BufferedInputStream(inZip.getInputStream(entry)).use { bis -> - if (entry.name.endsWith(".class")) { - val cr = ClassReader(bis) - val cn = ClassNode() - cr.accept(cn, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG - or ClassReader.SKIP_FRAMES) - if (!allClasses.addClass(cn)) { - log.w("Duplicate class found: ${cn.name}") - } - } else if (entry.name.endsWith(".dex")) { - // Seems like it's an ART jar file. We can't process it. - // It's a fatal error. - throw InvalidJarFileException( - "$inJar is not a desktop jar file. It contains a *.dex file.") - } else { - // Unknown file type. Skip. - while (bis.available() > 0) { - bis.skip((1024 * 1024).toLong()) - } - } - } - } - } - } - if (allClasses.size == 0) { - log.w("$inJar contains no *.class files.") - } - - val end = System.currentTimeMillis() - log.i("Done reading class structure in %.1f second(s).", (end - start) / 1000.0) - return allClasses - } - - /** * Build the filter, which decides what classes/methods/fields should be put in stub or impl * jars, and "how". (e.g. with substitution?) */ @@ -229,7 +179,7 @@ class HostStubGen(val options: HostStubGenOptions) { val intersectingJars = mutableMapOf<String, ClassNodes>() filenames.forEach { filename -> - intersectingJars[filename] = loadClassStructures(filename) + intersectingJars[filename] = ClassNodes.loadClassStructures(filename) } return intersectingJars } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Main.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenMain.kt index 4882c00d2b3c..45e7e301c0d1 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Main.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenMain.kt @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@file:JvmName("Main") +@file:JvmName("HostStubGenMain") package com.android.hoststubgen import java.io.PrintWriter -const val COMMAND_NAME = "HostStubGen" - /** * Entry point. */ fun main(args: Array<String>) { + executableName = "HostStubGen" + var success = false var clanupOnError = false @@ -33,7 +33,7 @@ fun main(args: Array<String>) { val options = HostStubGenOptions.parseArgs(args) clanupOnError = options.cleanUpOnError.get - log.v("HostStubGen started") + log.v("$executableName started") log.v("Options: $options") // Run. @@ -41,7 +41,7 @@ fun main(args: Array<String>) { success = true } catch (e: Throwable) { - log.e("$COMMAND_NAME: Error: ${e.message}") + log.e("$executableName: Error: ${e.message}") if (e !is UserErrorException) { e.printStackTrace(PrintWriter(log.getWriter(LogLevel.Error))) } @@ -49,7 +49,7 @@ fun main(args: Array<String>) { TODO("Remove output jars here") } } finally { - log.i("$COMMAND_NAME finished") + log.i("$executableName finished") log.flush() } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt index 9f5d524517d0..9ff798a4b5cb 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt @@ -268,7 +268,7 @@ class HostStubGenOptions( } if (!ret.outStubJar.isSet && !ret.outImplJar.isSet) { log.w("Neither --out-stub-jar nor --out-impl-jar is set." + - " $COMMAND_NAME will not generate jar files.") + " $executableName will not generate jar files.") } if (ret.enableNonStubMethodCallDetection.get) { diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt index 937e56c2cbb5..aa63d8d9f870 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt @@ -16,6 +16,11 @@ package com.android.hoststubgen /** + * Name of this executable. Set it in the main method. + */ +var executableName = "[command name not set]" + +/** * A regex that maches whitespate. */ val whitespaceRegex = """\s+""".toRegex() diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt index 0579c2bb0525..83e122feeeb2 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt @@ -34,6 +34,9 @@ val CLASS_INITIALIZER_NAME = "<clinit>" /** Descriptor of the class initializer method. */ val CLASS_INITIALIZER_DESC = "()V" +/** Name of constructors. */ +val CTOR_NAME = "<init>" + /** * Find any of [anyAnnotations] from the list of visible / invisible annotations. */ @@ -135,10 +138,10 @@ fun writeByteCodeToPushArguments( // Note, long and double will consume two local variable spaces, so the extra `i++`. when (type) { Type.VOID_TYPE -> throw HostStubGenInternalException("VOID_TYPE not expected") - Type.BOOLEAN_TYPE, Type.INT_TYPE, Type.SHORT_TYPE, Type.CHAR_TYPE + Type.BOOLEAN_TYPE, Type.CHAR_TYPE, Type.BYTE_TYPE, Type.SHORT_TYPE, Type.INT_TYPE -> writer.visitVarInsn(Opcodes.ILOAD, i) - Type.LONG_TYPE -> writer.visitVarInsn(Opcodes.LLOAD, i++) Type.FLOAT_TYPE -> writer.visitVarInsn(Opcodes.FLOAD, i) + Type.LONG_TYPE -> writer.visitVarInsn(Opcodes.LLOAD, i++) Type.DOUBLE_TYPE -> writer.visitVarInsn(Opcodes.DLOAD, i++) else -> writer.visitVarInsn(Opcodes.ALOAD, i) } @@ -154,10 +157,10 @@ fun writeByteCodeToReturn(methodDescriptor: String, writer: MethodVisitor) { // See https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions when (type) { Type.VOID_TYPE -> writer.visitInsn(Opcodes.RETURN) - Type.BOOLEAN_TYPE, Type.INT_TYPE, Type.SHORT_TYPE, Type.CHAR_TYPE + Type.BOOLEAN_TYPE, Type.CHAR_TYPE, Type.BYTE_TYPE, Type.SHORT_TYPE, Type.INT_TYPE -> writer.visitInsn(Opcodes.IRETURN) - Type.LONG_TYPE -> writer.visitInsn(Opcodes.LRETURN) Type.FLOAT_TYPE -> writer.visitInsn(Opcodes.FRETURN) + Type.LONG_TYPE -> writer.visitInsn(Opcodes.LRETURN) Type.DOUBLE_TYPE -> writer.visitInsn(Opcodes.DRETURN) else -> writer.visitInsn(Opcodes.ARETURN) } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt index bc34ef0dc8a7..92906a75b93a 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt @@ -16,13 +16,18 @@ package com.android.hoststubgen.asm import com.android.hoststubgen.ClassParseException +import com.android.hoststubgen.InvalidJarFileException +import com.android.hoststubgen.log +import org.objectweb.asm.ClassReader import org.objectweb.asm.tree.AnnotationNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.FieldNode import org.objectweb.asm.tree.MethodNode import org.objectweb.asm.tree.TypeAnnotationNode +import java.io.BufferedInputStream import java.io.PrintWriter import java.util.Arrays +import java.util.zip.ZipFile /** * Stores all classes loaded from a jar file, in a form of [ClassNode] @@ -62,8 +67,8 @@ class ClassNodes { /** Find a field, which may not exist. */ fun findField( - className: String, - fieldName: String, + className: String, + fieldName: String, ): FieldNode? { return findClass(className)?.fields?.firstOrNull { it.name == fieldName }?.let { fn -> return fn @@ -72,14 +77,14 @@ class ClassNodes { /** Find a method, which may not exist. */ fun findMethod( - className: String, - methodName: String, - descriptor: String, + className: String, + methodName: String, + descriptor: String, ): MethodNode? { return findClass(className)?.methods - ?.firstOrNull { it.name == methodName && it.desc == descriptor }?.let { mn -> - return mn - } + ?.firstOrNull { it.name == methodName && it.desc == descriptor }?.let { mn -> + return mn + } } /** @return true if a class has a class initializer. */ @@ -106,26 +111,33 @@ class ClassNodes { private fun dumpClass(pw: PrintWriter, cn: ClassNode) { pw.printf("Class: %s [access: %x]\n", cn.name, cn.access) - dumpAnnotations(pw, " ", - cn.visibleTypeAnnotations, cn.invisibleTypeAnnotations, - cn.visibleAnnotations, cn.invisibleAnnotations, - ) + dumpAnnotations( + pw, " ", + cn.visibleTypeAnnotations, cn.invisibleTypeAnnotations, + cn.visibleAnnotations, cn.invisibleAnnotations, + ) for (f in cn.fields ?: emptyList()) { - pw.printf(" Field: %s [sig: %s] [desc: %s] [access: %x]\n", - f.name, f.signature, f.desc, f.access) - dumpAnnotations(pw, " ", - f.visibleTypeAnnotations, f.invisibleTypeAnnotations, - f.visibleAnnotations, f.invisibleAnnotations, - ) + pw.printf( + " Field: %s [sig: %s] [desc: %s] [access: %x]\n", + f.name, f.signature, f.desc, f.access + ) + dumpAnnotations( + pw, " ", + f.visibleTypeAnnotations, f.invisibleTypeAnnotations, + f.visibleAnnotations, f.invisibleAnnotations, + ) } for (m in cn.methods ?: emptyList()) { - pw.printf(" Method: %s [sig: %s] [desc: %s] [access: %x]\n", - m.name, m.signature, m.desc, m.access) - dumpAnnotations(pw, " ", - m.visibleTypeAnnotations, m.invisibleTypeAnnotations, - m.visibleAnnotations, m.invisibleAnnotations, - ) + pw.printf( + " Method: %s [sig: %s] [desc: %s] [access: %x]\n", + m.name, m.signature, m.desc, m.access + ) + dumpAnnotations( + pw, " ", + m.visibleTypeAnnotations, m.invisibleTypeAnnotations, + m.visibleAnnotations, m.invisibleAnnotations, + ) } } @@ -136,7 +148,7 @@ class ClassNodes { invisibleTypeAnnotations: List<TypeAnnotationNode>?, visibleAnnotations: List<AnnotationNode>?, invisibleAnnotations: List<AnnotationNode>?, - ) { + ) { for (an in visibleTypeAnnotations ?: emptyList()) { pw.printf("%sTypeAnnotation(vis): %s\n", prefix, an.desc) } @@ -166,4 +178,55 @@ class ClassNodes { } } } + + companion object { + /** + * Load all the classes, without code. + */ + fun loadClassStructures(inJar: String): ClassNodes { + log.i("Reading class structure from $inJar ...") + val start = System.currentTimeMillis() + + val allClasses = ClassNodes() + + log.withIndent { + ZipFile(inJar).use { inZip -> + val inEntries = inZip.entries() + + while (inEntries.hasMoreElements()) { + val entry = inEntries.nextElement() + + BufferedInputStream(inZip.getInputStream(entry)).use { bis -> + if (entry.name.endsWith(".class")) { + val cr = ClassReader(bis) + val cn = ClassNode() + cr.accept(cn, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG + or ClassReader.SKIP_FRAMES) + if (!allClasses.addClass(cn)) { + log.w("Duplicate class found: ${cn.name}") + } + } else if (entry.name.endsWith(".dex")) { + // Seems like it's an ART jar file. We can't process it. + // It's a fatal error. + throw InvalidJarFileException( + "$inJar is not a desktop jar file. It contains a *.dex file.") + } else { + // Unknown file type. Skip. + while (bis.available() > 0) { + bis.skip((1024 * 1024).toLong()) + } + } + } + } + } + } + if (allClasses.size == 0) { + log.w("$inJar contains no *.class files.") + } + + val end = System.currentTimeMillis() + log.i("Done reading class structure in %.1f second(s).", (end - start) / 1000.0) + return allClasses + } + } }
\ No newline at end of file diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt index 78b13fd36f06..5a26fc69d473 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt @@ -19,14 +19,14 @@ import com.android.hoststubgen.HostStubGenErrors import com.android.hoststubgen.HostStubGenInternalException import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME -import com.android.hoststubgen.asm.isAnonymousInnerClass -import com.android.hoststubgen.log import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.isAnnotation +import com.android.hoststubgen.asm.isAnonymousInnerClass import com.android.hoststubgen.asm.isAutoGeneratedEnumMember import com.android.hoststubgen.asm.isEnum import com.android.hoststubgen.asm.isSynthetic import com.android.hoststubgen.asm.isVisibilityPrivateOrPackagePrivate +import com.android.hoststubgen.log import org.objectweb.asm.tree.ClassNode /** diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt index f70a17d9b7cd..fa8fe6cd384f 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt @@ -1833,7 +1833,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative super_class: #x // java/lang/Object - interfaces: 0, fields: 1, methods: 10, attributes: 2 + interfaces: 0, fields: 1, methods: 11, attributes: 2 int value; descriptor: I flags: (0x0000) @@ -1938,6 +1938,10 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative x: invokespecial #x // Method java/lang/RuntimeException."<init>":()V x: athrow LineNumberTable: + + public static native byte nativeBytePlus(byte, byte); + descriptor: (BB)B + flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE } SourceFile: "TinyFrameworkNative.java" RuntimeInvisibleAnnotations: @@ -1955,7 +1959,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host super_class: #x // java/lang/Object - interfaces: 0, fields: 0, methods: 4, attributes: 2 + interfaces: 0, fields: 0, methods: 5, attributes: 2 public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host(); descriptor: ()V flags: (0x0001) ACC_PUBLIC @@ -2013,6 +2017,22 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host Start Length Slot Name Signature 0 7 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative; 0 7 1 arg I + + public static byte nativeBytePlus(byte, byte); + descriptor: (BB)B + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=2, locals=2, args_size=2 + x: iload_0 + x: iload_1 + x: iadd + x: i2b + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 5 0 arg1 B + 0 5 1 arg2 B } SourceFile: "TinyFrameworkNative_host.java" RuntimeInvisibleAnnotations: diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt index 37de857b9780..c605f767f527 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt @@ -1554,7 +1554,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative super_class: #x // java/lang/Object - interfaces: 0, fields: 1, methods: 9, attributes: 3 + interfaces: 0, fields: 1, methods: 10, attributes: 3 int value; descriptor: I flags: (0x0000) @@ -1686,6 +1686,15 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenKeptInStub x: #x() com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static native byte nativeBytePlus(byte, byte); + descriptor: (BB)B + flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl } SourceFile: "TinyFrameworkNative.java" RuntimeVisibleAnnotations: diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt index c9c607c58c68..11d5939b7917 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt @@ -2236,7 +2236,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative super_class: #x // java/lang/Object - interfaces: 0, fields: 1, methods: 10, attributes: 3 + interfaces: 0, fields: 1, methods: 11, attributes: 3 int value; descriptor: I flags: (0x0000) @@ -2435,6 +2435,23 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenKeptInStub x: #x() com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static byte nativeBytePlus(byte, byte); + descriptor: (BB)B + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=2, locals=2, args_size=2 + x: iload_0 + x: iload_1 + x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeBytePlus:(BB)B + x: ireturn + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl } SourceFile: "TinyFrameworkNative.java" RuntimeVisibleAnnotations: @@ -2457,7 +2474,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host super_class: #x // java/lang/Object - interfaces: 0, fields: 0, methods: 4, attributes: 3 + interfaces: 0, fields: 0, methods: 5, attributes: 3 public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host(); descriptor: ()V flags: (0x0001) ACC_PUBLIC @@ -2551,6 +2568,31 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host RuntimeVisibleAnnotations: x: #x() com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static byte nativeBytePlus(byte, byte); + descriptor: (BB)B + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=4, locals=2, args_size=2 + x: ldc #x // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host + x: ldc #x // String nativeBytePlus + x: ldc #x // String (BB)B + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker; + x: invokevirtual #x // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class; + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V + x: iload_0 + x: iload_1 + x: iadd + x: i2b + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 15 5 0 arg1 B + 15 5 1 arg2 B + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl } SourceFile: "TinyFrameworkNative_host.java" RuntimeVisibleAnnotations: diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt index 37de857b9780..c605f767f527 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt @@ -1554,7 +1554,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative super_class: #x // java/lang/Object - interfaces: 0, fields: 1, methods: 9, attributes: 3 + interfaces: 0, fields: 1, methods: 10, attributes: 3 int value; descriptor: I flags: (0x0000) @@ -1686,6 +1686,15 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenKeptInStub x: #x() com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static native byte nativeBytePlus(byte, byte); + descriptor: (BB)B + flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl } SourceFile: "TinyFrameworkNative.java" RuntimeVisibleAnnotations: diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt index a57907d9398b..088bc80e11c5 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt @@ -2743,7 +2743,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative super_class: #x // java/lang/Object - interfaces: 0, fields: 1, methods: 11, attributes: 3 + interfaces: 0, fields: 1, methods: 12, attributes: 3 int value; descriptor: I flags: (0x0000) @@ -3002,6 +3002,28 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenKeptInStub x: #x() com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static byte nativeBytePlus(byte, byte); + descriptor: (BB)B + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=4, locals=2, args_size=2 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative + x: ldc #x // String nativeBytePlus + x: ldc #x // String (BB)B + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: iload_0 + x: iload_1 + x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeBytePlus:(BB)B + x: ireturn + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInStub + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl } SourceFile: "TinyFrameworkNative.java" RuntimeVisibleAnnotations: @@ -3024,7 +3046,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host super_class: #x // java/lang/Object - interfaces: 0, fields: 0, methods: 5, attributes: 3 + interfaces: 0, fields: 0, methods: 6, attributes: 3 private static {}; descriptor: ()V flags: (0x000a) ACC_PRIVATE, ACC_STATIC @@ -3148,6 +3170,36 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host RuntimeVisibleAnnotations: x: #x() com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl + + public static byte nativeBytePlus(byte, byte); + descriptor: (BB)B + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=4, locals=2, args_size=2 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host + x: ldc #x // String nativeBytePlus + x: ldc #x // String (BB)B + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: ldc #x // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host + x: ldc #x // String nativeBytePlus + x: ldc #x // String (BB)B + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker; + x: invokevirtual #x // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class; + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V + x: iload_0 + x: iload_1 + x: iadd + x: i2b + x: ireturn + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 26 5 0 arg1 B + 26 5 1 arg2 B + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenKeptInImpl } SourceFile: "TinyFrameworkNative_host.java" RuntimeVisibleAnnotations: diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java index 5a5e22db59e5..09ee183a2dcc 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java @@ -52,4 +52,6 @@ public class TinyFrameworkNative { public static void nativeStillNotSupported_should_be_like_this() { throw new RuntimeException(); } + + public static native byte nativeBytePlus(byte arg1, byte arg2); } diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java index 749ebaa378e3..b23c21602967 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java @@ -34,4 +34,8 @@ public class TinyFrameworkNative_host { public static int nativeNonStaticAddToValue(TinyFrameworkNative source, int arg) { return source.value + arg; } + + public static byte nativeBytePlus(byte arg1, byte arg2) { + return (byte) (arg1 + arg2); + } } diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java index ba17c75132f2..762180dcf74b 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java @@ -154,13 +154,22 @@ public class TinyFrameworkClassTest { } @Test + public void testNativeSubstitutionLong() { + assertThat(TinyFrameworkNative.nativeLongPlus(1L, 2L)).isEqualTo(3L); + } + + @Test + public void testNativeSubstitutionByte() { + assertThat(TinyFrameworkNative.nativeBytePlus((byte) 3, (byte) 4)).isEqualTo(7); + } + + @Test public void testNativeSubstitutionClass_nonStatic() { TinyFrameworkNative instance = new TinyFrameworkNative(); instance.setValue(5); assertThat(instance.nativeNonStaticAddToValue(3)).isEqualTo(8); } - @Test public void testSubstituteNativeWithThrow() throws Exception { // We can't use TinyFrameworkNative.nativeStillNotSupported() directly in this class, diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt index 837dae92b711..0f1373c34ce6 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt @@ -24,11 +24,20 @@ import com.github.javaparser.ParseProblemException import com.github.javaparser.ParserConfiguration import com.github.javaparser.StaticJavaParser import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.Modifier import com.github.javaparser.ast.NodeList import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration import com.github.javaparser.ast.body.InitializerDeclaration +import com.github.javaparser.ast.expr.ArrayAccessExpr +import com.github.javaparser.ast.expr.ArrayCreationExpr +import com.github.javaparser.ast.expr.ArrayInitializerExpr +import com.github.javaparser.ast.expr.AssignExpr +import com.github.javaparser.ast.expr.BooleanLiteralExpr +import com.github.javaparser.ast.expr.Expression import com.github.javaparser.ast.expr.FieldAccessExpr +import com.github.javaparser.ast.expr.IntegerLiteralExpr import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.ast.expr.MethodReferenceExpr import com.github.javaparser.ast.expr.NameExpr import com.github.javaparser.ast.expr.NullLiteralExpr import com.github.javaparser.ast.expr.ObjectCreationExpr @@ -168,6 +177,8 @@ object ProtoLogTool { val classNameNode = classDeclaration.findFirst(SimpleName::class.java).get() classNameNode.setId(protoLogImplGenName) + injectCacheClass(classDeclaration, groups, protoLogGroupsClassName) + injectConstants(classDeclaration, viewerConfigFilePath, legacyViewerConfigFilePath, legacyOutputFilePath, groups, protoLogGroupsClassName) @@ -238,6 +249,12 @@ object ProtoLogTool { field.setFinal(true) field.variables.first().setInitializer(treeMapCreation) } + ProtoLogToolInjected.Value.CACHE_UPDATER.name -> { + field.setFinal(true) + field.variables.first().setInitializer(MethodReferenceExpr() + .setScope(NameExpr("Cache")) + .setIdentifier("update")) + } else -> error("Unhandled ProtoLogToolInjected value: $valueName.") } } @@ -245,6 +262,61 @@ object ProtoLogTool { } } + private fun injectCacheClass( + classDeclaration: ClassOrInterfaceDeclaration, + groups: Map<String, LogGroup>, + protoLogGroupsClassName: String, + ) { + val cacheClass = ClassOrInterfaceDeclaration() + .setName("Cache") + .setPublic(true) + .setStatic(true) + for (group in groups) { + val nodeList = NodeList<Expression>() + val defaultVal = BooleanLiteralExpr(group.value.textEnabled || group.value.enabled) + repeat(LogLevel.entries.size) { nodeList.add(defaultVal) } + cacheClass.addFieldWithInitializer( + "boolean[]", + "${group.key}_enabled", + ArrayCreationExpr().setElementType("boolean[]").setInitializer( + ArrayInitializerExpr().setValues(nodeList) + ), + Modifier.Keyword.PUBLIC, + Modifier.Keyword.STATIC + ) + } + + val updateBlockStmt = BlockStmt() + for (group in groups) { + for (level in LogLevel.entries) { + updateBlockStmt.addStatement( + AssignExpr() + .setTarget( + ArrayAccessExpr() + .setName(NameExpr("${group.key}_enabled")) + .setIndex(IntegerLiteralExpr(level.ordinal)) + ).setValue( + MethodCallExpr() + .setName("isEnabled") + .setArguments(NodeList( + FieldAccessExpr() + .setScope(NameExpr(protoLogGroupsClassName)) + .setName(group.value.name), + FieldAccessExpr() + .setScope(NameExpr("LogLevel")) + .setName(level.toString()), + )) + ) + ) + } + } + + cacheClass.addMethod("update").setPrivate(true).setStatic(true) + .setBody(updateBlockStmt) + + classDeclaration.addMember(cacheClass) + } + private fun tryParse(code: String, fileName: String): CompilationUnit { try { return StaticJavaParser.parse(code) diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt index 2b7164191dd0..6a8a0717b2f1 100644 --- a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt +++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt @@ -22,6 +22,7 @@ import com.github.javaparser.StaticJavaParser import com.github.javaparser.ast.CompilationUnit import com.github.javaparser.ast.NodeList import com.github.javaparser.ast.body.VariableDeclarator +import com.github.javaparser.ast.expr.ArrayAccessExpr import com.github.javaparser.ast.expr.CastExpr import com.github.javaparser.ast.expr.Expression import com.github.javaparser.ast.expr.FieldAccessExpr @@ -35,6 +36,8 @@ import com.github.javaparser.ast.expr.TypeExpr import com.github.javaparser.ast.expr.VariableDeclarationExpr import com.github.javaparser.ast.stmt.BlockStmt import com.github.javaparser.ast.stmt.ExpressionStmt +import com.github.javaparser.ast.stmt.IfStmt +import com.github.javaparser.ast.stmt.Statement import com.github.javaparser.ast.type.ArrayType import com.github.javaparser.ast.type.ClassOrInterfaceType import com.github.javaparser.ast.type.PrimitiveType @@ -74,6 +77,8 @@ class SourceTransformer( private val protoLogImplClassNode = StaticJavaParser.parseExpression<FieldAccessExpr>(protoLogImplClassName) + private val protoLogImplCacheClassNode = + StaticJavaParser.parseExpression<FieldAccessExpr>("$protoLogImplClassName.Cache") private var processedCode: MutableList<String> = mutableListOf() private var offsets: IntArray = IntArray(0) /** The path of the file being processed, relative to $ANDROID_BUILD_TOP */ @@ -121,8 +126,9 @@ class SourceTransformer( group: LogGroup, level: LogLevel, messageString: String - ): BlockStmt { + ): Statement { val hash = CodeUtils.hash(packagePath, messageString, level, group) + val newCall = call.clone() if (!group.textEnabled) { // Remove message string if text logging is not enabled by default. @@ -166,11 +172,15 @@ class SourceTransformer( } blockStmt.addStatement(ExpressionStmt(newCall)) - return blockStmt + val isLogEnabled = ArrayAccessExpr() + .setName(NameExpr("$protoLogImplCacheClassNode.${group.name}_enabled")) + .setIndex(IntegerLiteralExpr(level.ordinal)) + + return IfStmt(isLogEnabled, blockStmt, null) } private fun injectProcessedCallStatementInCode( - processedCallStatement: BlockStmt, + processedCallStatement: Statement, parentStmt: ExpressionStmt ) { // Inline the new statement. diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt index de0b5bae118e..82aa93da613b 100644 --- a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt +++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt @@ -76,7 +76,7 @@ class SourceTransformerTest { class Test { void test() { - { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } } } """.trimIndent() @@ -86,7 +86,7 @@ class SourceTransformerTest { class Test { void test() { - { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); } } @@ -98,8 +98,8 @@ class SourceTransformerTest { class Test { void test() { - { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } - { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } } } """.trimIndent() @@ -109,7 +109,7 @@ class SourceTransformerTest { class Test { void test() { - { org.example.ProtoLogImpl.w(TEST_GROUP, 3218600869538902408L, 0, "test", (Object[]) null); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { org.example.ProtoLogImpl.w(TEST_GROUP, 3218600869538902408L, 0, "test", (Object[]) null); } } } """.trimIndent() @@ -119,7 +119,7 @@ class SourceTransformerTest { class Test { void test() { - { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, null, protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, null, protoLogParam0, protoLogParam1); } } } """.trimIndent() @@ -129,7 +129,7 @@ class SourceTransformerTest { class Test { void test() { - { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, null, protoLogParam0, protoLogParam1, protoLogParam2); + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, null, protoLogParam0, protoLogParam1, protoLogParam2); } } |