diff options
235 files changed, 6059 insertions, 2969 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 93c34cd5e5ec..982ab640af7c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -12507,7 +12507,7 @@ package android.content.pm { method public boolean hasShortcutHostPermission(); method @RequiresPermission(conditional=true, anyOf={"android.permission.ACCESS_HIDDEN_PROFILES_FULL", android.Manifest.permission.ACCESS_HIDDEN_PROFILES}) public boolean isActivityEnabled(android.content.ComponentName, android.os.UserHandle); method @RequiresPermission(conditional=true, anyOf={"android.permission.ACCESS_HIDDEN_PROFILES_FULL", android.Manifest.permission.ACCESS_HIDDEN_PROFILES}) public boolean isPackageEnabled(String, android.os.UserHandle); - method public void pinShortcuts(@NonNull String, @NonNull java.util.List<java.lang.String>, @NonNull android.os.UserHandle); + method @RequiresPermission(conditional=true, value="android.permission.ACCESS_SHORTCUTS") public void pinShortcuts(@NonNull String, @NonNull java.util.List<java.lang.String>, @NonNull android.os.UserHandle); method @RequiresPermission(conditional=true, anyOf={"android.permission.ACCESS_HIDDEN_PROFILES_FULL", android.Manifest.permission.ACCESS_HIDDEN_PROFILES}) public void registerCallback(android.content.pm.LauncherApps.Callback); method @RequiresPermission(conditional=true, anyOf={"android.permission.ACCESS_HIDDEN_PROFILES_FULL", android.Manifest.permission.ACCESS_HIDDEN_PROFILES}) public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 5ead3e11b387..22d39a4a0fa6 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -15342,7 +15342,7 @@ package android.telephony { method @Deprecated public boolean getDataEnabled(int); method @Nullable @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.ComponentName getDefaultRespondViaMessageApplication(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getDeviceSoftwareVersion(int); - method @FlaggedApi("android.permission.flags.get_emergency_role_holder_api_enabled") @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getEmergencyAssistancePackageName(); + method @FlaggedApi("android.permission.flags.get_emergency_role_holder_api_enabled") @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getEmergencyAssistancePackageName(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean getEmergencyCallbackMode(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain(); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 1cc2d25fb76d..a5dd4a7207c3 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -796,7 +796,7 @@ public class Activity extends ContextThemeWrapper private static final String SAVED_DIALOGS_TAG = "android:savedDialogs"; private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_"; private static final String SAVED_DIALOG_ARGS_KEY_PREFIX = "android:dialog_args_"; - private static final String HAS_CURENT_PERMISSIONS_REQUEST_KEY = + private static final String HAS_CURRENT_PERMISSIONS_REQUEST_KEY = "android:hasCurrentPermissionsRequest"; private static final String REQUEST_PERMISSIONS_WHO_PREFIX = "@android:requestPermissions:"; @@ -9318,14 +9318,14 @@ public class Activity extends ContextThemeWrapper private void storeHasCurrentPermissionRequest(Bundle bundle) { if (bundle != null && mHasCurrentPermissionsRequest) { - bundle.putBoolean(HAS_CURENT_PERMISSIONS_REQUEST_KEY, true); + bundle.putBoolean(HAS_CURRENT_PERMISSIONS_REQUEST_KEY, true); } } private void restoreHasCurrentPermissionRequest(Bundle bundle) { if (bundle != null) { mHasCurrentPermissionsRequest = bundle.getBoolean( - HAS_CURENT_PERMISSIONS_REQUEST_KEY, false); + HAS_CURRENT_PERMISSIONS_REQUEST_KEY, false); } } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index ff713d071a05..0ed25eb3125a 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -2685,8 +2685,7 @@ public class AppOpsManager { .setDefaultMode(getSystemAlertWindowDefault()).build(), new AppOpInfo.Builder(OP_ACCESS_NOTIFICATIONS, OPSTR_ACCESS_NOTIFICATIONS, "ACCESS_NOTIFICATIONS") - .setPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) - .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), + .setPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS).build(), new AppOpInfo.Builder(OP_CAMERA, OPSTR_CAMERA, "CAMERA") .setPermission(android.Manifest.permission.CAMERA) .setRestriction(UserManager.DISALLOW_CAMERA) diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index fe8655c13562..f092945a5d28 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -1135,6 +1135,9 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac } catch (RemoteException ex) { } onTimeout(startId); + if (Flags.introduceNewServiceOntimeoutCallback()) { + onTimeout(startId, ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE); + } } /** @@ -1146,6 +1149,12 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * doesn't finish even after it's timed out, * the app will be declared an ANR after a short grace period of several seconds. * + * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * {@link #onTimeout(int, int)} will also be called when a foreground service of type + * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE} times out. + * Developers do not need to implement both of the callbacks on + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and onwards. + * * <p>Note, even though * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE} * was added diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 2c0e035e80c4..57b5c13a659d 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -1384,7 +1384,8 @@ public class AppWidgetManager { * * @return {@code TRUE} if the launcher supports this feature. Note the API will return without * waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean - * the shortcut is pinned. {@code FALSE} if the launcher doesn't support this feature. + * the shortcut is pinned. {@code FALSE} if the launcher doesn't support this feature or if + * calling app belongs to a user-profile with items restricted on home screen. * * @see android.content.pm.ShortcutManager#isRequestPinShortcutSupported() * @see android.content.pm.ShortcutManager#requestPinShortcut(ShortcutInfo, IntentSender) diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 5e00b7a798d8..2c26389071ce 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -1086,7 +1086,7 @@ public final class CompanionDeviceManager { } Objects.requireNonNull(deviceAddress, "address cannot be null"); try { - mService.registerDevicePresenceListenerService(deviceAddress, + mService.legacyStartObservingDevicePresence(deviceAddress, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); @@ -1128,7 +1128,7 @@ public final class CompanionDeviceManager { } Objects.requireNonNull(deviceAddress, "address cannot be null"); try { - mService.unregisterDevicePresenceListenerService(deviceAddress, + mService.legacyStopObservingDevicePresence(deviceAddress, mContext.getPackageName(), mContext.getUserId()); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); @@ -1328,7 +1328,7 @@ public final class CompanionDeviceManager { @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceAppeared(int associationId) { try { - mService.notifyDeviceAppeared(associationId); + mService.notifySelfManagedDeviceAppeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1350,7 +1350,7 @@ public final class CompanionDeviceManager { @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceDisappeared(int associationId) { try { - mService.notifyDeviceDisappeared(associationId); + mService.notifySelfManagedDeviceDisappeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 57d59e5e5bf0..1b00f90e1fb3 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -59,12 +59,16 @@ interface ICompanionDeviceManager { int userId); @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void registerDevicePresenceListenerService(in String deviceAddress, in String callingPackage, - int userId); + void legacyStartObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId); @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void unregisterDevicePresenceListenerService(in String deviceAddress, in String callingPackage, - int userId); + void legacyStopObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId); + + @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") + void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); + + @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") + void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); boolean canPairWithoutPrompt(in String packageName, in String deviceMacAddress, int userId); @@ -93,9 +97,11 @@ interface ICompanionDeviceManager { @EnforcePermission("USE_COMPANION_TRANSPORTS") void removeOnMessageReceivedListener(int messageType, IOnMessageReceivedListener listener); - void notifyDeviceAppeared(int associationId); + @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED") + void notifySelfManagedDeviceAppeared(int associationId); - void notifyDeviceDisappeared(int associationId); + @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED") + void notifySelfManagedDeviceDisappeared(int associationId); PendingIntent buildPermissionTransferUserConsentIntent(String callingPackage, int userId, int associationId); @@ -135,10 +141,4 @@ interface ICompanionDeviceManager { byte[] getBackupPayload(int userId); void applyRestoredPayload(in byte[] payload, int userId); - - @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); - - @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); } diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index bd04634ac4f1..535cebb1d1af 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1280,6 +1280,26 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { 264301586L; // buganizer id /** + * Excludes the packages the override is applied to from the camera compatibility treatment + * in free-form windowing mode for fixed-orientation apps. + * + * <p>In free-form windowing mode, the compatibility treatment emulates running on a portrait + * device by letterboxing the app window and changing the camera characteristics to what apps + * commonly expect in a portrait device: 90 and 270 degree sensor rotation for back and front + * cameras, respectively, and setting display rotation to 0. + * + * <p>Use this flag to disable the compatibility treatment for apps that do not respond well to + * the treatment. + * + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT = + 314961188L; + + /** * This change id forces the packages it is applied to sandbox {@link android.view.View} API to * an activity bounds for: * diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 39b914975362..6168b6800adc 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -29,6 +29,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; @@ -694,9 +695,16 @@ public class LauncherApps { * <p>If the caller is running on a managed profile, it'll return only the current profile. * Otherwise it'll return the same list as {@link UserManager#getUserProfiles()} would. * - * <p> To get hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>To get hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public List<UserHandle> getProfiles() { @@ -756,15 +764,21 @@ public class LauncherApps { * list.</li> * </ul> * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName The specific package to query. If null, it checks all installed packages * in the profile. * @param user The UserHandle of the profile. * @return List of launchable activities. Can be an empty list but will not be null. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) { @@ -806,15 +820,21 @@ public class LauncherApps { * Returns information related to a user which is useful for displaying UI elements * to distinguish it from other users (eg, badges). * - * <p>If the user in question is a hidden profile like - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param userHandle user handle of the user for which LauncherUserInfo is requested. * @return the {@link LauncherUserInfo} object related to the user specified, null in case * the user is inaccessible. */ @Nullable + @SuppressLint("RequiresPermission") @FlaggedApi(Flags.FLAG_ALLOW_PRIVATE_PROFILE) @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) @@ -853,9 +873,14 @@ public class LauncherApps { * </ul> * </p> * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName the package for which intent sender to launch App Market Activity is * required. @@ -864,6 +889,7 @@ public class LauncherApps { * there is no such activity. */ @Nullable + @SuppressLint("RequiresPermission") @FlaggedApi(Flags.FLAG_ALLOW_PRIVATE_PROFILE) @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) @@ -887,9 +913,14 @@ public class LauncherApps { * <p>An empty list denotes that all system packages should be treated as pre-installed for that * user at creation. * - * <p>If the user in question is a hidden profile like - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param userHandle the user for which installed system packages are required. * @return {@link List} of {@link String}, representing the package name of the installed @@ -897,6 +928,7 @@ public class LauncherApps { */ @FlaggedApi(Flags.FLAG_ALLOW_PRIVATE_PROFILE) @NonNull + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public List<String> getPreInstalledSystemPackages(@NonNull UserHandle userHandle) { @@ -936,14 +968,20 @@ public class LauncherApps { * Returns the activity info for a given intent and user handle, if it resolves. Otherwise it * returns null. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param intent The intent to find a match for. * @param user The profile to look in for a match. * @return An activity info object if there is a match. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) { @@ -995,15 +1033,21 @@ public class LauncherApps { /** * Starts a Main activity in the specified profile. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param component The ComponentName of the activity to launch * @param user The UserHandle of the profile * @param sourceBounds The Rect containing the source bounds of the clicked icon * @param opts Options to pass to startActivity */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public void startMainActivity(ComponentName component, UserHandle user, Rect sourceBounds, @@ -1043,15 +1087,21 @@ public class LauncherApps { * Starts the settings activity to show the application details for a * package in the specified profile. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param component The ComponentName of the package to launch settings for. * @param user The UserHandle of the profile * @param sourceBounds The Rect containing the source bounds of the clicked icon * @param opts Options to pass to startActivity */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public void startAppDetailsActivity(ComponentName component, UserHandle user, @@ -1097,7 +1147,8 @@ public class LauncherApps { * @param packageName The specific package to query. If null, it checks all installed packages * in the profile. * @param user The UserHandle of the profile. - * @return List of config activities. Can be an empty list but will not be null. + * @return List of config activities. Can be an empty list but will not be null. Empty list will + * be returned for user-profiles that have items restricted on home screen. * * @see Intent#ACTION_CREATE_SHORTCUT * @see #getShortcutConfigActivityIntent(LauncherActivityInfo) @@ -1164,15 +1215,21 @@ public class LauncherApps { /** * Checks if the package is installed and enabled for a profile. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName The package to check. * @param user The UserHandle of the profile. * * @return true if the package exists and is enabled. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public boolean isPackageEnabled(String packageName, UserHandle user) { @@ -1192,9 +1249,14 @@ public class LauncherApps { * <p>The contents of this {@link Bundle} are supposed to be a contract between the suspending * app and the launcher. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * <p>Note: This just returns whatever extras were provided to the system, <em>which might * even be {@code null}.</em> @@ -1207,6 +1269,7 @@ public class LauncherApps { * @see Callback#onPackagesSuspended(String[], UserHandle, Bundle) * @see PackageManager#isPackageSuspended() */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public @Nullable Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user) { @@ -1223,14 +1286,20 @@ public class LauncherApps { * could be done because the package was marked as distracting to the user via * {@code PackageManager.setDistractingPackageRestrictions(String[], int)}. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName The package for which to check. * @param user the {@link UserHandle} of the profile. * @return */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public boolean shouldHideFromSuggestions(@NonNull String packageName, @@ -1247,9 +1316,14 @@ public class LauncherApps { /** * Returns {@link ApplicationInfo} about an application installed for a specific user profile. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName The package name of the application * @param flags Additional option flags {@link PackageManager#getApplicationInfo} @@ -1259,6 +1333,7 @@ public class LauncherApps { * {@code null} if the package isn't installed for the given profile, or the profile * isn't enabled. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public ApplicationInfo getApplicationInfo(@NonNull String packageName, @@ -1310,15 +1385,21 @@ public class LauncherApps { * <p>The activity may still not be exported, in which case {@link #startMainActivity} will * throw a {@link SecurityException} unless the caller has the same UID as the target app's. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param component The activity to check. * @param user The UserHandle of the profile. * * @return true if the activity exists and is enabled. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public boolean isActivityEnabled(ComponentName component, UserHandle user) { @@ -1488,6 +1569,9 @@ public class LauncherApps { * <p>The calling launcher application must be allowed to access the shortcut information, * as defined in {@link #hasShortcutHostPermission()}. * + * <p>For user-profiles with items restricted on home screen, caller must have the required + * permission. + * * @param packageName The target package name. * @param shortcutIds The IDs of the shortcut to be pinned. * @param user The UserHandle of the profile. @@ -1496,6 +1580,7 @@ public class LauncherApps { * * @see ShortcutManager */ + @RequiresPermission(conditional = true, value = android.Manifest.permission.ACCESS_SHORTCUTS) public void pinShortcuts(@NonNull String packageName, @NonNull List<String> shortcutIds, @NonNull UserHandle user) { logErrorForInvalidProfileAccess(user); @@ -1875,12 +1960,18 @@ public class LauncherApps { /** * Registers a callback for changes to packages in this user and managed profiles. * - * <p>To receive callbacks for hidden profile{@link UserManager.USER_TYPE_PROFILE_PRIVATE}, - * caller should have {@link android.app.role.RoleManager.ROLE_HOME} and either of the - * permissions required. + * <p>To receive callbacks for hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param callback The callback to register. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public void registerCallback(Callback callback) { @@ -1891,12 +1982,18 @@ public class LauncherApps { * Registers a callback for changes to packages in this user and managed profiles. * * <p>To receive callbacks for hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, - * caller should have {@link android.app.role.RoleManager.ROLE_HOME} and either of the - * permissions required. + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param callback The callback to register. * @param handler that should be used to post callbacks on, may be null. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public void registerCallback(Callback callback, Handler handler) { @@ -2298,6 +2395,9 @@ public class LauncherApps { * app's manifest, have the android.permission.QUERY_ALL_PACKAGES, or be the session owner to * watch for these events. * + * <p> Session callbacks are not sent for user-profiles that have items restricted on home + * screen. + * * @param callback The callback to register. * @param executor {@link Executor} to handle the callbacks, cannot be null. * @@ -2346,12 +2446,18 @@ public class LauncherApps { * package name in the app's manifest, have the android.permission.QUERY_ALL_PACKAGES, or be * the session owner to retrieve these details. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>To receive callbacks for hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @see PackageInstaller#getAllSessions() */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public @NonNull List<SessionInfo> getAllPackageInstallerSessions() { diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 17e6f167b06d..270fc32a4e32 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -177,6 +177,10 @@ public class PackageInstaller { * Broadcast Action: Explicit broadcast sent to the last known default launcher when a session * for a new install is committed. For managed profile, this is sent to the default launcher * of the primary profile. + * For user-profiles that have items restricted on home screen, this broadcast is sent to + * the default launcher of the primary profile, only if it has either + * {@link Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} or + * {@link Manifest.permission.ACCESS_HIDDEN_PROFILES} permission. * <p> * The associated session is defined in {@link #EXTRA_SESSION} and the user for which this * session was created in {@link Intent#EXTRA_USER}. diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index b5809cfb9170..41f093614e6c 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -3263,6 +3263,16 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: This device is capable of launching apps in automotive display + * compatibility mode. + * @hide + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_CAR_DISPLAY_COMPATIBILITY = + "android.software.car.display_compatibility"; + + /** + * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature(String, int)}: If this feature is supported, the device supports * {@link android.security.identity.IdentityCredentialStore} implemented in secure hardware * at the given feature version. diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index e48a02a192d2..3514914b1d86 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -583,8 +583,8 @@ public class ShortcutManager { * * @return {@code TRUE} if the launcher supports this feature. Note the API will return without * waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean - * the shortcut was pinned successfully. {@code FALSE} if the launcher doesn't support this - * feature. + * the shortcut was pinned successfully. {@code FALSE} if the launcher doesn't support this + * feature or if calling app belongs to a user-profile with items restricted on home screen. * * @see #isRequestPinShortcutSupported() * @see IntentSender diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index a8d54ed970b7..7d61c142fa04 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -123,6 +123,15 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan public static final int DISMISSED_REASON_CREDENTIAL_CONFIRMED = 7; /** + * Dialog is done animating away after user clicked on the button set via + * {@link PromptContentViewWithMoreOptionsButton.Builder#setMoreOptionsButtonListener(Executor, + * DialogInterface.OnClickListener)} )}. + * + * @hide + */ + public static final int DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS = 8; + + /** * @hide */ @IntDef({DISMISSED_REASON_BIOMETRIC_CONFIRMED, @@ -131,7 +140,8 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, DISMISSED_REASON_ERROR, DISMISSED_REASON_SERVER_REQUESTED, - DISMISSED_REASON_CREDENTIAL_CONFIRMED}) + DISMISSED_REASON_CREDENTIAL_CONFIRMED, + DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS}) @Retention(RetentionPolicy.SOURCE) public @interface DismissedReason {} @@ -654,8 +664,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan private final IAuthService mService; private final PromptInfo mPromptInfo; private final ButtonInfo mNegativeButtonInfo; - // TODO(b/328843028): add callback onContentViewMoreOptionsButtonClicked() in - // IBiometricServiceReceiver. private final ButtonInfo mContentViewMoreOptionsButtonInfo; private CryptoObject mCryptoObject; @@ -745,6 +753,13 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE); mIsPromptShowing = false; }); + } else if (reason == DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS) { + if (mContentViewMoreOptionsButtonInfo != null) { + mContentViewMoreOptionsButtonInfo.executor.execute(() -> { + mContentViewMoreOptionsButtonInfo.listener.onClick(null, + DialogInterface.BUTTON_NEGATIVE); + }); + } } else { mIsPromptShowing = false; Log.e(TAG, "Unknown reason: " + reason); diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 83b7edaec72d..6246dd77fd6d 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -20,10 +20,12 @@ import static java.util.Objects.requireNonNull; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; +import android.content.pm.PackageManager; import android.net.LinkProperties; import android.net.NetworkCapabilities; import android.os.Binder; @@ -69,8 +71,13 @@ import java.util.concurrent.Executor; * tasks. In Safe Mode, the system will allow underlying cellular networks to be used as default. * Additionally, during Safe Mode, the VCN will continue to retry the connections, and will * automatically exit Safe Mode if all active tunnels connect successfully. + * + * <p>Apps targeting Android 15 or newer should check the existence of {@link + * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION} before querying the service. If the feature is + * absent, {@link Context#getSystemService} may return null. */ @SystemService(Context.VCN_MANAGEMENT_SERVICE) +@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public class VcnManager { @NonNull private static final String TAG = VcnManager.class.getSimpleName(); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d0593e7398fc..5bb490336f5d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8502,6 +8502,19 @@ public final class Settings { public static final String ACCESSIBILITY_BUTTON_TARGETS = "accessibility_button_targets"; /** + * Setting specifying the accessibility services, shortcut targets or features + * to be toggled via the floating accessibility menu + * + * <p> This is a colon-separated string list which contains the flattened + * {@link ComponentName} and the class name of a system class + * implementing a supported accessibility feature. + * @hide + */ + @Readable + public static final String ACCESSIBILITY_FLOATING_MENU_TARGETS = + "accessibility_floating_menu_targets"; + + /** * Setting specifying the accessibility services, accessibility shortcut targets, * or features to be toggled via a tile in the quick settings panel. * diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig index 9245557bd488..d0cef83390b9 100644 --- a/core/java/android/provider/flags.aconfig +++ b/core/java/android/provider/flags.aconfig @@ -1,6 +1,13 @@ package: "android.provider" flag { + name: "a11y_standalone_fab_enabled" + namespace: "accessibility" + description: "Separating a11y software shortcut and floating a11y button" + bug: "297544054" +} + +flag { name: "system_settings_default" is_exported: true namespace: "package_manager_service" @@ -22,4 +29,4 @@ flag { namespace: "backstage_power" description: "Add a new settings page for the RUN_BACKUP_JOBS permission." bug: "320563660" -} +}
\ No newline at end of file diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java index 887b5751ffc8..b9f4c3272207 100644 --- a/core/java/android/service/voice/VisualQueryDetectionService.java +++ b/core/java/android/service/voice/VisualQueryDetectionService.java @@ -262,7 +262,6 @@ public abstract class VisualQueryDetectionService extends Service public void onStopDetection() { } - // TODO(b/324341724): Properly deprecate this API. /** * Informs the system that the attention is gained for the interaction intention * {@link VisualQueryAttentionResult#INTERACTION_INTENTION_AUDIO_VISUAL} with @@ -343,7 +342,6 @@ public abstract class VisualQueryDetectionService extends Service } } - // TODO(b/324341724): Properly deprecate this API. /** * Informs the {@link VisualQueryDetector} with the text content being captured about the * query from the audio source. {@code partialQuery} is provided to the diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java index bf8de06fd244..11858e841a8f 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -301,8 +301,15 @@ public class VisualQueryDetector { } /** - * A class that lets a VoiceInteractionService implementation interact with - * visual query detection APIs. + * A class that lets a VoiceInteractionService implementation interact with visual query + * detection APIs. + * + * Note that methods in this callbacks are not thread-safe so the invocation of each + * methods will have different order from how they are called in the + * {@link VisualQueryDetectionService}. It is expected to pass a single thread executor or a + * serial executor as the callback executor when creating the {@link VisualQueryDetector} + * with {@link VoiceInteractionService#createVisualQueryDetector( + * PersistableBundle, SharedMemory, Executor, Callback)}. */ public interface Callback { @@ -456,7 +463,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onResultDetected"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryDetected(partialResult); + mExecutor.execute(()->mCallback.onQueryDetected(partialResult)); } }); } diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index 306410c9a98b..2f2a6709f50b 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -932,7 +932,10 @@ public class VoiceInteractionService extends Service { * @param sharedMemory The unrestricted data blob to be provided to the * {@link VisualQueryDetectionService}. Use this to provide models or other such data to the * sandboxed process. - * @param callback The callback to notify of detection events. + * @param callback The callback to notify of detection events. Single threaded or sequential + * executors are recommended for the callback are not guaranteed to be executed + * in the order of how they were called from the + * {@link VisualQueryDetectionService}. * @return An instanece of {@link VisualQueryDetector}. * @throws IllegalStateException when there is an existing {@link VisualQueryDetector}, or when * there is a non-trusted hotword detector running. diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 1920fa3e949d..2fcffd06db62 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -737,15 +737,17 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @Override public void onIdMatch(InsetsSource source1, InsetsSource source2) { - final @InsetsType int type = source1.getType(); - if ((type & Type.systemBars()) == 0 + final Rect frame1 = source1.getFrame(); + final Rect frame2 = source2.getFrame(); + if (!source1.hasFlags(InsetsSource.FLAG_ANIMATE_RESIZING) + || !source2.hasFlags(InsetsSource.FLAG_ANIMATE_RESIZING) || !source1.isVisible() || !source2.isVisible() - || source1.getFrame().equals(source2.getFrame()) + || frame1.equals(frame2) || frame1.isEmpty() || frame2.isEmpty() || !(Rect.intersects(mFrame, source1.getFrame()) || Rect.intersects(mFrame, source2.getFrame()))) { return; } - mTypes |= type; + mTypes |= source1.getType(); if (mToState == null) { mToState = new InsetsState(); } @@ -877,7 +879,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation return false; } if (DEBUG) Log.d(TAG, "onStateChanged: " + state); - mLastDispatchedState.set(state, true /* copySources */); final InsetsState lastState = new InsetsState(mState, true /* copySources */); updateState(state); @@ -888,10 +889,13 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation true /* excludesInvisibleIme */)) { if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged"); mHost.notifyInsetsChanged(); - if (lastState.getDisplayFrame().equals(mState.getDisplayFrame())) { - InsetsState.traverse(lastState, mState, mStartResizingAnimationIfNeeded); + if (mLastDispatchedState.getDisplayFrame().equals(state.getDisplayFrame())) { + // Here compares the raw states instead of the overridden ones because we don't want + // to animate an insets source that its mServerVisible is false. + InsetsState.traverse(mLastDispatchedState, state, mStartResizingAnimationIfNeeded); } } + mLastDispatchedState.set(state, true /* copySources */); return true; } diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 4ac78f593530..5c10db19b403 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -99,11 +99,17 @@ public class InsetsSource implements Parcelable { */ public static final int FLAG_FORCE_CONSUMING = 1 << 2; + /** + * Controls whether the insets source will play an animation when resizing. + */ + public static final int FLAG_ANIMATE_RESIZING = 1 << 3; + @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = "FLAG_", value = { FLAG_SUPPRESS_SCRIM, FLAG_INSETS_ROUNDED_CORNER, FLAG_FORCE_CONSUMING, + FLAG_ANIMATE_RESIZING, }) public @interface Flags {} @@ -546,6 +552,9 @@ public class InsetsSource implements Parcelable { if ((flags & FLAG_FORCE_CONSUMING) != 0) { joiner.add("FORCE_CONSUMING"); } + if ((flags & FLAG_ANIMATE_RESIZING) != 0) { + joiner.add("ANIMATE_RESIZING"); + } return joiner.toString(); } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 4f5b51d04c4b..cfdf8fab05c2 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -2099,6 +2099,65 @@ public final class SurfaceControl implements Parcelable { } } + /** + * Contains information of the idle time of the screen after which the refresh rate is to be + * reduced. + * + * @hide + */ + public static final class IdleScreenRefreshRateConfig { + /** + * The time(in ms) after which the refresh rate is to be reduced. Defaults to -1, which + * means no timeout has been configured for the current conditions + */ + public int timeoutMillis; + + public IdleScreenRefreshRateConfig() { + timeoutMillis = -1; + } + + public IdleScreenRefreshRateConfig(int timeoutMillis) { + this.timeoutMillis = timeoutMillis; + } + + /** + * Checks whether the two objects have the same values. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof IdleScreenRefreshRateConfig) || other == null) { + return false; + } + + IdleScreenRefreshRateConfig + idleScreenRefreshRateConfig = (IdleScreenRefreshRateConfig) other; + return timeoutMillis == idleScreenRefreshRateConfig.timeoutMillis; + } + + @Override + public int hashCode() { + return Objects.hash(timeoutMillis); + } + + @Override + public String toString() { + return "timeoutMillis: " + timeoutMillis; + } + + /** + * Copies the supplied object's values to this object. + */ + public void copyFrom(IdleScreenRefreshRateConfig other) { + if (other != null) { + this.timeoutMillis = other.timeoutMillis; + } + } + } + /** * Contains information about desired display configuration. @@ -2132,6 +2191,15 @@ public final class SurfaceControl implements Parcelable { */ public final RefreshRateRanges appRequestRanges; + /** + * Represents the idle time of the screen after which the associated display's refresh rate + * is to be reduced to preserve power + * Defaults to null, meaning that the device is not configured to have a timeout. + * Timeout value of -1 refers that the current conditions require no timeout + */ + @Nullable + public IdleScreenRefreshRateConfig idleScreenRefreshRateConfig; + public DesiredDisplayModeSpecs() { this.primaryRanges = new RefreshRateRanges(); this.appRequestRanges = new RefreshRateRanges(); @@ -2144,13 +2212,17 @@ public final class SurfaceControl implements Parcelable { } public DesiredDisplayModeSpecs(int defaultMode, boolean allowGroupSwitching, - RefreshRateRanges primaryRanges, RefreshRateRanges appRequestRanges) { + RefreshRateRanges primaryRanges, RefreshRateRanges appRequestRanges, + @Nullable IdleScreenRefreshRateConfig idleScreenRefreshRateConfig) { this.defaultMode = defaultMode; this.allowGroupSwitching = allowGroupSwitching; this.primaryRanges = new RefreshRateRanges(primaryRanges.physical, primaryRanges.render); this.appRequestRanges = new RefreshRateRanges(appRequestRanges.physical, appRequestRanges.render); + this.idleScreenRefreshRateConfig = + (idleScreenRefreshRateConfig == null) ? null : new IdleScreenRefreshRateConfig( + idleScreenRefreshRateConfig.timeoutMillis); } @Override @@ -2165,7 +2237,9 @@ public final class SurfaceControl implements Parcelable { return other != null && defaultMode == other.defaultMode && allowGroupSwitching == other.allowGroupSwitching && primaryRanges.equals(other.primaryRanges) - && appRequestRanges.equals(other.appRequestRanges); + && appRequestRanges.equals(other.appRequestRanges) + && Objects.equals( + idleScreenRefreshRateConfig, other.idleScreenRefreshRateConfig); } @Override @@ -2181,6 +2255,7 @@ public final class SurfaceControl implements Parcelable { allowGroupSwitching = other.allowGroupSwitching; primaryRanges.copyFrom(other.primaryRanges); appRequestRanges.copyFrom(other.appRequestRanges); + copyIdleScreenRefreshRateConfig(other.idleScreenRefreshRateConfig); } @Override @@ -2188,7 +2263,21 @@ public final class SurfaceControl implements Parcelable { return "defaultMode=" + defaultMode + " allowGroupSwitching=" + allowGroupSwitching + " primaryRanges=" + primaryRanges - + " appRequestRanges=" + appRequestRanges; + + " appRequestRanges=" + appRequestRanges + + " idleScreenRefreshRate=" + String.valueOf(idleScreenRefreshRateConfig); + } + + private void copyIdleScreenRefreshRateConfig(IdleScreenRefreshRateConfig other) { + if (idleScreenRefreshRateConfig == null) { + if (other != null) { + idleScreenRefreshRateConfig = + new IdleScreenRefreshRateConfig(other.timeoutMillis); + } + } else if (other == null) { + idleScreenRefreshRateConfig = null; + } else { + idleScreenRefreshRateConfig.copyFrom(other); + } } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0a9ac2f4bea7..736e8159c8c6 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -40,6 +40,8 @@ import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout; import static android.view.flags.Flags.sensitiveContentAppProtection; +import static android.view.flags.Flags.toolkitFrameRateBySizeReadOnly; +import static android.view.flags.Flags.toolkitFrameRateDefaultNormalReadOnly; import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision; import static android.view.flags.Flags.toolkitSetFrameRateReadOnly; import static android.view.flags.Flags.viewVelocityApi; @@ -33796,9 +33798,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, || heightDp <= FRAME_RATE_NARROW_THRESHOLD || (widthDp <= FRAME_RATE_SMALL_SIZE_THRESHOLD && heightDp <= FRAME_RATE_SMALL_SIZE_THRESHOLD)) { - return FRAME_RATE_CATEGORY_NORMAL | FRAME_RATE_CATEGORY_REASON_SMALL; + int category = toolkitFrameRateBySizeReadOnly() + ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL; + return category | FRAME_RATE_CATEGORY_REASON_SMALL; } else { - return FRAME_RATE_CATEGORY_HIGH | FRAME_RATE_CATEGORY_REASON_LARGE; + int category = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + return category | FRAME_RATE_CATEGORY_REASON_LARGE; } } @@ -33846,8 +33852,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, frameRateCategory = FRAME_RATE_CATEGORY_HIGH | FRAME_RATE_CATEGORY_REASON_REQUESTED; } else { - // invalid frame rate, default to HIGH - frameRateCategory = FRAME_RATE_CATEGORY_HIGH + // invalid frame rate, use default + int category = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + frameRateCategory = category | FRAME_RATE_CATEGORY_REASON_INVALID; } } else { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 5fe8c0069c13..3c61854c89f0 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -116,6 +116,7 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl import static com.android.input.flags.Flags.enablePointerChoreographer; import static com.android.window.flags.Flags.activityWindowInfoFlag; import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay; +import static com.android.window.flags.Flags.setScPropertiesInClient; import android.Manifest; import android.accessibilityservice.AccessibilityService; @@ -3564,6 +3565,16 @@ public final class ViewRootImpl implements ViewParent, mTransaction.setDefaultFrameRateCompatibility(mSurfaceControl, Surface.FRAME_RATE_COMPATIBILITY_NO_VOTE).apply(); } + + if (setScPropertiesInClient()) { + if (surfaceControlChanged || windowAttributesChanged) { + boolean colorSpaceAgnostic = (lp.privateFlags + & WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) + != 0; + mTransaction.setColorSpaceAgnostic(mSurfaceControl, colorSpaceAgnostic) + .apply(); + } + } } if (DEBUG_LAYOUT) Log.v(mTag, "relayout: frame=" + frame.toShortString() diff --git a/core/java/android/window/flags/accessibility.aconfig b/core/java/android/window/flags/accessibility.aconfig index 90b54bd76a60..590e88ba11f0 100644 --- a/core/java/android/window/flags/accessibility.aconfig +++ b/core/java/android/window/flags/accessibility.aconfig @@ -13,6 +13,16 @@ flag { description: "Always draw fullscreen orange border in fullscreen magnification" bug: "291891390" metadata { - purpose: PURPOSE_BUGFIX + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "use_window_original_touchable_region_when_magnification_recompute_bounds" + namespace: "accessibility" + description: "The flag controls whether to use the window original touchable regions in accessibilityController recomputeBounds" + bug: "323366243" + metadata { + purpose: PURPOSE_BUGFIX } }
\ No newline at end of file diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 00b600c662f5..5c310484eff9 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -96,3 +96,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "window_surfaces" + name: "set_sc_properties_in_client" + description: "Set VRI SC properties in the client instead of system server" + is_fixed_read_only: true + bug: "308662081" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 247f28c887f5..a5c209db9f5c 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -32,6 +32,17 @@ flag { } flag { + name: "remove_prepare_surface_in_placement" + namespace: "windowing_frontend" + description: "Reduce unnecessary invocation to improve performance" + bug: "330721336" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "close_to_square_config_includes_status_bar" namespace: "windowing_frontend" description: "On close to square display, when necessary, configuration includes status bar" diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 1eab9910b651..1aa635c6ceb7 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -208,10 +208,17 @@ static struct { static struct { jclass clazz; jmethodID ctor; + jfieldID timeoutMillis; +} gIdleScreenRefreshRateConfigClassInfo; + +static struct { + jclass clazz; + jmethodID ctor; jfieldID defaultMode; jfieldID allowGroupSwitching; jfieldID primaryRanges; jfieldID appRequestRanges; + jfieldID idleScreenRefreshRateConfig; } gDesiredDisplayModeSpecsClassInfo; static struct { @@ -1407,6 +1414,18 @@ static jboolean nativeSetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobj return ranges; }; + const auto makeIdleScreenRefreshRateConfig = [env](jobject obj) + -> std::optional<gui::DisplayModeSpecs::IdleScreenRefreshRateConfig> { + if (obj == NULL) { + return std::nullopt; + } + gui::DisplayModeSpecs::IdleScreenRefreshRateConfig idleScreenRefreshRateConfig; + idleScreenRefreshRateConfig.timeoutMillis = + env->GetIntField(obj, gIdleScreenRefreshRateConfigClassInfo.timeoutMillis); + + return idleScreenRefreshRateConfig; + }; + gui::DisplayModeSpecs specs; specs.defaultMode = env->GetIntField(DesiredDisplayModeSpecs, gDesiredDisplayModeSpecsClassInfo.defaultMode); @@ -1421,6 +1440,10 @@ static jboolean nativeSetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobj makeRanges(env->GetObjectField(DesiredDisplayModeSpecs, gDesiredDisplayModeSpecsClassInfo.appRequestRanges)); + specs.idleScreenRefreshRateConfig = makeIdleScreenRefreshRateConfig( + env->GetObjectField(DesiredDisplayModeSpecs, + gDesiredDisplayModeSpecsClassInfo.idleScreenRefreshRateConfig)); + size_t result = SurfaceComposerClient::setDesiredDisplayModeSpecs(token, specs); return result == NO_ERROR ? JNI_TRUE : JNI_FALSE; } @@ -1440,6 +1463,17 @@ static jobject nativeGetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobje rangeToJava(ranges.physical), rangeToJava(ranges.render)); }; + const auto idleScreenRefreshRateConfigToJava = + [env](const std::optional<gui::DisplayModeSpecs::IdleScreenRefreshRateConfig>& + idleScreenRefreshRateConfig) -> jobject { + if (!idleScreenRefreshRateConfig.has_value()) { + return NULL; // Return null if input config is null + } + return env->NewObject(gIdleScreenRefreshRateConfigClassInfo.clazz, + gIdleScreenRefreshRateConfigClassInfo.ctor, + idleScreenRefreshRateConfig->timeoutMillis); + }; + gui::DisplayModeSpecs specs; if (SurfaceComposerClient::getDesiredDisplayModeSpecs(token, &specs) != NO_ERROR) { return nullptr; @@ -1448,7 +1482,8 @@ static jobject nativeGetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobje return env->NewObject(gDesiredDisplayModeSpecsClassInfo.clazz, gDesiredDisplayModeSpecsClassInfo.ctor, specs.defaultMode, specs.allowGroupSwitching, rangesToJava(specs.primaryRanges), - rangesToJava(specs.appRequestRanges)); + rangesToJava(specs.appRequestRanges), + idleScreenRefreshRateConfigToJava(specs.idleScreenRefreshRateConfig)); } static jobject nativeGetDisplayNativePrimaries(JNIEnv* env, jclass, jobject tokenObj) { @@ -2607,13 +2642,23 @@ int register_android_view_SurfaceControl(JNIEnv* env) GetFieldIDOrDie(env, RefreshRateRangesClazz, "render", "Landroid/view/SurfaceControl$RefreshRateRange;"); + jclass IdleScreenRefreshRateConfigClazz = + FindClassOrDie(env, "android/view/SurfaceControl$IdleScreenRefreshRateConfig"); + gIdleScreenRefreshRateConfigClassInfo.clazz = + MakeGlobalRefOrDie(env, IdleScreenRefreshRateConfigClazz); + gIdleScreenRefreshRateConfigClassInfo.ctor = + GetMethodIDOrDie(env, gIdleScreenRefreshRateConfigClassInfo.clazz, "<init>", "(I)V"); + gIdleScreenRefreshRateConfigClassInfo.timeoutMillis = + GetFieldIDOrDie(env, gIdleScreenRefreshRateConfigClassInfo.clazz, "timeoutMillis", "I"); + jclass DesiredDisplayModeSpecsClazz = FindClassOrDie(env, "android/view/SurfaceControl$DesiredDisplayModeSpecs"); gDesiredDisplayModeSpecsClassInfo.clazz = MakeGlobalRefOrDie(env, DesiredDisplayModeSpecsClazz); gDesiredDisplayModeSpecsClassInfo.ctor = GetMethodIDOrDie(env, gDesiredDisplayModeSpecsClassInfo.clazz, "<init>", "(IZLandroid/view/SurfaceControl$RefreshRateRanges;Landroid/view/" - "SurfaceControl$RefreshRateRanges;)V"); + "SurfaceControl$RefreshRateRanges;Landroid/view/" + "SurfaceControl$IdleScreenRefreshRateConfig;)V"); gDesiredDisplayModeSpecsClassInfo.defaultMode = GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "defaultMode", "I"); gDesiredDisplayModeSpecsClassInfo.allowGroupSwitching = @@ -2624,6 +2669,9 @@ int register_android_view_SurfaceControl(JNIEnv* env) gDesiredDisplayModeSpecsClassInfo.appRequestRanges = GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "appRequestRanges", "Landroid/view/SurfaceControl$RefreshRateRanges;"); + gDesiredDisplayModeSpecsClassInfo.idleScreenRefreshRateConfig = + GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "idleScreenRefreshRateConfig", + "Landroid/view/SurfaceControl$IdleScreenRefreshRateConfig;"); jclass jankDataClazz = FindClassOrDie(env, "android/view/SurfaceControl$JankData"); diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 6b0c2d28b776..fcc85b7ec90f 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -102,6 +102,7 @@ message SecureSettingsProto { optional SettingProto qs_targets = 54 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto accessibility_pinch_to_zoom_anywhere_enabled = 55 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto accessibility_single_finger_panning_enabled = 56 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto accessibility_floating_menu_targets = 57 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 89ac81ebce56..1d6b151e2278 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6984,4 +6984,7 @@ <!-- Whether WM DisplayContent supports high performance transitions (lower-end devices may want to disable) --> <bool name="config_deviceSupportsHighPerfTransitions">true</bool> + + <!-- Wear devices: An intent action that is used for remote intent. --> + <string name="config_wearRemoteIntentAction" translatable="false" /> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 2e029b23f6af..4322b55b3f35 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5385,4 +5385,6 @@ <!-- Whether WM DisplayContent supports high performance transitions --> <java-symbol type="bool" name="config_deviceSupportsHighPerfTransitions" /> + + <java-symbol type="string" name="config_wearRemoteIntentAction" /> </resources> diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index 316e191eecbd..97f894f8dcac 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -21,12 +21,11 @@ import static android.view.InsetsController.ANIMATION_TYPE_HIDE; import static android.view.InsetsController.ANIMATION_TYPE_NONE; import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.ANIMATION_TYPE_SHOW; -import static android.view.InsetsController.AnimationType; +import static android.view.InsetsSource.FLAG_ANIMATE_RESIZING; import static android.view.InsetsSource.ID_IME; import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED; import static android.view.InsetsSourceConsumer.ShowResult.SHOW_IMMEDIATELY; import static android.view.ViewRootImpl.CAPTION_ON_SHELL; -import static android.view.WindowInsets.Type.SIZE; import static android.view.WindowInsets.Type.all; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.defaultVisible; @@ -671,36 +670,81 @@ public class InsetsControllerTest { } @Test - public void testResizeAnimation_insetsTypes() { - for (int i = 0; i < SIZE; i++) { - final @InsetsType int type = 1 << i; - final @AnimationType int expectedAnimationType = (type & systemBars()) != 0 - ? ANIMATION_TYPE_RESIZE - : ANIMATION_TYPE_NONE; - doTestResizeAnimation_insetsTypes(type, expectedAnimationType); - } + public void testResizeAnimation_withFlagAnimateResizing() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + final int id = ID_NAVIGATION_BAR; + final @InsetsType int type = navigationBars(); + final InsetsState state1 = new InsetsState(); + state1.getOrCreateSource(id, type) + .setVisible(true) + .setFrame(0, 0, 500, 50) + .setFlags(FLAG_ANIMATE_RESIZING, FLAG_ANIMATE_RESIZING); + final InsetsState state2 = new InsetsState(state1, true /* copySources */); + state2.peekSource(id).setFrame(0, 0, 500, 60); + + // New insets source won't cause the resize animation. + mController.onStateChanged(state1); + assertEquals("There must not be resize animation.", ANIMATION_TYPE_NONE, + mController.getAnimationType(type)); + + // Changing frame of the source with FLAG_ANIMATE_RESIZING will cause the resize + // animation. + mController.onStateChanged(state2); + assertEquals("There must be resize animation.", ANIMATION_TYPE_RESIZE, + mController.getAnimationType(type)); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } - private void doTestResizeAnimation_insetsTypes(@InsetsType int type, - @AnimationType int expectedAnimationType) { - final int id = type; + @Test + public void testResizeAnimation_withoutFlagAnimateResizing() { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + final int id = ID_STATUS_BAR; + final @InsetsType int type = statusBars(); final InsetsState state1 = new InsetsState(); - state1.getOrCreateSource(id, type).setVisible(true).setFrame(0, 0, 500, 50); + state1.getOrCreateSource(id, type) + .setVisible(true) + .setFrame(0, 0, 500, 50) + .setFlags(0, FLAG_ANIMATE_RESIZING); final InsetsState state2 = new InsetsState(state1, true /* copySources */); state2.peekSource(id).setFrame(0, 0, 500, 60); - final String message = "Animation type of " + WindowInsets.Type.toString(type) + ":"; + final String message = "There must not be resize animation."; + + // New insets source won't cause the resize animation. + mController.onStateChanged(state1); + assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); + + // Changing frame of the source without FLAG_ANIMATE_RESIZING must not cause the resize + // animation. + mController.onStateChanged(state2); + assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + @Test + public void testResizeAnimation_sourceFrame() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + final int id = ID_STATUS_BAR; + final @InsetsType int type = statusBars(); + final InsetsState state1 = new InsetsState(); + state1.setDisplayFrame(new Rect(0, 0, 500, 1000)); + state1.getOrCreateSource(id, type).setFrame(0, 0, 500, 50); + final InsetsState state2 = new InsetsState(state1, true /* copySources */); + state2.setDisplayFrame(state1.getDisplayFrame()); + state2.peekSource(id).setFrame(0, 0, 500, 0); + final String message = "There must not be resize animation."; // New insets source won't cause the resize animation. mController.onStateChanged(state1); assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); - // Changing frame might cause the resize animation. This depends on the insets type. + // Changing frame won't cause the resize animation if the new frame is empty. mController.onStateChanged(state2); - assertEquals(message, expectedAnimationType, mController.getAnimationType(type)); + assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); - // Cancel the existing animations for the next iteration. - mController.cancelExistingAnimations(); + // Changing frame won't cause the resize animation if the existing frame is empty. + mController.onStateChanged(state1); assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index 90a8c5c57fc2..226629e2019e 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -16,7 +16,14 @@ package android.view; +import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; +import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; +import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; +import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_DEFAULT_NORMAL_READ_ONLY; +import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; +import static android.view.flags.Flags.toolkitFrameRateBySizeReadOnly; +import static android.view.flags.Flags.toolkitFrameRateDefaultNormalReadOnly; import static junit.framework.Assert.assertEquals; @@ -124,6 +131,7 @@ public class ViewFrameRateTest { } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategorySmall() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -141,12 +149,14 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a normal category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_NORMAL, - mViewRoot.getPreferredFrameRateCategory()); + int expected = toolkitFrameRateBySizeReadOnly() + ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); }); } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategoryNarrowWidth() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -164,12 +174,14 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a normal category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_NORMAL, - mViewRoot.getPreferredFrameRateCategory()); + int expected = toolkitFrameRateBySizeReadOnly() + ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); }); } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategoryNarrowHeight() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -187,12 +199,14 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a normal category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_NORMAL, - mViewRoot.getPreferredFrameRateCategory()); + int expected = toolkitFrameRateBySizeReadOnly() + ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); }); } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategoryLargeWidth() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -210,12 +224,14 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a high category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_HIGH, - mViewRoot.getPreferredFrameRateCategory()); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); }); } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategoryLargeHeight() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -233,7 +249,20 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a high category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_HIGH, + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); + }); + } + + @Test + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_DEFAULT_NORMAL_READ_ONLY}) + public void defaultNormal() throws Throwable { + waitForFrameRateCategoryToSettle(); + mActivityRule.runOnUiThread(() -> { + mMovingView.invalidate(); + assertEquals(FRAME_RATE_CATEGORY_NORMAL, mViewRoot.getPreferredFrameRateCategory()); }); } diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 41b67ce4d651..fa364e06a705 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -41,6 +41,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; +import static android.view.flags.Flags.toolkitFrameRateDefaultNormalReadOnly; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -593,8 +594,9 @@ public class ViewRootImplTest { sInstrumentation.runOnMainSync(() -> { view.setVisibility(View.VISIBLE); view.invalidate(); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), - FRAME_RATE_CATEGORY_HIGH); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, viewRootImpl.getPreferredFrameRateCategory()); }); sInstrumentation.waitForIdleSync(); @@ -658,7 +660,9 @@ public class ViewRootImplTest { ViewRootImpl viewRootImpl = view.getViewRootImpl(); sInstrumentation.runOnMainSync(() -> { view.invalidate(); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, viewRootImpl.getPreferredFrameRateCategory()); }); } @@ -1017,11 +1021,13 @@ public class ViewRootImplTest { ViewRootImpl viewRootImpl = view.getViewRootImpl(); - // In transistion from frequent update to infrequent update + // In transition from frequent update to infrequent update Thread.sleep(delay); sInstrumentation.runOnMainSync(() -> { view.invalidate(); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, viewRootImpl.getPreferredFrameRateCategory()); }); // reset the frame rate category counts @@ -1033,7 +1039,7 @@ public class ViewRootImplTest { sInstrumentation.waitForIdleSync(); } - // In transistion from frequent update to infrequent update + // In transition from frequent update to infrequent update Thread.sleep(delay); sInstrumentation.runOnMainSync(() -> { view.setRequestedFrameRate(view.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE); @@ -1041,6 +1047,13 @@ public class ViewRootImplTest { assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NO_PREFERENCE); }); + Thread.sleep(delay); + sInstrumentation.runOnMainSync(() -> { + view.setRequestedFrameRate(view.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_NO_PREFERENCE); + }); // Infrequent update Thread.sleep(delay); @@ -1100,8 +1113,9 @@ public class ViewRootImplTest { assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NO_PREFERENCE); view.invalidate(); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), - FRAME_RATE_CATEGORY_HIGH); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, viewRootImpl.getPreferredFrameRateCategory()); }); // reset the frame rate category counts diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java index f105072a32bf..2cac2e150919 100644 --- a/keystore/java/android/security/KeyStore.java +++ b/keystore/java/android/security/KeyStore.java @@ -17,7 +17,6 @@ package android.security; import android.compat.annotation.UnsupportedAppUsage; -import android.os.Build; import android.os.StrictMode; /** @@ -30,10 +29,6 @@ import android.os.StrictMode; */ public class KeyStore { - // ResponseCodes - see system/security/keystore/include/keystore/keystore.h - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int NO_ERROR = 1; - // Used for UID field to indicate the calling UID. public static final int UID_SELF = -1; @@ -48,8 +43,8 @@ public class KeyStore { * Add an authentication record to the keystore authorization table. * * @param authToken The packed bytes of a hw_auth_token_t to be provided to keymaster. - * @return {@code KeyStore.NO_ERROR} on success, otherwise an error value corresponding to - * a {@code KeymasterDefs.KM_ERROR_} value or {@code KeyStore} ResponseCode. + * @return 0 on success, otherwise an error value corresponding to a + * {@code KeymasterDefs.KM_ERROR_} value or {@code KeyStore} ResponseCode. */ public int addAuthToken(byte[] authToken) { StrictMode.noteDiskWrite(); diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java index 101a10e3d312..3f39eeb0cc6b 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java @@ -359,14 +359,12 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor } catch (KeyStoreException keyStoreException) { GeneralSecurityException e = KeyStoreCryptoOperationUtils.getExceptionForCipherInit( mKey, keyStoreException); - if (e != null) { - if (e instanceof InvalidKeyException) { - throw (InvalidKeyException) e; - } else if (e instanceof InvalidAlgorithmParameterException) { - throw (InvalidAlgorithmParameterException) e; - } else { - throw new ProviderException("Unexpected exception type", e); - } + if (e instanceof InvalidKeyException) { + throw (InvalidKeyException) e; + } else if (e instanceof InvalidAlgorithmParameterException) { + throw (InvalidAlgorithmParameterException) e; + } else { + throw new ProviderException("Unexpected exception type", e); } } diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java index 372e4cb3d72e..9b82206e5709 100644 --- a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java @@ -20,7 +20,6 @@ import android.app.ActivityThread; import android.hardware.biometrics.BiometricManager; import android.hardware.security.keymint.ErrorCode; import android.security.GateKeeper; -import android.security.KeyStore; import android.security.KeyStoreException; import android.security.KeyStoreOperation; import android.security.keymaster.KeymasterDefs; @@ -131,15 +130,10 @@ abstract class KeyStoreCryptoOperationUtils { /** * Returns the exception to be thrown by the {@code Cipher.init} method of the crypto operation - * in response to {@code KeyStore.begin} operation or {@code null} if the {@code init} method - * should succeed. + * in response to a failed {code IKeystoreSecurityLevel#createOperation()}. */ public static GeneralSecurityException getExceptionForCipherInit( AndroidKeyStoreKey key, KeyStoreException e) { - if (e.getErrorCode() == KeyStore.NO_ERROR) { - return null; - } - // Cipher-specific cases switch (e.getErrorCode()) { case KeymasterDefs.KM_ERROR_INVALID_NONCE: diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml index a4bbd8998cc5..147f99144b1d 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml @@ -16,13 +16,12 @@ --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@drawable/desktop_mode_resize_veil_background"> + android:layout_height="match_parent"> <ImageView android:id="@+id/veil_application_icon" - android:layout_width="96dp" - android:layout_height="96dp" + android:layout_width="@dimen/desktop_mode_resize_veil_icon_size" + android:layout_height="@dimen/desktop_mode_resize_veil_icon_size" android:layout_gravity="center" android:contentDescription="@string/app_icon_text" /> </FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index c032a8106c94..70371f6b18fc 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -506,6 +506,9 @@ <!-- The radius of the caption menu shadow. --> <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen> + <!-- The size of the icon shown in the resize veil. --> + <dimen name="desktop_mode_resize_veil_icon_size">96dp</dimen> + <dimen name="freeform_resize_handle">15dp</dimen> <dimen name="freeform_resize_corner">44dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index cdef4fddc95b..1b1c96764e88 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1166,7 +1166,7 @@ class DesktopTasksController( pendingIntentLaunchFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED ) isPendingIntentBackgroundActivityLaunchAllowedByPermission = true } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index eb82da8a8e9f..6a7d297e83e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -16,6 +16,7 @@ package com.android.wm.shell.draganddrop; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; @@ -301,16 +302,14 @@ public class DragAndDropPolicy { position); final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); + baseActivityOpts.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_DENIED); // TODO(b/255649902): Rework this so that SplitScreenController can always use the options // instead of a fillInIntent since it's assuming that the PendingIntent is mutable baseActivityOpts.setPendingIntentLaunchFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK); final Bundle opts = baseActivityOpts.toBundle(); - // Put BAL flags to avoid activity start aborted. - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); - mStarter.startIntent(session.launchableIntent, session.launchableIntent.getCreatorUserHandle().getIdentifier(), null /* fillIntent */, position, opts); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 62156fc7443b..6b5bdd2299e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -64,6 +64,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private TvPipBackgroundView mPipBackgroundView; private boolean mIsReloading; + private static final int PIP_MENU_FORCE_CLOSE_DELAY_MS = 10_000; + private final Runnable mClosePipMenuRunnable = this::closeMenu; @TvPipMenuMode private int mCurrentMenuMode = MODE_NO_MENU; @@ -280,6 +282,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: closeMenu()", TAG); requestMenuMode(MODE_NO_MENU); + mMainHandler.removeCallbacks(mClosePipMenuRunnable); } @Override @@ -488,13 +491,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private void requestMenuMode(@TvPipMenuMode int menuMode) { if (isMenuOpen() == isMenuOpen(menuMode)) { + if (mMainHandler.hasCallbacks(mClosePipMenuRunnable)) { + mMainHandler.removeCallbacks(mClosePipMenuRunnable); + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + } // No need to request a focus change. We can directly switch to the new mode. switchToMenuMode(menuMode); } else { if (isMenuOpen(menuMode)) { + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); mMenuModeOnFocus = menuMode; } - // Send a request to gain window focus if the menu is open, or lose window focus // otherwise. Once the focus change happens, we will request the new mode in the // callback {@link #onPipWindowFocusChanged}. @@ -584,6 +591,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override + public void onUserInteracting() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onUserInteracting - mCurrentMenuMode=%s", TAG, getMenuModeString()); + mMainHandler.removeCallbacks(mClosePipMenuRunnable); + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + + } + @Override public void onPipMovement(int keycode) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipMovement - mCurrentMenuMode=%s", TAG, getMenuModeString()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index b259e8d584a6..4a767ef2a113 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -491,30 +491,33 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP) { - if (event.getKeyCode() == KEYCODE_BACK) { mListener.onExitCurrentMenuMode(); return true; } - - if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { - switch (event.getKeyCode()) { - case KEYCODE_DPAD_UP: - case KEYCODE_DPAD_DOWN: - case KEYCODE_DPAD_LEFT: - case KEYCODE_DPAD_RIGHT: + switch (event.getKeyCode()) { + case KEYCODE_DPAD_UP: + case KEYCODE_DPAD_DOWN: + case KEYCODE_DPAD_LEFT: + case KEYCODE_DPAD_RIGHT: + mListener.onUserInteracting(); + if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { mListener.onPipMovement(event.getKeyCode()); return true; - case KEYCODE_ENTER: - case KEYCODE_DPAD_CENTER: + } + break; + case KEYCODE_ENTER: + case KEYCODE_DPAD_CENTER: + mListener.onUserInteracting(); + if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { mListener.onExitCurrentMenuMode(); return true; - default: - // Dispatch key event as normal below - } + } + break; + default: + // Dispatch key event as normal below } } - return super.dispatchKeyEvent(event); } @@ -637,6 +640,11 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L interface Listener { /** + * Called when any button (that affects the menu) on current menu mode was pressed. + */ + void onUserInteracting(); + + /** * Called when a button for exiting the current menu mode was pressed. */ void onExitCurrentMenuMode(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 4c9e17155625..ad290c6aeaa3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -451,7 +451,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * until a resize event calls showResizeVeil below. */ void createResizeVeil() { - mResizeVeil = new ResizeVeil(mContext, mAppIconDrawable, mTaskInfo, + mResizeVeil = new ResizeVeil(mContext, mAppIconDrawable, mTaskInfo, mTaskSurface, mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java index b0d3b5090ef0..d072f8cec194 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java @@ -23,13 +23,16 @@ import android.annotation.ColorRes; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.PixelFormat; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.Display; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -37,6 +40,7 @@ import android.widget.ImageView; import android.window.TaskConstants; import com.android.wm.shell.R; +import com.android.wm.shell.common.SurfaceUtils; import java.util.function.Supplier; @@ -45,19 +49,36 @@ import java.util.function.Supplier; */ public class ResizeVeil { private static final int RESIZE_ALPHA_DURATION = 100; + + private static final int VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL; + /** The background is a child of the veil container layer and goes at the bottom. */ + private static final int VEIL_BACKGROUND_LAYER = 0; + /** The icon is a child of the veil container layer and goes in front of the background. */ + private static final int VEIL_ICON_LAYER = 1; + private final Context mContext; private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; + private final SurfaceSession mSurfaceSession = new SurfaceSession(); private final Drawable mAppIcon; private ImageView mIconView; + private int mIconSize; private SurfaceControl mParentSurface; + + /** A container surface to host the veil background and icon child surfaces. */ private SurfaceControl mVeilSurface; + /** A color surface for the veil background. */ + private SurfaceControl mBackgroundSurface; + /** A surface that hosts a windowless window with the app icon. */ + private SurfaceControl mIconSurface; + private final RunningTaskInfo mTaskInfo; private SurfaceControlViewHost mViewHost; private final Display mDisplay; private ValueAnimator mVeilAnimator; public ResizeVeil(Context context, Drawable appIcon, RunningTaskInfo taskInfo, + SurfaceControl taskSurface, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Display display, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) { mContext = context; @@ -65,6 +86,7 @@ public class ResizeVeil { mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mTaskInfo = taskInfo; + mParentSurface = taskSurface; mDisplay = display; setupResizeVeil(); } @@ -73,34 +95,44 @@ public class ResizeVeil { * Create the veil in its default invisible state. */ private void setupResizeVeil() { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); - mVeilSurface = builder - .setName("Resize veil of Task= " + mTaskInfo.taskId) + mVeilSurface = mSurfaceControlBuilderSupplier.get() + .setContainerLayer() + .setName("Resize veil of Task=" + mTaskInfo.taskId) + .setHidden(true) + .setParent(mParentSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build(); + mBackgroundSurface = SurfaceUtils.makeColorLayer(mVeilSurface, + "Resize veil background of Task=" + mTaskInfo.taskId, mSurfaceSession); + mIconSurface = mSurfaceControlBuilderSupplier.get() + .setName("Resize veil icon of Task= " + mTaskInfo.taskId) .setContainerLayer() + .setParent(mVeilSurface) + .setHidden(true) + .setCallsite("ResizeVeil#setupResizeVeil") .build(); - View v = LayoutInflater.from(mContext) - .inflate(R.layout.desktop_mode_resize_veil, null); - t.setPosition(mVeilSurface, 0, 0) - .setLayer(mVeilSurface, TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL) - .apply(); - Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + mIconSize = mContext.getResources() + .getDimensionPixelSize(R.dimen.desktop_mode_resize_veil_icon_size); + final View root = LayoutInflater.from(mContext) + .inflate(R.layout.desktop_mode_resize_veil, null /* root */); + mIconView = root.findViewById(R.id.veil_application_icon); + mIconView.setImageDrawable(mAppIcon); + final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(taskBounds.width(), - taskBounds.height(), + new WindowManager.LayoutParams( + mIconSize, + mIconSize, WindowManager.LayoutParams.TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); - lp.setTitle("Resize veil of Task=" + mTaskInfo.taskId); + lp.setTitle("Resize veil icon window of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); - WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, - mVeilSurface, null /* hostInputToken */); - mViewHost = new SurfaceControlViewHost(mContext, mDisplay, windowManager, "ResizeVeil"); - mViewHost.setView(v, lp); - mIconView = mViewHost.getView().findViewById(R.id.veil_application_icon); - mIconView.setImageDrawable(mAppIcon); + final WindowlessWindowManager wwm = new WindowlessWindowManager(mTaskInfo.configuration, + mIconSurface, null /* hostInputToken */); + mViewHost = new SurfaceControlViewHost(mContext, mDisplay, wwm, "ResizeVeil"); + mViewHost.setView(root, lp); } /** @@ -120,46 +152,74 @@ public class ResizeVeil { mParentSurface = parentSurface; } - int backgroundColorId = getBackgroundColorId(); - mViewHost.getView().setBackgroundColor(mContext.getColor(backgroundColorId)); + t.show(mVeilSurface); + t.setLayer(mVeilSurface, VEIL_CONTAINER_LAYER); + t.setLayer(mIconSurface, VEIL_ICON_LAYER); + t.setLayer(mBackgroundSurface, VEIL_BACKGROUND_LAYER); + t.setColor(mBackgroundSurface, + Color.valueOf(mContext.getColor(getBackgroundColorId())).getComponents()); relayout(taskBounds, t); if (fadeIn) { cancelAnimation(); + final SurfaceControl.Transaction veilAnimT = mSurfaceControlTransactionSupplier.get(); mVeilAnimator = new ValueAnimator(); mVeilAnimator.setFloatValues(0f, 1f); mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); mVeilAnimator.addUpdateListener(animation -> { - t.setAlpha(mVeilSurface, mVeilAnimator.getAnimatedFraction()); - t.apply(); + veilAnimT.setAlpha(mBackgroundSurface, mVeilAnimator.getAnimatedFraction()); + veilAnimT.apply(); }); mVeilAnimator.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + veilAnimT.show(mBackgroundSurface) + .setAlpha(mBackgroundSurface, 0) + .apply(); + } + + @Override public void onAnimationEnd(Animator animation) { - t.setAlpha(mVeilSurface, 1); - t.apply(); + veilAnimT.setAlpha(mBackgroundSurface, 1).apply(); } }); + final SurfaceControl.Transaction iconAnimT = mSurfaceControlTransactionSupplier.get(); final ValueAnimator iconAnimator = new ValueAnimator(); iconAnimator.setFloatValues(0f, 1f); iconAnimator.setDuration(RESIZE_ALPHA_DURATION); iconAnimator.addUpdateListener(animation -> { - mIconView.setAlpha(animation.getAnimatedFraction()); + iconAnimT.setAlpha(mIconSurface, animation.getAnimatedFraction()); + iconAnimT.apply(); }); + iconAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + iconAnimT.show(mIconSurface) + .setAlpha(mIconSurface, 0) + .apply(); + } + + @Override + public void onAnimationEnd(Animator animation) { + iconAnimT.setAlpha(mIconSurface, 1).apply(); + } + }); + // Let the animators show it with the correct alpha value once the animation starts. + t.hide(mIconSurface); + t.hide(mBackgroundSurface); + t.apply(); - t.show(mVeilSurface) - .addTransactionCommittedListener( - mContext.getMainExecutor(), () -> { - mVeilAnimator.start(); - iconAnimator.start(); - }) - .setAlpha(mVeilSurface, 0); + mVeilAnimator.start(); + iconAnimator.start(); } else { - // Show the veil immediately at full opacity. - t.show(mVeilSurface).setAlpha(mVeilSurface, 1); + // Show the veil immediately. + t.show(mIconSurface); + t.show(mBackgroundSurface); + t.setAlpha(mIconSurface, 1); + t.setAlpha(mBackgroundSurface, 1); + t.apply(); } - mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); } /** @@ -175,8 +235,9 @@ public class ResizeVeil { * @param newBounds bounds to update veil to. */ private void relayout(Rect newBounds, SurfaceControl.Transaction t) { - mViewHost.relayout(newBounds.width(), newBounds.height()); t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height()); + final PointF iconPosition = calculateAppIconPosition(newBounds); + t.setPosition(mIconSurface, iconPosition.x, iconPosition.y); t.setPosition(mParentSurface, newBounds.left, newBounds.top); t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height()); } @@ -204,7 +265,7 @@ public class ResizeVeil { mVeilAnimator.end(); } relayout(newBounds, t); - mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); + t.apply(); } /** @@ -217,14 +278,16 @@ public class ResizeVeil { mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); mVeilAnimator.addUpdateListener(animation -> { SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.setAlpha(mVeilSurface, 1 - mVeilAnimator.getAnimatedFraction()); + t.setAlpha(mBackgroundSurface, 1 - mVeilAnimator.getAnimatedFraction()); + t.setAlpha(mIconSurface, 1 - mVeilAnimator.getAnimatedFraction()); t.apply(); }); mVeilAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.hide(mVeilSurface); + t.hide(mBackgroundSurface); + t.hide(mIconSurface); t.apply(); } }); @@ -242,6 +305,11 @@ public class ResizeVeil { } } + private PointF calculateAppIconPosition(Rect parentBounds) { + return new PointF((float) parentBounds.width() / 2 - (float) mIconSize / 2, + (float) parentBounds.height() / 2 - (float) mIconSize / 2); + } + private void cancelAnimation() { if (mVeilAnimator != null) { mVeilAnimator.removeAllUpdateListeners(); @@ -260,11 +328,19 @@ public class ResizeVeil { mViewHost.release(); mViewHost = null; } + final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + if (mBackgroundSurface != null) { + t.remove(mBackgroundSurface); + mBackgroundSurface = null; + } + if (mIconSurface != null) { + t.remove(mIconSurface); + mIconSurface = null; + } if (mVeilSurface != null) { - final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); t.remove(mVeilSurface); mVeilSurface = null; - t.apply(); } + t.apply(); } } diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index ce689aca51bf..19e59a776511 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -8,6 +8,13 @@ flag { } flag { + name: "enable_location_bypass" + namespace: "location" + description: "Enable location bypass feature" + bug: "301150056" +} + +flag { name: "location_bypass" is_exported: true namespace: "location" diff --git a/nfc/Android.bp b/nfc/Android.bp index 7698e2b2d054..0b3f291a49de 100644 --- a/nfc/Android.bp +++ b/nfc/Android.bp @@ -50,7 +50,7 @@ java_sdk_library { ], defaults: ["framework-module-defaults"], sdk_version: "module_current", - min_sdk_version: "34", // should be 35 (making it 34 for compiling for `-next`) + min_sdk_version: "current", installable: true, optimize: { enabled: false, diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl index c444740a5b1b..7a78f3d17e1e 100644 --- a/nfc/java/android/nfc/INfcAdapter.aidl +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -47,8 +47,8 @@ interface INfcAdapter INfcAdapterExtras getNfcAdapterExtrasInterface(in String pkg); INfcDta getNfcDtaInterface(in String pkg); int getState(); - boolean disable(boolean saveState); - boolean enable(); + boolean disable(boolean saveState, in String pkg); + boolean enable(in String pkg); void pausePolling(int timeoutInMs); void resumePolling(); diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 0ebc3f5178e0..7a7db31fd417 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -1117,7 +1117,7 @@ public final class NfcAdapter { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable() { try { - return sService.enable(); + return sService.enable(mContext.getPackageName()); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -1126,7 +1126,7 @@ public final class NfcAdapter { return false; } try { - return sService.enable(); + return sService.enable(mContext.getPackageName()); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } @@ -1156,7 +1156,7 @@ public final class NfcAdapter { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable() { try { - return sService.disable(true); + return sService.disable(true, mContext.getPackageName()); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -1165,7 +1165,7 @@ public final class NfcAdapter { return false; } try { - return sService.disable(true); + return sService.disable(true, mContext.getPackageName()); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } @@ -1181,7 +1181,7 @@ public final class NfcAdapter { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable(boolean persist) { try { - return sService.disable(persist); + return sService.disable(persist, mContext.getPackageName()); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -1190,7 +1190,7 @@ public final class NfcAdapter { return false; } try { - return sService.disable(persist); + return sService.disable(persist, mContext.getPackageName()); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java index a353df743520..572e20d1d9f8 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -723,7 +723,8 @@ public final class ApduServiceInfo implements Parcelable { * delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this * multiple times will cause the value to be overwritten each time. * @param pollingLoopFilter the polling loop filter to add, must be a valid hexadecimal string - * @param autoTransact whether Observe Mode should be disabled when this filter matches or not + * @param autoTransact when true, disable observe mode when this filter matches, when false, + * matching does not change the observe mode state */ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopFilter(@NonNull String pollingLoopFilter, @@ -748,7 +749,8 @@ public final class ApduServiceInfo implements Parcelable { * multiple times will cause the value to be overwritten each time. * @param pollingLoopPatternFilter the polling loop pattern filter to add, must be a valid * regex to match a hexadecimal string - * @param autoTransact whether Observe Mode should be disabled when this filter matches or not + * @param autoTransact when true, disable observe mode when this filter matches, when false, + * matching does not change the observe mode state */ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopPatternFilter(@NonNull String pollingLoopPatternFilter, diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml index a5ccdb6575bb..7a8c25bd12ab 100644 --- a/packages/CredentialManager/AndroidManifest.xml +++ b/packages/CredentialManager/AndroidManifest.xml @@ -23,6 +23,7 @@ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/> <uses-permission android:name="android.permission.ACCESS_INSTANT_APPS" /> + <uses-permission android:name="android.permission.USE_BIOMETRIC" /> <application android:allowBackup="true" diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 527701c06bc6..bc35a85e48f8 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -63,11 +63,11 @@ <!-- This appears as the description body of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] --> <string name="choose_provider_body">Select a password manager to save your info and sign in faster next time</string> <!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is passkey. [CHAR LIMIT=200] --> - <string name="choose_create_option_passkey_title">Create passkey to sign in to <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string> + <string name="choose_create_option_passkey_title">Create passkey to sign in to <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string> <!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is password. [CHAR LIMIT=200] --> - <string name="choose_create_option_password_title">Save password to sign in to <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string> + <string name="choose_create_option_password_title">Save password to sign in to <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string> <!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is others. [CHAR LIMIT=200] --> - <string name="choose_create_option_sign_in_title">Save sign-in info for <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string> + <string name="choose_create_option_sign_in_title">Save sign-in info for <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string> <!-- Types which are inserted as a placeholder as credentialTypes for other strings. [CHAR LIMIT=200] --> <string name="passkey">passkey</string> <string name="password">password</string> @@ -122,6 +122,8 @@ <string name="get_dialog_title_use_passkey_for">Use your saved passkey for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> <!-- This appears as the title of the modal bottom sheet asking for user confirmation to use the single previously saved password to sign in to the app. [CHAR LIMIT=200] --> <string name="get_dialog_title_use_password_for">Use your saved password for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> + <!-- This appears as a description of the modal bottom sheet when the single tap sign in flow is used for the get flow. [CHAR LIMIT=200] --> + <string name="get_dialog_title_single_tap_for">Use your screen lock to sign in to <xliff:g id="app_name" example="Shrine">%1$s</xliff:g> with <xliff:g id="username" example="beckett-bakery@gmail.com">%2$s</xliff:g></string> <!-- This appears as the title of the dialog asking for user confirmation to use the single user credential (previously saved or to be created) to sign in to the app. [CHAR LIMIT=200] --> <string name="get_dialog_title_use_sign_in_for">Use your sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> <!-- This appears as the title of the dialog asking for user confirmation to unlock / authenticate (e.g. via fingerprint, faceId, passcode etc.) so that we can retrieve their sign-in options. [CHAR LIMIT=200] --> diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt index 12cb7ffddd5d..f2c252ec6422 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt @@ -48,6 +48,7 @@ import com.android.credentialmanager.model.get.ProviderInfo import com.android.credentialmanager.model.get.RemoteEntryInfo import com.android.credentialmanager.shared.R import com.android.credentialmanager.TAG +import com.android.credentialmanager.model.BiometricRequestInfo import com.android.credentialmanager.model.EntryInfo fun EntryInfo.getIntentSenderRequest( @@ -139,6 +140,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), + biometricRequest = predetermineAndValidateBiometricFlow(it), ) ) } @@ -167,6 +169,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), + biometricRequest = predetermineAndValidateBiometricFlow(it), ) ) } @@ -194,6 +197,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), + biometricRequest = predetermineAndValidateBiometricFlow(it), ) ) } @@ -205,6 +209,36 @@ private fun getCredentialOptionInfoList( } return result } + +/** + * This validates if this is a biometric flow or not, and if it is, this returns the expected + * [BiometricRequestInfo]. Namely, the biometric flow must have at least the + * ALLOWED_AUTHENTICATORS bit passed from Jetpack. + * Note that the required values, such as the provider info's icon or display name, or the entries + * credential type or userName, and finally the display info's app name, are non-null and must + * exist to run through the flow. + * // TODO(b/326243754) : Presently, due to dependencies, the opId bit is parsed but is never + * // expected to be used. When it is added, it should be lightly validated. + */ +private fun predetermineAndValidateBiometricFlow( + it: Entry +): BiometricRequestInfo? { + // TODO(b/326243754) : When available, use the official jetpack structured type + val allowedAuthenticators: Int? = it.slice.items.firstOrNull { + it.hasHint("androidx.credentials." + + "provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS") + }?.int + + // This is optional and does not affect validating the biometric flow in any case + val opId: Int? = it.slice.items.firstOrNull { + it.hasHint("androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID") + }?.int + if (allowedAuthenticators != null) { + return BiometricRequestInfo(opId = opId, allowedAuthenticators = allowedAuthenticators) + } + return null +} + val Slice.credentialEntry: CredentialEntry? get() = try { @@ -221,7 +255,6 @@ val Slice.credentialEntry: CredentialEntry? CustomCredentialEntry.fromSlice(this) } - /** * Note: caller required handle empty list due to parsing error. */ diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt new file mode 100644 index 000000000000..486cfe7123dd --- /dev/null +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.model + +/** + * This allows reading the data from the request, and holding that state around the framework. + * The [opId] bit is required for some authentication flows where CryptoObjects are used. + * The [allowedAuthenticators] is needed for all flows, and our flow ensures this value is never + * null. + */ +data class BiometricRequestInfo( + val opId: Int? = null, + val allowedAuthenticators: Int +)
\ No newline at end of file diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt index d6189eb15ff3..fe02e5ba026d 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt @@ -19,6 +19,7 @@ package com.android.credentialmanager.model.creation import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Drawable +import com.android.credentialmanager.model.BiometricRequestInfo import com.android.credentialmanager.model.EntryInfo import java.time.Instant @@ -36,6 +37,7 @@ class CreateOptionInfo( val lastUsedTime: Instant, val footerDescription: String?, val allowAutoSelect: Boolean, + val biometricRequest: BiometricRequestInfo? = null, ) : EntryInfo( providerId, entryKey, diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt index a657e97de3cc..8913397db072 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt @@ -19,6 +19,7 @@ package com.android.credentialmanager.model.get import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Drawable +import com.android.credentialmanager.model.BiometricRequestInfo import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.EntryInfo import java.time.Instant @@ -49,6 +50,7 @@ class CredentialEntryInfo( // "For <value-of-entryGroupId>" on the more-option screen. val isDefaultIconPreferredAsSingleProvider: Boolean, val affiliatedDomain: String?, + val biometricRequest: BiometricRequestInfo? = null, ) : EntryInfo( providerId, entryKey, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 879d64c761ec..b17a98b30eee 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -40,6 +40,7 @@ import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.findAutoSelectEntry import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.createflow.isFlowAutoSelectable +import com.android.credentialmanager.getflow.findBiometricFlowEntry /** * Client for interacting with Credential Manager. Also holds data inputs from it. @@ -148,10 +149,17 @@ class CredentialManagerRepo( ) } RequestInfo.TYPE_GET -> { - val getCredentialInitialUiState = getCredentialInitialUiState(originName, + var getCredentialInitialUiState = getCredentialInitialUiState(originName, isReqForAllOptions)!! val autoSelectEntry = findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo) + val biometricEntry = findBiometricFlowEntry( + getCredentialInitialUiState.providerDisplayInfo, + autoSelectEntry != null) + if (biometricEntry != null) { + getCredentialInitialUiState = getCredentialInitialUiState.copy( + activeEntry = biometricEntry) + } UiState( createCredentialUiState = null, getCredentialUiState = getCredentialInitialUiState, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 1f2fa200e43d..28c40479962e 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager import android.app.Activity +import android.hardware.biometrics.BiometricPrompt import android.os.IBinder import android.text.TextUtils import android.util.Log @@ -28,6 +29,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.android.credentialmanager.common.BiometricResult +import com.android.credentialmanager.common.BiometricState import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.common.Constants import com.android.credentialmanager.common.DialogState @@ -54,6 +57,7 @@ data class UiState( val isAutoSelectFlow: Boolean = false, val cancelRequestState: CancelUiRequestState?, val isInitialRender: Boolean, + val biometricState: BiometricState = BiometricState() ) data class CancelUiRequestState( @@ -113,12 +117,21 @@ class CredentialSelectorViewModel( launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult> ) { val entry = uiState.selectedEntry + val biometricState = uiState.biometricState val pendingIntent = entry?.pendingIntent if (pendingIntent != null) { Log.d(Constants.LOG_TAG, "Launching provider activity") uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING) val entryIntent = entry.fillInIntent entryIntent?.putExtra(Constants.IS_AUTO_SELECTED_KEY, uiState.isAutoSelectFlow) + if (biometricState.biometricResult != null) { + if (uiState.isAutoSelectFlow) { + Log.w(Constants.LOG_TAG, "Unexpected biometric result exists when " + + "autoSelect is preferred.") + } + entryIntent?.putExtra(Constants.BIOMETRIC_AUTH_TYPE, + biometricState.biometricResult.biometricAuthenticationResult.authenticationType) + } val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent) .setFillInIntent(entryIntent).build() try { @@ -200,13 +213,20 @@ class CredentialSelectorViewModel( /**************************************************************************/ /***** Get Flow Callbacks *****/ /**************************************************************************/ - fun getFlowOnEntrySelected(entry: EntryInfo) { + fun getFlowOnEntrySelected( + entry: EntryInfo, + authResult: BiometricPrompt.AuthenticationResult? = null + ) { Log.d(Constants.LOG_TAG, "credential selected: {provider=${entry.providerId}" + ", key=${entry.entryKey}, subkey=${entry.entrySubkey}}") uiState = if (entry.pendingIntent != null) { uiState.copy( selectedEntry = entry, providerActivityState = ProviderActivityState.READY_TO_LAUNCH, + biometricState = if (authResult == null) uiState.biometricState else uiState + .biometricState.copy(biometricResult = BiometricResult( + biometricAuthenticationResult = authResult) + ) ) } else { credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey) @@ -347,4 +367,9 @@ class CredentialSelectorViewModel( fun logUiEvent(uiEventEnum: UiEventEnum) { this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.packageName) } + + companion object { + // TODO(b/326243754) : Replace/remove once all failure flows added in + const val TEMPORARY_FAILURE_CODE = Integer.MIN_VALUE + } }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 6a1998a5e24e..fd6fc6a44c7c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -503,6 +503,8 @@ class CreateFlowUtils { it.hasHint("androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_" + "SELECT_ALLOWED") }?.text == "true", + // TODO(b/326243754) : Handle this when the create flow is added; for now the + // create flow does not support biometric values ) ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt new file mode 100644 index 000000000000..db5ab569535f --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.common + +import android.content.Context +import android.graphics.Bitmap +import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.BiometricPrompt +import android.os.CancellationSignal +import android.util.Log +import androidx.core.content.ContextCompat.getMainExecutor +import androidx.core.graphics.drawable.toBitmap +import com.android.credentialmanager.R +import com.android.credentialmanager.createflow.EnabledProviderInfo +import com.android.credentialmanager.getflow.ProviderDisplayInfo +import com.android.credentialmanager.getflow.RequestDisplayInfo +import com.android.credentialmanager.getflow.generateDisplayTitleTextResCode +import com.android.credentialmanager.model.BiometricRequestInfo +import com.android.credentialmanager.model.EntryInfo +import com.android.credentialmanager.model.get.CredentialEntryInfo +import com.android.credentialmanager.model.get.ProviderInfo +import java.lang.Exception + +/** + * Aggregates common display information used for the Biometric Flow. + * Namely, this adds the ability to encapsulate the [providerIcon], the providers icon, the + * [providerName], which represents the name of the provider, the [displayTitleText] which is + * the large text displaying the flow in progress, and the [descriptionAboveBiometricButton], which + * describes details of where the credential is being saved, and how. + */ +data class BiometricDisplayInfo( + val providerIcon: Bitmap, + val providerName: String, + val displayTitleText: String, + val descriptionAboveBiometricButton: String, + val biometricRequestInfo: BiometricRequestInfo, +) + +/** + * Sets up generic state used by the create and get flows to hold the holistic states for the flow. + * These match all the present callback values from [BiometricPrompt], and may be extended to hold + * additional states that may improve the flow. + */ +data class BiometricState( + val biometricResult: BiometricResult? = null, + val biometricError: BiometricError? = null, + val biometricHelp: BiometricHelp? = null, + val biometricAcquireInfo: Int? = null, +) + +/** + * When a result exists, it must be retrievable. This encapsulates the result + * so that should this object exist, the result will be retrievable. + */ +data class BiometricResult( + val biometricAuthenticationResult: BiometricPrompt.AuthenticationResult +) + +/** + * Encapsulates the error callback results to easily manage biometric error states in the flow. + */ +data class BiometricError( + val errorCode: Int, + val errString: CharSequence? = null +) + +/** + * Encapsulates the help callback results to easily manage biometric help states in the flow. + * To specify, this allows us to parse the onAuthenticationHelp method in the [BiometricPrompt]. + */ +data class BiometricHelp( + val helpCode: Int, + var helpString: CharSequence? = null +) + +/** + * This will handle the logic for integrating credential manager with the biometric prompt for the + * single account biometric experience. This simultaneously handles both the get and create flows, + * by retrieving all the data from credential manager, and properly parsing that data into the + * biometric prompt. + */ +fun runBiometricFlow( + biometricEntry: EntryInfo, + context: Context, + openMoreOptionsPage: () -> Unit, + sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, + onCancelFlowAndFinish: (String) -> Unit, + getRequestDisplayInfo: RequestDisplayInfo? = null, + getProviderInfoList: List<ProviderInfo>? = null, + getProviderDisplayInfo: ProviderDisplayInfo? = null, + onBiometricFailureFallback: () -> Unit, + createRequestDisplayInfo: com.android.credentialmanager.createflow + .RequestDisplayInfo? = null, + createProviderInfo: EnabledProviderInfo? = null, +) { + var biometricDisplayInfo: BiometricDisplayInfo? = null + if (getRequestDisplayInfo != null) { + biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(getRequestDisplayInfo, + getProviderInfoList, + getProviderDisplayInfo, + context, biometricEntry) + } else if (createRequestDisplayInfo != null) { + // TODO(b/326243754) : Create Flow to be implemented in follow up + biometricDisplayInfo = validateBiometricCreateFlow( + createRequestDisplayInfo, + createProviderInfo + ) + } + + if (biometricDisplayInfo == null) { + onBiometricFailureFallback() + return + } + + val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage, + biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators) + + val callback: BiometricPrompt.AuthenticationCallback = + setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry, + onCancelFlowAndFinish) + + val cancellationSignal = CancellationSignal() + cancellationSignal.setOnCancelListener { + Log.d(TAG, "Your cancellation signal was called.") + // TODO(b/326243754) : Migrate towards passing along the developer cancellation signal + // or validate the necessity for this + } + + val executor = getMainExecutor(context) + + try { + biometricPrompt.authenticate(cancellationSignal, executor, callback) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n") + onBiometricFailureFallback() + } +} + +/** + * Sets up the biometric prompt with the UI specific bits. + * // TODO(b/326243754) : Pass in opId once dependency is confirmed via CryptoObject + * // TODO(b/326243754) : Given fallbacks aren't allowed, for now we validate that device creds + * // are NOT allowed to be passed in to avoid throwing an error. Later, however, once target + * // alignments occur, we should add the bit back properly. + */ +private fun setupBiometricPrompt( + context: Context, + biometricDisplayInfo: BiometricDisplayInfo, + openMoreOptionsPage: () -> Unit, + requestAllowedAuthenticators: Int, +): BiometricPrompt { + val finalAuthenticators = removeDeviceCredential(requestAllowedAuthenticators) + + val biometricPrompt = BiometricPrompt.Builder(context) + .setTitle(biometricDisplayInfo.displayTitleText) + // TODO(b/326243754) : Migrate to using new methods recently aligned upon + .setNegativeButton(context.getString(R.string + .dropdown_presentation_more_sign_in_options_text), + getMainExecutor(context)) { _, _ -> + openMoreOptionsPage() + } + .setAllowedAuthenticators(finalAuthenticators) + .setConfirmationRequired(true) + // TODO(b/326243754) : Add logo back once new permission privileges sorted out + .setDescription(biometricDisplayInfo.descriptionAboveBiometricButton) + .build() + + return biometricPrompt +} + +// TODO(b/326243754) : Remove after larger level alignments made on fallback negative button +// For the time being, we do not support the pin fallback until UX is decided. +private fun removeDeviceCredential(requestAllowedAuthenticators: Int): Int { + var finalAuthenticators = requestAllowedAuthenticators + + if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL or + BiometricManager.Authenticators.BIOMETRIC_WEAK)) { + finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK + } + + if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL or + BiometricManager.Authenticators.BIOMETRIC_STRONG)) { + finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK + } + + if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL)) { + finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK + } + + return finalAuthenticators +} + +/** + * Sets up the biometric authentication callback. + */ +private fun setupBiometricAuthenticationCallback( + sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, + selectedEntry: EntryInfo, + onCancelFlowAndFinish: (String) -> Unit +): BiometricPrompt.AuthenticationCallback { + val callback: BiometricPrompt.AuthenticationCallback = + object : BiometricPrompt.AuthenticationCallback() { + // TODO(b/326243754) : Validate remaining callbacks + override fun onAuthenticationSucceeded( + authResult: BiometricPrompt.AuthenticationResult? + ) { + super.onAuthenticationSucceeded(authResult) + try { + if (authResult != null) { + sendDataToProvider(selectedEntry, authResult) + } else { + onCancelFlowAndFinish("The biometric flow succeeded but unexpectedly " + + "returned a null value.") + // TODO(b/326243754) : Propagate to provider + } + } catch (e: Exception) { + onCancelFlowAndFinish("The biometric flow succeeded but failed on handling " + + "the result. See: \n$e\n") + // TODO(b/326243754) : Propagate to provider + } + } + + override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) { + super.onAuthenticationHelp(helpCode, helpString) + Log.d(TAG, "Authentication help discovered: $helpCode and $helpString") + // TODO(b/326243754) : Decide on strategy with provider (a simple log probably + // suffices here) + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { + super.onAuthenticationError(errorCode, errString) + Log.d(TAG, "Authentication error-ed out: $errorCode and $errString") + // TODO(b/326243754) : Propagate to provider + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.d(TAG, "Authentication failed.") + // TODO(b/326243754) : Propagate to provider + } + } + return callback +} + +/** + * Creates the [BiometricDisplayInfo] for get flows, and early handles conditional + * checking between the two. Note that while this method's main purpose is to retrieve the info + * required to display the biometric prompt, it acts as a secondary validator to handle any null + * checks at the beginning of the biometric flow and supports a quick fallback. + * While it's not expected for the flow to be triggered if values are + * missing, some values are by default nullable when they are pulled, such as entries. Thus, this + * acts as a final validation failsafe, without requiring null checks or null forcing around the + * codebase. + */ +private fun validateAndRetrieveBiometricGetDisplayInfo( + getRequestDisplayInfo: RequestDisplayInfo?, + getProviderInfoList: List<ProviderInfo>?, + getProviderDisplayInfo: ProviderDisplayInfo?, + context: Context, + selectedEntry: EntryInfo +): BiometricDisplayInfo? { + if (getRequestDisplayInfo != null && getProviderInfoList != null && + getProviderDisplayInfo != null) { + if (selectedEntry !is CredentialEntryInfo) { return null } + return getBiometricDisplayValues(getProviderInfoList, + context, getRequestDisplayInfo, selectedEntry) + } + return null +} + +/** + * Creates the [BiometricDisplayInfo] for create flows, and early handles conditional + * checking between the two. The reason for this method matches the logic for the + * [validateBiometricGetFlow] with the only difference being that this is for the create flow. + */ +private fun validateBiometricCreateFlow( + createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo?, + createProviderInfo: EnabledProviderInfo?, +): BiometricDisplayInfo? { + if (createRequestDisplayInfo != null && createProviderInfo != null) { + } else if (createRequestDisplayInfo != null && createProviderInfo != null) { + // TODO(b/326243754) : Create Flow to be implemented in follow up + return createFlowDisplayValues() + } + return null +} + +/** + * Handles the biometric sign in via the 'get credentials' flow. + * If any expected value is not present, the flow is considered unreachable and we will fallback + * to the original selector. Note that these redundant checks are just failsafe; the original + * flow should never reach here with invalid params. + */ +private fun getBiometricDisplayValues( + getProviderInfoList: List<ProviderInfo>, + context: Context, + getRequestDisplayInfo: RequestDisplayInfo, + selectedEntry: CredentialEntryInfo, +): BiometricDisplayInfo? { + var icon: Bitmap? = null + var providerName: String? = null + var displayTitleText: String? = null + var descriptionText: String? = null + val primaryAccountsProviderInfo = retrievePrimaryAccountProviderInfo(selectedEntry.providerId, + getProviderInfoList) + icon = primaryAccountsProviderInfo?.icon?.toBitmap() + providerName = primaryAccountsProviderInfo?.displayName + if (icon == null || providerName == null) { + Log.d(TAG, "Unexpectedly found invalid provider information.") + return null + } + if (selectedEntry.biometricRequest == null) { + Log.d(TAG, "Unexpectedly in biometric flow without a biometric request.") + return null + } + val singleEntryType = selectedEntry.credentialType + val username = selectedEntry.userName + displayTitleText = context.getString( + generateDisplayTitleTextResCode(singleEntryType), + getRequestDisplayInfo.appName + ) + descriptionText = context.getString( + R.string.get_dialog_title_single_tap_for, + getRequestDisplayInfo.appName, + username + ) + return BiometricDisplayInfo(providerIcon = icon, providerName = providerName, + displayTitleText = displayTitleText, descriptionAboveBiometricButton = descriptionText, + biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo) +} + +/** + * Handles the biometric sign in via the 'create credentials' flow, or early validates this flow + * needs to fallback. + */ +private fun createFlowDisplayValues(): BiometricDisplayInfo? { + // TODO(b/326243754) : Create Flow to be implemented in follow up + return null +} + +/** + * During a get flow with single tap sign in enabled, this will match the credentialEntry that + * will single tap with the correct provider info. Namely, it's the first provider info that + * contains a matching providerId to the selected entry. + */ +private fun retrievePrimaryAccountProviderInfo( + providerId: String, + getProviderInfoList: List<ProviderInfo> +): ProviderInfo? { + var discoveredProviderInfo: ProviderInfo? = null + getProviderInfoList.forEach { provider -> + if (provider.id == providerId) { + discoveredProviderInfo = provider + return@forEach + } + } + return discoveredProviderInfo +} + +const val TAG = "BiometricHandler" diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt index 51ca5971cec4..7e7a74fd3107 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt @@ -22,5 +22,7 @@ class Constants { const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS = "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED" const val IS_AUTO_SELECTED_KEY = "IS_AUTO_SELECTED" + const val BIOMETRIC_AUTH_TYPE = "BIOMETRIC_AUTH_TYPE" + const val BIOMETRIC_AUTH_FAILURE = "BIOMETRIC_AUTH_FAILURE" } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index b9c9d8994c45..b59ccc264630 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -16,8 +16,10 @@ package com.android.credentialmanager.getflow +import android.credentials.flags.Flags.credmanBiometricApiEnabled import android.credentials.flags.Flags.selectorUiImprovementsEnabled import android.graphics.drawable.Drawable +import android.hardware.biometrics.BiometricPrompt import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.result.ActivityResult import androidx.activity.result.IntentSenderRequest @@ -41,6 +43,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextLayoutResult @@ -49,30 +52,31 @@ import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.R -import com.android.credentialmanager.model.EntryInfo -import com.android.credentialmanager.model.CredentialType -import com.android.credentialmanager.model.get.ProviderInfo import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.common.material.ModalBottomSheetDefaults +import com.android.credentialmanager.common.runBiometricFlow import com.android.credentialmanager.common.ui.ActionButton import com.android.credentialmanager.common.ui.ActionEntry import com.android.credentialmanager.common.ui.ConfirmButton import com.android.credentialmanager.common.ui.CredentialContainerCard +import com.android.credentialmanager.common.ui.CredentialListSectionHeader import com.android.credentialmanager.common.ui.CtaButtonRow import com.android.credentialmanager.common.ui.Entry +import com.android.credentialmanager.common.ui.HeadlineIcon +import com.android.credentialmanager.common.ui.HeadlineText +import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant import com.android.credentialmanager.common.ui.ModalBottomSheet import com.android.credentialmanager.common.ui.MoreOptionTopAppBar import com.android.credentialmanager.common.ui.SheetContainerCard -import com.android.credentialmanager.common.ui.SnackbarActionText -import com.android.credentialmanager.common.ui.HeadlineText -import com.android.credentialmanager.common.ui.CredentialListSectionHeader -import com.android.credentialmanager.common.ui.HeadlineIcon -import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant import com.android.credentialmanager.common.ui.Snackbar +import com.android.credentialmanager.common.ui.SnackbarActionText import com.android.credentialmanager.logging.GetCredentialEvent +import com.android.credentialmanager.model.CredentialType +import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.get.ActionEntryInfo import com.android.credentialmanager.model.get.AuthenticationEntryInfo import com.android.credentialmanager.model.get.CredentialEntryInfo +import com.android.credentialmanager.model.get.ProviderInfo import com.android.credentialmanager.model.get.RemoteEntryInfo import com.android.credentialmanager.userAndDisplayNameForPasskey import com.android.internal.logging.UiEventLogger.UiEventEnum @@ -82,7 +86,7 @@ import kotlin.math.max fun GetCredentialScreen( viewModel: CredentialSelectorViewModel, getCredentialUiState: GetCredentialUiState, - providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult> + providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>, ) { if (getCredentialUiState.currentScreenState == GetScreenState.REMOTE_ONLY) { RemoteCredentialSnackBarScreen( @@ -137,6 +141,22 @@ fun GetCredentialScreen( } viewModel.uiMetrics.log(GetCredentialEvent .CREDMAN_GET_CRED_SCREEN_PRIMARY_SELECTION) + } else if (credmanBiometricApiEnabled() && getCredentialUiState + .currentScreenState == GetScreenState.BIOMETRIC_SELECTION) { + BiometricSelectionPage( + // TODO(b/326243754) : Utilize expected entry for this flow, confirm + // activeEntry will always be what represents the single tap flow + biometricEntry = getCredentialUiState.activeEntry, + onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected, + onCancelFlowAndFinish = viewModel::onIllegalUiState, + requestDisplayInfo = getCredentialUiState.requestDisplayInfo, + providerInfoList = getCredentialUiState.providerInfoList, + providerDisplayInfo = getCredentialUiState.providerDisplayInfo, + onBiometricEntrySelected = + viewModel::getFlowOnEntrySelected, + fallbackToOriginalFlow = + viewModel::getFlowOnBackToPrimarySelectionScreen, + ) } else { AllSignInOptionCard( providerInfoList = getCredentialUiState.providerInfoList, @@ -189,6 +209,34 @@ fun GetCredentialScreen( } } +@Composable +internal fun BiometricSelectionPage( + biometricEntry: EntryInfo?, + onCancelFlowAndFinish: (String) -> Unit, + onMoreOptionSelected: () -> Unit, + requestDisplayInfo: RequestDisplayInfo, + providerInfoList: List<ProviderInfo>, + providerDisplayInfo: ProviderDisplayInfo, + onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult?) -> Unit, + fallbackToOriginalFlow: () -> Unit, +) { + if (biometricEntry == null) { + fallbackToOriginalFlow() + return + } + runBiometricFlow( + biometricEntry = biometricEntry, + context = LocalContext.current, + openMoreOptionsPage = onMoreOptionSelected, + sendDataToProvider = onBiometricEntrySelected, + onCancelFlowAndFinish = onCancelFlowAndFinish, + getRequestDisplayInfo = requestDisplayInfo, + getProviderInfoList = providerInfoList, + getProviderDisplayInfo = providerDisplayInfo, + onBiometricFailureFallback = fallbackToOriginalFlow + ) +} + /** Draws the primary credential selection page, used in Android U. */ // TODO(b/327518384) - remove after flag selectorUiImprovementsEnabled is enabled. @Composable @@ -256,13 +304,8 @@ fun PrimarySelectionCard( if (hasSingleEntry) { val singleEntryType = sortedUserNameToCredentialEntryList.firstOrNull() ?.sortedCredentialEntryList?.firstOrNull()?.credentialType - if (singleEntryType == CredentialType.PASSKEY) - R.string.get_dialog_title_use_passkey_for - else if (singleEntryType == CredentialType.PASSWORD) - R.string.get_dialog_title_use_password_for - else if (authenticationEntryList.isNotEmpty()) - R.string.get_dialog_title_unlock_options_for - else R.string.get_dialog_title_use_sign_in_for + generateDisplayTitleTextResCode(singleEntryType!!, + authenticationEntryList) } else { if (authenticationEntryList.isNotEmpty() || sortedUserNameToCredentialEntryList.any { perNameEntryList -> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index e35acae547a6..6d5b52a7a5f9 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -17,8 +17,11 @@ package com.android.credentialmanager.getflow import android.credentials.flags.Flags.selectorUiImprovementsEnabled +import android.credentials.flags.Flags.credmanBiometricApiEnabled import android.graphics.drawable.Drawable import androidx.credentials.PriorityHints +import com.android.credentialmanager.R +import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.get.ProviderInfo import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.get.AuthenticationEntryInfo @@ -39,6 +42,59 @@ data class GetCredentialUiState( val isNoAccount: Boolean = false, ) +/** + * Checks if this get flow is a biometric selection flow by ensuring that the first account has a + * single credential entry to display. The presently agreed upon condition validates this flow for + * a single account. In the case when there's a single credential, this flow matches the auto + * select criteria, but with the possibility that the two flows (autoselect and biometric) may + * collide. In those collision cases, the auto select flow is supported over the biometric flow. + * If there is a single account but more than one credential, and the first ranked credential has + * the biometric bit flipped on, we will use the biometric flow. If all conditions are valid, this + * responds with the entry utilized by the biometricFlow, or null otherwise. + */ +internal fun findBiometricFlowEntry( + providerDisplayInfo: ProviderDisplayInfo, + isAutoSelectFlow: Boolean +): CredentialEntryInfo? { + if (!credmanBiometricApiEnabled()) { + return null + } + if (isAutoSelectFlow) { + // For this to be true, it must be the case that there is a single entry and a single + // account. If that is the case, and auto-select is enabled along side the one-tap flow, we + // always favor that over the one tap flow. + return null + } + // The flow through an authentication entry, even if only a singular entry exists, is deemed + // as not being eligible for the single tap flow given that it adds any number of credentials + // once unlocked; essentially, this entry contains additional complexities behind it, making it + // invalid. + if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) { + return null + } + val singleAccountEntryList = getCredentialEntryListIffSingleAccount( + providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null + + val firstEntry = singleAccountEntryList.firstOrNull() + return if (firstEntry?.biometricRequest != null) firstEntry else null +} + +/** + * A utility method that will procure the credential entry list if and only if the credential entry + * list is for a singular account use case. This can be used for various flows that condition on + * a singular account. + */ +internal fun getCredentialEntryListIffSingleAccount( + sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList> +): List<CredentialEntryInfo>? { + if (sortedUserNameToCredentialEntryList.size != 1) { + return null + } + val entryList = sortedUserNameToCredentialEntryList.firstOrNull() ?: return null + val sortedEntryList = entryList.sortedCredentialEntryList + return sortedEntryList +} + internal fun hasContentToDisplay(state: GetCredentialUiState): Boolean { return state.providerDisplayInfo.sortedUserNameToCredentialEntryList.isNotEmpty() || state.providerDisplayInfo.authenticationEntryList.isNotEmpty() || @@ -50,15 +106,14 @@ internal fun findAutoSelectEntry(providerDisplayInfo: ProviderDisplayInfo): Cred if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) { return null } - if (providerDisplayInfo.sortedUserNameToCredentialEntryList.size == 1) { - val entryList = providerDisplayInfo.sortedUserNameToCredentialEntryList.firstOrNull() - ?: return null - if (entryList.sortedCredentialEntryList.size == 1) { - val entry = entryList.sortedCredentialEntryList.firstOrNull() ?: return null - if (entry.isAutoSelectable) { - return entry - } - } + val entryList = getCredentialEntryListIffSingleAccount( + providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null + if (entryList.size != 1) { + return null + } + val entry = entryList.firstOrNull() ?: return null + if (entry.isAutoSelectable) { + return entry } return null } @@ -105,6 +160,9 @@ enum class GetScreenState { /** The primary credential selection page. */ PRIMARY_SELECTION, + /** The single tap biometric selection page. */ + BIOMETRIC_SELECTION, + /** The secondary credential selection page, where all sign-in options are listed. */ ALL_SIGN_IN_OPTIONS, @@ -177,6 +235,22 @@ fun toProviderDisplayInfo( ) } +/** + * This generates the res code for the large display title text for the selector. For example, it + * retrieves the resource for strings like: "Use your saved passkey for *rpName*". + */ +internal fun generateDisplayTitleTextResCode( + singleEntryType: CredentialType, + authenticationEntryList: List<AuthenticationEntryInfo> = emptyList() +): Int = + if (singleEntryType == CredentialType.PASSKEY) + R.string.get_dialog_title_use_passkey_for + else if (singleEntryType == CredentialType.PASSWORD) + R.string.get_dialog_title_use_password_for + else if (authenticationEntryList.isNotEmpty()) + R.string.get_dialog_title_unlock_options_for + else R.string.get_dialog_title_use_sign_in_for + fun toActiveEntry( providerDisplayInfo: ProviderDisplayInfo, ): EntryInfo? { @@ -211,9 +285,18 @@ private fun toGetScreenState( GetScreenState.REMOTE_ONLY else if (isRequestForAllOptions) GetScreenState.ALL_SIGN_IN_OPTIONS + else if (isBiometricFlow(providerDisplayInfo)) + GetScreenState.BIOMETRIC_SELECTION else GetScreenState.PRIMARY_SELECTION } +/** + * Determines if the flow is a biometric flow by taking into account autoselect criteria. + */ +internal fun isBiometricFlow(providerDisplayInfo: ProviderDisplayInfo) = + findBiometricFlowEntry(providerDisplayInfo, + findAutoSelectEntry(providerDisplayInfo) != null) != null + internal class CredentialEntryInfoComparatorByTypeThenTimestamp( val typePriorityMap: Map<String, Int>, ) : Comparator<CredentialEntryInfo> { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index 7c313e8a871d..a4c6ac7d95c7 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -80,7 +80,7 @@ public class InstallStart extends Activity { mUserManager = getSystemService(UserManager.class); Intent intent = getIntent(); - String callingPackage = getCallingPackage(); + String callingPackage = getLaunchedFromPackage(); String callingAttributionTag = null; // Uid of the source package, coming from ActivityManager @@ -89,30 +89,33 @@ public class InstallStart extends Activity { Log.w(TAG, "Could not determine the launching uid."); } + // The UID of the origin of the installation. Note that it can be different than the + // "installer" of the session. For instance, if a 3P caller launched PIA with an ACTION_VIEW + // intent, the originatingUid is the 3P caller, but the "installer" in this case would + // be PIA. + int originatingUid = callingUid; + final boolean isSessionInstall = PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction()) || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction()); - // If the activity was started via a PackageInstaller session, we retrieve the calling - // package from that session + // If the activity was started via a PackageInstaller session, we retrieve the originating + // UID from that session final int sessionId = (isSessionInstall ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID) : SessionInfo.INVALID_ID); - int originatingUidFromSession = callingUid; - if (callingPackage == null && sessionId != SessionInfo.INVALID_ID) { + if (sessionId != SessionInfo.INVALID_ID) { PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); if (sessionInfo != null) { - callingPackage = sessionInfo.getInstallerPackageName(); callingAttributionTag = sessionInfo.getInstallerAttributionTag(); - originatingUidFromSession = sessionInfo.getOriginatingUid(); + if (sessionInfo.getOriginatingUid() != Process.INVALID_UID) { + originatingUid = sessionInfo.getOriginatingUid(); + } } } final ApplicationInfo sourceInfo = getSourceInfo(callingPackage); - // Uid of the source package, with a preference to uid from ApplicationInfo - final int originatingUid = sourceInfo != null ? sourceInfo.uid : callingUid; - if (callingUid == Process.INVALID_UID && sourceInfo == null) { Log.e(TAG, "Cannot determine caller since UID is invalid and sourceInfo is null"); mAbortInstall = true; @@ -125,28 +128,28 @@ public class InstallStart extends Activity { boolean isTrustedSource = false; if (sourceInfo != null && sourceInfo.isPrivilegedApp()) { isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) || ( - originatingUid != Process.INVALID_UID && checkPermission( - Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, originatingUid) - == PackageManager.PERMISSION_GRANTED); + callingUid != Process.INVALID_UID && checkPermission( + Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, callingUid) + == PackageManager.PERMISSION_GRANTED); } if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager - && originatingUid != Process.INVALID_UID) { - final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, originatingUid); + && callingUid != Process.INVALID_UID) { + final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, callingUid); if (targetSdkVersion < 0) { - Log.e(TAG, "Cannot get target sdk version for uid " + originatingUid); + Log.e(TAG, "Cannot get target sdk version for uid " + callingUid); // Invalid originating uid supplied. Abort install. mAbortInstall = true; } else if (targetSdkVersion >= Build.VERSION_CODES.O && !isUidRequestingPermission( - originatingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) { - Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission " + callingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) { + Log.e(TAG, "Requesting uid " + callingUid + " needs to declare permission " + Manifest.permission.REQUEST_INSTALL_PACKAGES); mAbortInstall = true; } } - if (sessionId != -1 && !isCallerSessionOwner(originatingUid, sessionId)) { - Log.e(TAG, "UID " + originatingUid + " is not the owner of session " + + if (sessionId != -1 && !isCallerSessionOwner(callingUid, sessionId)) { + Log.e(TAG, "CallingUid " + callingUid + " is not the owner of session " + sessionId); mAbortInstall = true; } @@ -156,10 +159,9 @@ public class InstallStart extends Activity { final String installerPackageNameFromIntent = getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME); if (installerPackageNameFromIntent != null) { - final String callingPkgName = getLaunchedFromPackage(); - if (!TextUtils.equals(installerPackageNameFromIntent, callingPkgName) + if (!TextUtils.equals(installerPackageNameFromIntent, callingPackage) && mPackageManager.checkPermission(Manifest.permission.INSTALL_PACKAGES, - callingPkgName) != PackageManager.PERMISSION_GRANTED) { + callingPackage) != PackageManager.PERMISSION_GRANTED) { Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent + " is invalid. Remove it."); EventLog.writeEvent(0x534e4554, "236687884", getLaunchedFromUid(), @@ -187,8 +189,6 @@ public class InstallStart extends Activity { callingAttributionTag); nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo); nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid); - nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINATING_UID_FROM_SESSION_INFO, - originatingUidFromSession); nextActivity.putExtra(PackageInstallerActivity.EXTRA_IS_TRUSTED_SOURCE, isTrustedSource); if (isSessionInstall) { @@ -291,8 +291,8 @@ public class InstallStart extends Activity { return false; } - private boolean isCallerSessionOwner(int originatingUid, int sessionId) { - if (originatingUid == Process.ROOT_UID) { + private boolean isCallerSessionOwner(int callingUid, int sessionId) { + if (callingUid == Process.ROOT_UID) { return true; } PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); @@ -300,7 +300,7 @@ public class InstallStart extends Activity { return false; } int installerUid = sessionInfo.getInstallerUid(); - return originatingUid == installerUid; + return callingUid == installerUid; } private void checkDevicePolicyRestrictions() { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index 1b93c10a8c13..8bed945af32c 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -84,8 +84,6 @@ public class PackageInstallerActivity extends Activity { static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO"; static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID"; static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET"; - static final String EXTRA_ORIGINATING_UID_FROM_SESSION_INFO = - "EXTRA_ORIGINATING_UID_FROM_SESSION_INFO"; static final String EXTRA_IS_TRUSTED_SOURCE = "EXTRA_IS_TRUSTED_SOURCE"; private static final String ALLOW_UNKNOWN_SOURCES_KEY = PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY"; @@ -99,10 +97,6 @@ public class PackageInstallerActivity extends Activity { * The package name corresponding to #mOriginatingUid */ private String mOriginatingPackage; - /** - * The package name corresponding to the app updater in the update-ownership confirmation dialog - */ - private String mOriginatingPackageFromSessionInfo; private int mActivityResultCode = Activity.RESULT_CANCELED; private int mPendingUserActionReason = -1; @@ -155,8 +149,7 @@ public class PackageInstallerActivity extends Activity { viewToEnable = mDialog.requireViewById(R.id.install_confirm_question_update); final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(); - final CharSequence requestedUpdateOwnerLabel = - getApplicationLabel(mOriginatingPackageFromSessionInfo); + final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mOriginatingPackage); if (!TextUtils.isEmpty(existingUpdateOwnerLabel) && mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) { String updateOwnerString = @@ -370,15 +363,9 @@ public class PackageInstallerActivity extends Activity { mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE); mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG); mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO); - mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, - Process.INVALID_UID); + mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID); mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID) ? getPackageNameForUid(mOriginatingUid) : null; - int originatingUidFromSessionInfo = - intent.getIntExtra(EXTRA_ORIGINATING_UID_FROM_SESSION_INFO, Process.INVALID_UID); - mOriginatingPackageFromSessionInfo = (originatingUidFromSessionInfo != Process.INVALID_UID) - ? getPackageNameForUid(originatingUidFromSessionInfo) : mCallingPackage; - final Object packageSource; if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(action)) { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index e48c0f42e62e..08028b1713b8 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -136,7 +136,7 @@ class InstallRepository(private val context: Context) { callingPackage = callerInfo.packageName - if (callingPackage == null && sessionId != SessionInfo.INVALID_ID) { + if (sessionId != SessionInfo.INVALID_ID) { val sessionInfo: SessionInfo? = packageInstaller.getSessionInfo(sessionId) callingPackage = sessionInfo?.getInstallerPackageName() callingAttributionTag = sessionInfo?.getInstallerAttributionTag() diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml new file mode 100644 index 000000000000..77979f0dc7ec --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:viewportWidth="18" + android:viewportHeight="18" + android:width="24dp" + android:height="24dp"> + <path + android:pathData="M7 10l5 5 5 -5z" + android:fillColor="@color/settingslib_materialColorOnPrimaryContainer"/> +</vector> diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_spinner_background.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_spinner_background.xml new file mode 100644 index 000000000000..20ee38190b01 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_spinner_background.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/settingslib_ripple_color"> + + <item android:id="@android:id/background"> + <layer-list + android:paddingMode="stack" + android:paddingStart="0dp" + android:paddingEnd="24dp"> + <item + android:top="8dp" + android:bottom="8dp"> + + <shape> + <corners android:radius="28dp"/> + <solid android:color="@color/settingslib_materialColorPrimaryContainer"/> + <size android:height="@dimen/settingslib_spinner_height"/> + </shape> + </item> + + <item + android:gravity="center|end" + android:width="18dp" + android:height="18dp" + android:end="12dp" + android:drawable="@drawable/settingslib_arrow_drop_down"/> + </layer-list> + </item> +</ripple> diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_spinner_dropdown_background.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_spinner_dropdown_background.xml new file mode 100644 index 000000000000..b287c3bb546f --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_spinner_dropdown_background.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/settingslib_ripple_color"> + + <item android:id="@android:id/background"> + <layer-list + android:paddingMode="stack" + android:paddingStart="0dp" + android:paddingEnd="12dp"> + + <item> + <shape> + <corners android:radius="10dp"/> + <solid android:color="@color/settingslib_materialColorSecondaryContainer"/> + </shape> + </item> + </layer-list> + </item> +</ripple> diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml index e3645b55981b..beed90efb508 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml @@ -37,4 +37,8 @@ <!-- Material next track off color--> <color name="settingslib_track_off_color">@android:color/system_surface_container_highest_dark </color> + + <color name="settingslib_text_color_primary_device_default">@android:color/system_on_surface_dark</color> + + <color name="settingslib_text_color_secondary_device_default">@android:color/system_on_surface_variant_dark</color> </resources> diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml new file mode 100644 index 000000000000..229d9e330882 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <color name="settingslib_materialColorSurfaceContainerLowest">@android:color/system_surface_container_lowest_dark</color> + <color name="settingslib_materialColorOnSecondaryContainer">@android:color/system_on_secondary_container_dark</color> + <color name="settingslib_materialColorOnTertiaryContainer">@android:color/system_on_tertiary_container_dark</color> + <color name="settingslib_materialColorSurfaceContainerLow">@android:color/system_surface_container_low_dark</color> + <color name="settingslib_materialColorOnPrimaryContainer">@android:color/system_on_primary_container_dark</color> + <color name="settingslib_materialColorOnErrorContainer">@android:color/system_on_error_container_dark</color> + <color name="settingslib_materialColorOnSurfaceInverse">@android:color/system_on_surface_light</color> + <color name="settingslib_materialColorSecondaryContainer">@android:color/system_secondary_container_dark</color> + <color name="settingslib_materialColorErrorContainer">@android:color/system_error_container_dark</color> + <color name="settingslib_materialColorPrimaryInverse">@android:color/system_primary_light</color> + <color name="settingslib_materialColorSurfaceInverse">@android:color/system_surface_light</color> + <color name="settingslib_materialColorSurfaceVariant">@android:color/system_surface_variant_dark</color> + <color name="settingslib_materialColorTertiaryContainer">@android:color/system_tertiary_container_dark</color> + <color name="settingslib_materialColorPrimaryContainer">@android:color/system_primary_container_dark</color> + <color name="settingslib_materialColorOnBackground">@android:color/system_on_background_dark</color> + <color name="settingslib_materialColorOnSecondary">@android:color/system_on_secondary_dark</color> + <color name="settingslib_materialColorOnTertiary">@android:color/system_on_tertiary_dark</color> + <color name="settingslib_materialColorSurfaceDim">@android:color/system_surface_dim_dark</color> + <color name="settingslib_materialColorSurfaceBright">@android:color/system_surface_bright_dark</color> + <color name="settingslib_materialColorOnError">@android:color/system_on_error_dark</color> + <color name="settingslib_materialColorSurface">@android:color/system_surface_dark</color> + <color name="settingslib_materialColorSurfaceContainerHigh">@android:color/system_surface_container_high_dark</color> + <color name="settingslib_materialColorSurfaceContainerHighest">@android:color/system_surface_container_highest_dark</color> + <color name="settingslib_materialColorOnSurfaceVariant">@android:color/system_on_surface_variant_dark</color> + <color name="settingslib_materialColorOutline">@android:color/system_outline_dark</color> + <color name="settingslib_materialColorOutlineVariant">@android:color/system_outline_variant_dark</color> + <color name="settingslib_materialColorOnPrimary">@android:color/system_on_primary_dark</color> + <color name="settingslib_materialColorOnSurface">@android:color/system_on_surface_dark</color> + <color name="settingslib_materialColorSurfaceContainer">@android:color/system_surface_container_dark</color> + <color name="settingslib_materialColorPrimary">@android:color/system_primary_dark</color> + <color name="settingslib_materialColorSecondary">@android:color/system_secondary_dark</color> + <color name="settingslib_materialColorTertiary">@android:color/system_tertiary_dark</color> +</resources> diff --git a/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml index fdd96ec78efc..3709b5d13056 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml @@ -39,4 +39,8 @@ <!-- Material next track outline color--> <color name="settingslib_track_online_color">@color/settingslib_switch_track_outline_color</color> + + <color name="settingslib_text_color_primary_device_default">@android:color/system_on_surface_light</color> + + <color name="settingslib_text_color_secondary_device_default">@android:color/system_on_surface_variant_light</color> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml new file mode 100644 index 000000000000..2691344bfdb0 --- /dev/null +++ b/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <!-- The text color of spinner title --> + <color name="settingslib_spinner_title_color">@color/settingslib_materialColorOnPrimaryContainer</color> + <!-- The text color of dropdown item title --> + <color name="settingslib_spinner_dropdown_color">@color/settingslib_materialColorOnPrimaryContainer</color> + + + <color name="settingslib_materialColorOnSecondaryFixedVariant">@android:color/system_on_secondary_fixed_variant</color> + <color name="settingslib_materialColorOnTertiaryFixedVariant">@android:color/system_on_tertiary_fixed_variant</color> + <color name="settingslib_materialColorSurfaceContainerLowest">@android:color/system_surface_container_lowest_light</color> + <color name="settingslib_materialColorOnPrimaryFixedVariant">@android:color/system_on_primary_fixed_variant</color> + <color name="settingslib_materialColorOnSecondaryContainer">@android:color/system_on_secondary_container_light</color> + <color name="settingslib_materialColorOnTertiaryContainer">@android:color/system_on_tertiary_container_light</color> + <color name="settingslib_materialColorSurfaceContainerLow">@android:color/system_surface_container_low_light</color> + <color name="settingslib_materialColorOnPrimaryContainer">@android:color/system_on_primary_container_light</color> + <color name="settingslib_materialColorSecondaryFixedDim">@android:color/system_secondary_fixed_dim</color> + <color name="settingslib_materialColorOnErrorContainer">@android:color/system_on_error_container_light</color> + <color name="settingslib_materialColorOnSecondaryFixed">@android:color/system_on_secondary_fixed</color> + <color name="settingslib_materialColorOnSurfaceInverse">@android:color/system_on_surface_dark</color> + <color name="settingslib_materialColorTertiaryFixedDim">@android:color/system_tertiary_fixed_dim</color> + <color name="settingslib_materialColorOnTertiaryFixed">@android:color/system_on_tertiary_fixed</color> + <color name="settingslib_materialColorPrimaryFixedDim">@android:color/system_primary_fixed_dim</color> + <color name="settingslib_materialColorSecondaryContainer">@android:color/system_secondary_container_light</color> + <color name="settingslib_materialColorErrorContainer">@android:color/system_error_container_light</color> + <color name="settingslib_materialColorOnPrimaryFixed">@android:color/system_on_primary_fixed</color> + <color name="settingslib_materialColorPrimaryInverse">@android:color/system_primary_dark</color> + <color name="settingslib_materialColorSecondaryFixed">@android:color/system_secondary_fixed</color> + <color name="settingslib_materialColorSurfaceInverse">@android:color/system_surface_dark</color> + <color name="settingslib_materialColorSurfaceVariant">@android:color/system_surface_variant_light</color> + <color name="settingslib_materialColorTertiaryContainer">@android:color/system_tertiary_container_light</color> + <color name="settingslib_materialColorTertiaryFixed">@android:color/system_tertiary_fixed</color> + <color name="settingslib_materialColorPrimaryContainer">@android:color/system_primary_container_light</color> + <color name="settingslib_materialColorOnBackground">@android:color/system_on_background_light</color> + <color name="settingslib_materialColorPrimaryFixed">@android:color/system_primary_fixed</color> + <color name="settingslib_materialColorOnSecondary">@android:color/system_on_secondary_light</color> + <color name="settingslib_materialColorOnTertiary">@android:color/system_on_tertiary_light</color> + <color name="settingslib_materialColorSurfaceDim">@android:color/system_surface_dim_light</color> + <color name="settingslib_materialColorSurfaceBright">@android:color/system_surface_bright_light</color> + <color name="settingslib_materialColorOnError">@android:color/system_on_error_light</color> + <color name="settingslib_materialColorSurface">@android:color/system_surface_light</color> + <color name="settingslib_materialColorSurfaceContainerHigh">@android:color/system_surface_container_high_light</color> + <color name="settingslib_materialColorSurfaceContainerHighest">@android:color/system_surface_container_highest_light</color> + <color name="settingslib_materialColorOnSurfaceVariant">@android:color/system_on_surface_variant_light</color> + <color name="settingslib_materialColorOutline">@android:color/system_outline_light</color> + <color name="settingslib_materialColorOutlineVariant">@android:color/system_outline_variant_light</color> + <color name="settingslib_materialColorOnPrimary">@android:color/system_on_primary_light</color> + <color name="settingslib_materialColorOnSurface">@android:color/system_on_surface_light</color> + <color name="settingslib_materialColorSurfaceContainer">@android:color/system_surface_container_light</color> + <color name="settingslib_materialColorPrimary">@android:color/system_primary_light</color> + <color name="settingslib_materialColorSecondary">@android:color/system_secondary_light</color> + <color name="settingslib_materialColorTertiary">@android:color/system_tertiary_light</color> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt index f812f959db32..5a6c0a1bf275 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt @@ -27,9 +27,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.delay import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -72,12 +71,13 @@ class DisposableBroadcastReceiverAsUserTest { DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {} } } + composeTestRule.delay() assertThat(registeredBroadcastReceiver).isNotNull() } @Test - fun broadcastReceiver_isCalledOnReceive() = runBlocking { + fun broadcastReceiver_isCalledOnReceive() { var onReceiveIsCalled = false composeTestRule.setContent { CompositionLocalProvider( @@ -91,7 +91,7 @@ class DisposableBroadcastReceiverAsUserTest { } registeredBroadcastReceiver!!.onReceive(context, Intent()) - delay(100) + composeTestRule.delay() assertThat(onReceiveIsCalled).isTrue() } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt index 2e6a39603deb..c1d298d0b613 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt @@ -48,18 +48,7 @@ class SettingsGlobalChangeFlowTest { } @Test - fun settingsGlobalChangeFlow_collectAfterValueChanged_onlyKeepLatest() = runBlocking { - var value by context.settingsGlobalBoolean(TEST_NAME) - value = false - - val flow = context.settingsGlobalChangeFlow(TEST_NAME) - value = true - - assertThat(flow.toListWithTimeout()).hasSize(1) - } - - @Test - fun settingsGlobalChangeFlow_collectBeforeValueChanged_getBoth() = runBlocking { + fun settingsGlobalChangeFlow_changed() = runBlocking { var value by context.settingsGlobalBoolean(TEST_NAME) value = false @@ -69,7 +58,7 @@ class SettingsGlobalChangeFlowTest { delay(100) value = true - assertThat(listDeferred.await()).hasSize(2) + assertThat(listDeferred.await().size).isAtLeast(2) } private companion object { diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index 60a05296d8c4..ad0e6f46d9c8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -42,7 +42,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.print.PrintManager; import android.provider.Settings; -import android.provider.Settings.Secure; import android.telephony.AccessNetworkConstants; import android.telephony.NetworkRegistrationInfo; import android.telephony.ServiceState; @@ -784,29 +783,4 @@ public class Utils { } return false; } - - /** Whether to show the wireless charging warning in Settings. */ - public static boolean shouldShowWirelessChargingWarningTip( - @NonNull Context context, @NonNull String tag) { - try { - return Secure.getInt(context.getContentResolver(), WIRELESS_CHARGING_WARNING_ENABLED, 0) - == 1; - } catch (Exception e) { - Log.e(tag, "shouldShowWirelessChargingWarningTip()", e); - } - return false; - } - - /** Stores the state of whether the wireless charging warning in Settings is enabled. */ - public static void updateWirelessChargingWarningEnabled( - @NonNull Context context, boolean enabled, @NonNull String tag) { - try { - Secure.putInt( - context.getContentResolver(), - WIRELESS_CHARGING_WARNING_ENABLED, - enabled ? 1 : 0); - } catch (Exception e) { - Log.e(tag, "setWirelessChargingWarningEnabled()", e); - } - } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java index 6f31fad104d0..0931b685d967 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java @@ -16,7 +16,6 @@ package com.android.settingslib; import static com.android.settingslib.Utils.STORAGE_MANAGER_ENABLED_PROPERTY; -import static com.android.settingslib.Utils.shouldShowWirelessChargingWarningTip; import static com.google.common.truth.Truth.assertThat; @@ -543,20 +542,6 @@ public class UtilsTest { assertThat(Utils.containsIncompatibleChargers(mContext, TAG)).isFalse(); } - @Test - public void shouldShowWirelessChargingWarningTip_enabled_returnTrue() { - Utils.updateWirelessChargingWarningEnabled(mContext, true, TAG); - - assertThat(shouldShowWirelessChargingWarningTip(mContext, TAG)).isTrue(); - } - - @Test - public void shouldShowWirelessChargingWarningTip_disabled_returnFalse() { - Utils.updateWirelessChargingWarningEnabled(mContext, false, TAG); - - assertThat(shouldShowWirelessChargingWarningTip(mContext, TAG)).isFalse(); - } - private void setupIncompatibleCharging() { setupIncompatibleCharging(UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY); } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 5629a7bf7b21..87a7f823edfe 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -269,6 +269,7 @@ public class SecureSettings { Settings.Secure.CAMERA_EXTENSIONS_FALLBACK, Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, - Settings.Secure.AUDIO_DEVICE_INVENTORY + Settings.Secure.AUDIO_DEVICE_INVENTORY, + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_TARGETS }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index b8d95eb5329d..edef286b6ac0 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -326,6 +326,9 @@ public class SecureSettingsValidators { Secure.ACCESSIBILITY_BUTTON_TARGETS, ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); VALIDATORS.put( + Secure.ACCESSIBILITY_FLOATING_MENU_TARGETS, + ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); + VALIDATORS.put( Secure.ACCESSIBILITY_QS_TARGETS, ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index e5d62f8f9fac..8e320054028c 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -75,7 +75,10 @@ import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.zip.CRC32; /** @@ -103,6 +106,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { // fatal crash. Creating a backup with a different key will prevent Android 12 versions from // restoring this data. private static final String KEY_SIM_SPECIFIC_SETTINGS_2 = "sim_specific_settings_2"; + private static final String KEY_WIFI_SETTINGS_BACKUP_DATA = "wifi_settings_backup_data"; // Versioning of the state file. Increment this version // number any time the set of state items is altered. @@ -126,8 +130,9 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static final int STATE_WIFI_NEW_CONFIG = 9; private static final int STATE_DEVICE_CONFIG = 10; private static final int STATE_SIM_SPECIFIC_SETTINGS = 11; + private static final int STATE_WIFI_SETTINGS = 12; - private static final int STATE_SIZE = 12; // The current number of state items + private static final int STATE_SIZE = 13; // The current number of state items // Number of entries in the checksum array at various version numbers private static final int STATE_SIZES[] = { @@ -140,7 +145,8 @@ public class SettingsBackupAgent extends BackupAgentHelper { 9, // version 6 added STATE_NETWORK_POLICIES 10, // version 7 added STATE_WIFI_NEW_CONFIG 11, // version 8 added STATE_DEVICE_CONFIG - STATE_SIZE // version 9 added STATE_SIM_SPECIFIC_SETTINGS + 12, // version 9 added STATE_SIM_SPECIFIC_SETTINGS + STATE_SIZE // version 10 added STATE_WIFI_SETTINGS }; private static final int FULL_BACKUP_ADDED_GLOBAL = 2; // added the "global" entry @@ -230,6 +236,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { byte[] wifiFullConfigData = getNewWifiConfigData(); byte[] deviceSpecificInformation = getDeviceSpecificConfiguration(); byte[] simSpecificSettingsData = getSimSpecificSettingsData(); + byte[] wifiSettingsData = getWifiSettingsBackupData(); long[] stateChecksums = readOldChecksums(oldState); @@ -265,6 +272,9 @@ public class SettingsBackupAgent extends BackupAgentHelper { stateChecksums[STATE_SIM_SPECIFIC_SETTINGS] = writeIfChanged(stateChecksums[STATE_SIM_SPECIFIC_SETTINGS], KEY_SIM_SPECIFIC_SETTINGS_2, simSpecificSettingsData, data); + stateChecksums[STATE_WIFI_SETTINGS] = + writeIfChanged(stateChecksums[STATE_WIFI_SETTINGS], + KEY_WIFI_SETTINGS_BACKUP_DATA, wifiSettingsData, data); writeNewChecksums(stateChecksums, newState); } @@ -413,7 +423,13 @@ public class SettingsBackupAgent extends BackupAgentHelper { data.readEntityData(restoredSimSpecificSettings, 0, size); restoreSimSpecificSettings(restoredSimSpecificSettings); break; - + case KEY_WIFI_SETTINGS_BACKUP_DATA: + byte[] restoredWifiData = new byte[size]; + data.readEntityData(restoredWifiData, 0, size); + if (!isWatch()) { + restoreWifiData(restoredWifiData); + } + break; default : data.skipEntityData(); @@ -1346,6 +1362,45 @@ public class SettingsBackupAgent extends BackupAgentHelper { } } + private static final class Mutable<E> { + public volatile E value; + + Mutable() { + value = null; + } + } + + private byte[] getWifiSettingsBackupData() { + final CountDownLatch latch = new CountDownLatch(1); + final Mutable<byte[]> backupWifiData = new Mutable<byte[]>(); + + try { + mWifiManager.retrieveWifiBackupData(getBaseContext().getMainExecutor(), + new Consumer<byte[]>() { + @Override + public void accept(byte[] value) { + backupWifiData.value = value; + latch.countDown(); + } + }); + // cts requires B&R with 10 seconds + if (latch.await(10, TimeUnit.SECONDS) && backupWifiData.value != null) { + return backupWifiData.value; + } + } catch (InterruptedException ie) { + Log.e(TAG, "fail to retrieveWifiBackupData, " + ie); + } + Log.e(TAG, "fail to retrieveWifiBackupData"); + return new byte[0]; + } + + private void restoreWifiData(byte[] data) { + if (DEBUG_BACKUP) { + Log.v(TAG, "Applying restored all wifi data"); + } + mWifiManager.restoreWifiBackupData(data); + } + private void updateWindowManagerIfNeeded(Integer previousDensity) { int newDensity; try { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index dba3bac4a4b8..4603b43b0ab5 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1838,6 +1838,9 @@ class SettingsProtoDumpUtil { Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED, SecureSettingsProto.Accessibility.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED); dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_TARGETS, + SecureSettingsProto.Accessibility.ACCESSIBILITY_FLOATING_MENU_TARGETS); + dumpSetting(s, p, Settings.Secure.ODI_CAPTIONS_VOLUME_UI_ENABLED, SecureSettingsProto.Accessibility.ODI_CAPTIONS_VOLUME_UI_ENABLED); dumpSetting(s, p, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/selector/ui/composable/VolumePanelRadioButtons.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/selector/ui/composable/VolumePanelRadioButtons.kt index ae267e2b002a..98d1afd7af7e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/selector/ui/composable/VolumePanelRadioButtons.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/selector/ui/composable/VolumePanelRadioButtons.kt @@ -16,38 +16,38 @@ package com.android.systemui.volume.panel.component.selector.ui.composable -import androidx.compose.animation.core.animateOffsetAsState -import androidx.compose.foundation.Canvas +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirst +import kotlinx.coroutines.launch /** * Radio button group for the Volume Panel. It allows selecting a single item @@ -65,8 +65,8 @@ fun VolumePanelRadioButtonBar( spacing: Dp = VolumePanelRadioButtonBarDefaults.DefaultSpacing, labelIndicatorBackgroundSpacing: Dp = VolumePanelRadioButtonBarDefaults.DefaultLabelIndicatorBackgroundSpacing, - indicatorCornerRadius: CornerRadius = - VolumePanelRadioButtonBarDefaults.defaultIndicatorCornerRadius(), + indicatorCornerSize: CornerSize = + CornerSize(VolumePanelRadioButtonBarDefaults.DefaultIndicatorCornerRadius), indicatorBackgroundCornerSize: CornerSize = CornerSize(VolumePanelRadioButtonBarDefaults.DefaultIndicatorBackgroundCornerRadius), colors: VolumePanelRadioButtonBarColors = VolumePanelRadioButtonBarDefaults.defaultColors(), @@ -76,60 +76,41 @@ fun VolumePanelRadioButtonBar( VolumePanelRadioButtonBarScopeImpl().apply(content).apply { require(hasSelectedItem) { "At least one item should be selected" } } - val items = scope.items - var selectedIndex by remember { mutableIntStateOf(items.indexOfFirst { it.isSelected }) } - - var size by remember { mutableStateOf(IntSize(0, 0)) } - val spacingPx = with(LocalDensity.current) { spacing.toPx() } - val indicatorWidth = size.width / items.size - (spacingPx * (items.size - 1) / items.size) - val offset by - animateOffsetAsState( - targetValue = - Offset( - selectedIndex * indicatorWidth + (spacingPx * selectedIndex), - 0f, - ), - label = "VolumePanelRadioButtonOffsetAnimation", - finishedListener = { - for (itemIndex in items.indices) { - val item = items[itemIndex] - if (itemIndex == selectedIndex) { - item.onItemSelected() - break - } - } - } - ) - - Column(modifier = modifier) { - Box(modifier = Modifier.height(IntrinsicSize.Max)) { - Canvas( + val coroutineScope = rememberCoroutineScope() + val offsetAnimatable = remember { Animatable(UNSET_OFFSET, Int.VectorConverter) } + Layout( + modifier = modifier, + content = { + Spacer( modifier = - Modifier.fillMaxSize() + Modifier.layoutId(RadioButtonBarComponent.ButtonsBackground) .background( colors.indicatorBackgroundColor, RoundedCornerShape(indicatorBackgroundCornerSize), ) + ) + Spacer( + modifier = + Modifier.layoutId(RadioButtonBarComponent.Indicator) + .offset { IntOffset(offsetAnimatable.value, 0) } .padding(indicatorBackgroundPadding) - .onGloballyPositioned { size = it.size } - ) { - drawRoundRect( - color = colors.indicatorColor, - topLeft = offset, - size = Size(indicatorWidth, size.height.toFloat()), - cornerRadius = indicatorCornerRadius, - ) - } + .background( + colors.indicatorColor, + RoundedCornerShape(indicatorCornerSize), + ) + ) Row( - modifier = Modifier.padding(indicatorBackgroundPadding), + modifier = + Modifier.layoutId(RadioButtonBarComponent.Buttons) + .padding(indicatorBackgroundPadding), horizontalArrangement = Arrangement.spacedBy(spacing) ) { for (itemIndex in items.indices) { TextButton( modifier = Modifier.weight(1f), - onClick = { selectedIndex = itemIndex }, + onClick = { items[itemIndex].onItemSelected() }, ) { val item = items[itemIndex] if (item.icon !== Empty) { @@ -138,28 +119,116 @@ fun VolumePanelRadioButtonBar( } } } - } + Row( + modifier = + Modifier.layoutId(RadioButtonBarComponent.Labels) + .padding( + start = indicatorBackgroundPadding, + top = labelIndicatorBackgroundSpacing, + end = indicatorBackgroundPadding + ), + horizontalArrangement = Arrangement.spacedBy(spacing), + ) { + for (itemIndex in items.indices) { + TextButton( + modifier = Modifier.weight(1f), + onClick = { items[itemIndex].onItemSelected() }, + ) { + val item = items[itemIndex] + if (item.icon !== Empty) { + with(items[itemIndex]) { label() } + } + } + } + } + }, + measurePolicy = + with(LocalDensity.current) { + val spacingPx = + (spacing - indicatorBackgroundPadding * 2).roundToPx().coerceAtLeast(0) - Row( - modifier = - Modifier.padding( - start = indicatorBackgroundPadding, - top = labelIndicatorBackgroundSpacing, - end = indicatorBackgroundPadding - ), - horizontalArrangement = Arrangement.spacedBy(spacing), - ) { - for (itemIndex in items.indices) { - TextButton( - modifier = Modifier.weight(1f), - onClick = { selectedIndex = itemIndex }, + BarMeasurePolicy( + buttonsCount = items.size, + selectedIndex = scope.selectedIndex, + spacingPx = spacingPx, ) { - val item = items[itemIndex] - if (item.icon !== Empty) { - with(items[itemIndex]) { label() } + coroutineScope.launch { + if (offsetAnimatable.value == UNSET_OFFSET) { + offsetAnimatable.snapTo(it) + } else { + offsetAnimatable.animateTo(it) + } } } - } + }, + ) +} + +private class BarMeasurePolicy( + private val buttonsCount: Int, + private val selectedIndex: Int, + private val spacingPx: Int, + private val onTargetIndicatorOffsetMeasured: (Int) -> Unit, +) : MeasurePolicy { + + override fun MeasureScope.measure( + measurables: List<Measurable>, + constraints: Constraints + ): MeasureResult { + val fillWidthConstraints = constraints.copy(minWidth = constraints.maxWidth) + val buttonsPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == RadioButtonBarComponent.Buttons } + .measure(fillWidthConstraints) + val labelsPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == RadioButtonBarComponent.Labels } + .measure(fillWidthConstraints) + + val buttonsBackgroundPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == RadioButtonBarComponent.ButtonsBackground } + .measure( + Constraints( + minWidth = buttonsPlaceable.width, + maxWidth = buttonsPlaceable.width, + minHeight = buttonsPlaceable.height, + maxHeight = buttonsPlaceable.height, + ) + ) + + val totalSpacing = spacingPx * (buttonsCount - 1) + val indicatorWidth = (buttonsBackgroundPlaceable.width - totalSpacing) / buttonsCount + val indicatorPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == RadioButtonBarComponent.Indicator } + .measure( + Constraints( + minWidth = indicatorWidth, + maxWidth = indicatorWidth, + minHeight = buttonsBackgroundPlaceable.height, + maxHeight = buttonsBackgroundPlaceable.height, + ) + ) + + onTargetIndicatorOffsetMeasured( + selectedIndex * indicatorWidth + (spacingPx * selectedIndex) + ) + + return layout(constraints.maxWidth, buttonsPlaceable.height + labelsPlaceable.height) { + buttonsBackgroundPlaceable.placeRelative( + 0, + 0, + RadioButtonBarComponent.ButtonsBackground.zIndex, + ) + indicatorPlaceable.placeRelative(0, 0, RadioButtonBarComponent.Indicator.zIndex) + + buttonsPlaceable.placeRelative(0, 0, RadioButtonBarComponent.Buttons.zIndex) + labelsPlaceable.placeRelative( + 0, + buttonsBackgroundPlaceable.height, + RadioButtonBarComponent.Labels.zIndex, + ) } } } @@ -179,12 +248,6 @@ object VolumePanelRadioButtonBarDefaults { val DefaultIndicatorCornerRadius = 20.dp val DefaultIndicatorBackgroundCornerRadius = 20.dp - @Composable - fun defaultIndicatorCornerRadius( - x: Dp = DefaultIndicatorCornerRadius, - y: Dp = DefaultIndicatorCornerRadius, - ): CornerRadius = with(LocalDensity.current) { CornerRadius(x.toPx(), y.toPx()) } - /** * Returns the default VolumePanelRadioButtonBar colors. * @@ -225,9 +288,12 @@ private val Empty: @Composable RowScope.() -> Unit = {} private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScope { - var hasSelectedItem: Boolean = false + var selectedIndex: Int = UNSET_INDEX private set + val hasSelectedItem: Boolean + get() = selectedIndex != UNSET_INDEX + private val mutableItems: MutableList<Item> = mutableListOf() val items: List<Item> = mutableItems @@ -238,21 +304,34 @@ private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScop label: @Composable RowScope.() -> Unit, ) { require(!isSelected || !hasSelectedItem) { "Only one item should be selected at a time" } - hasSelectedItem = hasSelectedItem || isSelected + if (isSelected) { + selectedIndex = mutableItems.size + } mutableItems.add( Item( - isSelected = isSelected, onItemSelected = onItemSelected, icon = icon, label = label, ) ) } + + private companion object { + const val UNSET_INDEX = -1 + } } private class Item( - val isSelected: Boolean, val onItemSelected: () -> Unit, val icon: @Composable RowScope.() -> Unit, val label: @Composable RowScope.() -> Unit, ) + +private const val UNSET_OFFSET = -1 + +private enum class RadioButtonBarComponent(val zIndex: Float) { + ButtonsBackground(0f), + Indicator(1f), + Buttons(2f), + Labels(2f), +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt index bed0ae80e377..71b3e8a6a102 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt @@ -65,7 +65,7 @@ constructor( return } - val enabledModelStates by viewModel.spatialAudioButtonByEnabled.collectAsState() + val enabledModelStates by viewModel.spatialAudioButtons.collectAsState() if (enabledModelStates.isEmpty()) { return } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java index 343280de17b8..289896e01a9d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -444,6 +444,17 @@ public class AuthControllerTest extends SysuiTestCase { AdditionalMatchers.aryEq(credentialAttestation)); } + @Test + public void testSendsReasonContentViewMoreOptions_whenButtonPressed() throws Exception { + showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS, + null, /* credentialAttestation */ + mAuthController.mCurrentDialog.getRequestId()); + verify(mReceiver).onDialogDismissed( + eq(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS), + eq(null) /* credentialAttestation */); + } + // Statusbar tests @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt index e7aaddd94695..857b9f82f8bc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt @@ -68,9 +68,7 @@ class LockscreenToGoneTransitionViewModelTest : SysuiTestCase() { repository.sendTransitionStep(step(0f)) assertThat(alpha).isEqualTo(0.5f) - repository.sendTransitionStep(step(0.25f)) - assertThat(alpha).isEqualTo(0.25f) - + // Before the halfway point, it will have reached zero repository.sendTransitionStep(step(.5f)) assertThat(alpha).isEqualTo(0f) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 53a8e5dbda32..5256bb956bc4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -720,6 +720,59 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun alphaDoesNotUpdateWhileGoneTransitionIsRunning() = + testScope.runTest { + val viewState = ViewStateAccessor() + val alpha by collectLastValue(underTest.keyguardAlpha(viewState)) + + showLockscreen() + // GONE transition gets to 90% complete + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.STARTED, + value = 0f, + ) + ) + runCurrent() + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.RUNNING, + value = 0.9f, + ) + ) + runCurrent() + + // At this point, alpha should be zero + assertThat(alpha).isEqualTo(0f) + + // An attempt to override by the shade should be ignored + shadeRepository.setQsExpansion(0.5f) + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun alphaWhenGoneIsSetToOne() = + testScope.runTest { + val viewState = ViewStateAccessor() + val alpha by collectLastValue(underTest.keyguardAlpha(viewState)) + + showLockscreen() + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope + ) + keyguardRepository.setStatusBarState(StatusBarState.SHADE) + + assertThat(alpha).isEqualTo(1f) + } + + @Test fun shadeCollapseFadeIn() = testScope.runTest { val fadeIn by collectValues(underTest.shadeCollapseFadeIn) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java index 91699381ae7a..781a9a85edb3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java @@ -46,7 +46,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.DozeInteractor; import com.android.systemui.shade.NotificationShadeWindowViewController; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.StatusBarState; diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml b/packages/SystemUI/res/drawable/biometric_prompt_vertical_list_content_view_background.xml index 1f3e3a4c5b22..fdafe6d8e335 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml +++ b/packages/SystemUI/res/drawable/biometric_prompt_vertical_list_content_view_background.xml @@ -1,6 +1,5 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -14,7 +13,11 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape android:shape="rectangle" - xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="@android:color/white" /> + +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerHigh"/> + <corners android:radius="@dimen/biometric_prompt_content_corner_radius"/> </shape> diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_pin_content_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_pin_content_view.xml index 24222f7642be..1517f83814b1 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_pin_content_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_pin_content_view.xml @@ -53,6 +53,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + <LinearLayout + android:id="@+id/customized_view_container" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </RelativeLayout> <FrameLayout diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index d5af37733b3b..dd0c584d88a3 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -60,6 +60,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"/> + <LinearLayout + android:id="@+id/customized_view_container" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + <TextView android:id="@+id/error" style="?errorTextAppearanceLand" diff --git a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml index 2d63c8da54f9..1777bdf92786 100644 --- a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml @@ -2,6 +2,8 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" +xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" +android:id="@+id/biometric_prompt_constraint_layout" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -19,7 +21,7 @@ android:layout_height="match_parent"> android:id="@+id/panel" android:layout_width="0dp" android:layout_height="0dp" - android:background="?android:attr/colorBackgroundFloating" + android:background="?androidprv:attr/materialColorSurfaceBright" android:clickable="true" android:clipToOutline="true" android:importantForAccessibility="no" @@ -74,8 +76,9 @@ android:layout_height="match_parent"> <ImageView android:id="@+id/logo" - android:layout_width="@dimen/biometric_auth_icon_size" - android:layout_height="@dimen/biometric_auth_icon_size" + android:contentDescription="@string/biometric_dialog_logo" + android:layout_width="@dimen/biometric_prompt_logo_size" + android:layout_height="@dimen/biometric_prompt_logo_size" android:layout_gravity="center" android:scaleType="fitXY" android:visibility="visible" @@ -84,12 +87,9 @@ android:layout_height="match_parent"> <TextView android:id="@+id/logo_description" + style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="0dp" android:layout_height="wrap_content" - android:ellipsize="marquee" - android:gravity="@integer/biometric_dialog_text_gravity" - android:marqueeRepeatLimit="1" - android:singleLine="true" android:textAlignment="viewStart" android:paddingLeft="8dp" app:layout_constraintBottom_toBottomOf="@+id/logo" @@ -97,12 +97,6 @@ android:layout_height="match_parent"> app:layout_constraintStart_toEndOf="@+id/logo" app:layout_constraintTop_toTopOf="@+id/logo" /> - <Space - android:id="@+id/space_above_content" - android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_space_above_content" - android:visibility="gone" /> - <TextView android:id="@+id/title" style="@style/TextAppearance.AuthCredential.Title" @@ -137,11 +131,10 @@ android:layout_height="match_parent"> <LinearLayout android:id="@+id/customized_view_container" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="vertical" - android:paddingHorizontal="0dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -165,7 +158,6 @@ android:layout_height="match_parent"> app:layout_constraintTop_toBottomOf="@+id/subtitle" app:layout_constraintVertical_bias="0.0" /> - <androidx.constraintlayout.widget.Barrier android:id="@+id/contentBarrier" android:layout_width="wrap_content" @@ -179,16 +171,14 @@ android:layout_height="match_parent"> <TextView android:id="@+id/indicator" + style="@style/TextAppearance.AuthCredential.Indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:accessibilityLiveRegion="polite" android:fadingEdge="horizontal" android:gravity="center_horizontal" - android:marqueeRepeatLimit="marquee_forever" android:scrollHorizontally="true" - android:textColor="@color/biometric_dialog_gray" - android:textSize="12sp" app:layout_constraintBottom_toTopOf="@+id/buttonBarrier" app:layout_constraintEnd_toEndOf="@+id/biometric_icon" app:layout_constraintStart_toStartOf="@+id/biometric_icon" diff --git a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml index 329fc466d378..8b886a7fdffb 100644 --- a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml @@ -2,6 +2,8 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/biometric_prompt_constraint_layout" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -19,7 +21,7 @@ android:id="@+id/panel" android:layout_width="0dp" android:layout_height="0dp" - android:background="?android:attr/colorBackgroundFloating" + android:background="?androidprv:attr/materialColorSurfaceBright" android:clickable="true" android:clipToOutline="true" android:importantForAccessibility="no" @@ -61,8 +63,9 @@ <ImageView android:id="@+id/logo" - android:layout_width="@dimen/biometric_auth_icon_size" - android:layout_height="@dimen/biometric_auth_icon_size" + android:contentDescription="@string/biometric_dialog_logo" + android:layout_width="@dimen/biometric_prompt_logo_size" + android:layout_height="@dimen/biometric_prompt_logo_size" android:layout_gravity="center" android:scaleType="fitXY" android:visibility="visible" @@ -73,24 +76,14 @@ <TextView android:id="@+id/logo_description" + style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="marquee" - android:gravity="@integer/biometric_dialog_text_gravity" - android:marqueeRepeatLimit="1" - android:singleLine="true" - android:paddingTop="16dp" app:layout_constraintBottom_toTopOf="@+id/title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/logo" /> - <Space - android:id="@+id/space_above_content" - android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_space_above_content" - android:visibility="gone" /> - <TextView android:id="@+id/title" style="@style/TextAppearance.AuthCredential.Title" @@ -117,11 +110,10 @@ <LinearLayout android:id="@+id/customized_view_container" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="vertical" - android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -153,16 +145,14 @@ <!-- Cancel Button, replaces negative button when biometric is accepted --> <TextView android:id="@+id/indicator" + style="@style/TextAppearance.AuthCredential.Indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:accessibilityLiveRegion="polite" android:fadingEdge="horizontal" android:gravity="center_horizontal" - android:marqueeRepeatLimit="marquee_forever" android:scrollHorizontally="true" - android:textColor="@color/biometric_dialog_gray" - android:textSize="12sp" app:layout_constraintBottom_toTopOf="@+id/buttonBarrier" app:layout_constraintEnd_toEndOf="@+id/panel" app:layout_constraintStart_toStartOf="@+id/panel" diff --git a/packages/SystemUI/res/layout/auth_credential_password_pin_content_view.xml b/packages/SystemUI/res/layout/auth_credential_password_pin_content_view.xml index 11284fd2237b..9f4fcb368a66 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_pin_content_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_pin_content_view.xml @@ -55,6 +55,13 @@ android:layout_height="wrap_content" android:layout_below="@id/subtitle" /> + <LinearLayout + android:id="@+id/customized_view_container" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_below="@id/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> </RelativeLayout> </ScrollView> diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index 59828fde309f..baeb94ef2b60 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -56,6 +56,14 @@ android:layout_below="@id/subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content"/> + + <LinearLayout + android:id="@+id/customized_view_container" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_below="@id/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> </RelativeLayout> <RelativeLayout diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml index 6391813754d0..74bf318465b6 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml @@ -2,6 +2,8 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/biometric_prompt_constraint_layout" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -19,7 +21,7 @@ android:id="@+id/panel" android:layout_width="0dp" android:layout_height="0dp" - android:background="?android:attr/colorBackgroundFloating" + android:background="?androidprv:attr/materialColorSurfaceBright" android:clickable="true" android:clipToOutline="true" android:importantForAccessibility="no" @@ -61,8 +63,9 @@ <ImageView android:id="@+id/logo" - android:layout_width="@dimen/biometric_auth_icon_size" - android:layout_height="@dimen/biometric_auth_icon_size" + android:contentDescription="@string/biometric_dialog_logo" + android:layout_width="@dimen/biometric_prompt_logo_size" + android:layout_height="@dimen/biometric_prompt_logo_size" android:layout_gravity="center" android:scaleType="fitXY" app:layout_constraintBottom_toTopOf="@+id/logo_description" @@ -73,21 +76,14 @@ <TextView android:id="@+id/logo_description" + style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:singleLine="true" app:layout_constraintBottom_toTopOf="@+id/title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/logo" /> - <Space - android:id="@+id/space_above_content" - android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_space_above_content" - android:visibility="gone" /> - <TextView android:id="@+id/title" style="@style/TextAppearance.AuthCredential.Title" @@ -119,7 +115,7 @@ android:gravity="center_vertical" android:orientation="vertical" android:visibility="gone" - android:paddingTop="8dp" + android:paddingTop="24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -131,7 +127,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="@integer/biometric_dialog_text_gravity" - android:paddingTop="16dp" + android:paddingTop="24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -150,6 +146,7 @@ <TextView android:id="@+id/indicator" + style="@style/TextAppearance.AuthCredential.Indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" @@ -229,5 +226,4 @@ app:layout_constraintEnd_toEndOf="@+id/biometric_icon" app:layout_constraintStart_toStartOf="@+id/biometric_icon" app:layout_constraintTop_toTopOf="@+id/biometric_icon" /> - </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml index e39f60f349bc..bc827081292e 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml @@ -17,5 +17,5 @@ <TextView xmlns:android="http://schemas.android.com/apk/res/android" style="@style/TextAppearance.AuthCredential.ContentViewListItem" android:layout_width="0dp" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:layout_weight="1.0" />
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml index 6c867365e92c..f0125b60c6d8 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml @@ -16,6 +16,6 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_content_list_row_height" + android:layout_height="wrap_content" android:gravity="center_vertical|start" android:orientation="horizontal" /> diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_with_button_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_with_button_layout.xml new file mode 100644 index 000000000000..81f4bcc0bafc --- /dev/null +++ b/packages/SystemUI/res/layout/biometric_prompt_content_with_button_layout.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/customized_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/AuthCredentialContentViewStyle"> + + <TextView + android:id="@+id/customized_view_description" + style="@style/TextAppearance.AuthCredential.ContentViewWithButtonDescription" + android:paddingBottom="16dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <Button + android:id="@+id/customized_view_more_options_button" + style="@style/AuthCredentialContentViewMoreOptionsButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/biometric_dialog_content_view_more_options_button"/> +</LinearLayout> diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml index 984210906e68..ff89ed9e6e7a 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml @@ -44,7 +44,7 @@ android:singleLine="true" android:marqueeRepeatLimit="1" android:ellipsize="marquee" - style="@style/TextAppearance.AuthCredential.Title"/> + style="@style/TextAppearance.AuthCredential.OldTitle"/> <TextView android:id="@+id/subtitle" @@ -54,20 +54,21 @@ android:singleLine="true" android:marqueeRepeatLimit="1" android:ellipsize="marquee" - style="@style/TextAppearance.AuthCredential.Subtitle"/> + style="@style/TextAppearance.AuthCredential.OldSubtitle"/> <TextView android:id="@+id/description" android:layout_width="match_parent" android:layout_height="wrap_content" + android:gravity="@integer/biometric_dialog_text_gravity" android:scrollbars ="vertical" android:importantForAccessibility="no" - style="@style/TextAppearance.AuthCredential.Description"/> + style="@style/TextAppearance.AuthCredential.OldDescription"/> <Space android:id="@+id/space_above_content" android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_space_above_content" + android:layout_height="24dp" android:visibility="gone" /> <LinearLayout @@ -77,7 +78,6 @@ android:fadeScrollbars="false" android:gravity="center_vertical" android:orientation="vertical" - android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal" android:scrollbars="vertical" android:visibility="gone" /> diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_vertical_list_content_layout.xml index 390875702cfe..a754cb43908a 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_vertical_list_content_layout.xml @@ -17,16 +17,11 @@ android:id="@+id/customized_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="center_vertical" - android:orientation="vertical" - style="@style/AuthCredentialContentLayoutStyle"> + style="@style/AuthCredentialVerticalListContentViewStyle"> <TextView - android:id="@+id/customized_view_title" - style="@style/TextAppearance.AuthCredential.ContentViewTitle" + android:id="@+id/customized_view_description" + style="@style/TextAppearance.AuthCredential.VerticalListContentViewDescription" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:ellipsize="marquee" - android:marqueeRepeatLimit="1" - android:singleLine="true" /> + android:layout_height="wrap_content" /> </LinearLayout> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 307a6192a570..590dc682564e 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -140,9 +140,6 @@ <color name="biometric_dialog_gray">#ff757575</color> <color name="biometric_dialog_accent">@color/material_dynamic_primary40</color> <color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 --> - <!-- Color for biometric prompt content view --> - <color name="biometric_prompt_content_background_color">#8AB4F8</color> - <color name="biometric_prompt_content_list_item_bullet_color">#1d873b</color> <!-- SFPS colors --> <color name="sfps_chevron_fill">@color/material_dynamic_primary90</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2285550d94c7..e004ee9fa157 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1101,15 +1101,15 @@ <dimen name="biometric_dialog_width">240dp</dimen> <dimen name="biometric_dialog_height">240dp</dimen> - <!-- Dimensions for biometric prompt content view. --> - <dimen name="biometric_prompt_space_above_content">48dp</dimen> - <dimen name="biometric_prompt_content_container_padding_horizontal">24dp</dimen> - <dimen name="biometric_prompt_content_padding_horizontal">10dp</dimen> - <dimen name="biometric_prompt_content_list_row_height">24dp</dimen> - <dimen name="biometric_prompt_content_list_item_padding_horizontal">10dp</dimen> - <dimen name="biometric_prompt_content_list_item_text_size">14sp</dimen> - <dimen name="biometric_prompt_content_list_item_bullet_gap_width">10dp</dimen> - <dimen name="biometric_prompt_content_list_item_bullet_radius">5dp</dimen> + <!-- Dimensions for biometric prompt custom content view. --> + <dimen name="biometric_prompt_logo_size">32dp</dimen> + <dimen name="biometric_prompt_content_corner_radius">28dp</dimen> + <dimen name="biometric_prompt_content_padding_horizontal">24dp</dimen> + <dimen name="biometric_prompt_content_padding_vertical">16dp</dimen> + <dimen name="biometric_prompt_content_space_width_between_items">16dp</dimen> + <dimen name="biometric_prompt_content_list_item_padding_top">12dp</dimen> + <dimen name="biometric_prompt_content_list_item_bullet_gap_width">8.5dp</dimen> + <dimen name="biometric_prompt_content_list_item_bullet_radius">1.5dp</dimen> <!-- Biometric Auth Credential values --> <dimen name="biometric_auth_icon_size">48dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 3029888c7e54..a9151e88facc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -380,6 +380,8 @@ <!-- Button name for "Cancel". [CHAR LIMIT=NONE] --> <string name="cancel">Cancel</string> + <!-- Content description for the app logo icon on biometric prompt. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_logo">App logo</string> <!-- Message shown when a biometric is authenticated, asking the user to confirm authentication [CHAR LIMIT=30] --> <string name="biometric_dialog_confirm">Confirm</string> <!-- Button name on BiometricPrompt shown when a biometric is detected but not authenticated. Tapping the button resumes authentication [CHAR LIMIT=30] --> @@ -408,6 +410,8 @@ <string name="biometric_dialog_authenticated">Authenticated</string> <!-- Talkback string when a canceling authentication [CHAR LIMIT=NONE] --> <string name="biometric_dialog_cancel_authentication">Cancel Authentication</string> + <!-- Content description for the more options button on biometric prompt content view. [CHAR LIMIT=60] --> + <string name="biometric_dialog_content_view_more_options_button">More Options</string> <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pin) [CHAR LIMIT=30] --> <string name="biometric_dialog_use_pin">Use PIN</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 0483a0734a83..455b1920706f 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -174,43 +174,61 @@ <item name="android:textColor">?android:attr/textColorPrimary</item> </style> - <style name="TextAppearance.AuthCredential.Title"> + <style name="TextAppearance.AuthCredential.OldTitle"> <item name="android:fontFamily">google-sans</item> <item name="android:paddingTop">12dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">24sp</item> </style> - <style name="TextAppearance.AuthCredential.Subtitle"> + <style name="TextAppearance.AuthCredential.OldSubtitle"> <item name="android:fontFamily">google-sans</item> <item name="android:paddingTop">8dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">16sp</item> </style> - <style name="TextAppearance.AuthCredential.Description"> + <style name="TextAppearance.AuthCredential.OldDescription"> <item name="android:fontFamily">google-sans</item> <item name="android:paddingTop">8dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">14sp</item> </style> - <style name="TextAppearance.AuthCredential.ContentViewTitle"> - <item name="android:fontFamily">google-sans</item> - <item name="android:paddingTop">8dp</item> - <item name="android:paddingHorizontal">24dp</item> - <item name="android:textSize">14sp</item> - <item name="android:gravity">start</item> + <style name="TextAppearance.AuthCredential.LogoDescription" parent="TextAppearance.Material3.LabelLarge" > + <item name="android:ellipsize">marquee</item> + <item name="android:gravity">@integer/biometric_dialog_text_gravity</item> + <item name="android:marqueeRepeatLimit">1</item> + <item name="android:singleLine">true</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item> </style> - <style name="TextAppearance.AuthCredential.ContentViewListItem"> - <item name="android:fontFamily">google-sans</item> - <item name="android:paddingTop">8dp</item> - <item name="android:paddingHorizontal"> - @dimen/biometric_prompt_content_list_item_padding_horizontal - </item> - <item name="android:textSize">@dimen/biometric_prompt_content_list_item_text_size</item> - <item name="android:gravity">start</item> + <style name="TextAppearance.AuthCredential.Title" parent="TextAppearance.Material3.HeadlineSmall" > + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.AuthCredential.Subtitle" parent="TextAppearance.Material3.BodyMedium" > + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.AuthCredential.Description" parent="TextAppearance.Material3.BodyMedium" > + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.AuthCredential.VerticalListContentViewDescription" parent="TextAppearance.Material3.TitleSmall"> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.AuthCredential.ContentViewWithButtonDescription" parent="TextAppearance.AuthCredential.Description" /> + + <style name="TextAppearance.AuthCredential.ContentViewListItem" parent="TextAppearance.Material3.BodySmall"> + <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item> + <item name="android:paddingTop">@dimen/biometric_prompt_content_list_item_padding_top</item> + </style> + + <style name="TextAppearance.AuthCredential.Indicator" parent="TextAppearance.Material3.BodyMedium"> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + <item name="android:marqueeRepeatLimit">marquee_forever</item> </style> <style name="TextAppearance.AuthCredential.Error"> @@ -312,9 +330,27 @@ <item name="android:textSize">16sp</item> </style> - <style name="AuthCredentialContentLayoutStyle"> - <item name="android:background">@color/biometric_prompt_content_background_color</item> + <style name="AuthCredentialContentViewStyle"> + <item name="android:gravity">center_vertical</item> + <item name="android:orientation">vertical</item> + </style> + + <style name="AuthCredentialVerticalListContentViewStyle" parent="AuthCredentialContentViewStyle"> + <item name="android:background">@drawable/biometric_prompt_vertical_list_content_view_background</item> <item name="android:paddingHorizontal">@dimen/biometric_prompt_content_padding_horizontal</item> + <item name="android:paddingVertical">@dimen/biometric_prompt_content_padding_vertical</item> + </style> + + <style name="AuthCredentialContentViewMoreOptionsButtonStyle" parent="TextAppearance.Material3.LabelLarge"> + <item name="android:background">@color/transparent</item> + <item name="android:gravity">start</item> + <item name="enforceTextAppearance">false</item> + <item name="android:height">40dp</item> + <item name="android:maxWidth">@dimen/m3_btn_max_width</item> + <item name="android:minWidth">48dp</item> + <item name="android:paddingLeft">0dp</item> + <item name="android:paddingRight">12dp</item> + <item name="android:textColor">?androidprv:attr/materialColorPrimary</item> </style> <style name="DeviceManagementDialogTitle"> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java index c3c42399f1f7..9b09265763a2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java @@ -24,7 +24,7 @@ import androidx.annotation.Nullable; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.statusbar.phone.BiometricUnlockController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.KeyguardBypassController; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 99cdc0181553..fd0e7fc04ef3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -218,6 +218,11 @@ public class AuthContainerView extends LinearLayout } @Override + public void onContentViewMoreOptionsButtonPressed() { + animateAway(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS); + } + + @Override public void onError() { animateAway(AuthDialogCallback.DISMISSED_ERROR); } @@ -513,7 +518,8 @@ public class AuthContainerView extends LinearLayout mConfig.mOpPackageName); final CredentialViewModel vm = mCredentialViewModelProvider.get(); vm.setAnimateContents(animateContents); - ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel); + ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel, + mBiometricCallback); mLayout.addView(mCredentialView); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index a40b4d733382..d85b81d4d953 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -205,12 +205,12 @@ public class AuthController implements if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { String reason = intent.getStringExtra("reason"); reason = (reason != null) ? reason : "unknown"; - closeDioalog(reason); + closeDialog(reason); } } }; - private void closeDioalog(String reason) { + private void closeDialog(String reason) { if (isShowing()) { Log.i(TAG, "Close BP, reason :" + reason); mCurrentDialog.dismissWithoutCallback(true /* animate */); @@ -571,6 +571,11 @@ public class AuthController implements credentialAttestation); break; + case AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS: + sendResultAndCleanUp( + BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS, + credentialAttestation); + break; default: Log.e(TAG, "Unhandled reason: " + reason); break; @@ -579,7 +584,7 @@ public class AuthController implements @Override public void handleShowGlobalActionsMenu() { - closeDioalog("PowerMenu shown"); + closeDialog("PowerMenu shown"); } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java index 9a2194025a1a..024c6eaa75bb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java @@ -32,6 +32,7 @@ public interface AuthDialogCallback { int DISMISSED_ERROR = 5; int DISMISSED_BY_SYSTEM_SERVER = 6; int DISMISSED_CREDENTIAL_AUTHENTICATED = 7; + int DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS = 8; @IntDef({DISMISSED_USER_CANCELED, DISMISSED_BUTTON_NEGATIVE, @@ -39,7 +40,8 @@ public interface AuthDialogCallback { DISMISSED_BIOMETRIC_AUTHENTICATED, DISMISSED_ERROR, DISMISSED_BY_SYSTEM_SERVER, - DISMISSED_CREDENTIAL_AUTHENTICATED}) + DISMISSED_CREDENTIAL_AUTHENTICATED, + DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS}) @interface DismissedReason {} /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt index 4d88f4945952..9ad3f4313838 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -155,7 +155,8 @@ constructor( constraintBp() && !Utils.isBiometricAllowed(promptInfo) && isDeviceCredentialAllowed(promptInfo) && - promptInfo.contentView != null + promptInfo.contentView != null && + !promptInfo.isContentViewMoreOptionsButtonUsed _showBpWithoutIconForCredential.value = showBpForCredential && !hasCredentialViewShown } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt index 94cea5702fe3..b7c0fa802db3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt @@ -30,11 +30,11 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.lastOrNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext @@ -61,11 +61,12 @@ constructor( val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing /** - * If biometric prompt without icon needs to show for displaying content prior to credential - * view. + * If vertical list content view is shown, credential view should hide subtitle and content view */ - val showBpWithoutIconForCredential: StateFlow<Boolean> = - biometricPromptRepository.showBpWithoutIconForCredential + val showTitleOnly: Flow<Boolean> = + biometricPromptRepository.promptInfo.map { promptInfo -> + promptInfo?.contentView != null && !promptInfo.isContentViewMoreOptionsButtonUsed + } /** Metadata about the current credential prompt, including app-supplied preferences. */ val prompt: Flow<BiometricPromptRequest.Credential?> = diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt index 2f493ac1dccf..b28733f5cc55 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt @@ -10,10 +10,11 @@ import android.view.WindowInsets import android.view.accessibility.AccessibilityManager import android.widget.LinearLayout import android.widget.TextView -import com.android.systemui.res.R import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.ui.binder.CredentialViewBinder +import com.android.systemui.biometrics.ui.binder.Spaghetti import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel +import com.android.systemui.res.R /** PIN or password credential view for BiometricPrompt. */ class CredentialPasswordView(context: Context, attrs: AttributeSet?) : @@ -31,8 +32,16 @@ class CredentialPasswordView(context: Context, attrs: AttributeSet?) : host: CredentialView.Host, panelViewController: AuthPanelController, animatePanel: Boolean, + legacyCallback: Spaghetti.Callback, ) { - CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) + CredentialViewBinder.bind( + this, + host, + viewModel, + panelViewController, + animatePanel, + legacyCallback + ) } override fun onFinishInflate() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt index 10868970fcbb..d9d286fe7035 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt @@ -9,6 +9,7 @@ import android.view.WindowInsets.Type import android.widget.LinearLayout import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.ui.binder.CredentialViewBinder +import com.android.systemui.biometrics.ui.binder.Spaghetti import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel /** Pattern credential view for BiometricPrompt. */ @@ -21,8 +22,16 @@ class CredentialPatternView(context: Context, attrs: AttributeSet?) : host: CredentialView.Host, panelViewController: AuthPanelController, animatePanel: Boolean, + legacyCallback: Spaghetti.Callback, ) { - CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) + CredentialViewBinder.bind( + this, + host, + viewModel, + panelViewController, + animatePanel, + legacyCallback + ) } override fun onFinishInflate() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt index b7c6a4566108..e2f98958ab55 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt @@ -1,6 +1,7 @@ package com.android.systemui.biometrics.ui import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.binder.Spaghetti import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel /** A credential variant of BiometricPrompt. */ @@ -27,5 +28,6 @@ sealed interface CredentialView { host: Host, panelViewController: AuthPanelController, animatePanel: Boolean, + legacyCallback: Spaghetti.Callback, ) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt index e58c8ff92c03..88aef5675240 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt @@ -18,101 +18,167 @@ package com.android.systemui.biometrics.ui.binder import android.content.Context import android.content.res.Resources -import android.content.res.Resources.Theme -import android.graphics.Paint import android.hardware.biometrics.PromptContentItem import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentItemPlainText import android.hardware.biometrics.PromptContentView +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptVerticalListContentView import android.text.SpannableString import android.text.Spanned +import android.text.TextPaint import android.text.style.BulletSpan import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewTreeObserver +import android.widget.Button import android.widget.LinearLayout import android.widget.Space import android.widget.TextView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.lifecycleScope +import com.android.settingslib.Utils import com.android.systemui.biometrics.ui.BiometricPromptLayout -import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import kotlin.math.ceil -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** Sub-binder for [BiometricPromptLayout.customized_view_container]. */ object BiometricCustomizedViewBinder { - fun bind(customizedViewContainer: LinearLayout, spaceAbove: Space, viewModel: PromptViewModel) { - customizedViewContainer.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - val contentView: PromptContentView? = viewModel.contentView.first() - - if (contentView != null) { - val context = customizedViewContainer.context - customizedViewContainer.addView(contentView.toView(context)) - customizedViewContainer.visibility = View.VISIBLE - spaceAbove.visibility = View.VISIBLE - } else { - customizedViewContainer.visibility = View.GONE - spaceAbove.visibility = View.GONE + fun bind( + customizedViewContainer: LinearLayout, + contentView: PromptContentView?, + legacyCallback: Spaghetti.Callback + ) { + customizedViewContainer.repeatWhenAttached { containerView -> + lifecycleScope.launch { + if (contentView == null) { + containerView.visibility = View.GONE + return@launch + } + + containerView.width { containerWidth -> + if (containerWidth == 0) { + return@width } + (containerView as LinearLayout).addView( + contentView.toView(containerView.context, containerWidth, legacyCallback) + ) + containerView.visibility = View.VISIBLE } } } } } -private fun PromptContentView.toView(context: Context): View { - val resources = context.resources +private fun PromptContentView.toView( + context: Context, + containerViewWidth: Int, + legacyCallback: Spaghetti.Callback +): View { + return when (this) { + is PromptVerticalListContentView -> initLayout(context, containerViewWidth) + is PromptContentViewWithMoreOptionsButton -> initLayout(context, legacyCallback) + else -> { + throw IllegalStateException("No such PromptContentView: $this") + } + } +} + +private fun LayoutInflater.inflateContentView(id: Int, description: String?): LinearLayout { + val contentView = inflate(id, null) as LinearLayout + + val descriptionView = contentView.requireViewById<TextView>(R.id.customized_view_description) + if (!description.isNullOrEmpty()) { + descriptionView.text = description + } else { + descriptionView.visibility = View.GONE + } + return contentView +} + +private fun PromptContentViewWithMoreOptionsButton.initLayout( + context: Context, + legacyCallback: Spaghetti.Callback +): View { val inflater = LayoutInflater.from(context) - when (this) { - is PromptVerticalListContentView -> { - val contentView = - inflater.inflate(R.layout.biometric_prompt_content_layout, null) as LinearLayout - - val descriptionView = contentView.requireViewById<TextView>(R.id.customized_view_title) - if (!description.isNullOrEmpty()) { - descriptionView.text = description - } else { - descriptionView.visibility = View.GONE - } + val contentView = + inflater.inflateContentView( + R.layout.biometric_prompt_content_with_button_layout, + description + ) + val buttonView = contentView.requireViewById<Button>(R.id.customized_view_more_options_button) + buttonView.setOnClickListener { legacyCallback.onContentViewMoreOptionsButtonPressed() } + return contentView +} - // Show two column by default, once there is an item exceeding max lines, show single - // item instead. - val showTwoColumn = listItems.all { !it.doesExceedMaxLinesIfTwoColumn(resources) } - var currRowView = createNewRowLayout(inflater) - for (item in listItems) { - val itemView = item.toView(resources, inflater, context.theme) - currRowView.addView(itemView) - - if (!showTwoColumn || currRowView.childCount == 2) { - contentView.addView(currRowView) - currRowView = createNewRowLayout(inflater) - } - } - if (currRowView.childCount > 0) { - contentView.addView(currRowView) - } +private fun PromptVerticalListContentView.initLayout( + context: Context, + containerViewWidth: Int +): View { + val inflater = LayoutInflater.from(context) + val resources = context.resources + val contentView = + inflater.inflateContentView( + R.layout.biometric_prompt_vertical_list_content_layout, + description + ) + // Show two column by default, once there is an item exceeding max lines, show single + // item instead. + val showTwoColumn = + listItems.all { !it.doesExceedMaxLinesIfTwoColumn(context, containerViewWidth) } + var currRowView = createNewRowLayout(inflater) + for (item in listItems) { + val itemView = item.toView(context, inflater) + // If this item will be in the first row (contentView only has description view) and + // description is empty, remove top padding of this item. + if (contentView.childCount == 1 && description.isNullOrEmpty()) { + itemView.setPadding( + itemView.paddingLeft, + 0, + itemView.paddingRight, + itemView.paddingBottom + ) + } + currRowView.addView(itemView) - return contentView + // If this is the first item in the current row, add space behind it. + if (currRowView.childCount == 1 && showTwoColumn) { + currRowView.addSpaceView( + resources.getDimensionPixelSize( + R.dimen.biometric_prompt_content_space_width_between_items + ), + MATCH_PARENT + ) } - else -> { - throw IllegalStateException("No such PromptContentView: $this") + + // If there are already two items (plus the space view) in the current row, or it + // should be one column, start a new row + if (currRowView.childCount == 3 || !showTwoColumn) { + contentView.addView(currRowView) + currRowView = createNewRowLayout(inflater) } } + if (currRowView.childCount > 0) { + contentView.addView(currRowView) + } + return contentView } private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout { return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout } +private fun LinearLayout.addSpaceView(width: Int, height: Int) { + addView(Space(context), LinearLayout.LayoutParams(width, height)) +} + private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( - resources: Resources, + context: Context, + containerViewWidth: Int, ): Boolean { + val resources = context.resources val passedInText: String = when (this) { is PromptContentItemPlainText -> text @@ -125,32 +191,26 @@ private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( when (this) { is PromptContentItemPlainText, is PromptContentItemBulletedText -> { - val dialogMargin = - resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding) - val halfDialogWidth = - Resources.getSystem().displayMetrics.widthPixels / 2 - dialogMargin - val containerPadding = - resources.getDimensionPixelSize( - R.dimen.biometric_prompt_content_container_padding_horizontal - ) - val contentPadding = + val contentViewPadding = resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_padding_horizontal) val listItemPadding = getListItemPadding(resources) - val maxWidth = halfDialogWidth - containerPadding - contentPadding - listItemPadding + val maxWidth = containerViewWidth / 2 - contentViewPadding - listItemPadding - val text = "$passedInText" - val textSize = - resources.getDimensionPixelSize( - R.dimen.biometric_prompt_content_list_item_text_size + val paint = TextPaint() + val attributes = + context.obtainStyledAttributes( + R.style.TextAppearance_AuthCredential_ContentViewListItem, + intArrayOf(android.R.attr.textSize) ) - val paint = Paint() - paint.textSize = textSize.toFloat() + paint.textSize = attributes.getDimensionPixelSize(0, 0).toFloat() + val textWidth = paint.measureText(passedInText) + attributes.recycle() val maxLines = resources.getInteger( R.integer.biometric_prompt_content_list_item_max_lines_if_two_column ) - val numLines = ceil(paint.measureText(text).toDouble() / maxWidth).toInt() + val numLines = ceil(textWidth / maxWidth).toInt() return numLines > maxLines } else -> { @@ -160,10 +220,10 @@ private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( } private fun PromptContentItem.toView( - resources: Resources, + context: Context, inflater: LayoutInflater, - theme: Theme, ): TextView { + val resources = context.resources val textView = inflater.inflate(R.layout.biometric_prompt_content_row_item_text_view, null) as TextView val lp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f) @@ -178,7 +238,7 @@ private fun PromptContentItem.toView( val span = BulletSpan( getListItemBulletGapWidth(resources), - getListItemBulletColor(resources, theme), + getListItemBulletColor(context), getListItemBulletRadius(resources) ) bulletedText.setSpan(span, 0 /* start */, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -194,8 +254,8 @@ private fun PromptContentItem.toView( private fun PromptContentItem.getListItemPadding(resources: Resources): Int { var listItemPadding = resources.getDimensionPixelSize( - R.dimen.biometric_prompt_content_list_item_padding_horizontal - ) * 2 + R.dimen.biometric_prompt_content_space_width_between_items + ) / 2 when (this) { is PromptContentItemPlainText -> {} is PromptContentItemBulletedText -> { @@ -215,5 +275,20 @@ private fun getListItemBulletRadius(resources: Resources): Int = private fun getListItemBulletGapWidth(resources: Resources): Int = resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_gap_width) -private fun getListItemBulletColor(resources: Resources, theme: Theme): Int = - resources.getColor(R.color.biometric_prompt_content_list_item_bullet_color, theme) +private fun getListItemBulletColor(context: Context): Int = + Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.materialColorOnSurface) + +private fun <T : View> T.width(function: (Int) -> Unit) { + if (width == 0) + viewTreeObserver.addOnGlobalLayoutListener( + object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + if (measuredWidth > 0) { + viewTreeObserver.removeOnGlobalLayoutListener(this) + } + function(measuredWidth) + } + } + ) + else function(measuredWidth) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 7bb75bf5ca9b..b2ade4fa1e8a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -135,7 +135,7 @@ object BiometricViewBinder { val confirmationButton = view.requireViewById<Button>(R.id.button_confirm) val retryButton = view.requireViewById<Button>(R.id.button_try_again) - // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers + // TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers val adapter = Spaghetti( view = view, @@ -171,8 +171,8 @@ object BiometricViewBinder { if (Flags.customBiometricPrompt() && constraintBp()) { BiometricCustomizedViewBinder.bind( customizedViewContainer, - view.requireViewById(R.id.space_above_content), - viewModel + viewModel.contentView.first(), + legacyCallback ) } @@ -476,7 +476,7 @@ object BiometricViewBinder { * * Do not reference the [view] for anything other than [asView]. */ -@Deprecated("TODO(b/251476085): remove after replacing AuthContainerView") +@Deprecated("TODO(b/330788871): remove after replacing AuthContainerView") class Spaghetti( private val view: View, private val viewModel: PromptViewModel, @@ -484,19 +484,20 @@ class Spaghetti( private val applicationScope: CoroutineScope, ) { - @Deprecated("TODO(b/251476085): remove after replacing AuthContainerView") + @Deprecated("TODO(b/330788871): remove after replacing AuthContainerView") interface Callback { fun onAuthenticated() fun onUserCanceled() fun onButtonNegative() fun onButtonTryAgain() + fun onContentViewMoreOptionsButtonPressed() fun onError() fun onUseDeviceCredential() fun onStartDelayedFingerprintSensor() fun onAuthenticatedAndConfirmed() } - @Deprecated("TODO(b/251476085): remove after replacing AuthContainerView") + @Deprecated("TODO(b/330788871): remove after replacing AuthContainerView") enum class BiometricState { /** Authentication hardware idle. */ STATE_IDLE, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index 1dfd2e5f9cc9..e3c0cba42e2d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -121,10 +121,6 @@ object BiometricViewSizeBinder { val largeConstraintSet = ConstraintSet() largeConstraintSet.clone(mediumConstraintSet) - largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) - largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) - largeConstraintSet.setVisibility(R.id.indicator, View.GONE) - largeConstraintSet.setVisibility(R.id.scrollView, View.GONE) // TODO: Investigate better way to handle 180 rotations val flipConstraintSet = ConstraintSet() @@ -286,6 +282,10 @@ object BiometricViewSizeBinder { fun setVisibilities(size: PromptSize) { viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) } + largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) + largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) + largeConstraintSet.setVisibility(R.id.indicator, View.GONE) + largeConstraintSet.setVisibility(R.id.scrollView, View.GONE) if (viewModel.showBpWithoutIconForCredential.value) { smallConstraintSet.setVisibility(iconHolderView.id, View.GONE) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt index ce52e1d78fda..18e2a56e5e78 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -1,20 +1,23 @@ package com.android.systemui.biometrics.ui.binder +import android.hardware.biometrics.Flags import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators -import com.android.systemui.res.R +import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.ui.CredentialPasswordView import com.android.systemui.biometrics.ui.CredentialPatternView import com.android.systemui.biometrics.ui.CredentialView import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filter @@ -40,12 +43,15 @@ object CredentialViewBinder { viewModel: CredentialViewModel, panelViewController: AuthPanelController, animatePanel: Boolean, + legacyCallback: Spaghetti.Callback, maxErrorDuration: Long = 3_000L, requestFocusForInput: Boolean = true, ) { val titleView: TextView = view.requireViewById(R.id.title) val subtitleView: TextView = view.requireViewById(R.id.subtitle) val descriptionView: TextView = view.requireViewById(R.id.description) + val customizedViewContainer: LinearLayout = + view.requireViewById(R.id.customized_view_container) val iconView: ImageView? = view.findViewById(R.id.icon) val errorView: TextView = view.requireViewById(R.id.error) val cancelButton: Button? = view.findViewById(R.id.cancel_button) @@ -76,6 +82,13 @@ object CredentialViewBinder { subtitleView.textOrHide = header.subtitle descriptionView.textOrHide = header.description + if (Flags.customBiometricPrompt() && constraintBp()) { + BiometricCustomizedViewBinder.bind( + customizedViewContainer, + header.contentView, + legacyCallback + ) + } iconView?.setImageDrawable(header.icon) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt index c6d90855e7d2..8b8c90a479d8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt @@ -1,6 +1,7 @@ package com.android.systemui.biometrics.ui.viewmodel import android.graphics.drawable.Drawable +import android.hardware.biometrics.PromptContentView import com.android.systemui.biometrics.shared.model.BiometricUserInfo /** View model for the top-level header / info area of BiometricPrompt. */ @@ -9,6 +10,7 @@ interface CredentialHeaderViewModel { val title: String val subtitle: String val description: String + val contentView: PromptContentView? val icon: Drawable val showEmergencyCallButton: Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt index 46be8c74cee3..31af126eb3f0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt @@ -2,8 +2,11 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context import android.graphics.drawable.Drawable +import android.hardware.biometrics.Flags.customBiometricPrompt +import android.hardware.biometrics.PromptContentView import android.text.InputType import com.android.internal.widget.LockPatternView +import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.CredentialStatus import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor @@ -34,14 +37,19 @@ constructor( val header: Flow<CredentialHeaderViewModel> = combine( credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>(), - credentialInteractor.showBpWithoutIconForCredential - ) { request, showBpWithoutIconForCredential -> + credentialInteractor.showTitleOnly + ) { request, showTitleOnly -> + val flagEnabled = customBiometricPrompt() && constraintBp() + val showTitleOnlyForCredential = showTitleOnly && flagEnabled BiometricPromptHeaderViewModelImpl( request, user = request.userInfo, title = request.title, - subtitle = if (showBpWithoutIconForCredential) "" else request.subtitle, - description = if (showBpWithoutIconForCredential) "" else request.description, + subtitle = if (showTitleOnlyForCredential) "" else request.subtitle, + contentView = + if (flagEnabled && !showTitleOnlyForCredential) request.contentView else null, + description = + if (flagEnabled && request.contentView != null) "" else request.description, icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId), showEmergencyCallButton = request.showEmergencyCallButton ) @@ -188,6 +196,7 @@ private class BiometricPromptHeaderViewModelImpl( override val title: String, override val subtitle: String, override val description: String, + override val contentView: PromptContentView?, override val icon: Drawable, override val showEmergencyCallButton: Boolean, ) : CredentialHeaderViewModel diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt index 7e5b26732e00..c110d06aeedb 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt @@ -28,7 +28,6 @@ import android.os.UserHandle import android.service.controls.Control import android.service.controls.ControlsProviderService import android.util.Log -import java.lang.ClassCastException /** * Proxy to launch in user 0 @@ -61,22 +60,28 @@ class ControlsRequestReceiver : BroadcastReceiver() { } val targetComponent = try { - intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME) - } catch (e: ClassCastException) { + intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java) + } catch (e: Exception) { Log.e(TAG, "Malformed intent extra ComponentName", e) return + } ?: run { + Log.e(TAG, "Null target component") + return } val control = try { - intent.getParcelableExtra<Control>(ControlsProviderService.EXTRA_CONTROL) - } catch (e: ClassCastException) { + intent.getParcelableExtra(ControlsProviderService.EXTRA_CONTROL, Control::class.java) + } catch (e: Exception) { Log.e(TAG, "Malformed intent extra Control", e) return + } ?: run { + Log.e(TAG, "Null control") + return } - val packageName = targetComponent?.packageName + val packageName = targetComponent.packageName - if (packageName == null || !isPackageInForeground(context, packageName)) { + if (!isPackageInForeground(context, packageName)) { return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index f700e037f2fe..a293afcc28dc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -154,7 +154,7 @@ import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeDepthController; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 12b27eb195fb..2649d4347495 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -289,7 +289,10 @@ constructor( .collect { pair -> val (isKeyguardGoingAway, lastStartedStep) = pair if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { - startTransitionTo(KeyguardState.GONE) + startTransitionTo( + KeyguardState.GONE, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) } } } @@ -303,20 +306,6 @@ constructor( startTransitionTo(KeyguardState.GONE) } } - - return - } - - scope.launch { - keyguardInteractor.isKeyguardGoingAway - .sample(startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - KeyguardWmStateRefactor.assertInLegacyMode() - val (isKeyguardGoingAway, lastStartedStep) = pair - if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { - startTransitionTo(KeyguardState.GONE) - } - } } } @@ -413,7 +402,7 @@ constructor( val TO_OCCLUDED_DURATION = 450.milliseconds val TO_AOD_DURATION = 500.milliseconds val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION - val TO_GONE_DURATION = DEFAULT_DURATION + val TO_GONE_DURATION = 633.milliseconds val TO_GLANCEABLE_HUB_DURATION = 1.seconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt index c40902871388..cbbb82039329 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt @@ -30,8 +30,6 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map /** * Breaks down AOD->LOCKSCREEN transition into discrete steps for corresponding views to consume. @@ -53,6 +51,8 @@ constructor( to = KeyguardState.LOCKSCREEN, ) + private var isShadeExpanded = false + /** * Begin the transition from wherever the y-translation value is currently. This helps ensure a * smooth transition if a transition in canceled. @@ -77,22 +77,21 @@ constructor( } val notificationAlpha: Flow<Float> = - combine( - shadeInteractor.shadeExpansion.map { it > 0f }, - shadeInteractor.qsExpansion.map { it > 0f }, - transitionAnimation.sharedFlow( - duration = 500.milliseconds, - onStep = { it }, - onCancel = { 1f }, - ), - ) { isShadeExpanded, isQsExpanded, alpha -> - if (isShadeExpanded || isQsExpanded) { - // One example of this happening is dragging a notification while pulsing on AOD - 1f - } else { - alpha - } - } + transitionAnimation.sharedFlow( + duration = 500.milliseconds, + onStart = { + isShadeExpanded = + shadeInteractor.shadeExpansion.value > 0f || + shadeInteractor.qsExpansion.value > 0f + }, + onStep = { + if (isShadeExpanded) { + 1f + } else { + it + } + }, + ) val shortcutsAlpha: Flow<Float> = transitionAnimation.sharedFlow( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt index 89a9ba7b61a3..963c602b3d1e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt @@ -18,17 +18,11 @@ package com.android.systemui.media.controls.ui.controller import android.content.Context import android.content.res.Configuration -import android.database.ContentObserver -import android.net.Uri -import android.os.Handler -import android.os.UserHandle -import android.provider.Settings import android.view.View import android.view.ViewGroup import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.media.controls.ui.view.MediaHost @@ -43,7 +37,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.asIndenting import com.android.systemui.util.println -import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.withIncreasedIndent import java.io.PrintWriter import javax.inject.Inject @@ -61,8 +54,6 @@ constructor( private val bypassController: KeyguardBypassController, private val statusBarStateController: SysuiStatusBarStateController, private val context: Context, - private val secureSettings: SecureSettings, - @Main private val handler: Handler, configurationController: ConfigurationController, private val splitShadeStateController: SplitShadeStateController, private val logger: KeyguardMediaControllerLogger, @@ -91,26 +82,6 @@ constructor( } ) - val settingsObserver: ContentObserver = - object : ContentObserver(handler) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - if (uri == lockScreenMediaPlayerUri) { - allowMediaPlayerOnLockScreen = - secureSettings.getBoolForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - true, - UserHandle.USER_CURRENT - ) - refreshMediaPosition(reason = "allowMediaPlayerOnLockScreen changed") - } - } - } - secureSettings.registerContentObserverForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - settingsObserver, - UserHandle.USER_ALL - ) - // First let's set the desired state that we want for this host mediaHost.expansion = MediaHostState.EXPANDED mediaHost.showsOnlyActiveMedia = true @@ -156,16 +127,6 @@ constructor( private set private var splitShadeContainer: ViewGroup? = null - /** Track the media player setting status on lock screen. */ - private var allowMediaPlayerOnLockScreen: Boolean = - secureSettings.getBoolForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - true, - UserHandle.USER_CURRENT - ) - private val lockScreenMediaPlayerUri = - secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) - /** * Attaches media container in single pane mode, situated at the top of the notifications list */ @@ -229,14 +190,12 @@ constructor( // mediaHost.visible required for proper animations handling val isMediaHostVisible = mediaHost.visible val isBypassNotEnabled = !bypassController.bypassEnabled - val currentAllowMediaPlayerOnLockScreen = allowMediaPlayerOnLockScreen val useSplitShade = useSplitShade val shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade() visible = isMediaHostVisible && isBypassNotEnabled && keyguardOrUserSwitcher && - currentAllowMediaPlayerOnLockScreen && shouldBeVisibleForSplitShade logger.logRefreshMediaPosition( reason = reason, @@ -246,7 +205,6 @@ constructor( keyguardOrUserSwitcher = keyguardOrUserSwitcher, mediaHostVisible = isMediaHostVisible, bypassNotEnabled = isBypassNotEnabled, - currentAllowMediaPlayerOnLockScreen = currentAllowMediaPlayerOnLockScreen, shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade, ) val currActiveContainer = activeContainer @@ -321,7 +279,6 @@ constructor( println("Self", this@KeyguardMediaController) println("visible", visible) println("useSplitShade", useSplitShade) - println("allowMediaPlayerOnLockScreen", allowMediaPlayerOnLockScreen) println("bypassController.bypassEnabled", bypassController.bypassEnabled) println("isDozeWakeUpAnimationWaiting", isDozeWakeUpAnimationWaiting) println("singlePaneContainer", singlePaneContainer) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerLogger.kt index c0d9dc23a6d5..4d1827efe82f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerLogger.kt @@ -36,7 +36,6 @@ constructor(@KeyguardMediaControllerLog private val logBuffer: LogBuffer) { keyguardOrUserSwitcher: Boolean, mediaHostVisible: Boolean, bypassNotEnabled: Boolean, - currentAllowMediaPlayerOnLockScreen: Boolean, shouldBeVisibleForSplitShade: Boolean, ) { logBuffer.log( @@ -50,8 +49,7 @@ constructor(@KeyguardMediaControllerLog private val logBuffer: LogBuffer) { bool3 = keyguardOrUserSwitcher bool4 = mediaHostVisible int2 = if (bypassNotEnabled) 1 else 0 - str2 = currentAllowMediaPlayerOnLockScreen.toString() - str3 = shouldBeVisibleForSplitShade.toString() + str2 = shouldBeVisibleForSplitShade.toString() }, { "refreshMediaPosition(reason=$str1, " + @@ -60,8 +58,7 @@ constructor(@KeyguardMediaControllerLog private val logBuffer: LogBuffer) { "keyguardOrUserSwitcher=$bool3, " + "mediaHostVisible=$bool4, " + "bypassNotEnabled=${int2 == 1}, " + - "currentAllowMediaPlayerOnLockScreen=$str2, " + - "shouldBeVisibleForSplitShade=$str3)" + "shouldBeVisibleForSplitShade=$str2)" } ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 655e6a55fb95..c3c1e83546df 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -22,6 +22,7 @@ import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration import android.database.ContentObserver +import android.os.UserHandle import android.provider.Settings import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.util.Log @@ -44,6 +45,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.media.controls.domain.pipeline.MediaDataManager @@ -76,6 +78,8 @@ import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import java.util.Locale @@ -83,10 +87,16 @@ import java.util.TreeMap import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Provider +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext private const val TAG = "MediaCarouselController" private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) @@ -108,6 +118,7 @@ constructor( private val systemClock: SystemClock, @Main executor: DelayableExecutor, @Background private val bgExecutor: Executor, + @Background private val backgroundDispatcher: CoroutineDispatcher, private val mediaManager: MediaDataManager, configurationController: ConfigurationController, falsingManager: FalsingManager, @@ -118,6 +129,7 @@ constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val globalSettings: GlobalSettings, + private val secureSettings: SecureSettings, ) : Dumpable { /** The current width of the carousel */ var currentCarouselWidth: Int = 0 @@ -191,6 +203,8 @@ constructor( } } + private var allowMediaPlayerOnLockScreen = false + /** Whether the media card currently has the "expanded" layout */ @VisibleForTesting var currentlyExpanded = true @@ -532,8 +546,9 @@ constructor( keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) mediaCarousel.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - // A backup to show media carousel (if available) once the keyguard is gone. listenForAnyStateToGoneKeyguardTransition(this) + listenForAnyStateToLockscreenTransition(this) + listenForLockscreenSettingChanges(this) } } @@ -587,7 +602,49 @@ constructor( return scope.launch { keyguardTransitionInteractor.anyStateToGoneTransition .filter { it.transitionState == TransitionState.FINISHED } - .collect { showMediaCarousel() } + .collect { + showMediaCarousel() + updateHostVisibility() + } + } + } + + @VisibleForTesting + internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.anyStateToLockscreenTransition + .filter { it.transitionState == TransitionState.FINISHED } + .collect { + if (!allowMediaPlayerOnLockScreen) { + updateHostVisibility() + } + } + } + } + + @VisibleForTesting + internal fun listenForLockscreenSettingChanges(scope: CoroutineScope): Job { + return scope.launch { + secureSettings + .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) + // query to get initial value + .onStart { emit(Unit) } + .map { getMediaLockScreenSetting() } + .distinctUntilChanged() + .collectLatest { + allowMediaPlayerOnLockScreen = it + updateHostVisibility() + } + } + } + + private suspend fun getMediaLockScreenSetting(): Boolean { + return withContext(backgroundDispatcher) { + secureSettings.getBoolForUser( + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT + ) } } @@ -600,6 +657,13 @@ constructor( updatePlayers(recreateMedia = true) } + /** Return true if the carousel should be hidden because lockscreen is currently visible */ + fun isLockedAndHidden(): Boolean { + val keyguardState = keyguardTransitionInteractor.getFinishedState() + return !allowMediaPlayerOnLockScreen && + KeyguardState.lockscreenVisibleInState(keyguardState) + } + private fun reorderAllPlayers( previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, key: String? = null diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt index d92168bf9fa4..eca76b603b1a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt @@ -23,6 +23,7 @@ import android.view.View.OnAttachStateChangeListener import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.ui.controller.MediaCarouselController import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.controller.MediaHostStatesManager import com.android.systemui.media.controls.ui.controller.MediaLocation @@ -33,12 +34,12 @@ import com.android.systemui.util.animation.UniqueObjectHostView import java.util.Objects import javax.inject.Inject -class MediaHost -constructor( +class MediaHost( private val state: MediaHostStateHolder, private val mediaHierarchyManager: MediaHierarchyManager, private val mediaDataManager: MediaDataManager, - private val mediaHostStatesManager: MediaHostStatesManager + private val mediaHostStatesManager: MediaHostStatesManager, + private val mediaCarouselController: MediaCarouselController, ) : MediaHostState by state { lateinit var hostView: UniqueObjectHostView var location: Int = -1 @@ -202,7 +203,9 @@ constructor( */ fun updateViewVisibility() { state.visible = - if (showsOnlyActiveMedia) { + if (mediaCarouselController.isLockedAndHidden()) { + false + } else if (showsOnlyActiveMedia) { mediaDataManager.hasActiveMediaOrRecommendation() } else { mediaDataManager.hasAnyMediaOrRecommendation() diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index 0fa3605ecd6d..59b98b2792be 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -21,6 +21,7 @@ import com.android.systemui.log.LogBuffer; import com.android.systemui.log.LogBufferFactory; import com.android.systemui.media.controls.domain.MediaDomainModule; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; +import com.android.systemui.media.controls.ui.controller.MediaCarouselController; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; import com.android.systemui.media.controls.ui.controller.MediaHostStatesManager; import com.android.systemui.media.controls.ui.view.MediaHost; @@ -59,8 +60,9 @@ public interface MediaModule { @Named(QS_PANEL) static MediaHost providesQSMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** */ @@ -69,8 +71,9 @@ public interface MediaModule { @Named(QUICK_QS_PANEL) static MediaHost providesQuickQSMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** */ @@ -79,8 +82,9 @@ public interface MediaModule { @Named(KEYGUARD) static MediaHost providesKeyguardMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** */ @@ -89,8 +93,9 @@ public interface MediaModule { @Named(DREAM) static MediaHost providesDreamMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** */ @@ -99,8 +104,9 @@ public interface MediaModule { @Named(COMMUNAL_HUB) static MediaHost providesCommunalMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** Provides a logging buffer related to the media tap-to-transfer chip on the sender device. */ diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt index abdbd6880b33..97acccde2524 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -26,34 +26,45 @@ import com.android.systemui.res.R import javax.inject.Inject /** - * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI + * Provides actions for screenshots. This class can be overridden by a vendor-specific SysUI * implementation. */ interface ScreenshotActionsProvider { data class ScreenshotAction( - val icon: Drawable?, - val text: String?, - val overrideTransition: Boolean, + val icon: Drawable? = null, + val text: String? = null, + val description: String, + val overrideTransition: Boolean = false, val retrieveIntent: (Uri) -> Intent ) - fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent - fun getActions(context: Context, user: UserHandle): List<ScreenshotAction> -} + interface ScreenshotActionsCallback { + fun setPreviewAction(overrideTransition: Boolean = false, retrieveIntent: (Uri) -> Intent) + fun addAction(action: ScreenshotAction) = addActions(listOf(action)) + fun addActions(actions: List<ScreenshotAction>) + } -class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider { - override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent { - return ActionIntentCreator.createEdit(uri, context) + interface Factory { + fun create( + context: Context, + user: UserHandle?, + callback: ScreenshotActionsCallback + ): ScreenshotActionsProvider } +} - override fun getActions( - context: Context, - user: UserHandle - ): List<ScreenshotActionsProvider.ScreenshotAction> { +class DefaultScreenshotActionsProvider( + private val context: Context, + private val user: UserHandle?, + private val callback: ScreenshotActionsProvider.ScreenshotActionsCallback +) : ScreenshotActionsProvider { + init { + callback.setPreviewAction(true) { ActionIntentCreator.createEdit(it, context) } val editAction = ScreenshotActionsProvider.ScreenshotAction( AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit), context.resources.getString(R.string.screenshot_edit_label), + context.resources.getString(R.string.screenshot_edit_description), true ) { uri -> ActionIntentCreator.createEdit(uri, context) @@ -62,10 +73,21 @@ class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActions ScreenshotActionsProvider.ScreenshotAction( AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share), context.resources.getString(R.string.screenshot_share_label), + context.resources.getString(R.string.screenshot_share_description), false ) { uri -> ActionIntentCreator.createShare(uri) } - return listOf(editAction, shareAction) + callback.addActions(listOf(editAction, shareAction)) + } + + class Factory @Inject constructor() : ScreenshotActionsProvider.Factory { + override fun create( + context: Context, + user: UserHandle?, + callback: ScreenshotActionsProvider.ScreenshotActionsCallback + ): ScreenshotActionsProvider { + return DefaultScreenshotActionsProvider(context, user, callback) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index c8e13bb8c2fc..b796a206b5b4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -19,6 +19,7 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; +import static com.android.systemui.Flags.screenshotShelfUi; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; @@ -237,6 +238,7 @@ public class ScreenshotController { private final WindowContext mContext; private final FeatureFlags mFlags; private final ScreenshotViewProxy mViewProxy; + private final ScreenshotActionsProvider.Factory mActionsProviderFactory; private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotSmartActions mScreenshotSmartActions; private final UiEventLogger mUiEventLogger; @@ -271,6 +273,8 @@ public class ScreenshotController { private boolean mScreenshotTakenInPortrait; private boolean mBlockAttach; + private ScreenshotActionsProvider mActionsProvider; + private Animator mScreenshotAnimation; private RequestCallback mCurrentRequestCallback; private String mPackageName = ""; @@ -298,6 +302,7 @@ public class ScreenshotController { Context context, FeatureFlags flags, ScreenshotViewProxy.Factory viewProxyFactory, + ScreenshotActionsProvider.Factory actionsProviderFactory, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, ScrollCaptureClient scrollCaptureClient, @@ -349,6 +354,7 @@ public class ScreenshotController { mAssistContentRequester = assistContentRequester; mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId); + mActionsProviderFactory = actionsProviderFactory; mScreenshotHandler.setOnTimeoutRunnable(() -> { if (DEBUG_UI) { @@ -393,6 +399,7 @@ public class ScreenshotController { void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, RequestCallback requestCallback) { Assert.isMainThread(); + mCurrentRequestCallback = requestCallback; if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) { Rect bounds = getFullScreenRect(); @@ -496,7 +503,7 @@ public class ScreenshotController { return mDisplayId == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay; } - void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) { + void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) { withWindowAttached(() -> { if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) { mViewProxy.announceForAccessibility(mContext.getResources().getString( @@ -509,6 +516,11 @@ public class ScreenshotController { mViewProxy.reset(); + if (screenshotShelfUi()) { + mActionsProvider = mActionsProviderFactory.create(mContext, screenshot.getUserHandle(), + ((ScreenshotActionsProvider.ScreenshotActionsCallback) mViewProxy)); + } + if (mViewProxy.isAttachedToWindow()) { // if we didn't already dismiss for another reason if (!mViewProxy.isDismissing()) { @@ -983,20 +995,16 @@ public class ScreenshotController { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - doPostAnimation(imageData); + mViewProxy.setChipIntents(imageData); } }); } else { - doPostAnimation(imageData); + mViewProxy.setChipIntents(imageData); } }); } } - private void doPostAnimation(ScreenshotController.SavedImageData imageData) { - mViewProxy.setChipIntents(imageData); - } - /** * Sets up the action shade and its entrance animation, once we get the Quick Share action data. */ diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt index 9354fd27ce5a..88bca951beb6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -20,8 +20,10 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.Notification import android.content.Context +import android.content.Intent import android.graphics.Bitmap import android.graphics.Rect +import android.net.Uri import android.view.KeyEvent import android.view.LayoutInflater import android.view.ScrollCaptureResponse @@ -37,6 +39,7 @@ import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW +import com.android.systemui.screenshot.ScreenshotController.SavedImageData import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER import com.android.systemui.screenshot.scroll.ScrollCaptureController import com.android.systemui.screenshot.ui.ScreenshotAnimationController @@ -54,10 +57,9 @@ class ScreenshotShelfViewProxy constructor( private val logger: UiEventLogger, private val viewModel: ScreenshotViewModel, - private val staticActionsProvider: ScreenshotActionsProvider, @Assisted private val context: Context, @Assisted private val displayId: Int -) : ScreenshotViewProxy { +) : ScreenshotViewProxy, ScreenshotActionsProvider.ScreenshotActionsCallback { override val view: ScreenshotShelfView = LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView override val screenshotPreview: View @@ -75,6 +77,8 @@ constructor( override var isPendingSharedTransition = false private val animationController = ScreenshotAnimationController(view) + private var imageData: SavedImageData? = null + private var runOnImageDataAcquired: ((SavedImageData) -> Unit)? = null init { ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) @@ -87,8 +91,9 @@ constructor( override fun reset() { animationController.cancel() isPendingSharedTransition = false - viewModel.setScreenshotBitmap(null) - viewModel.setActions(listOf()) + imageData = null + viewModel.reset() + runOnImageDataAcquired = null } override fun updateInsets(insets: WindowInsets) {} override fun updateOrientation(insets: WindowInsets) {} @@ -99,18 +104,9 @@ constructor( override fun addQuickShareChip(quickShareAction: Notification.Action) {} - override fun setChipIntents(imageData: ScreenshotController.SavedImageData) { - val staticActions = - staticActionsProvider.getActions(context, imageData.owner).map { - ActionButtonViewModel(it.icon, it.text) { - val intent = it.retrieveIntent(imageData.uri) - debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" } - isPendingSharedTransition = true - callbacks?.onAction(intent, imageData.owner, it.overrideTransition) - } - } - - viewModel.setActions(staticActions) + override fun setChipIntents(data: SavedImageData) { + imageData = data + runOnImageDataAcquired?.invoke(data) } override fun requestDismissal(event: ScreenshotEvent) { @@ -223,4 +219,41 @@ constructor( interface Factory : ScreenshotViewProxy.Factory { override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy } + + override fun setPreviewAction(overrideTransition: Boolean, retrieveIntent: (Uri) -> Intent) { + viewModel.setPreviewAction { + imageData?.let { + val intent = retrieveIntent(it.uri) + debugLog(DEBUG_ACTIONS) { "Preview tapped: $intent" } + isPendingSharedTransition = true + callbacks?.onAction(intent, it.owner, overrideTransition) + } + } + } + + override fun addActions(actions: List<ScreenshotActionsProvider.ScreenshotAction>) { + viewModel.addActions( + actions.map { action -> + ActionButtonViewModel(action.icon, action.text, action.description) { + val actionRunnable = + getActionRunnable(action.retrieveIntent, action.overrideTransition) + imageData?.let { actionRunnable(it) } + ?: run { runOnImageDataAcquired = actionRunnable } + } + } + ) + } + + private fun getActionRunnable( + retrieveIntent: (Uri) -> Intent, + overrideTransition: Boolean + ): (SavedImageData) -> Unit { + val onClick: (SavedImageData) -> Unit = { + val intent = retrieveIntent(it.uri) + debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" } + isPendingSharedTransition = true + callbacks!!.onAction(intent, it.owner, overrideTransition) + } + return onClick + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index 9118ee1dfc73..2ce6d8380e36 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -94,8 +94,8 @@ public abstract class ScreenshotModule { ScreenshotSoundControllerImpl screenshotSoundProviderImpl); @Binds - abstract ScreenshotActionsProvider bindScreenshotActionsProvider( - DefaultScreenshotActionsProvider defaultScreenshotActionsProvider); + abstract ScreenshotActionsProvider.Factory bindScreenshotActionsProviderFactory( + DefaultScreenshotActionsProvider.Factory defaultScreenshotActionsProviderFactory); @Provides @SysUISingleton 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 a5825b5f7797..c7fe3f608a2f 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.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 3bcd52cbc99e..d8782009e24b 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 @@ -59,6 +59,11 @@ object ScreenshotShelfViewBinder { } } launch { + viewModel.previewAction.collect { onClick -> + previewView.setOnClickListener { onClick?.run() } + } + } + launch { viewModel.actions.collect { actions -> if (actions.isNotEmpty()) { view 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 6ee970534352..05bfed159527 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 @@ -20,6 +20,7 @@ import android.graphics.drawable.Drawable data class ActionButtonViewModel( val icon: Drawable?, - val name: String?, + val name: CharSequence?, + val description: CharSequence, val onClicked: (() -> Unit)? ) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt index 3a652d90bb78..dc61d1e9c37b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt @@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.StateFlow class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) { private val _preview = MutableStateFlow<Bitmap?>(null) val preview: StateFlow<Bitmap?> = _preview + private val _previewAction = MutableStateFlow<Runnable?>(null) + val previewAction: StateFlow<Runnable?> = _previewAction private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>()) val actions: StateFlow<List<ActionButtonViewModel>> = _actions val showDismissButton: Boolean @@ -33,7 +35,19 @@ class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager _preview.value = bitmap } - fun setActions(actions: List<ActionButtonViewModel>) { - _actions.value = actions + fun setPreviewAction(runnable: Runnable) { + _previewAction.value = runnable + } + + fun addActions(actions: List<ActionButtonViewModel>) { + val actionList = _actions.value.toMutableList() + actionList.addAll(actions) + _actions.value = actionList + } + + fun reset() { + _preview.value = null + _previewAction.value = null + _actions.value = listOf() } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt index 25e27ae586c8..7425807b716d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt @@ -27,6 +27,7 @@ import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorEmpt import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorEmptyImpl +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import dagger.Binds import dagger.Module diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 3e9a32b9cde4..2d3833c55199 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -36,6 +36,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorSceneContainerImpl +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractorImpl import dagger.Binds import dagger.Module diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt index 5c276b189ba7..d02c2154279b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade import android.view.ViewPropertyAnimator import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.GestureRecorder import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.policy.HeadsUpManager diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index 9902a32a536d..b2837208a516 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -71,14 +71,6 @@ interface ShadeViewController { fun updateTouchableRegion() /** - * Reconfigures the shade to show the AOD UI (clock, smartspace, etc). This is called by the - * screen off animation controller in order to animate in AOD without "actually" fully switching - * to the KEYGUARD state, which is a heavy transition that causes jank as 10+ files react to the - * change. - */ - fun showAodUi() - - /** * Sends an external (e.g. Status Bar) touch event to the Shade touch handler. * * This is different from [startInputFocusTransfer] as it doesn't rely on setting the launcher diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt index 93c3772c6e36..bfb5ad3782b1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt @@ -20,6 +20,7 @@ import android.view.MotionEvent import android.view.ViewGroup import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.phone.HeadsUpAppearanceController import java.util.function.Consumer diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index bc60c838b703..cde45f2060e5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -66,7 +66,7 @@ interface BaseShadeInteractor { val isAnyExpanded: StateFlow<Boolean> /** The amount [0-1] that the shade has been opened. */ - val shadeExpansion: Flow<Float> + val shadeExpansion: StateFlow<Float> /** * The amount [0-1] QS has been opened. Normal shade with notifications (QQS) visible will diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index e9bb4c623013..5fbd2cfaec79 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -29,7 +29,7 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { private val inactiveFlowBoolean = MutableStateFlow(false) private val inactiveFlowFloat = MutableStateFlow(0f) override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean - override val shadeExpansion: Flow<Float> = inactiveFlowFloat + override val shadeExpansion: StateFlow<Float> = inactiveFlowFloat override val qsExpansion: StateFlow<Float> = inactiveFlowFloat override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt index 421a76163346..ac881b5bfa97 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt @@ -50,7 +50,7 @@ constructor( * The amount [0-1] that the shade has been opened. Uses stateIn to avoid redundant calculations * in downstream flows. */ - override val shadeExpansion: Flow<Float> = + override val shadeExpansion: StateFlow<Float> = combine( repository.lockscreenShadeExpansion, keyguardRepository.statusBarState, diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt index 7785eda4bd6a..7f35f17954c4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt @@ -49,7 +49,9 @@ constructor( sharedNotificationContainerInteractor: SharedNotificationContainerInteractor, shadeRepository: ShadeRepository, ) : BaseShadeInteractor { - override val shadeExpansion: Flow<Float> = sceneBasedExpansion(sceneInteractor, Scenes.Shade) + override val shadeExpansion: StateFlow<Float> = + sceneBasedExpansion(sceneInteractor, Scenes.Shade) + .stateIn(scope, SharingStarted.Eagerly, 0f) private val sceneBasedQsExpansion = sceneBasedExpansion(sceneInteractor, Scenes.QuickSettings) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractor.kt index 859fce53a371..2611092553ed 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractor.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.shade +package com.android.systemui.shade.domain.interactor /** Allows the lockscreen to control the shade. */ interface ShadeLockscreenInteractor { @@ -73,4 +73,12 @@ interface ShadeLockscreenInteractor { * @param alpha value between 0 and 1. -1 if the value is to be reset. */ @Deprecated("TODO(b/325072511) delete this") fun setKeyguardStatusBarAlpha(alpha: Float) + + /** + * Reconfigures the shade to show the AOD UI (clock, smartspace, etc). This is called by the + * screen off animation controller in order to animate in AOD without "actually" fully switching + * to the KEYGUARD state, which is a heavy transition that causes jank as 10+ files react to the + * change. + */ + fun showAodUi() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt index d9c441fa0517..318da557ed2a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt @@ -20,7 +20,6 @@ import com.android.keyguard.LockIconViewController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.ShadeLockscreenInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -84,6 +83,11 @@ constructor( // TODO(b/325072511) delete this } + override fun showAodUi() { + sceneInteractor.changeScene(Scenes.Lockscreen, "showAodUi") + // TODO(b/330311871) implement transition to AOD + } + private fun changeToShadeScene() { sceneInteractor.changeScene( Scenes.Shade, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt index 9f098e79f759..72f2aa5aa10b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt @@ -6,7 +6,7 @@ import android.util.MathUtils import com.android.systemui.dump.DumpManager import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.res.R -import com.android.systemui.shade.ShadeLockscreenInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.SplitShadeStateController import dagger.assisted.Assisted diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index d974bc44bf03..fc1dc62ed094 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -33,7 +33,7 @@ import com.android.systemui.plugins.qs.QS import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.res.R -import com.android.systemui.shade.ShadeLockscreenInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.collection.NotificationEntry 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 ab6c14892eea..d112edb9772c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -35,7 +35,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED -import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel @@ -366,21 +366,34 @@ constructor( } } } - .onStart { emit(0f) } + .onStart { emit(1f) } .dumpWhileCollecting("alphaForShadeAndQsExpansion") - private val alphaWhenGoneAndShadeState: Flow<Float> = - combineTransform( - keyguardTransitionInteractor.transitions - .map { step -> step.to == GONE && step.transitionState == FINISHED } - .distinctUntilChanged(), - keyguardInteractor.statusBarState, - ) { isGoneTransitionFinished, statusBarState -> - if (isGoneTransitionFinished && statusBarState == SHADE) { - emit(1f) + private val isGoneTransitionRunning: Flow<Boolean> = + flow { + while (currentCoroutineContext().isActive) { + emit(false) + // Ensure start where GONE is inactive + keyguardTransitionInteractor.transitionValue(GONE).first { it == 0f } + // Wait for a GONE transition to begin + keyguardTransitionInteractor.transitionStepsToState(GONE).first { + it.value > 0f && it.transitionState == RUNNING + } + emit(true) + // Now await the signal that SHADE state has been reached or the GONE transition + // was reversed. Until SHADE state has been replaced and merged with GONE, it is + // the only source of when it is considered safe to reset alpha to 1f for HUNs. + combine( + keyguardInteractor.statusBarState, + // Emit -1f on start to make sure the flow runs + keyguardTransitionInteractor.transitionValue(GONE).onStart { emit(-1f) } + ) { statusBarState, goneValue -> + statusBarState == SHADE || goneValue == 0f + } + .first { it } } } - .dumpWhileCollecting("alphaWhenGoneAndShadeState") + .dumpWhileCollecting("goneTransitionInProgress") fun keyguardAlpha(viewState: ViewStateAccessor): Flow<Float> { // All transition view models are mututally exclusive, and safe to merge @@ -407,12 +420,11 @@ constructor( return merge( alphaTransitions, - // Sends a final alpha value of 1f when truly gone, to make sure HUNs appear - alphaWhenGoneAndShadeState, // These remaining cases handle alpha changes within an existing state, such as // shade expansion or swipe to dismiss combineTransform( isOnLockscreenWithoutShade, + isGoneTransitionRunning, shadeCollapseFadeIn, alphaForShadeAndQsExpansion, keyguardInteractor.dismissAlpha.dumpWhileCollecting( @@ -420,6 +432,7 @@ constructor( ), ) { isOnLockscreenWithoutShade, + isGoneTransitionRunning, shadeCollapseFadeIn, alphaForShadeAndQsExpansion, dismissAlpha -> @@ -427,7 +440,7 @@ constructor( if (!shadeCollapseFadeIn && dismissAlpha != null) { emit(dismissAlpha) } - } else { + } else if (!isGoneTransitionRunning) { emit(alphaForShadeAndQsExpansion) } }, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index 442e43a9dae2..7abcf1337602 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -41,7 +41,7 @@ import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.DozeInteractor; import com.android.systemui.shade.NotificationShadeWindowViewController; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.StatusBarState; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index f99817aa4aad..a99834ad3456 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -89,7 +89,7 @@ import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shade.ShadeExpansionListener; import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -106,6 +106,8 @@ import com.android.systemui.util.kotlin.JavaAdapter; import dagger.Lazy; +import kotlin.Unit; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashSet; @@ -115,7 +117,6 @@ import java.util.Set; import javax.inject.Inject; -import kotlin.Unit; import kotlinx.coroutines.CoroutineDispatcher; import kotlinx.coroutines.ExperimentalCoroutinesApi; import kotlinx.coroutines.Job; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index f3c70907b182..479aef167b5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -23,6 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.statusbar.CircleReveal @@ -69,11 +70,11 @@ constructor( private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>, private val interactionJankMonitor: InteractionJankMonitor, private val powerManager: PowerManager, + private val shadeLockscreenInteractorLazy: Lazy<ShadeLockscreenInteractor>, private val panelExpansionInteractorLazy: Lazy<PanelExpansionInteractor>, private val handler: Handler = Handler(), ) : WakefulnessLifecycle.Observer, ScreenOffAnimation { private lateinit var centralSurfaces: CentralSurfaces - private lateinit var shadeViewController: ShadeViewController /** * Whether or not [initialize] has been called to provide us with the StatusBar, * NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen @@ -159,7 +160,6 @@ constructor( this.initialized = true this.lightRevealScrim = lightRevealScrim this.centralSurfaces = centralSurfaces - this.shadeViewController = shadeViewController updateAnimatorDurationScale() globalSettings.registerContentObserver( @@ -322,7 +322,7 @@ constructor( // Show AOD. That'll cause the KeyguardVisibilityHelper to call // #animateInKeyguard. - shadeViewController.showAodUi() + shadeLockscreenInteractorLazy.get().showAodUi() } }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong() diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt index 30715d167c25..f260d6130c4b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt @@ -56,7 +56,7 @@ constructor( val isAvailable: StateFlow<Boolean> = availabilityCriteria.isAvailable().stateIn(scope, SharingStarted.Eagerly, true) - val spatialAudioButtonByEnabled: StateFlow<List<SpatialAudioButtonViewModel>> = + val spatialAudioButtons: StateFlow<List<SpatialAudioButtonViewModel>> = combine(interactor.isEnabled, interactor.isAvailable) { currentIsEnabled, isAvailable -> SpatialAudioEnabledModel.values .filter { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 072569d0e69b..33a6010d816c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -20,6 +20,8 @@ import android.content.pm.PackageManager import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView import android.hardware.face.FaceSensorPropertiesInternal @@ -35,6 +37,7 @@ import android.view.View import android.view.WindowInsets import android.view.WindowManager import android.widget.ScrollView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.InteractionJankMonitor @@ -82,6 +85,7 @@ import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever + private const val OP_PACKAGE_NAME = "biometric.testapp" @RunWith(AndroidJUnit4::class) @@ -386,10 +390,38 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test + fun testShowBiometricUI_ContentViewWithMoreOptionsButton() { + mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + var isButtonClicked = false + val contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setMoreOptionsButtonListener( + fakeExecutor) { _, _ -> isButtonClicked = true } + .build() + + val container = + initializeFingerprintContainer(contentViewWithMoreOptionsButton = contentView) + + waitForIdleSync() + + assertThat(container.hasCredentialView()).isFalse() + assertThat(container.hasConstraintBiometricPrompt()).isTrue() + + // TODO(b/328843028): Use button.performClick() instead of calling + // onContentViewMoreOptionsButtonPressed() directly, and check |isButtonClicked| is true. + container.mBiometricCallback.onContentViewMoreOptionsButtonPressed() + waitForIdleSync() + // container is gone + assertThat(container.mContainerState).isEqualTo(5) + } + + @Test fun testShowCredentialUI_withDescription() { - val container = initializeFingerprintContainer( - authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL - ) + val container = + initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) waitForIdleSync() assertThat(container.hasCredentialView()).isTrue() @@ -397,14 +429,44 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test - @Ignore("b/302735104") - fun testShowCredentialUI_withCustomBp() { - mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) - val container = initializeFingerprintContainer( - authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, - isUsingContentView = true - ) - checkBpShowsForCredentialAndGoToCredential(container) + fun testShowCredentialUI_withVerticalListContentView() { + mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val container = + initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, + verticalListContentView = PromptVerticalListContentView.Builder().build() + ) + // Two-step credential view should show - + // 1. biometric prompt without sensor 2. credential view ui + waitForIdleSync() + assertThat(container.hasConstraintBiometricPrompt()).isTrue() + assertThat(container.hasCredentialView()).isFalse() + + container.animateToCredentialUI(false) + waitForIdleSync() + // TODO(b/302735104): Check the reason why hasConstraintBiometricPrompt() is still true + // assertThat(container.hasConstraintBiometricPrompt()).isFalse() + assertThat(container.hasCredentialView()).isTrue() + } + + @Test + fun testShowCredentialUI_withContentViewWithMoreOptionsButton() { + mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> } + .build() + val container = + initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, + contentViewWithMoreOptionsButton = contentView + ) + waitForIdleSync() + + assertThat(container.hasCredentialView()).isTrue() + assertThat(container.hasBiometricPrompt()).isFalse() } @Test @@ -509,12 +571,13 @@ open class AuthContainerViewTest : SysuiTestCase() { private fun initializeFingerprintContainer( authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, addToView: Boolean = true, - isUsingContentView: Boolean = false, + verticalListContentView: PromptVerticalListContentView? = null, + contentViewWithMoreOptionsButton: PromptContentViewWithMoreOptionsButton? = null, ) = initializeContainer( TestAuthContainerView( authenticators = authenticators, fingerprintProps = fingerprintSensorPropertiesInternal(), - isUsingContentView = isUsingContentView, + verticalListContentView = verticalListContentView, ), addToView ) @@ -548,7 +611,8 @@ open class AuthContainerViewTest : SysuiTestCase() { authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, fingerprintProps: List<FingerprintSensorPropertiesInternal> = listOf(), faceProps: List<FaceSensorPropertiesInternal> = listOf(), - isUsingContentView: Boolean = false, + verticalListContentView: PromptVerticalListContentView? = null, + contentViewWithMoreOptionsButton: PromptContentViewWithMoreOptionsButton? = null, ) : AuthContainerView( Config().apply { mContext = this@AuthContainerViewTest.context @@ -558,8 +622,10 @@ open class AuthContainerViewTest : SysuiTestCase() { mSkipAnimation = true mPromptInfo = PromptInfo().apply { this.authenticators = authenticators - if (isUsingContentView) { - this.contentView = PromptVerticalListContentView.Builder().build() + if (verticalListContentView != null) { + this.contentView = verticalListContentView + } else if (contentViewWithMoreOptionsButton != null) { + this.contentView = contentViewWithMoreOptionsButton } } mOpPackageName = OP_PACKAGE_NAME @@ -616,19 +682,11 @@ open class AuthContainerViewTest : SysuiTestCase() { val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.systemBars()) == 0).isTrue() } - - private fun checkBpShowsForCredentialAndGoToCredential(container: TestAuthContainerView) { - waitForIdleSync() - assertThat(container.hasBiometricPrompt()).isTrue() - assertThat(container.hasCredentialView()).isFalse() - - container.animateToCredentialUI(false) - waitForIdleSync() - assertThat(container.hasBiometricPrompt()).isFalse() - assertThat(container.hasCredentialView()).isTrue() - } } +private fun AuthContainerView.hasConstraintBiometricPrompt() = + (findViewById<ConstraintLayout>(R.id.biometric_prompt_constraint_layout)?.childCount ?: 0) > 0 + private fun AuthContainerView.hasBiometricPrompt() = (findViewById<ScrollView>(R.id.biometric_scrollview)?.childCount ?: 0) > 0 diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt index 81d4e8302c3f..df0e5a718ed9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.biometrics.data.repository import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest @@ -26,8 +27,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList @@ -58,6 +61,7 @@ class PromptRepositoryImplTest : SysuiTestCase() { private val testScope = TestScope() private val faceSettings = FakeFaceSettingsRepository() + private val fakeExecutor = FakeExecutor(FakeSystemClock()) @Mock private lateinit var authController: AuthController @@ -135,7 +139,7 @@ class PromptRepositoryImplTest : SysuiTestCase() { } @Test - fun showBpWithoutIconForCredential_withCustomBp() = + fun showBpWithoutIconForCredential_withVerticalListContentView() = testScope.runTest { mSetFlagsRule.enableFlags(Flags.FLAG_CONSTRAINT_BP) mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) @@ -161,8 +165,37 @@ class PromptRepositoryImplTest : SysuiTestCase() { } @Test + fun showBpWithoutIconForCredential_withContentViewWithMoreOptionsButton() = + testScope.runTest { + mSetFlagsRule.enableFlags(Flags.FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val promptInfo = + PromptInfo().apply { + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL + contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> } + .build() + } + for (case in + listOf( + PromptKind.Biometric(), + PromptKind.Pin, + PromptKind.Password, + PromptKind.Pattern + )) { + repository.setPrompt(promptInfo, USER_ID, CHALLENGE, case, OP_PACKAGE_NAME) + repository.setShouldShowBpWithoutIconForCredential(promptInfo) + + assertThat(repository.showBpWithoutIconForCredential.value).isFalse() + } + } + + @Test fun showBpWithoutIconForCredential_withDescription() = testScope.runTest { + mSetFlagsRule.enableFlags(Flags.FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) for (case in listOf( PromptKind.Biometric(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt index 8a46c0c6da9f..2172bc5ee8e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt @@ -1,6 +1,8 @@ package com.android.systemui.biometrics.domain.interactor +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo +import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.Utils @@ -10,6 +12,8 @@ import com.android.systemui.biometrics.domain.model.BiometricPromptRequest import com.android.systemui.biometrics.promptInfo import com.android.systemui.biometrics.shared.model.BiometricUserInfo import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -43,6 +47,7 @@ class PromptCredentialInteractorTest : SysuiTestCase() { private val testScope = TestScope(testDispatcher) private val biometricPromptRepository = FakePromptRepository() private val credentialInteractor = FakeCredentialInteractor() + private val fakeExecutor = FakeExecutor(FakeSystemClock()) private lateinit var interactor: PromptCredentialInteractor @@ -90,6 +95,82 @@ class PromptCredentialInteractorTest : SysuiTestCase() { assertThat(prompt).isNull() } + @Test + fun testShowTitleOnlyValue_description() = + testScope.runTest { + val title = "what a prompt" + val subtitle = "s" + val description = "something to see" + + val showTitleOnly by collectLastValue(interactor.showTitleOnly) + + interactor.useCredentialsForAuthentication( + PromptInfo().also { + it.title = title + it.description = description + it.subtitle = subtitle + }, + kind = Utils.CREDENTIAL_PIN, + userId = USER_ID, + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME + ) + assertThat(showTitleOnly).isFalse() + } + + @Test + fun testShowTitleOnlyValue_verticalListContentView() = + testScope.runTest { + val title = "what a prompt" + val subtitle = "s" + val description = "something to see" + val contentView = PromptVerticalListContentView.Builder().build() + + val showTitleOnly by collectLastValue(interactor.showTitleOnly) + + interactor.useCredentialsForAuthentication( + PromptInfo().also { + it.title = title + it.description = description + it.subtitle = subtitle + it.contentView = contentView + }, + kind = Utils.CREDENTIAL_PIN, + userId = USER_ID, + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME + ) + assertThat(showTitleOnly).isTrue() + } + + @Test + fun testShowTitleOnlyValue_ContentViewWithButton() = + testScope.runTest { + val title = "what a prompt" + val subtitle = "s" + val description = "something to see" + val contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> } + .build() + + val showTitleOnly by collectLastValue(interactor.showTitleOnly) + + interactor.useCredentialsForAuthentication( + PromptInfo().also { + it.title = title + it.description = description + it.subtitle = subtitle + it.contentView = contentView + }, + kind = Utils.CREDENTIAL_PIN, + userId = USER_ID, + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME + ) + assertThat(showTitleOnly).isFalse() + } + @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN) @Test fun usePasswordCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PASSWORD) @@ -106,12 +187,14 @@ class PromptCredentialInteractorTest : SysuiTestCase() { val title = "what a prompt" val subtitle = "s" val description = "something to see" + val contentView = PromptVerticalListContentView.Builder().build() interactor.useCredentialsForAuthentication( PromptInfo().also { it.title = title it.description = description it.subtitle = subtitle + it.contentView = contentView }, kind = kind, userId = USER_ID, @@ -122,6 +205,7 @@ class PromptCredentialInteractorTest : SysuiTestCase() { assertThat(prompt?.title).isEqualTo(title) assertThat(prompt?.subtitle).isEqualTo(subtitle) assertThat(prompt?.description).isEqualTo(description) + assertThat(prompt?.contentView).isEqualTo(contentView) assertThat(prompt?.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) assertThat(prompt?.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) assertThat(prompt) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt index 8fab2332c00e..d10b93534f3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.biometrics.domain.model import android.graphics.Bitmap import android.hardware.biometrics.PromptContentItemBulletedText +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -11,6 +12,7 @@ import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricUserInfo import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -59,7 +61,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) - assertThat(request.contentView).isEqualTo(contentView) + assertThat(request.contentView).isSameInstanceAs(contentView) assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) assertThat(request.modalities) @@ -67,6 +69,29 @@ class BiometricPromptRequestTest : SysuiTestCase() { } @Test + fun biometricRequestContentViewWithMoreOptionsButtonFromPromptInfo() { + val title = "what" + val description = "request" + val executor = MoreExecutors.directExecutor() + val contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setDescription("test") + .setMoreOptionsButtonListener(executor) { _, _ -> } + .build() + + val fpPros = fingerprintSensorPropertiesInternal().first() + val request = + BiometricPromptRequest.Biometric( + promptInfo(title = title, description = description, contentView = contentView), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID), + BiometricModalities(fingerprintProperties = fpPros), + OP_PACKAGE_NAME, + ) + assertThat(request.contentView).isSameInstanceAs(contentView) + } + + @Test fun biometricRequestLogoBitmapFromPromptInfo() { val logoBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888) val fpPros = fingerprintSensorPropertiesInternal().first() @@ -89,6 +114,12 @@ class BiometricPromptRequestTest : SysuiTestCase() { val subtitle = "a" val description = "request" val stealth = true + val contentView = + PromptVerticalListContentView.Builder() + .setDescription("content description") + .addListItem(PromptContentItemBulletedText("content item 1")) + .addListItem(PromptContentItemBulletedText("content item 2"), 1) + .build() val toCheck = listOf( @@ -97,6 +128,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { title = title, subtitle = subtitle, description = description, + contentView = contentView, credentialTitle = null, credentialSubtitle = null, credentialDescription = null, @@ -106,6 +138,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { ), BiometricPromptRequest.Credential.Password( promptInfo( + contentView = contentView, credentialTitle = title, credentialSubtitle = subtitle, credentialDescription = description, @@ -117,6 +150,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { promptInfo( subtitle = subtitle, description = description, + contentView = contentView, credentialTitle = title, credentialSubtitle = null, credentialDescription = null, @@ -131,6 +165,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) + assertThat(request.contentView).isEqualTo(contentView) assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) if (request is BiometricPromptRequest.Credential.Pattern) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 7db4ca966890..5b0df5d05703 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -25,6 +25,7 @@ import android.graphics.drawable.BitmapDrawable import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentView +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView import android.hardware.face.FaceSensorPropertiesInternal @@ -122,6 +123,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private lateinit var viewModel: PromptViewModel private lateinit var iconViewModel: PromptIconViewModel private lateinit var promptContentView: PromptContentView + private lateinit var promptContentViewWithMoreOptionsButton: + PromptContentViewWithMoreOptionsButton @Before fun setup() { @@ -163,6 +166,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa .addListItem(PromptContentItemBulletedText("content item 2"), 1) .build() + promptContentViewWithMoreOptionsButton = + PromptContentViewWithMoreOptionsButton.Builder() + .setDescription("test") + .setMoreOptionsButtonListener(fakeExecutor, { _, _ -> }) + .build() + viewModel = PromptViewModel( displayStateInteractor, @@ -1254,7 +1263,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - fun descriptionOverriddenByContentView() = + fun descriptionOverriddenByVerticalListContentView() = runGenericTest(contentView = promptContentView, description = "test description") { mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) @@ -1266,6 +1275,21 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test + fun descriptionOverriddenByContentViewWithMoreOptionsButton() = + runGenericTest( + contentView = promptContentViewWithMoreOptionsButton, + description = "test description" + ) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) + val contentView by collectLastValue(viewModel.contentView) + val description by collectLastValue(viewModel.description) + + assertThat(description).isEqualTo("") + assertThat(contentView).isEqualTo(promptContentViewWithMoreOptionsButton) + } + + @Test fun descriptionWithoutContentView() = runGenericTest(description = "test description") { mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt index 890b9aec69bf..ae77d1f590e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt @@ -24,6 +24,9 @@ import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable import android.os.UserHandle import android.service.controls.Control import android.service.controls.ControlsProviderService @@ -176,6 +179,64 @@ class ControlsRequestReceiverTest : SysuiTestCase() { assertNull(wrapper.intent) } + @Test + fun testClassNotFoundExceptionComponent_noCrash() { + val bundle = Bundle().apply { + putParcelable(Intent.EXTRA_COMPONENT_NAME, PrivateParcelable()) + putParcelable(ControlsProviderService.EXTRA_CONTROL, control) + } + val parcel = Parcel.obtain() + bundle.writeToParcel(parcel, 0) + + parcel.setDataPosition(0) + + val badIntent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply { + parcel.readBundle()?.let { putExtras(it) } + } + receiver.onReceive(wrapper, badIntent) + + assertNull(wrapper.intent) + } + + @Test + fun testClassNotFoundExceptionControl_noCrash() { + val bundle = Bundle().apply { + putParcelable(Intent.EXTRA_COMPONENT_NAME, componentName) + putParcelable(ControlsProviderService.EXTRA_CONTROL, PrivateParcelable()) + } + val parcel = Parcel.obtain() + bundle.writeToParcel(parcel, 0) + + parcel.setDataPosition(0) + + val badIntent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply { + parcel.readBundle()?.let { putExtras(it) } + } + receiver.onReceive(wrapper, badIntent) + + assertNull(wrapper.intent) + } + + @Test + fun testMissingComponentName_noCrash() { + val badIntent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply { + putExtra(ControlsProviderService.EXTRA_CONTROL, control) + } + receiver.onReceive(wrapper, badIntent) + + assertNull(wrapper.intent) + } + + @Test + fun testMissingControl_noCrash() { + val badIntent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply { + putExtra(Intent.EXTRA_COMPONENT_NAME, componentName) + } + receiver.onReceive(wrapper, badIntent) + + assertNull(wrapper.intent) + } + class MyWrapper(context: Context) : ContextWrapper(context) { var intent: Intent? = null @@ -189,4 +250,20 @@ class ControlsRequestReceiverTest : SysuiTestCase() { this.intent = intent } } + + class PrivateParcelable : Parcelable { + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) {} + + companion object CREATOR : Parcelable.Creator<PrivateParcelable?> { + override fun createFromParcel(source: Parcel?): PrivateParcelable { + return PrivateParcelable() + } + + override fun newArray(size: Int): Array<PrivateParcelable?> { + return arrayOfNulls(size) + } + } + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt index 9f5260c252e4..37dea11ccaaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.media.controls.ui.controller -import android.provider.Settings import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -37,8 +36,6 @@ import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.FakeSettings -import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertTrue import org.junit.Before @@ -65,10 +62,7 @@ class KeyguardMediaControllerTest : SysuiTestCase() { private val mediaContainerView: MediaContainerView = MediaContainerView(context, null) private val hostView = UniqueObjectHostView(context) - private val settings = FakeSettings() private lateinit var keyguardMediaController: KeyguardMediaController - private lateinit var testableLooper: TestableLooper - private lateinit var fakeHandler: FakeHandler private lateinit var statusBarStateListener: StatusBarStateController.StateListener @Before @@ -84,16 +78,12 @@ class KeyguardMediaControllerTest : SysuiTestCase() { whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) whenever(mediaHost.hostView).thenReturn(hostView) hostView.layoutParams = FrameLayout.LayoutParams(100, 100) - testableLooper = TestableLooper.get(this) - fakeHandler = FakeHandler(testableLooper.looper) keyguardMediaController = KeyguardMediaController( mediaHost, bypassController, statusBarStateController, context, - settings, - fakeHandler, configurationController, ResourcesSplitShadeStateController(), mock<KeyguardMediaControllerLogger>(), @@ -126,24 +116,6 @@ class KeyguardMediaControllerTest : SysuiTestCase() { } @Test - fun testHiddenOnKeyguard_whenMediaOnLockScreenDisabled() { - settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 0) - - keyguardMediaController.refreshMediaPosition(TEST_REASON) - - assertThat(mediaContainerView.visibility).isEqualTo(GONE) - } - - @Test - fun testAvailableOnKeyguard_whenMediaOnLockScreenEnabled() { - settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1) - - keyguardMediaController.refreshMediaPosition(TEST_REASON) - - assertThat(mediaContainerView.visibility).isEqualTo(VISIBLE) - } - - @Test fun testActivatesSplitShadeContainerInSplitShadeMode() { val splitShadeContainer = FrameLayout(context) keyguardMediaController.attachSplitShadeContainer(splitShadeContainer) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index 59e2696c6123..c3daf8485634 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -34,9 +34,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.domain.pipeline.MediaDataManager @@ -59,7 +59,9 @@ import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.time.FakeSystemClock import java.util.Locale import javax.inject.Provider @@ -67,6 +69,7 @@ import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before @@ -114,8 +117,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var pageIndicator: PageIndicator @Mock lateinit var mediaFlags: MediaFlags @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock lateinit var globalSettings: GlobalSettings + private lateinit var secureSettings: SecureSettings private val transitionRepository = kosmos.fakeKeyguardTransitionRepository @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> @Captor @@ -127,13 +130,16 @@ class MediaCarouselControllerTest : SysuiTestCase() { private val clock = FakeSystemClock() private lateinit var bgExecutor: FakeExecutor + private lateinit var testDispatcher: TestDispatcher private lateinit var mediaCarouselController: MediaCarouselController @Before fun setup() { MockitoAnnotations.initMocks(this) + secureSettings = FakeSettings() context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK)) bgExecutor = FakeExecutor(clock) + testDispatcher = UnconfinedTestDispatcher() mediaCarouselController = MediaCarouselController( context, @@ -144,6 +150,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { clock, executor, bgExecutor, + testDispatcher, mediaDataManager, configurationController, falsingManager, @@ -153,7 +160,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { mediaFlags, keyguardUpdateMonitor, kosmos.keyguardTransitionInteractor, - globalSettings + globalSettings, + secureSettings, ) verify(configurationController).addCallback(capture(configListener)) verify(mediaDataManager).addListener(capture(listener)) @@ -807,7 +815,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { @ExperimentalCoroutinesApi @Test fun testKeyguardGone_showMediaCarousel() = - runTest(UnconfinedTestDispatcher()) { + kosmos.testScope.runTest { + var updatedVisibility = false + mediaCarouselController.updateHostVisibility = { updatedVisibility = true } mediaCarouselController.mediaCarousel = mediaCarousel val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this) @@ -818,10 +828,64 @@ class MediaCarouselControllerTest : SysuiTestCase() { ) verify(mediaCarousel).visibility = View.VISIBLE + assertEquals(true, updatedVisibility) + assertEquals(false, mediaCarouselController.isLockedAndHidden()) job.cancel() } + @ExperimentalCoroutinesApi + @Test + fun keyguardShowing_notAllowedOnLockscreen_updateVisibility() { + kosmos.testScope.runTest { + var updatedVisibility = false + mediaCarouselController.updateHostVisibility = { updatedVisibility = true } + mediaCarouselController.mediaCarousel = mediaCarousel + + val settingsJob = mediaCarouselController.listenForLockscreenSettingChanges(this) + secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false) + + val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this) + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this + ) + + assertEquals(true, updatedVisibility) + assertEquals(true, mediaCarouselController.isLockedAndHidden()) + + settingsJob.cancel() + keyguardJob.cancel() + } + } + + @ExperimentalCoroutinesApi + @Test + fun keyguardShowing_allowedOnLockscreen_updateVisibility() { + kosmos.testScope.runTest { + var updatedVisibility = false + mediaCarouselController.updateHostVisibility = { updatedVisibility = true } + mediaCarouselController.mediaCarousel = mediaCarousel + + val settingsJob = mediaCarouselController.listenForLockscreenSettingChanges(this) + secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true) + + val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this) + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this + ) + + assertEquals(true, updatedVisibility) + assertEquals(false, mediaCarouselController.isLockedAndHidden()) + + settingsJob.cancel() + keyguardJob.cancel() + } + } + @Test fun testInvisibleToUserAndExpanded_playersNotListening() { // Add players to carousel. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index d35c7dd2d027..a92cf8c96339 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -21,7 +21,7 @@ import com.android.systemui.plugins.qs.QS import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.res.R -import com.android.systemui.shade.ShadeLockscreenInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index dcd000aaa011..648358234771 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -23,7 +23,6 @@ import android.platform.test.annotations.DisableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest -import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase @@ -39,6 +38,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsIntera import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.data.repository.FakeCommandQueue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository @@ -49,6 +49,7 @@ import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInterac import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository @@ -80,9 +81,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyFloat -import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito import org.mockito.Mockito.mock @@ -101,7 +100,6 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromPrimaryBouncerTransitionInteractor: FromPrimaryBouncerTransitionInteractor - private val interactionJankMonitor = mock<InteractionJankMonitor>() private val mockDarkAnimator = mock<ObjectAnimator>() private val deviceEntryUdfpsInteractor = mock<DeviceEntryUdfpsInteractor>() private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>() @@ -112,15 +110,13 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true) - whenever(interactionJankMonitor.end(anyInt())).thenReturn(true) uiEventLogger = UiEventLoggerFake() underTest = object : StatusBarStateControllerImpl( uiEventLogger, - interactionJankMonitor, + kosmos.interactionJankMonitor, JavaAdapter(testScope.backgroundScope), { shadeInteractor }, { kosmos.deviceUnlockedInteractor }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index b0b9bec4f721..054680df1582 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -94,7 +94,7 @@ import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt index de1891355f29..c8ff20b31aae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.StatusBarStateControllerImpl @@ -68,6 +69,8 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { @Mock private lateinit var shadeViewController: ShadeViewController @Mock + private lateinit var shadeLockscreenInteractor: ShadeLockscreenInteractor + @Mock private lateinit var panelExpansionInteractor: PanelExpansionInteractor @Mock private lateinit var notifShadeWindowController: NotificationShadeWindowController @@ -100,6 +103,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { { notifShadeWindowController }, interactionJankMonitor, powerManager, + { shadeLockscreenInteractor }, { panelExpansionInteractor }, handler, ) @@ -133,7 +137,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { callbackCaptor.value.run() - verify(shadeViewController, times(1)).showAodUi() + verify(shadeLockscreenInteractor, times(1)).showAodUi() } @Test @@ -149,7 +153,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { callbackCaptor.value.run() - verify(shadeViewController, never()).showAodUi() + verify(shadeLockscreenInteractor, never()).showAodUi() } /** @@ -171,7 +175,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { verify(handler).postDelayed(callbackCaptor.capture(), anyLong()) callbackCaptor.value.run() - verify(shadeViewController, never()).showAodUi() + verify(shadeLockscreenInteractor, never()).showAodUi() } @Test @@ -186,7 +190,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { verify(handler).postDelayed(callbackCaptor.capture(), anyLong()) callbackCaptor.value.run() - verify(shadeViewController).showAodUi() + verify(shadeLockscreenInteractor).showAodUi() } @Test diff --git a/ravenwood/README.md b/ravenwood/README.md index 8cafb433736f..9c4fda7a50a6 100644 --- a/ravenwood/README.md +++ b/ravenwood/README.md @@ -1,11 +1,9 @@ # Ravenwood -Ravenwood is a lightweight unit testing environment for Android platform code that runs on the host. +Ravenwood is an officially-supported lightweight unit testing environment for Android platform code that runs on the host. Ravenwood’s focus on Android platform use-cases, improved maintainability, and device consistency distinguishes it from Robolectric, which remains a popular choice for app testing. -> **Note:** Active development of Ravenwood has been paused as of March 2024. Existing Ravenwood tests will continue running, but support has moved to a self-service model. - ## Background Executing tests on a typical Android device has substantial overhead, such as flashing the build, waiting for the boot to complete, and retrying tests that fail due to general flakiness. diff --git a/ravenwood/api-maintainers.md b/ravenwood/api-maintainers.md index c059cabd14e2..4b2f96804c97 100644 --- a/ravenwood/api-maintainers.md +++ b/ravenwood/api-maintainers.md @@ -4,7 +4,7 @@ By default, Android APIs aren’t opted-in to Ravenwood, and they default to thr To opt-in to supporting an API under Ravenwood, you can use the inline annotations documented below to customize your API behavior when running under Ravenwood. Because these annotations are inline in the relevant platform source code, they serve as valuable reminders to future API maintainers of Ravenwood support expectations. -> **Note:** Active development of Ravenwood has been paused as of March 2024. Currently supported APIs will continue working, but the addition of new APIs is not currently being supported. There is an allowlist that restricts where Ravenwood-specific annotations can be used, and that allowlist is not being expanded while development is paused. +> **Note:** to ensure that API teams are well-supported during early Ravenwood onboarding, the Ravenwood team is manually maintaining an allow-list of classes that are able to use Ravenwood annotations. Please reach out to ravenwood@ so we can offer design advice and allow-list your APIs. These Ravenwood-specific annotations have no bearing on the status of an API being public, `@SystemApi`, `@TestApi`, `@hide`, etc. Ravenwood annotations are an orthogonal concept that are only consumed by the internal `hoststubgen` tool during a post-processing step that generates the Ravenwood runtime environment. Teams that own APIs can continue to refactor opted-in `@hide` implementation details, as long as the test-visible behavior continues passing. diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 3e7682a645ee..880a68776055 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2703,13 +2703,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Map<ComponentName, AccessibilityServiceConnection> componentNameToServiceMap = userState.mComponentNameToServiceMap; boolean isUnlockingOrUnlocked = mUmi.isUserUnlockingOrUnlocked(userState.mUserId); - Set<ComponentName> installedComponentNames = new HashSet<>(); for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) { AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i); ComponentName componentName = ComponentName.unflattenFromString( installedService.getId()); - installedComponentNames.add(componentName); AccessibilityServiceConnection service = componentNameToServiceMap.get(componentName); @@ -2769,28 +2767,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub audioManager.setAccessibilityServiceUids(mTempIntArray); } mActivityTaskManagerService.setAccessibilityServiceUids(mTempIntArray); - final Iterator<ComponentName> it = userState.mEnabledServices.iterator(); - boolean anyServiceRemoved = false; - while (it.hasNext()) { - final ComponentName comp = it.next(); - if (!installedComponentNames.contains(comp)) { - it.remove(); - userState.mTouchExplorationGrantedServices.remove(comp); - anyServiceRemoved = true; - } - } - 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/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 14a331c6ffe0..272d63d36ede 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -33,8 +33,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManagerInternal; import android.content.ComponentName; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.graphics.Rect; import android.metrics.LogMaker; @@ -251,6 +253,31 @@ final class AutofillManagerServiceImpl @Override // from PerUserSystemService protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) throws NameNotFoundException { + final List<ResolveInfo> resolveInfos = + getContext().getPackageManager().queryIntentServicesAsUser( + new Intent(AutofillService.SERVICE_INTERFACE), + // The MATCH_INSTANT flag is added because curret autofill CTS module is + // defined in one apk, which makes the test autofill service installed in a + // instant app when the CTS tests are running in instant app mode. + // TODO: Remove MATCH_INSTANT flag after completing refactoring the CTS module + // to make the test autofill service a separate apk. + PackageManager.GET_META_DATA | PackageManager.MATCH_INSTANT, + mUserId); + boolean serviceHasAutofillIntentFilter = false; + for (ResolveInfo resolveInfo : resolveInfos) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (serviceInfo.getComponentName().equals(serviceComponent)) { + serviceHasAutofillIntentFilter = true; + break; + } + } + if (!serviceHasAutofillIntentFilter) { + Slog.w(TAG, + "Autofill service from '" + serviceComponent.getPackageName() + "' does" + + "not have intent filter " + AutofillService.SERVICE_INTERFACE); + throw new SecurityException("Service does not declare intent filter " + + AutofillService.SERVICE_INTERFACE); + } mInfo = new AutofillServiceInfo(getContext(), serviceComponent, mUserId); return mInfo.getServiceInfo(); } diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java deleted file mode 100644 index 0a4148535451..000000000000 --- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java +++ /dev/null @@ -1,567 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.companion; - -import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SuppressLint; -import android.annotation.UserIdInt; -import android.companion.AssociationInfo; -import android.companion.CompanionDeviceService; -import android.companion.DevicePresenceEvent; -import android.content.ComponentName; -import android.content.Context; -import android.hardware.power.Mode; -import android.os.Handler; -import android.os.ParcelUuid; -import android.os.PowerManagerInternal; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.infra.PerUser; -import com.android.server.companion.association.AssociationStore; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; -import com.android.server.companion.presence.ObservableUuid; -import com.android.server.companion.presence.ObservableUuidStore; -import com.android.server.companion.utils.PackageUtils; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Manages communication with companion applications via - * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to - * the services, maintaining the connection (the binding), and invoking callback methods such as - * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)}, - * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and - * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the - * application process. - * - * <p> - * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be - * utilized by {@link CompanionDeviceManagerService}): - * <ul> - * <li> {@link #bindCompanionApplication(int, String, boolean)} - * <li> {@link #unbindCompanionApplication(int, String)} - * <li> {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} - * <li> {@link #isCompanionApplicationBound(int, String)} - * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)} - * </ul> - * - * @see CompanionDeviceService - * @see android.companion.ICompanionDeviceService - * @see CompanionDeviceServiceConnector - */ -@SuppressLint("LongLogTag") -public class CompanionApplicationController { - static final boolean DEBUG = false; - private static final String TAG = "CDM_CompanionApplicationController"; - - private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec - - private final @NonNull Context mContext; - private final @NonNull AssociationStore mAssociationStore; - private final @NonNull ObservableUuidStore mObservableUuidStore; - private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor; - private final @NonNull CompanionServicesRegister mCompanionServicesRegister; - - private final PowerManagerInternal mPowerManagerInternal; - - @GuardedBy("mBoundCompanionApplications") - private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>> - mBoundCompanionApplications; - @GuardedBy("mScheduledForRebindingCompanionApplications") - private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications; - - CompanionApplicationController(Context context, AssociationStore associationStore, - ObservableUuidStore observableUuidStore, - CompanionDevicePresenceMonitor companionDevicePresenceMonitor, - PowerManagerInternal powerManagerInternal) { - mContext = context; - mAssociationStore = associationStore; - mObservableUuidStore = observableUuidStore; - mDevicePresenceMonitor = companionDevicePresenceMonitor; - mPowerManagerInternal = powerManagerInternal; - mCompanionServicesRegister = new CompanionServicesRegister(); - mBoundCompanionApplications = new AndroidPackageMap<>(); - mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>(); - } - - void onPackagesChanged(@UserIdInt int userId) { - mCompanionServicesRegister.invalidate(userId); - } - - /** - * CDM binds to the companion app. - */ - public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName, - boolean isSelfManaged) { - if (DEBUG) { - Log.i(TAG, "bind() u" + userId + "/" + packageName - + " isSelfManaged=" + isSelfManaged); - } - - final List<ComponentName> companionServices = - mCompanionServicesRegister.forPackage(userId, packageName); - if (companionServices.isEmpty()) { - Slog.w(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": " - + "eligible CompanionDeviceService not found.\n" - + "A CompanionDeviceService should declare an intent-filter for " - + "\"android.companion.CompanionDeviceService\" action and require " - + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission."); - return; - } - - final List<CompanionDeviceServiceConnector> serviceConnectors = new ArrayList<>(); - synchronized (mBoundCompanionApplications) { - if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { - if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is ALREADY bound."); - return; - } - - for (int i = 0; i < companionServices.size(); i++) { - boolean isPrimary = i == 0; - serviceConnectors.add(CompanionDeviceServiceConnector.newInstance(mContext, userId, - companionServices.get(i), isSelfManaged, isPrimary)); - } - - mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors); - } - - // Set listeners for both Primary and Secondary connectors. - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.setListener(this::onBinderDied); - } - - // Now "bind" all the connectors: the primary one and the rest of them. - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.connect(); - } - } - - /** - * CDM unbinds the companion app. - */ - public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) { - if (DEBUG) Log.i(TAG, "unbind() u" + userId + "/" + packageName); - - final List<CompanionDeviceServiceConnector> serviceConnectors; - - synchronized (mBoundCompanionApplications) { - serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName); - } - - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); - } - - if (serviceConnectors == null) { - if (DEBUG) { - Log.e(TAG, "unbindCompanionApplication(): " - + "u" + userId + "/" + packageName + " is NOT bound"); - Log.d(TAG, "Stacktrace", new Throwable()); - } - return; - } - - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.postUnbind(); - } - } - - /** - * @return whether the companion application is bound now. - */ - public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) { - synchronized (mBoundCompanionApplications) { - return mBoundCompanionApplications.containsValueForPackage(userId, packageName); - } - } - - private void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName, - CompanionDeviceServiceConnector serviceConnector) { - Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName); - - if (isRebindingCompanionApplicationScheduled(userId, packageName)) { - if (DEBUG) { - Log.i(TAG, "CompanionApplication rebinding has been scheduled, skipping " - + serviceConnector.getComponentName()); - } - return; - } - - if (serviceConnector.isPrimary()) { - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.setValueForPackage( - userId, packageName, true); - } - } - - // Rebinding in 10 seconds. - Handler.getMain().postDelayed(() -> - onRebindingCompanionApplicationTimeout(userId, packageName, serviceConnector), - REBIND_TIMEOUT); - } - - private boolean isRebindingCompanionApplicationScheduled( - @UserIdInt int userId, @NonNull String packageName) { - synchronized (mScheduledForRebindingCompanionApplications) { - return mScheduledForRebindingCompanionApplications.containsValueForPackage( - userId, packageName); - } - } - - private void onRebindingCompanionApplicationTimeout( - @UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector) { - // Re-mark the application is bound. - if (serviceConnector.isPrimary()) { - synchronized (mBoundCompanionApplications) { - if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { - List<CompanionDeviceServiceConnector> serviceConnectors = - Collections.singletonList(serviceConnector); - mBoundCompanionApplications.setValueForPackage(userId, packageName, - serviceConnectors); - } - } - - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); - } - } - - serviceConnector.connect(); - } - - /** - * Notify the app that the device appeared. - * - * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead - */ - @Deprecated - public void notifyCompanionApplicationDeviceAppeared(AssociationInfo association) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - Slog.i(TAG, "notifyDevice_Appeared() id=" + association.getId() + " u" + userId - + "/" + packageName); - - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - if (primaryServiceConnector == null) { - Slog.e(TAG, "notify_CompanionApplicationDevice_Appeared(): " - + "u" + userId + "/" + packageName + " is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Log.i(TAG, "Calling onDeviceAppeared to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() + "]"); - - primaryServiceConnector.postOnDeviceAppeared(association); - } - - /** - * Notify the app that the device disappeared. - * - * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead - */ - @Deprecated - public void notifyCompanionApplicationDeviceDisappeared(AssociationInfo association) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - Slog.i(TAG, "notifyDevice_Disappeared() id=" + association.getId() + " u" + userId - + "/" + packageName); - - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - if (primaryServiceConnector == null) { - Slog.e(TAG, "notify_CompanionApplicationDevice_Disappeared(): " - + "u" + userId + "/" + packageName + " is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Log.i(TAG, "Calling onDeviceDisappeared to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() + "]"); - - primaryServiceConnector.postOnDeviceDisappeared(association); - } - - /** - * Notify the app that the device appeared. - */ - public void notifyCompanionDevicePresenceEvent(AssociationInfo association, int event) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - final DevicePresenceEvent devicePresenceEvent = - new DevicePresenceEvent(association.getId(), event, null); - - if (primaryServiceConnector == null) { - Slog.e(TAG, "notifyCompanionApplicationDevicePresenceEvent(): " - + "u" + userId + "/" + packageName - + " event=[ " + event + " ] is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() - + "] event=[" + event + "]"); - - primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent); - } - - /** - * Notify the app that the device disappeared. - */ - public void notifyUuidDevicePresenceEvent(ObservableUuid uuid, int event) { - final int userId = uuid.getUserId(); - final ParcelUuid parcelUuid = uuid.getUuid(); - final String packageName = uuid.getPackageName(); - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - final DevicePresenceEvent devicePresenceEvent = - new DevicePresenceEvent(DevicePresenceEvent.NO_ASSOCIATION, event, parcelUuid); - - if (primaryServiceConnector == null) { - Slog.e(TAG, "notifyApplicationDevicePresenceChanged(): " - + "u" + userId + "/" + packageName - + " event=[ " + event + " ] is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=[" - + packageName + "]" + "event= [" + event + "]"); - - primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent); - } - - void dump(@NonNull PrintWriter out) { - out.append("Companion Device Application Controller: \n"); - - synchronized (mBoundCompanionApplications) { - out.append(" Bound Companion Applications: "); - if (mBoundCompanionApplications.size() == 0) { - out.append("<empty>\n"); - } else { - out.append("\n"); - mBoundCompanionApplications.dump(out); - } - } - - out.append(" Companion Applications Scheduled For Rebinding: "); - if (mScheduledForRebindingCompanionApplications.size() == 0) { - out.append("<empty>\n"); - } else { - out.append("\n"); - mScheduledForRebindingCompanionApplications.dump(out); - } - } - - /** - * Rebinding for Self-Managed secondary services OR Non-Self-Managed services. - */ - private void onBinderDied(@UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector) { - - boolean isPrimary = serviceConnector.isPrimary(); - Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary); - - // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY. - if (isPrimary) { - final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); - - for (AssociationInfo association : associations) { - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); - break; - } - } - - synchronized (mBoundCompanionApplications) { - mBoundCompanionApplications.removePackage(userId, packageName); - } - } - - // Second: schedule rebinding if needed. - final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary); - - if (shouldScheduleRebind) { - scheduleRebinding(userId, packageName, serviceConnector); - } - } - - private @Nullable CompanionDeviceServiceConnector getPrimaryServiceConnector( - @UserIdInt int userId, @NonNull String packageName) { - final List<CompanionDeviceServiceConnector> connectors; - synchronized (mBoundCompanionApplications) { - connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName); - } - return connectors != null ? connectors.get(0) : null; - } - - private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) { - // Make sure do not schedule rebind for the case ServiceConnector still gets callback after - // app is uninstalled. - boolean stillAssociated = false; - // Make sure to clean up the state for all the associations - // that associate with this package. - boolean shouldScheduleRebind = false; - boolean shouldScheduleRebindForUuid = false; - final List<ObservableUuid> uuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (AssociationInfo ai : - mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) { - final int associationId = ai.getId(); - stillAssociated = true; - if (ai.isSelfManaged()) { - // Do not rebind if primary one is died for selfManaged application. - if (isPrimary - && mDevicePresenceMonitor.isDevicePresent(associationId)) { - mDevicePresenceMonitor.onSelfManagedDeviceReporterBinderDied(associationId); - shouldScheduleRebind = false; - } - // Do not rebind if both primary and secondary services are died for - // selfManaged application. - shouldScheduleRebind = isCompanionApplicationBound(userId, packageName); - } else if (ai.isNotifyOnDeviceNearby()) { - // Always rebind for non-selfManaged devices. - shouldScheduleRebind = true; - } - } - - for (ObservableUuid uuid : uuids) { - if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) { - shouldScheduleRebindForUuid = true; - break; - } - } - - return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid; - } - - private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> { - @Override - public synchronized @NonNull Map<String, List<ComponentName>> forUser( - @UserIdInt int userId) { - return super.forUser(userId); - } - - synchronized @NonNull List<ComponentName> forPackage( - @UserIdInt int userId, @NonNull String packageName) { - return forUser(userId).getOrDefault(packageName, Collections.emptyList()); - } - - synchronized void invalidate(@UserIdInt int userId) { - remove(userId); - } - - @Override - protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) { - return PackageUtils.getCompanionServicesForUser(mContext, userId); - } - } - - /** - * Associates an Android package (defined by userId + packageName) with a value of type T. - */ - private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> { - - void setValueForPackage( - @UserIdInt int userId, @NonNull String packageName, @NonNull T value) { - Map<String, T> forUser = get(userId); - if (forUser == null) { - forUser = /* Map<String, T> */ new HashMap(); - put(userId, forUser); - } - - forUser.put(packageName, value); - } - - boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, ?> forUser = get(userId); - return forUser != null && forUser.containsKey(packageName); - } - - T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, T> forUser = get(userId); - return forUser != null ? forUser.get(packageName) : null; - } - - T removePackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, T> forUser = get(userId); - if (forUser == null) return null; - return forUser.remove(packageName); - } - - void dump() { - if (size() == 0) { - Log.d(TAG, "<empty>"); - return; - } - - for (int i = 0; i < size(); i++) { - final int userId = keyAt(i); - final Map<String, T> forUser = get(userId); - if (forUser.isEmpty()) { - Log.d(TAG, "u" + userId + ": <empty>"); - } - - for (Map.Entry<String, T> packageValue : forUser.entrySet()) { - final String packageName = packageValue.getKey(); - final T value = packageValue.getValue(); - Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value); - } - } - } - - private void dump(@NonNull PrintWriter out) { - for (int i = 0; i < size(); i++) { - final int userId = keyAt(i); - final Map<String, T> forUser = get(userId); - if (forUser.isEmpty()) { - out.append(" u").append(String.valueOf(userId)).append(": <empty>\n"); - } - - for (Map.Entry<String, T> packageValue : forUser.entrySet()) { - final String packageName = packageValue.getKey(); - final T value = packageValue.getValue(); - out.append(" u").append(String.valueOf(userId)).append("\\") - .append(packageName).append(" -> ") - .append(value.toString()).append('\n'); - } - } - } - } -} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 712162b2d3b5..f4f6c13e74e4 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -20,15 +20,10 @@ package com.android.server.companion; import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES; import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; +import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED; import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE; import static android.Manifest.permission.USE_COMPANION_TRANSPORTS; -import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; -import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; import static android.content.pm.PackageManager.CERT_INPUT_SHA256; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; @@ -42,13 +37,10 @@ import static com.android.server.companion.utils.PackageUtils.getPackageInfo; import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageCompanionDevice; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; -import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId; -import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks; import static java.util.Objects.requireNonNull; -import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MINUTES; import android.annotation.EnforcePermission; @@ -64,7 +56,6 @@ import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.companion.AssociationInfo; import android.companion.AssociationRequest; -import android.companion.DeviceNotAssociatedException; import android.companion.IAssociationRequestCallback; import android.companion.ICompanionDeviceManager; import android.companion.IOnAssociationsChangedListener; @@ -79,7 +70,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; -import android.hardware.power.Mode; import android.net.MacAddress; import android.net.NetworkPolicyManager; import android.os.Binder; @@ -91,7 +81,6 @@ import android.os.PowerExemptionManager; import android.os.PowerManagerInternal; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.util.ArraySet; @@ -118,7 +107,8 @@ import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.CompanionAppBinder; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.presence.ObservableUuid; import com.android.server.companion.presence.ObservableUuidStore; import com.android.server.companion.transport.CompanionTransportManager; @@ -131,10 +121,7 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; @SuppressLint("LongLogTag") @@ -146,10 +133,6 @@ public class CompanionDeviceManagerService extends SystemService { private static final String PREF_FILE_NAME = "companion_device_preferences.xml"; private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done"; - private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = - "debug.cdm.cdmservice.removal_time_window"; - - private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); private static final int MAX_CN_LENGTH = 500; private final ActivityTaskManagerInternal mAtmInternal; @@ -165,10 +148,11 @@ public class CompanionDeviceManagerService extends SystemService { private final AssociationRequestsProcessor mAssociationRequestsProcessor; private final SystemDataTransferProcessor mSystemDataTransferProcessor; private final BackupRestoreProcessor mBackupRestoreProcessor; - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; - private final CompanionApplicationController mCompanionAppController; + private final DevicePresenceProcessor mDevicePresenceMonitor; + private final CompanionAppBinder mCompanionAppController; private final CompanionTransportManager mTransportManager; private final DisassociationProcessor mDisassociationProcessor; + private final InactiveAssociationsRemovalService mInactiveAssociationsRemovalService; private final CrossDeviceSyncController mCrossDeviceSyncController; public CompanionDeviceManagerService(Context context) { @@ -185,7 +169,7 @@ public class CompanionDeviceManagerService extends SystemService { mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); final AssociationDiskStore associationDiskStore = new AssociationDiskStore(); - mAssociationStore = new AssociationStore(userManager, associationDiskStore); + mAssociationStore = new AssociationStore(context, userManager, associationDiskStore); mSystemDataTransferRequestStore = new SystemDataTransferRequestStore(); mObservableUuidStore = new ObservableUuidStore(); @@ -196,11 +180,11 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore, mAssociationRequestsProcessor); - mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(userManager, - mAssociationStore, mObservableUuidStore, mDevicePresenceCallback); + mCompanionAppController = new CompanionAppBinder( + context, mAssociationStore, mObservableUuidStore, mPowerManagerInternal); - mCompanionAppController = new CompanionApplicationController( - context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor, + mDevicePresenceMonitor = new DevicePresenceProcessor(context, + mCompanionAppController, userManager, mAssociationStore, mObservableUuidStore, mPowerManagerInternal); mTransportManager = new CompanionTransportManager(context, mAssociationStore); @@ -209,6 +193,9 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore, mPackageManagerInternal, mDevicePresenceMonitor, mCompanionAppController, mSystemDataTransferRequestStore, mTransportManager); + mInactiveAssociationsRemovalService = new InactiveAssociationsRemovalService( + mAssociationStore, mDisassociationProcessor); + mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); @@ -302,181 +289,6 @@ public class CompanionDeviceManagerService extends SystemService { } } - @NonNull - AssociationInfo getAssociationWithCallerChecks( - @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) { - AssociationInfo association = mAssociationStore.getFirstAssociationByAddress( - userId, packageName, macAddress); - association = sanitizeWithCallerChecks(getContext(), association); - if (association != null) { - return association; - } else { - throw new IllegalArgumentException("Association does not exist " - + "or the caller does not have permissions to manage it " - + "(ie. it belongs to a different package or a different user)."); - } - } - - @NonNull - AssociationInfo getAssociationWithCallerChecks(int associationId) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = sanitizeWithCallerChecks(getContext(), association); - if (association != null) { - return association; - } else { - throw new IllegalArgumentException("Association does not exist " - + "or the caller does not have permissions to manage it " - + "(ie. it belongs to a different package or a different user)."); - } - } - - private void onDeviceAppearedInternal(int associationId) { - if (DEBUG) Log.i(TAG, "onDevice_Appeared_Internal() id=" + associationId); - - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (DEBUG) Log.d(TAG, " association=" + association); - - if (!association.shouldBindWhenPresent()) return; - - bindApplicationIfNeeded(association); - - mCompanionAppController.notifyCompanionApplicationDeviceAppeared(association); - } - - private void onDeviceDisappearedInternal(int associationId) { - if (DEBUG) Log.i(TAG, "onDevice_Disappeared_Internal() id=" + associationId); - - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (DEBUG) Log.d(TAG, " association=" + association); - - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - - if (association.shouldBindWhenPresent()) { - mCompanionAppController.notifyCompanionApplicationDeviceDisappeared(association); - } - } - - private void onDevicePresenceEventInternal(int associationId, int event) { - Slog.i(TAG, "onDevicePresenceEventInternal() id=" + associationId + " event= " + event); - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - final String packageName = association.getPackageName(); - final int userId = association.getUserId(); - switch (event) { - case EVENT_BLE_APPEARED: - case EVENT_BT_CONNECTED: - case EVENT_SELF_MANAGED_APPEARED: - if (!association.shouldBindWhenPresent()) return; - - bindApplicationIfNeeded(association); - - mCompanionAppController.notifyCompanionDevicePresenceEvent( - association, event); - break; - case EVENT_BLE_DISAPPEARED: - case EVENT_BT_DISCONNECTED: - case EVENT_SELF_MANAGED_DISAPPEARED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - if (association.shouldBindWhenPresent()) { - mCompanionAppController.notifyCompanionDevicePresenceEvent( - association, event); - } - // Check if there are other devices associated to the app that are present. - if (shouldBindPackage(userId, packageName)) return; - mCompanionAppController.unbindCompanionApplication(userId, packageName); - break; - default: - Slog.e(TAG, "Event: " + event + "is not supported"); - break; - } - } - - private void onDevicePresenceEventByUuidInternal(ObservableUuid uuid, int event) { - Slog.i(TAG, "onDevicePresenceEventByUuidInternal() id=" + uuid.getUuid() - + "for package=" + uuid.getPackageName() + " event=" + event); - final String packageName = uuid.getPackageName(); - final int userId = uuid.getUserId(); - - switch (event) { - case EVENT_BT_CONNECTED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - mCompanionAppController.bindCompanionApplication( - userId, packageName, /*bindImportant*/ false); - - } else if (DEBUG) { - Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound"); - } - - mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event); - - break; - case EVENT_BT_DISCONNECTED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - - mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event); - // Check if there are other devices associated to the app or the UUID to be - // observed are present. - if (shouldBindPackage(userId, packageName)) return; - - mCompanionAppController.unbindCompanionApplication(userId, packageName); - - break; - default: - Slog.e(TAG, "Event: " + event + "is not supported"); - break; - } - } - - private void bindApplicationIfNeeded(AssociationInfo association) { - final String packageName = association.getPackageName(); - final int userId = association.getUserId(); - // Set bindImportant to true when the association is self-managed to avoid the target - // service being killed. - final boolean bindImportant = association.isSelfManaged(); - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - mCompanionAppController.bindCompanionApplication( - userId, packageName, bindImportant); - } else if (DEBUG) { - Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound"); - } - } - - /** - * @return whether the package should be bound (i.e. at least one of the devices associated with - * the package is currently present OR the UUID to be observed by this package is - * currently present). - */ - private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) { - final List<AssociationInfo> packageAssociations = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); - final List<ObservableUuid> observableUuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (AssociationInfo association : packageAssociations) { - if (!association.shouldBindWhenPresent()) continue; - if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true; - } - - for (ObservableUuid uuid : observableUuids) { - if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) { - return true; - } - } - - return false; - } - private void onPackageRemoveOrDataClearedInternal( @UserIdInt int userId, @NonNull String packageName) { if (DEBUG) { @@ -522,27 +334,8 @@ public class CompanionDeviceManagerService extends SystemService { mBackupRestoreProcessor.restorePendingAssociations(userId, packageName); } - // Revoke associations if the selfManaged companion device does not connect for 3 months. void removeInactiveSelfManagedAssociations() { - final long currentTime = System.currentTimeMillis(); - long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1); - if (removalWindow <= 0) { - // 0 or negative values indicate that the sysprop was never set or should be ignored. - removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; - } - - for (AssociationInfo association : mAssociationStore.getAssociations()) { - if (!association.isSelfManaged()) continue; - - final boolean isInactive = - currentTime - association.getLastTimeConnectedMs() >= removalWindow; - if (!isInactive) continue; - - final int id = association.getId(); - - Slog.i(TAG, "Removing inactive self-managed association id=" + id); - mDisassociationProcessor.disassociate(id); - } + mInactiveAssociationsRemovalService.removeIdleSelfManagedAssociations(); } public class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub { @@ -679,24 +472,15 @@ public class CompanionDeviceManagerService extends SystemService { @Deprecated @Override public void legacyDisassociate(String deviceMacAddress, String packageName, int userId) { - Log.i(TAG, "legacyDisassociate() pkg=u" + userId + "/" + packageName - + ", macAddress=" + deviceMacAddress); - requireNonNull(deviceMacAddress); requireNonNull(packageName); - final AssociationInfo association = - getAssociationWithCallerChecks(userId, packageName, deviceMacAddress); - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(userId, packageName, deviceMacAddress); } @Override public void disassociate(int associationId) { - Slog.i(TAG, "disassociate() associationId=" + associationId); - - final AssociationInfo association = - getAssociationWithCallerChecks(associationId); - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(associationId); } @Override @@ -758,21 +542,25 @@ public class CompanionDeviceManagerService extends SystemService { } @Override + @Deprecated @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) - public void registerDevicePresenceListenerService(String deviceAddress, - String callingPackage, int userId) throws RemoteException { - registerDevicePresenceListenerService_enforcePermission(); - // TODO: take the userId into account. - registerDevicePresenceListenerActive(callingPackage, deviceAddress, true); + public void legacyStartObservingDevicePresence(String deviceAddress, String callingPackage, + int userId) throws RemoteException { + legacyStartObservingDevicePresence_enforcePermission(); + + mDevicePresenceMonitor.startObservingDevicePresence(userId, callingPackage, + deviceAddress); } @Override + @Deprecated @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) - public void unregisterDevicePresenceListenerService(String deviceAddress, - String callingPackage, int userId) throws RemoteException { - unregisterDevicePresenceListenerService_enforcePermission(); - // TODO: take the userId into account. - registerDevicePresenceListenerActive(callingPackage, deviceAddress, false); + public void legacyStopObservingDevicePresence(String deviceAddress, String callingPackage, + int userId) throws RemoteException { + legacyStopObservingDevicePresence_enforcePermission(); + + mDevicePresenceMonitor.stopObservingDevicePresence(userId, callingPackage, + deviceAddress); } @Override @@ -780,7 +568,8 @@ public class CompanionDeviceManagerService extends SystemService { public void startObservingDevicePresence(ObservingDevicePresenceRequest request, String packageName, int userId) { startObservingDevicePresence_enforcePermission(); - registerDevicePresenceListener(request, packageName, userId, /* active */ true); + + mDevicePresenceMonitor.startObservingDevicePresence(request, packageName, userId); } @Override @@ -788,80 +577,8 @@ public class CompanionDeviceManagerService extends SystemService { public void stopObservingDevicePresence(ObservingDevicePresenceRequest request, String packageName, int userId) { stopObservingDevicePresence_enforcePermission(); - registerDevicePresenceListener(request, packageName, userId, /* active */ false); - } - - private void registerDevicePresenceListener(ObservingDevicePresenceRequest request, - String packageName, int userId, boolean active) { - enforceUsesCompanionDeviceFeature(getContext(), userId, packageName); - enforceCallerIsSystemOr(userId, packageName); - - final int associationId = request.getAssociationId(); - final AssociationInfo associationInfo = mAssociationStore.getAssociationById( - associationId); - final ParcelUuid uuid = request.getUuid(); - if (uuid != null) { - enforceCallerCanObservingDevicePresenceByUuid(getContext()); - if (active) { - startObservingDevicePresenceByUuid(uuid, packageName, userId); - } else { - stopObservingDevicePresenceByUuid(uuid, packageName, userId); - } - } else if (associationInfo == null) { - throw new IllegalArgumentException("App " + packageName - + " is not associated with device " + request.getAssociationId() - + " for user " + userId); - } else { - processDevicePresenceListener( - associationInfo, userId, packageName, active); - } - } - - private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName, - int userId) { - final List<ObservableUuid> observableUuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (ObservableUuid observableUuid : observableUuids) { - if (observableUuid.getUuid().equals(uuid)) { - Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName - + "has been already scheduled for observing"); - return; - } - } - - final ObservableUuid observableUuid = new ObservableUuid(userId, uuid, - packageName, System.currentTimeMillis()); - - mObservableUuidStore.writeObservableUuid(userId, observableUuid); - } - - private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName, - int userId) { - final List<ObservableUuid> uuidsTobeObserved = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - boolean isScheduledObserving = false; - - for (ObservableUuid observableUuid : uuidsTobeObserved) { - if (observableUuid.getUuid().equals(uuid)) { - isScheduledObserving = true; - break; - } - } - - if (!isScheduledObserving) { - Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName - + "has NOT been scheduled for observing yet"); - return; - } - - mObservableUuidStore.removeObservableUuid(userId, uuid, packageName); - mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid); - - if (!shouldBindPackage(userId, packageName)) { - mCompanionAppController.unbindCompanionApplication(userId, packageName); - } + mDevicePresenceMonitor.stopObservingDevicePresence(request, packageName, userId); } @Override @@ -874,8 +591,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public boolean isPermissionTransferUserConsented(String packageName, int userId, int associationId) { - return mSystemDataTransferProcessor.isPermissionTransferUserConsented(packageName, - userId, associationId); + return mSystemDataTransferProcessor.isPermissionTransferUserConsented(associationId); } @Override @@ -891,8 +607,7 @@ public class CompanionDeviceManagerService extends SystemService { ParcelFileDescriptor fd) { attachSystemDataTransport_enforcePermission(); - getAssociationWithCallerChecks(associationId); - mTransportManager.attachSystemDataTransport(packageName, userId, associationId, fd); + mTransportManager.attachSystemDataTransport(associationId, fd); } @Override @@ -900,96 +615,56 @@ public class CompanionDeviceManagerService extends SystemService { public void detachSystemDataTransport(String packageName, int userId, int associationId) { detachSystemDataTransport_enforcePermission(); - getAssociationWithCallerChecks(associationId); - mTransportManager.detachSystemDataTransport(packageName, userId, associationId); + mTransportManager.detachSystemDataTransport(associationId); + } + + @Override + @EnforcePermission(MANAGE_COMPANION_DEVICES) + public void enableSecureTransport(boolean enabled) { + enableSecureTransport_enforcePermission(); + + mTransportManager.enableSecureTransport(enabled); } @Override public void enableSystemDataSync(int associationId, int flags) { - getAssociationWithCallerChecks(associationId); mAssociationRequestsProcessor.enableSystemDataSync(associationId, flags); } @Override public void disableSystemDataSync(int associationId, int flags) { - getAssociationWithCallerChecks(associationId); mAssociationRequestsProcessor.disableSystemDataSync(associationId, flags); } @Override public void enablePermissionsSync(int associationId) { - getAssociationWithCallerChecks(associationId); mSystemDataTransferProcessor.enablePermissionsSync(associationId); } @Override public void disablePermissionsSync(int associationId) { - getAssociationWithCallerChecks(associationId); mSystemDataTransferProcessor.disablePermissionsSync(associationId); } @Override public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - // TODO: temporary fix, will remove soon - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (association == null) { - return null; - } - getAssociationWithCallerChecks(associationId); return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId); } @Override - @EnforcePermission(MANAGE_COMPANION_DEVICES) - public void enableSecureTransport(boolean enabled) { - enableSecureTransport_enforcePermission(); - mTransportManager.enableSecureTransport(enabled); - } + @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED) + public void notifySelfManagedDeviceAppeared(int associationId) { + notifySelfManagedDeviceAppeared_enforcePermission(); - @Override - public void notifyDeviceAppeared(int associationId) { - if (DEBUG) Log.i(TAG, "notifyDevice_Appeared() id=" + associationId); - - AssociationInfo association = getAssociationWithCallerChecks(associationId); - if (!association.isSelfManaged()) { - throw new IllegalArgumentException("Association with ID " + associationId - + " is not self-managed. notifyDeviceAppeared(int) can only be called for" - + " self-managed associations."); - } - // AssociationInfo class is immutable: create a new AssociationInfo object with updated - // timestamp. - association = (new AssociationInfo.Builder(association)) - .setLastTimeConnected(System.currentTimeMillis()) - .build(); - mAssociationStore.updateAssociation(association); - - mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId); - - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true); - } + mDevicePresenceMonitor.notifySelfManagedDevicePresenceEvent(associationId, true); } @Override - public void notifyDeviceDisappeared(int associationId) { - if (DEBUG) Log.i(TAG, "notifyDevice_Disappeared() id=" + associationId); - - final AssociationInfo association = getAssociationWithCallerChecks(associationId); - if (!association.isSelfManaged()) { - throw new IllegalArgumentException("Association with ID " + associationId - + " is not self-managed. notifyDeviceAppeared(int) can only be called for" - + " self-managed associations."); - } - - mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId); + @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED) + public void notifySelfManagedDeviceDisappeared(int associationId) { + notifySelfManagedDeviceDisappeared_enforcePermission(); - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); - } + mDevicePresenceMonitor.notifySelfManagedDevicePresenceEvent(associationId, false); } @Override @@ -997,66 +672,6 @@ public class CompanionDeviceManagerService extends SystemService { return mCompanionAppController.isCompanionApplicationBound(userId, packageName); } - private void registerDevicePresenceListenerActive(String packageName, String deviceAddress, - boolean active) throws RemoteException { - if (DEBUG) { - Log.i(TAG, "registerDevicePresenceListenerActive()" - + " active=" + active - + " deviceAddress=" + deviceAddress); - } - final int userId = getCallingUserId(); - enforceCallerIsSystemOr(userId, packageName); - - AssociationInfo association = mAssociationStore.getFirstAssociationByAddress( - userId, packageName, deviceAddress); - - if (association == null) { - throw new RemoteException(new DeviceNotAssociatedException("App " + packageName - + " is not associated with device " + deviceAddress - + " for user " + userId)); - } - - processDevicePresenceListener(association, userId, packageName, active); - } - - private void processDevicePresenceListener(AssociationInfo association, - int userId, String packageName, boolean active) { - // If already at specified state, then no-op. - if (active == association.isNotifyOnDeviceNearby()) { - if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state."); - return; - } - - // AssociationInfo class is immutable: create a new AssociationInfo object with updated - // flag. - association = (new AssociationInfo.Builder(association)) - .setNotifyOnDeviceNearby(active) - .build(); - // Do not need to call {@link BleCompanionDeviceScanner#restartScan()} since it will - // trigger {@link BleCompanionDeviceScanner#restartScan(int, AssociationInfo)} when - // an application sets/unsets the mNotifyOnDeviceNearby flag. - mAssociationStore.updateAssociation(association); - - int associationId = association.getId(); - // If device is already present, then trigger callback. - if (active && mDevicePresenceMonitor.isDevicePresent(associationId)) { - Slog.i(TAG, "Device is already present. Triggering callback."); - if (mDevicePresenceMonitor.isBlePresent(associationId) - || mDevicePresenceMonitor.isSimulatePresent(associationId)) { - onDeviceAppearedInternal(associationId); - onDevicePresenceEventInternal(associationId, EVENT_BLE_APPEARED); - } else if (mDevicePresenceMonitor.isBtConnected(associationId)) { - onDevicePresenceEventInternal(associationId, EVENT_BT_CONNECTED); - } - } - - // If last listener is unregistered, then unbind application. - if (!active && !shouldBindPackage(userId, packageName)) { - if (DEBUG) Log.d(TAG, "Last listener unregistered. Unbinding application."); - mCompanionAppController.unbindCompanionApplication(userId, packageName); - } - } - @Override @EnforcePermission(ASSOCIATE_COMPANION_DEVICES) public void createAssociation(String packageName, String macAddress, int userId, @@ -1070,7 +685,8 @@ public class CompanionDeviceManagerService extends SystemService { } final MacAddress macAddressObj = MacAddress.fromString(macAddress); - createNewAssociation(userId, packageName, macAddressObj, null, null, false); + mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddressObj, + null, null, null, false, null, null); } private void checkCanCallNotificationApi(String callingPackage, int userId) { @@ -1099,9 +715,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void setAssociationTag(int associationId, String tag) { - AssociationInfo association = getAssociationWithCallerChecks(associationId); - association = (new AssociationInfo.Builder(association)).setTag(tag).build(); - mAssociationStore.updateAssociation(association); + mAssociationRequestsProcessor.setAssociationTag(associationId, tag); } @Override @@ -1146,14 +760,6 @@ public class CompanionDeviceManagerService extends SystemService { } } - void createNewAssociation(@UserIdInt int userId, @NonNull String packageName, - @Nullable MacAddress macAddress, @Nullable CharSequence displayName, - @Nullable String deviceProfile, boolean isSelfManaged) { - mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, - displayName, deviceProfile, /* associatedDevice */ null, isSelfManaged, - /* callback */ null, /* resultReceiver */ null); - } - /** * Update special access for the association's package */ @@ -1169,8 +775,6 @@ public class CompanionDeviceManagerService extends SystemService { return; } - Slog.i(TAG, "Updating special access for package=[" + packageInfo.packageName + "]..."); - if (containsEither(packageInfo.requestedPermissions, android.Manifest.permission.RUN_IN_BACKGROUND, android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) { @@ -1280,29 +884,6 @@ public class CompanionDeviceManagerService extends SystemService { } }; - private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback = - new CompanionDevicePresenceMonitor.Callback() { - @Override - public void onDeviceAppeared(int associationId) { - onDeviceAppearedInternal(associationId); - } - - @Override - public void onDeviceDisappeared(int associationId) { - onDeviceDisappearedInternal(associationId); - } - - @Override - public void onDevicePresenceEvent(int associationId, int event) { - onDevicePresenceEventInternal(associationId, event); - } - - @Override - public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) { - onDevicePresenceEventByUuidInternal(uuid, event); - } - }; - private final PackageMonitor mPackageMonitor = new PackageMonitor() { @Override public void onPackageRemoved(String packageName, int uid) { @@ -1315,7 +896,7 @@ public class CompanionDeviceManagerService extends SystemService { } @Override - public void onPackageModified(String packageName) { + public void onPackageModified(@NonNull String packageName) { onPackageModifiedInternal(getChangingUserId(), packageName); } @@ -1325,28 +906,12 @@ public class CompanionDeviceManagerService extends SystemService { } }; - private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) { - final Map<String, Set<Integer>> copy = new HashMap<>(); - - for (Map.Entry<String, Set<Integer>> entry : orig.entrySet()) { - final Set<Integer> valueCopy = new HashSet<>(entry.getValue()); - copy.put(entry.getKey(), Collections.unmodifiableSet(valueCopy)); - } - - return Collections.unmodifiableMap(copy); - } - private static <T> boolean containsEither(T[] array, T a, T b) { return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b); } private class LocalService implements CompanionDeviceManagerServiceInternal { @Override - public void removeInactiveSelfManagedAssociations() { - CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations(); - } - - @Override public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback, @CrossDeviceSyncControllerCallback.Type int type) { if (CompanionDeviceConfig.isEnabled( diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java index cdf832f8c788..e3b4c95a7dab 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java @@ -28,11 +28,6 @@ import java.util.Collection; */ public interface CompanionDeviceManagerServiceInternal { /** - * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations - */ - void removeInactiveSelfManagedAssociations(); - - /** * Registers a callback from an InCallService / ConnectionService to CDM to process sync * requests and perform call control actions. */ diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index a7a73cb6bddb..a78938400a1e 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -18,8 +18,6 @@ package com.android.server.companion; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC; -import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks; - import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; import android.companion.Flags; @@ -38,7 +36,7 @@ import com.android.server.companion.association.DisassociationProcessor; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.contextsync.BitmapUtils; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.presence.ObservableUuid; import com.android.server.companion.transport.CompanionTransportManager; @@ -51,7 +49,7 @@ class CompanionDeviceShellCommand extends ShellCommand { private final CompanionDeviceManagerService mService; private final DisassociationProcessor mDisassociationProcessor; private final AssociationStore mAssociationStore; - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private final DevicePresenceProcessor mDevicePresenceProcessor; private final CompanionTransportManager mTransportManager; private final SystemDataTransferProcessor mSystemDataTransferProcessor; @@ -60,7 +58,7 @@ class CompanionDeviceShellCommand extends ShellCommand { CompanionDeviceShellCommand(CompanionDeviceManagerService service, AssociationStore associationStore, - CompanionDevicePresenceMonitor devicePresenceMonitor, + DevicePresenceProcessor devicePresenceProcessor, CompanionTransportManager transportManager, SystemDataTransferProcessor systemDataTransferProcessor, AssociationRequestsProcessor associationRequestsProcessor, @@ -68,7 +66,7 @@ class CompanionDeviceShellCommand extends ShellCommand { DisassociationProcessor disassociationProcessor) { mService = service; mAssociationStore = associationStore; - mDevicePresenceMonitor = devicePresenceMonitor; + mDevicePresenceProcessor = devicePresenceProcessor; mTransportManager = transportManager; mSystemDataTransferProcessor = systemDataTransferProcessor; mAssociationRequestsProcessor = associationRequestsProcessor; @@ -85,7 +83,7 @@ class CompanionDeviceShellCommand extends ShellCommand { if ("simulate-device-event".equals(cmd) && Flags.devicePresence()) { associationId = getNextIntArgRequired(); int event = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, event); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, event); return 0; } @@ -97,7 +95,7 @@ class CompanionDeviceShellCommand extends ShellCommand { ObservableUuid observableUuid = new ObservableUuid( userId, ParcelUuid.fromString(uuid), packageName, System.currentTimeMillis()); - mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event); + mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event); return 0; } @@ -124,8 +122,9 @@ class CompanionDeviceShellCommand extends ShellCommand { String address = getNextArgRequired(); String deviceProfile = getNextArg(); final MacAddress macAddress = MacAddress.fromString(address); - mService.createNewAssociation(userId, packageName, macAddress, - /* displayName= */ deviceProfile, deviceProfile, false); + mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, + deviceProfile, deviceProfile, /* associatedDevice */ null, false, + /* callback */ null, /* resultReceiver */ null); } break; @@ -134,8 +133,13 @@ class CompanionDeviceShellCommand extends ShellCommand { final String packageName = getNextArgRequired(); final String address = getNextArgRequired(); final AssociationInfo association = - mService.getAssociationWithCallerChecks(userId, packageName, address); - mDisassociationProcessor.disassociate(association.getId()); + mAssociationStore.getFirstAssociationByAddress(userId, packageName, + address); + if (association == null) { + out.println("Association doesn't exist."); + } else { + mDisassociationProcessor.disassociate(association.getId()); + } } break; @@ -144,9 +148,7 @@ class CompanionDeviceShellCommand extends ShellCommand { final List<AssociationInfo> userAssociations = mAssociationStore.getAssociationsByUser(userId); for (AssociationInfo association : userAssociations) { - if (sanitizeWithCallerChecks(mService.getContext(), association) != null) { - mDisassociationProcessor.disassociate(association.getId()); - } + mDisassociationProcessor.disassociate(association.getId()); } } break; @@ -157,12 +159,12 @@ class CompanionDeviceShellCommand extends ShellCommand { case "simulate-device-appeared": associationId = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 0); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 0); break; case "simulate-device-disappeared": associationId = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 1); break; case "get-backup-payload": { @@ -410,10 +412,9 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" Remove an existing Association."); pw.println(" disassociate-all USER_ID"); pw.println(" Remove all Associations for a user."); - pw.println(" clear-association-memory-cache"); + pw.println(" refresh-cache"); pw.println(" Clear the in-memory association cache and reload all association "); - pw.println(" information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY."); - pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + pw.println(" information from disk. USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); pw.println(" simulate-device-appeared ASSOCIATION_ID"); pw.println(" Make CDM act as if the given companion device has appeared."); diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index a02d9f912bcd..a18776e67200 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -145,7 +145,8 @@ public class AssociationRequestsProcessor { /** * Handle incoming {@link AssociationRequest}s, sent via - * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)} + * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, + * IAssociationRequestCallback, String, int)} */ public void processNewAssociationRequest(@NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @@ -212,7 +213,8 @@ public class AssociationRequestsProcessor { // 2b.4. Send the PendingIntent back to the app. try { callback.onAssociationPending(pendingIntent); - } catch (RemoteException ignore) { } + } catch (RemoteException ignore) { + } } /** @@ -252,7 +254,8 @@ public class AssociationRequestsProcessor { // forward it back to the application via the callback. try { callback.onFailure(e.getMessage()); - } catch (RemoteException ignore) { } + } catch (RemoteException ignore) { + } return; } @@ -322,7 +325,8 @@ public class AssociationRequestsProcessor { * Enable system data sync. */ public void enableSystemDataSync(int associationId, int flags) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) .setSystemDataSyncFlags(association.getSystemDataSyncFlags() | flags).build(); mAssociationStore.updateAssociation(updated); @@ -332,12 +336,23 @@ public class AssociationRequestsProcessor { * Disable system data sync. */ public void disableSystemDataSync(int associationId, int flags) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) .setSystemDataSyncFlags(association.getSystemDataSyncFlags() & (~flags)).build(); mAssociationStore.updateAssociation(updated); } + /** + * Set association tag. + */ + public void setAssociationTag(int associationId, String tag) { + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + association = (new AssociationInfo.Builder(association)).setTag(tag).build(); + mAssociationStore.updateAssociation(association); + } + private void sendCallbackAndFinish(@Nullable AssociationInfo association, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver) { @@ -396,14 +411,14 @@ public class AssociationRequestsProcessor { // If the application already has a pending association request, that PendingIntent // will be cancelled except application wants to cancel the request by the system. return Binder.withCleanCallingIdentity(() -> - PendingIntent.getActivityAsUser( - mContext, /*requestCode */ packageUid, intent, - FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, - ActivityOptions.makeBasic() - .setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) - .toBundle(), - UserHandle.CURRENT) + PendingIntent.getActivityAsUser( + mContext, /*requestCode */ packageUid, intent, + FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, + ActivityOptions.makeBasic() + .setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .toBundle(), + UserHandle.CURRENT) ); } diff --git a/services/companion/java/com/android/server/companion/association/AssociationStore.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java index edebb55233d0..ae2b70852a35 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java @@ -18,6 +18,7 @@ package com.android.server.companion.association; import static com.android.server.companion.utils.MetricUtils.logCreateAssociation; import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation; +import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageAssociationsForPackage; import android.annotation.IntDef; import android.annotation.NonNull; @@ -26,6 +27,7 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; import android.companion.IOnAssociationsChangedListener; +import android.content.Context; import android.content.pm.UserInfo; import android.net.MacAddress; import android.os.Binder; @@ -57,21 +59,22 @@ import java.util.concurrent.Executors; @SuppressLint("LongLogTag") public class AssociationStore { - @IntDef(prefix = { "CHANGE_TYPE_" }, value = { + @IntDef(prefix = {"CHANGE_TYPE_"}, value = { CHANGE_TYPE_ADDED, CHANGE_TYPE_REMOVED, CHANGE_TYPE_UPDATED_ADDRESS_CHANGED, CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED, }) @Retention(RetentionPolicy.SOURCE) - public @interface ChangeType {} + public @interface ChangeType { + } public static final int CHANGE_TYPE_ADDED = 0; public static final int CHANGE_TYPE_REMOVED = 1; public static final int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2; public static final int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3; - /** Listener for any changes to associations. */ + /** Listener for any changes to associations. */ public interface OnChangeListener { /** * Called when there are association changes. @@ -100,25 +103,30 @@ public class AssociationStore { /** * Called when an association is added. */ - default void onAssociationAdded(AssociationInfo association) {} + default void onAssociationAdded(AssociationInfo association) { + } /** * Called when an association is removed. */ - default void onAssociationRemoved(AssociationInfo association) {} + default void onAssociationRemoved(AssociationInfo association) { + } /** * Called when an association is updated. */ - default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {} + default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) { + } } private static final String TAG = "CDM_AssociationStore"; - private final Object mLock = new Object(); - + private final Context mContext; + private final UserManager mUserManager; + private final AssociationDiskStore mDiskStore; private final ExecutorService mExecutor; + private final Object mLock = new Object(); @GuardedBy("mLock") private boolean mPersisted = false; @GuardedBy("mLock") @@ -132,10 +140,9 @@ public class AssociationStore { private final RemoteCallbackList<IOnAssociationsChangedListener> mRemoteListeners = new RemoteCallbackList<>(); - private final UserManager mUserManager; - private final AssociationDiskStore mDiskStore; - - public AssociationStore(UserManager userManager, AssociationDiskStore diskStore) { + public AssociationStore(Context context, UserManager userManager, + AssociationDiskStore diskStore) { + mContext = context; mUserManager = userManager; mDiskStore = diskStore; mExecutor = Executors.newSingleThreadExecutor(); @@ -202,7 +209,7 @@ public class AssociationStore { synchronized (mLock) { if (mIdToAssociationMap.containsKey(id)) { - Slog.e(TAG, "Association with id=[" + id + "] already exists."); + Slog.e(TAG, "Association id=[" + id + "] already exists."); return; } @@ -449,6 +456,26 @@ public class AssociationStore { } /** + * Get association by id with caller checks. + */ + @NonNull + public AssociationInfo getAssociationWithCallerChecks(int associationId) { + AssociationInfo association = getAssociationById(associationId); + if (association == null) { + throw new IllegalArgumentException( + "getAssociationWithCallerChecks() Association id=[" + associationId + + "] doesn't exist."); + } + if (checkCallerCanManageAssociationsForPackage(mContext, association.getUserId(), + association.getPackageName())) { + return association; + } + + throw new IllegalArgumentException( + "The caller can't interact with the association id=[" + associationId + "]."); + } + + /** * Register a local listener for association changes. */ public void registerLocalListener(@NonNull OnChangeListener listener) { diff --git a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java index ec8977918c56..20de1210dd9d 100644 --- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java +++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java @@ -33,18 +33,19 @@ import android.os.Binder; import android.os.UserHandle; import android.util.Slog; -import com.android.server.companion.CompanionApplicationController; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.CompanionAppBinder; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.transport.CompanionTransportManager; /** - * A class response for Association removal. + * This class responsible for disassociation. */ @SuppressLint("LongLogTag") public class DisassociationProcessor { private static final String TAG = "CDM_DisassociationProcessor"; + @NonNull private final Context mContext; @NonNull @@ -52,11 +53,11 @@ public class DisassociationProcessor { @NonNull private final PackageManagerInternal mPackageManagerInternal; @NonNull - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private final DevicePresenceProcessor mDevicePresenceMonitor; @NonNull private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; @NonNull - private final CompanionApplicationController mCompanionAppController; + private final CompanionAppBinder mCompanionAppController; @NonNull private final CompanionTransportManager mTransportManager; private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; @@ -66,8 +67,8 @@ public class DisassociationProcessor { @NonNull ActivityManager activityManager, @NonNull AssociationStore associationStore, @NonNull PackageManagerInternal packageManager, - @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor, - @NonNull CompanionApplicationController applicationController, + @NonNull DevicePresenceProcessor devicePresenceMonitor, + @NonNull CompanionAppBinder applicationController, @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, @NonNull CompanionTransportManager companionTransportManager) { mContext = context; @@ -89,11 +90,7 @@ public class DisassociationProcessor { public void disassociate(int id) { Slog.i(TAG, "Disassociating id=[" + id + "]..."); - final AssociationInfo association = mAssociationStore.getAssociationById(id); - if (association == null) { - Slog.e(TAG, "Can't disassociate id=[" + id + "]. It doesn't exist."); - return; - } + final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id); final int userId = association.getUserId(); final String packageName = association.getPackageName(); @@ -118,12 +115,12 @@ public class DisassociationProcessor { return; } + // Detach transport if exists + mTransportManager.detachSystemDataTransport(id); + // Association cleanup. - mAssociationStore.removeAssociation(association.getId()); mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); - - // Detach transport if exists - mTransportManager.detachSystemDataTransport(packageName, userId, id); + mAssociationStore.removeAssociation(association.getId()); // If role is not in use by other associations, revoke the role. // Do not need to remove the system role since it was pre-granted by the system. @@ -147,6 +144,24 @@ public class DisassociationProcessor { } } + /** + * @deprecated Use {@link #disassociate(int)} instead. + */ + @Deprecated + public void disassociate(int userId, String packageName, String macAddress) { + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, macAddress); + + if (association == null) { + throw new IllegalArgumentException( + "Association for mac address=[" + macAddress + "] doesn't exist"); + } + + mAssociationStore.getAssociationWithCallerChecks(association.getId()); + + disassociate(association.getId()); + } + @SuppressLint("MissingPermission") private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { return Binder.withCleanCallingIdentity(() -> { @@ -163,7 +178,7 @@ public class DisassociationProcessor { () -> mActivityManager.addOnUidImportanceListener( mOnPackageVisibilityChangeListener, ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE)); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e) { Slog.e(TAG, "Failed to start listening to uid importance changes."); } } diff --git a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java index f28731548dcc..b52904aa5301 100644 --- a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java +++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java @@ -22,15 +22,14 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; +import android.companion.AssociationInfo; import android.content.ComponentName; import android.content.Context; +import android.os.SystemProperties; import android.util.Slog; -import com.android.server.LocalServices; -import com.android.server.companion.CompanionDeviceManagerServiceInternal; - /** - * A Job Service responsible for clean up idle self-managed associations. + * A Job Service responsible for clean up self-managed associations if it's idle for 90 days. * * The job will be executed only if the device is charging and in idle mode due to the application * will be killed if association/role are revoked. See {@link DisassociationProcessor} @@ -41,14 +40,25 @@ public class InactiveAssociationsRemovalService extends JobService { private static final String JOB_NAMESPACE = "companion"; private static final int JOB_ID = 1; private static final long ONE_DAY_INTERVAL = DAYS.toMillis(1); + private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = + "debug.cdm.cdmservice.removal_time_window"; + private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); + + private final AssociationStore mAssociationStore; + private final DisassociationProcessor mDisassociationProcessor; + + public InactiveAssociationsRemovalService(AssociationStore associationStore, + DisassociationProcessor disassociationProcessor) { + mAssociationStore = associationStore; + mDisassociationProcessor = disassociationProcessor; + } @Override public boolean onStartJob(final JobParameters params) { Slog.i(TAG, "Execute the Association Removal job"); - // Special policy for selfManaged that need to revoke associations if the device - // does not connect for 90 days. - LocalServices.getService(CompanionDeviceManagerServiceInternal.class) - .removeInactiveSelfManagedAssociations(); + + removeIdleSelfManagedAssociations(); + jobFinished(params, false); return true; } @@ -77,4 +87,29 @@ public class InactiveAssociationsRemovalService extends JobService { .build(); jobScheduler.schedule(job); } + + /** + * Remove idle self-managed associations. + */ + public void removeIdleSelfManagedAssociations() { + final long currentTime = System.currentTimeMillis(); + long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1); + if (removalWindow <= 0) { + // 0 or negative values indicate that the sysprop was never set or should be ignored. + removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; + } + + for (AssociationInfo association : mAssociationStore.getAssociations()) { + if (!association.isSelfManaged()) continue; + + final boolean isInactive = + currentTime - association.getLastTimeConnectedMs() >= removalWindow; + if (!isInactive) continue; + + final int id = association.getId(); + + Slog.i(TAG, "Removing inactive self-managed association id=" + id); + mDisassociationProcessor.disassociate(id); + } + } } diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index c5ca0bf7e9c5..9069689ee5eb 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -31,7 +31,6 @@ import android.annotation.UserIdInt; import android.app.ActivityOptions; import android.app.PendingIntent; import android.companion.AssociationInfo; -import android.companion.DeviceNotAssociatedException; import android.companion.IOnMessageReceivedListener; import android.companion.ISystemDataTransferCallback; import android.companion.datatransfer.PermissionSyncRequest; @@ -56,7 +55,6 @@ import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.association.AssociationStore; import com.android.server.companion.transport.CompanionTransportManager; import com.android.server.companion.utils.PackageUtils; -import com.android.server.companion.utils.PermissionsUtils; import java.util.List; import java.util.concurrent.ExecutorService; @@ -120,28 +118,10 @@ public class SystemDataTransferProcessor { } /** - * Resolve the requested association, throwing if the caller doesn't have - * adequate permissions. - */ - @NonNull - private AssociationInfo resolveAssociation(String packageName, int userId, - int associationId) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association); - if (association == null) { - throw new DeviceNotAssociatedException("Association " - + associationId + " is not associated with the app " + packageName - + " for user " + userId); - } - return association; - } - - /** * Return whether the user has consented to the permission transfer for the association. */ - public boolean isPermissionTransferUserConsented(String packageName, @UserIdInt int userId, - int associationId) { - resolveAssociation(packageName, userId, associationId); + public boolean isPermissionTransferUserConsented(int associationId) { + mAssociationStore.getAssociationWithCallerChecks(associationId); PermissionSyncRequest request = getPermissionSyncRequest(associationId); if (request == null) { @@ -167,7 +147,8 @@ public class SystemDataTransferProcessor { return null; } - final AssociationInfo association = resolveAssociation(packageName, userId, associationId); + final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId + "] associationId [" + associationId + "]"); @@ -207,7 +188,7 @@ public class SystemDataTransferProcessor { Slog.i(LOG_TAG, "Start system data transfer for package [" + packageName + "] userId [" + userId + "] associationId [" + associationId + "]"); - final AssociationInfo association = resolveAssociation(packageName, userId, associationId); + mAssociationStore.getAssociationWithCallerChecks(associationId); // Check if the request has been consented by the user. PermissionSyncRequest request = getPermissionSyncRequest(associationId); @@ -239,24 +220,20 @@ public class SystemDataTransferProcessor { * Enable perm sync for the association */ public void enablePermissionsSync(int associationId) { - Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - PermissionSyncRequest request = new PermissionSyncRequest(associationId); - request.setUserConsented(true); - mSystemDataTransferRequestStore.writeRequest(userId, request); - }); + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId(); + PermissionSyncRequest request = new PermissionSyncRequest(associationId); + request.setUserConsented(true); + mSystemDataTransferRequestStore.writeRequest(userId, request); } /** * Disable perm sync for the association */ public void disablePermissionsSync(int associationId) { - Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - PermissionSyncRequest request = new PermissionSyncRequest(associationId); - request.setUserConsented(false); - mSystemDataTransferRequestStore.writeRequest(userId, request); - }); + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId(); + PermissionSyncRequest request = new PermissionSyncRequest(associationId); + request.setUserConsented(false); + mSystemDataTransferRequestStore.writeRequest(userId, request); } /** @@ -264,18 +241,17 @@ public class SystemDataTransferProcessor { */ @Nullable public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - return Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - List<SystemDataTransferRequest> requests = - mSystemDataTransferRequestStore.readRequestsByAssociationId(userId, - associationId); - for (SystemDataTransferRequest request : requests) { - if (request instanceof PermissionSyncRequest) { - return (PermissionSyncRequest) request; - } + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId) + .getUserId(); + List<SystemDataTransferRequest> requests = + mSystemDataTransferRequestStore.readRequestsByAssociationId(userId, + associationId); + for (SystemDataTransferRequest request : requests) { + if (request instanceof PermissionSyncRequest) { + return (PermissionSyncRequest) request; } - return null; - }); + } + return null; } /** diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java index c89ce11c169d..9c37881499bd 100644 --- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java +++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java @@ -33,7 +33,7 @@ import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_FIRST_MATCH; import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST; import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER; -import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG; +import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG; import static com.android.server.companion.utils.Utils.btDeviceToString; import static java.util.Objects.requireNonNull; diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java index cb363a7c9d7f..2d345c48a8eb 100644 --- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java +++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java @@ -19,7 +19,7 @@ package com.android.server.companion.presence; import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG; +import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG; import static com.android.server.companion.utils.Utils.btDeviceToString; import android.annotation.NonNull; diff --git a/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java new file mode 100644 index 000000000000..4ba4e2ce6899 --- /dev/null +++ b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.presence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.UserIdInt; +import android.companion.AssociationInfo; +import android.companion.CompanionDeviceService; +import android.companion.DevicePresenceEvent; +import android.content.ComponentName; +import android.content.Context; +import android.os.Handler; +import android.os.PowerManagerInternal; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.infra.PerUser; +import com.android.server.companion.CompanionDeviceManagerService; +import com.android.server.companion.association.AssociationStore; +import com.android.server.companion.utils.PackageUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages communication with companion applications via + * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to + * the services, maintaining the connection (the binding), and invoking callback methods such as + * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)}, + * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the + * application process. + * + * <p> + * The following is the list of the APIs provided by {@link CompanionAppBinder} (to be + * utilized by {@link CompanionDeviceManagerService}): + * <ul> + * <li> {@link #bindCompanionApplication(int, String, boolean, CompanionServiceConnector.Listener)} + * <li> {@link #unbindCompanionApplication(int, String)} + * <li> {@link #isCompanionApplicationBound(int, String)} + * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)} + * </ul> + * + * @see CompanionDeviceService + * @see android.companion.ICompanionDeviceService + * @see CompanionServiceConnector + */ +@SuppressLint("LongLogTag") +public class CompanionAppBinder { + private static final String TAG = "CDM_CompanionAppBinder"; + + private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec + + @NonNull + private final Context mContext; + @NonNull + private final AssociationStore mAssociationStore; + @NonNull + private final ObservableUuidStore mObservableUuidStore; + @NonNull + private final CompanionServicesRegister mCompanionServicesRegister; + + private final PowerManagerInternal mPowerManagerInternal; + + @NonNull + @GuardedBy("mBoundCompanionApplications") + private final AndroidPackageMap<List<CompanionServiceConnector>> + mBoundCompanionApplications; + @NonNull + @GuardedBy("mScheduledForRebindingCompanionApplications") + private final AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications; + + public CompanionAppBinder(@NonNull Context context, + @NonNull AssociationStore associationStore, + @NonNull ObservableUuidStore observableUuidStore, + @NonNull PowerManagerInternal powerManagerInternal) { + mContext = context; + mAssociationStore = associationStore; + mObservableUuidStore = observableUuidStore; + mPowerManagerInternal = powerManagerInternal; + mCompanionServicesRegister = new CompanionServicesRegister(); + mBoundCompanionApplications = new AndroidPackageMap<>(); + mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>(); + } + + /** + * On package changed. + */ + public void onPackagesChanged(@UserIdInt int userId) { + mCompanionServicesRegister.invalidate(userId); + } + + /** + * CDM binds to the companion app. + */ + public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName, + boolean isSelfManaged, CompanionServiceConnector.Listener listener) { + Slog.i(TAG, "Binding user=[" + userId + "], package=[" + packageName + "], isSelfManaged=[" + + isSelfManaged + "]..."); + + final List<ComponentName> companionServices = + mCompanionServicesRegister.forPackage(userId, packageName); + if (companionServices.isEmpty()) { + Slog.e(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": " + + "eligible CompanionDeviceService not found.\n" + + "A CompanionDeviceService should declare an intent-filter for " + + "\"android.companion.CompanionDeviceService\" action and require " + + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission."); + return; + } + + final List<CompanionServiceConnector> serviceConnectors = new ArrayList<>(); + synchronized (mBoundCompanionApplications) { + if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { + Slog.w(TAG, "The package is ALREADY bound."); + return; + } + + for (int i = 0; i < companionServices.size(); i++) { + boolean isPrimary = i == 0; + serviceConnectors.add(CompanionServiceConnector.newInstance(mContext, userId, + companionServices.get(i), isSelfManaged, isPrimary)); + } + + mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors); + } + + // Set listeners for both Primary and Secondary connectors. + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.setListener(listener); + } + + // Now "bind" all the connectors: the primary one and the rest of them. + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.connect(); + } + } + + /** + * CDM unbinds the companion app. + */ + public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) { + Slog.i(TAG, "Unbinding user=[" + userId + "], package=[" + packageName + "]..."); + + final List<CompanionServiceConnector> serviceConnectors; + + synchronized (mBoundCompanionApplications) { + serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName); + } + + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); + } + + if (serviceConnectors == null) { + Slog.e(TAG, "The package is not bound."); + return; + } + + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.postUnbind(); + } + } + + /** + * @return whether the companion application is bound now. + */ + public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) { + synchronized (mBoundCompanionApplications) { + return mBoundCompanionApplications.containsValueForPackage(userId, packageName); + } + } + + /** + * Remove bound apps for package. + */ + public void removePackage(int userId, String packageName) { + synchronized (mBoundCompanionApplications) { + mBoundCompanionApplications.removePackage(userId, packageName); + } + } + + /** + * Schedule rebinding for the package. + */ + public void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName, + CompanionServiceConnector serviceConnector) { + Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName); + + if (isRebindingCompanionApplicationScheduled(userId, packageName)) { + Slog.i(TAG, "CompanionApplication rebinding has been scheduled, skipping " + + serviceConnector.getComponentName()); + return; + } + + if (serviceConnector.isPrimary()) { + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.setValueForPackage( + userId, packageName, true); + } + } + + // Rebinding in 10 seconds. + Handler.getMain().postDelayed(() -> + onRebindingCompanionApplicationTimeout(userId, packageName, + serviceConnector), + REBIND_TIMEOUT); + } + + private boolean isRebindingCompanionApplicationScheduled( + @UserIdInt int userId, @NonNull String packageName) { + synchronized (mScheduledForRebindingCompanionApplications) { + return mScheduledForRebindingCompanionApplications.containsValueForPackage( + userId, packageName); + } + } + + private void onRebindingCompanionApplicationTimeout( + @UserIdInt int userId, @NonNull String packageName, + @NonNull CompanionServiceConnector serviceConnector) { + // Re-mark the application is bound. + if (serviceConnector.isPrimary()) { + synchronized (mBoundCompanionApplications) { + if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { + List<CompanionServiceConnector> serviceConnectors = + Collections.singletonList(serviceConnector); + mBoundCompanionApplications.setValueForPackage(userId, packageName, + serviceConnectors); + } + } + + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); + } + } + + serviceConnector.connect(); + } + + /** + * Dump bound apps. + */ + public void dump(@NonNull PrintWriter out) { + out.append("Companion Device Application Controller: \n"); + + synchronized (mBoundCompanionApplications) { + out.append(" Bound Companion Applications: "); + if (mBoundCompanionApplications.size() == 0) { + out.append("<empty>\n"); + } else { + out.append("\n"); + mBoundCompanionApplications.dump(out); + } + } + + out.append(" Companion Applications Scheduled For Rebinding: "); + synchronized (mScheduledForRebindingCompanionApplications) { + if (mScheduledForRebindingCompanionApplications.size() == 0) { + out.append("<empty>\n"); + } else { + out.append("\n"); + mScheduledForRebindingCompanionApplications.dump(out); + } + } + } + + @Nullable + CompanionServiceConnector getPrimaryServiceConnector( + @UserIdInt int userId, @NonNull String packageName) { + final List<CompanionServiceConnector> connectors; + synchronized (mBoundCompanionApplications) { + connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName); + } + return connectors != null ? connectors.get(0) : null; + } + + private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> { + @Override + public synchronized @NonNull Map<String, List<ComponentName>> forUser( + @UserIdInt int userId) { + return super.forUser(userId); + } + + synchronized @NonNull List<ComponentName> forPackage( + @UserIdInt int userId, @NonNull String packageName) { + return forUser(userId).getOrDefault(packageName, Collections.emptyList()); + } + + synchronized void invalidate(@UserIdInt int userId) { + remove(userId); + } + + @Override + protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) { + return PackageUtils.getCompanionServicesForUser(mContext, userId); + } + } + + /** + * Associates an Android package (defined by userId + packageName) with a value of type T. + */ + private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> { + + void setValueForPackage( + @UserIdInt int userId, @NonNull String packageName, @NonNull T value) { + Map<String, T> forUser = get(userId); + if (forUser == null) { + forUser = /* Map<String, T> */ new HashMap(); + put(userId, forUser); + } + + forUser.put(packageName, value); + } + + boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) { + final Map<String, ?> forUser = get(userId); + return forUser != null && forUser.containsKey(packageName); + } + + T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) { + final Map<String, T> forUser = get(userId); + return forUser != null ? forUser.get(packageName) : null; + } + + T removePackage(@UserIdInt int userId, @NonNull String packageName) { + final Map<String, T> forUser = get(userId); + if (forUser == null) return null; + return forUser.remove(packageName); + } + + void dump() { + if (size() == 0) { + Log.d(TAG, "<empty>"); + return; + } + + for (int i = 0; i < size(); i++) { + final int userId = keyAt(i); + final Map<String, T> forUser = get(userId); + if (forUser.isEmpty()) { + Log.d(TAG, "u" + userId + ": <empty>"); + } + + for (Map.Entry<String, T> packageValue : forUser.entrySet()) { + final String packageName = packageValue.getKey(); + final T value = packageValue.getValue(); + Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value); + } + } + } + + private void dump(@NonNull PrintWriter out) { + for (int i = 0; i < size(); i++) { + final int userId = keyAt(i); + final Map<String, T> forUser = get(userId); + if (forUser.isEmpty()) { + out.append(" u").append(String.valueOf(userId)).append(": <empty>\n"); + } + + for (Map.Entry<String, T> packageValue : forUser.entrySet()) { + final String packageName = packageValue.getKey(); + final T value = packageValue.getValue(); + out.append(" u").append(String.valueOf(userId)).append("\\") + .append(packageName).append(" -> ") + .append(value.toString()).append('\n'); + } + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java deleted file mode 100644 index 7a1a83f53315..000000000000 --- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java +++ /dev/null @@ -1,620 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.companion.presence; - -import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; -import static android.os.Process.ROOT_UID; -import static android.os.Process.SHELL_UID; - -import android.annotation.NonNull; -import android.annotation.SuppressLint; -import android.annotation.TestApi; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.companion.AssociationInfo; -import android.content.Context; -import android.os.Binder; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.ParcelUuid; -import android.os.UserManager; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.companion.association.AssociationStore; - -import java.io.PrintWriter; -import java.util.HashSet; -import java.util.Set; - -/** - * Class responsible for monitoring companion devices' "presence" status (i.e. - * connected/disconnected for Bluetooth devices; nearby or not for BLE devices). - * - * <p> - * Should only be used by - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * to which it provides the following API: - * <ul> - * <li> {@link #onSelfManagedDeviceConnected(int)} - * <li> {@link #onSelfManagedDeviceDisconnected(int)} - * <li> {@link #isDevicePresent(int)} - * <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)} - * <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)} - * <li> {@link Callback#onDevicePresenceEvent(int, int)}} - * </ul> - */ -@SuppressLint("LongLogTag") -public class CompanionDevicePresenceMonitor implements AssociationStore.OnChangeListener, - BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback { - static final boolean DEBUG = false; - private static final String TAG = "CDM_CompanionDevicePresenceMonitor"; - - /** Callback for notifying about changes to status of companion devices. */ - public interface Callback { - /** Invoked when companion device is found nearby or connects. */ - void onDeviceAppeared(int associationId); - - /** Invoked when a companion device no longer seen nearby or disconnects. */ - void onDeviceDisappeared(int associationId); - - /** Invoked when device has corresponding event changes. */ - void onDevicePresenceEvent(int associationId, int event); - - /** Invoked when device has corresponding event changes base on the UUID */ - void onDevicePresenceEventByUuid(ObservableUuid uuid, int event); - } - - private final @NonNull AssociationStore mAssociationStore; - private final @NonNull ObservableUuidStore mObservableUuidStore; - private final @NonNull Callback mCallback; - private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener; - private final @NonNull BleCompanionDeviceScanner mBleScanner; - - // NOTE: Same association may appear in more than one of the following sets at the same time. - // (E.g. self-managed devices that have MAC addresses, could be reported as present by their - // companion applications, while at the same be connected via BT, or detected nearby by BLE - // scanner) - private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>(); - private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>(); - private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>(); - private final @NonNull Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>(); - @GuardedBy("mBtDisconnectedDevices") - private final @NonNull Set<Integer> mBtDisconnectedDevices = new HashSet<>(); - - // A map to track device presence within 10 seconds of Bluetooth disconnection. - // The key is the association ID, and the boolean value indicates if the device - // was detected again within that time frame. - @GuardedBy("mBtDisconnectedDevices") - private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence = - new SparseBooleanArray(); - - // Tracking "simulated" presence. Used for debugging and testing only. - private final @NonNull Set<Integer> mSimulated = new HashSet<>(); - private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper = - new SimulatedDevicePresenceSchedulerHelper(); - - private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler = - new BleDeviceDisappearedScheduler(); - - public CompanionDevicePresenceMonitor(UserManager userManager, - @NonNull AssociationStore associationStore, - @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) { - mAssociationStore = associationStore; - mObservableUuidStore = observableUuidStore; - mCallback = callback; - mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager, - associationStore, mObservableUuidStore, - /* BluetoothCompanionDeviceConnectionListener.Callback */ this); - mBleScanner = new BleCompanionDeviceScanner(associationStore, - /* BleCompanionDeviceScanner.Callback */ this); - } - - /** Initialize {@link CompanionDevicePresenceMonitor} */ - public void init(Context context) { - if (DEBUG) Log.i(TAG, "init()"); - - final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); - if (btAdapter != null) { - mBtConnectionListener.init(btAdapter); - mBleScanner.init(context, btAdapter); - } else { - Log.w(TAG, "BluetoothAdapter is NOT available."); - } - - mAssociationStore.registerLocalListener(this); - } - - /** - * @return current connected UUID devices. - */ - public Set<ParcelUuid> getCurrentConnectedUuidDevices() { - return mConnectedUuidDevices; - } - - /** - * Remove current connected UUID device. - */ - public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) { - mConnectedUuidDevices.remove(uuid); - } - - /** - * @return whether the associated companion devices is present. I.e. device is nearby (for BLE); - * or devices is connected (for Bluetooth); or reported (by the application) to be - * nearby (for "self-managed" associations). - */ - public boolean isDevicePresent(int associationId) { - return mReportedSelfManagedDevices.contains(associationId) - || mConnectedBtDevices.contains(associationId) - || mNearbyBleDevices.contains(associationId) - || mSimulated.contains(associationId); - } - - /** - * @return whether the current uuid to be observed is present. - */ - public boolean isDeviceUuidPresent(ParcelUuid uuid) { - return mConnectedUuidDevices.contains(uuid); - } - - /** - * @return whether the current device is BT connected and had already reported to the app. - */ - - public boolean isBtConnected(int associationId) { - return mConnectedBtDevices.contains(associationId); - } - - /** - * @return whether the current device in BLE range and had already reported to the app. - */ - public boolean isBlePresent(int associationId) { - return mNearbyBleDevices.contains(associationId); - } - - /** - * @return whether the current device had been already reported by the simulator. - */ - public boolean isSimulatePresent(int associationId) { - return mSimulated.contains(associationId); - } - - /** - * Marks a "self-managed" device as connected. - * - * <p> - * Must ONLY be invoked by the - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * when an application invokes - * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()} - */ - public void onSelfManagedDeviceConnected(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_APPEARED); - } - - /** - * Marks a "self-managed" device as disconnected. - * - * <p> - * Must ONLY be invoked by the - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * when an application invokes - * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()} - */ - public void onSelfManagedDeviceDisconnected(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_DISAPPEARED); - } - - /** - * Marks a "self-managed" device as disconnected when binderDied. - */ - public void onSelfManagedDeviceReporterBinderDied(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_DISAPPEARED); - } - - @Override - public void onBluetoothCompanionDeviceConnected(int associationId) { - synchronized (mBtDisconnectedDevices) { - // A device is considered reconnected within 10 seconds if a pending BLE lost report is - // followed by a detected Bluetooth connection. - boolean isReconnected = mBtDisconnectedDevices.contains(associationId); - if (isReconnected) { - Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s."); - mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); - } - - Slog.i(TAG, "onBluetoothCompanionDeviceConnected: " - + "associationId( " + associationId + " )"); - onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); - - // Stop the BLE scan if all devices report BT connected status and BLE was present. - if (canStopBleScan()) { - mBleScanner.stopScanIfNeeded(); - } - - } - } - - @Override - public void onBluetoothCompanionDeviceDisconnected(int associationId) { - Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected " - + "associationId( " + associationId + " )"); - // Start BLE scanning when the device is disconnected. - mBleScanner.startScan(); - - onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED); - // If current device is BLE present but BT is disconnected , means it will be - // potentially out of range later. Schedule BLE disappeared callback. - if (isBlePresent(associationId)) { - synchronized (mBtDisconnectedDevices) { - mBtDisconnectedDevices.add(associationId); - } - mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId); - } - } - - @Override - public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) { - final ParcelUuid parcelUuid = uuid.getUuid(); - - switch(event) { - case EVENT_BT_CONNECTED: - boolean added = mConnectedUuidDevices.add(parcelUuid); - - if (!added) { - Slog.w(TAG, "Uuid= " + parcelUuid + "is ALREADY reported as " - + "present by this event=" + event); - } - - break; - case EVENT_BT_DISCONNECTED: - final boolean removed = mConnectedUuidDevices.remove(parcelUuid); - - if (!removed) { - Slog.w(TAG, "UUID= " + parcelUuid + " was NOT reported " - + "as present by this event= " + event); - - return; - } - - break; - } - - mCallback.onDevicePresenceEventByUuid(uuid, event); - } - - - @Override - public void onBleCompanionDeviceFound(int associationId) { - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); - synchronized (mBtDisconnectedDevices) { - final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId); - if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) { - mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); - } - } - } - - @Override - public void onBleCompanionDeviceLost(int associationId) { - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); - } - - /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ - @TestApi - public void simulateDeviceEvent(int associationId, int event) { - // IMPORTANT: this API should only be invoked via the - // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to - // make this call are SHELL and ROOT. - // No other caller (including SYSTEM!) should be allowed. - enforceCallerShellOrRoot(); - // Make sure the association exists. - enforceAssociationExists(associationId); - - switch (event) { - case EVENT_BLE_APPEARED: - simulateDeviceAppeared(associationId, event); - break; - case EVENT_BT_CONNECTED: - onBluetoothCompanionDeviceConnected(associationId); - break; - case EVENT_BLE_DISAPPEARED: - simulateDeviceDisappeared(associationId, event); - break; - case EVENT_BT_DISCONNECTED: - onBluetoothCompanionDeviceDisconnected(associationId); - break; - default: - throw new IllegalArgumentException("Event: " + event + "is not supported"); - } - } - - /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ - @TestApi - public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) { - // IMPORTANT: this API should only be invoked via the - // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to - // make this call are SHELL and ROOT. - // No other caller (including SYSTEM!) should be allowed. - enforceCallerShellOrRoot(); - onDevicePresenceEventByUuid(uuid, event); - } - - private void simulateDeviceAppeared(int associationId, int state) { - onDevicePresenceEvent(mSimulated, associationId, state); - mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); - } - - private void simulateDeviceDisappeared(int associationId, int state) { - mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); - onDevicePresenceEvent(mSimulated, associationId, state); - } - - private void enforceAssociationExists(int associationId) { - if (mAssociationStore.getAssociationById(associationId) == null) { - throw new IllegalArgumentException( - "Association with id " + associationId + " does not exist."); - } - } - - private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource, - int associationId, int event) { - Slog.i(TAG, "onDevicePresenceEvent() id=" + associationId + ", event=" + event); - - switch (event) { - case EVENT_BLE_APPEARED: - synchronized (mBtDisconnectedDevices) { - // If a BLE device is detected within 10 seconds after BT is disconnected, - // flag it as BLE is present. - if (mBtDisconnectedDevices.contains(associationId)) { - Slog.i(TAG, "Device ( " + associationId + " ) is present," - + " do not need to send the callback with event ( " - + EVENT_BLE_APPEARED + " )."); - mBtDisconnectedDevicesBlePresence.append(associationId, true); - } - } - case EVENT_BT_CONNECTED: - case EVENT_SELF_MANAGED_APPEARED: - final boolean added = presentDevicesForSource.add(associationId); - - if (!added) { - Slog.w(TAG, "Association with id " - + associationId + " is ALREADY reported as " - + "present by this source, event=" + event); - } - - mCallback.onDeviceAppeared(associationId); - - break; - case EVENT_BLE_DISAPPEARED: - case EVENT_BT_DISCONNECTED: - case EVENT_SELF_MANAGED_DISAPPEARED: - final boolean removed = presentDevicesForSource.remove(associationId); - - if (!removed) { - Slog.w(TAG, "Association with id " + associationId + " was NOT reported " - + "as present by this source, event= " + event); - - return; - } - - mCallback.onDeviceDisappeared(associationId); - - break; - default: - Slog.e(TAG, "Event: " + event + " is not supported"); - return; - } - - mCallback.onDevicePresenceEvent(associationId, event); - } - - /** - * Implements - * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)} - */ - @Override - public void onAssociationRemoved(@NonNull AssociationInfo association) { - final int id = association.getId(); - if (DEBUG) { - Log.i(TAG, "onAssociationRemoved() id=" + id); - Log.d(TAG, " > association=" + association); - } - - mConnectedBtDevices.remove(id); - mNearbyBleDevices.remove(id); - mReportedSelfManagedDevices.remove(id); - mSimulated.remove(id); - mBtDisconnectedDevices.remove(id); - mBtDisconnectedDevicesBlePresence.delete(id); - - // Do NOT call mCallback.onDeviceDisappeared()! - // CompanionDeviceManagerService will know that the association is removed, and will do - // what's needed. - } - - /** - * Return a set of devices that pending to report connectivity - */ - public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() { - synchronized (mBtConnectionListener.mPendingConnectedDevices) { - return mBtConnectionListener.mPendingConnectedDevices; - } - } - - private static void enforceCallerShellOrRoot() { - final int callingUid = Binder.getCallingUid(); - if (callingUid == SHELL_UID || callingUid == ROOT_UID) return; - - throw new SecurityException("Caller is neither Shell nor Root"); - } - - /** - * The BLE scan can be only stopped if all the devices have been reported - * BT connected and BLE presence and are not pending to report BLE lost. - */ - private boolean canStopBleScan() { - for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) { - int id = ai.getId(); - synchronized (mBtDisconnectedDevices) { - if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id) - && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) { - Slog.i(TAG, "The BLE scan cannot be stopped, " - + "device( " + id + " ) is not yet connected " - + "OR the BLE is not current present Or is pending to report BLE lost"); - return false; - } - } - } - return true; - } - - /** - * Dumps system information about devices that are marked as "present". - */ - public void dump(@NonNull PrintWriter out) { - out.append("Companion Device Present: "); - if (mConnectedBtDevices.isEmpty() - && mNearbyBleDevices.isEmpty() - && mReportedSelfManagedDevices.isEmpty()) { - out.append("<empty>\n"); - return; - } else { - out.append("\n"); - } - - out.append(" Connected Bluetooth Devices: "); - if (mConnectedBtDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mConnectedBtDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - - out.append(" Nearby BLE Devices: "); - if (mNearbyBleDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mNearbyBleDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - - out.append(" Self-Reported Devices: "); - if (mReportedSelfManagedDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mReportedSelfManagedDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - } - - private class SimulatedDevicePresenceSchedulerHelper extends Handler { - SimulatedDevicePresenceSchedulerHelper() { - super(Looper.getMainLooper()); - } - - void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { - // First, unschedule if it was scheduled previously. - if (hasMessages(/* what */ associationId)) { - removeMessages(/* what */ associationId); - } - - sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */); - } - - void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { - removeMessages(/* what */ associationId); - } - - @Override - public void handleMessage(@NonNull Message msg) { - final int associationId = msg.what; - if (mSimulated.contains(associationId)) { - onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED); - } - } - } - - private class BleDeviceDisappearedScheduler extends Handler { - BleDeviceDisappearedScheduler() { - super(Looper.getMainLooper()); - } - - void scheduleBleDeviceDisappeared(int associationId) { - if (hasMessages(associationId)) { - removeMessages(associationId); - } - Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " )."); - sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */); - } - - void unScheduleDeviceDisappeared(int associationId) { - if (hasMessages(associationId)) { - Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )"); - synchronized (mBtDisconnectedDevices) { - mBtDisconnectedDevices.remove(associationId); - mBtDisconnectedDevicesBlePresence.delete(associationId); - } - - removeMessages(associationId); - } - } - - @Override - public void handleMessage(@NonNull Message msg) { - final int associationId = msg.what; - synchronized (mBtDisconnectedDevices) { - final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get( - associationId); - // If a device hasn't reported after 10 seconds and is not currently present, - // assume BLE is lost and trigger the onDeviceEvent callback with the - // EVENT_BLE_DISAPPEARED event. - if (mBtDisconnectedDevices.contains(associationId) - && !isCurrentPresent) { - Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, " - + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )"); - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); - } - - mBtDisconnectedDevices.remove(associationId); - mBtDisconnectedDevicesBlePresence.delete(associationId); - } - } - } -} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java index 5abdb42b34fc..c01c3195e04d 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java +++ b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.companion; +package com.android.server.companion.presence; import static android.content.Context.BIND_ALMOST_PERCEPTIBLE; import static android.content.Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE; @@ -33,36 +33,42 @@ import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; -import android.util.Log; +import android.util.Slog; import com.android.internal.infra.ServiceConnector; import com.android.server.ServiceThread; +import com.android.server.companion.CompanionDeviceManagerService; /** * Manages a connection (binding) to an instance of {@link CompanionDeviceService} running in the * application process. */ @SuppressLint("LongLogTag") -class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> { - private static final String TAG = "CDM_CompanionServiceConnector"; - private static final boolean DEBUG = false; +public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> { - /* Unbinding before executing the callbacks can cause problems. Wait 5-seconds before unbind. */ - private static final long UNBIND_POST_DELAY_MS = 5_000; - - /** Listener for changes to the state of the {@link CompanionDeviceServiceConnector} */ - interface Listener { + /** Listener for changes to the state of the {@link CompanionServiceConnector} */ + public interface Listener { + /** + * Called when service binding is died. + */ void onBindingDied(@UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector); + @NonNull CompanionServiceConnector serviceConnector); } - private final @UserIdInt int mUserId; - private final @NonNull ComponentName mComponentName; + private static final String TAG = "CDM_CompanionServiceConnector"; + + /* Unbinding before executing the callbacks can cause problems. Wait 5-seconds before unbind. */ + private static final long UNBIND_POST_DELAY_MS = 5_000; + @UserIdInt + private final int mUserId; + @NonNull + private final ComponentName mComponentName; + private final boolean mIsPrimary; // IMPORTANT: this can (and will!) be null (at the moment, CompanionApplicationController only // installs a listener to the primary ServiceConnector), hence we should always null-check the // reference before calling on it. - private @Nullable Listener mListener; - private boolean mIsPrimary; + @Nullable + private Listener mListener; /** * Create a CompanionDeviceServiceConnector instance. @@ -79,16 +85,16 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * IMPORTANCE_FOREGROUND_SERVICE = 125. In order to kill the one time permission session, the * service importance level should be higher than 125. */ - static CompanionDeviceServiceConnector newInstance(@NonNull Context context, + static CompanionServiceConnector newInstance(@NonNull Context context, @UserIdInt int userId, @NonNull ComponentName componentName, boolean isSelfManaged, boolean isPrimary) { final int bindingFlags = isSelfManaged ? BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE : BIND_ALMOST_PERCEPTIBLE; - return new CompanionDeviceServiceConnector( + return new CompanionServiceConnector( context, userId, componentName, bindingFlags, isPrimary); } - private CompanionDeviceServiceConnector(@NonNull Context context, @UserIdInt int userId, + private CompanionServiceConnector(@NonNull Context context, @UserIdInt int userId, @NonNull ComponentName componentName, int bindingFlags, boolean isPrimary) { super(context, buildIntent(componentName), bindingFlags, userId, null); mUserId = userId; @@ -133,6 +139,7 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe return mIsPrimary; } + @NonNull ComponentName getComponentName() { return mComponentName; } @@ -140,17 +147,15 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe @Override protected void onServiceConnectionStatusChanged( @NonNull ICompanionDeviceService service, boolean isConnected) { - if (DEBUG) { - Log.d(TAG, "onServiceConnection_StatusChanged() " + mComponentName.toShortString() - + " connected=" + isConnected); - } + Slog.d(TAG, "onServiceConnectionStatusChanged() " + mComponentName.toShortString() + + " connected=" + isConnected); } @Override public void binderDied() { super.binderDied(); - if (DEBUG) Log.d(TAG, "binderDied() " + mComponentName.toShortString()); + Slog.d(TAG, "binderDied() " + mComponentName.toShortString()); // Handle primary process being killed if (mListener != null) { @@ -172,7 +177,8 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * within system_server and thus tends to get heavily congested) */ @Override - protected @NonNull Handler getJobHandler() { + @NonNull + protected Handler getJobHandler() { return getServiceThread().getThreadHandler(); } @@ -182,12 +188,14 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe return -1; } - private static @NonNull Intent buildIntent(@NonNull ComponentName componentName) { + @NonNull + private static Intent buildIntent(@NonNull ComponentName componentName) { return new Intent(CompanionDeviceService.SERVICE_INTERFACE) .setComponent(componentName); } - private static @NonNull ServiceThread getServiceThread() { + @NonNull + private static ServiceThread getServiceThread() { if (sServiceThread == null) { synchronized (CompanionDeviceManagerService.class) { if (sServiceThread == null) { @@ -206,5 +214,6 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * <p> * Do NOT reference directly, use {@link #getServiceThread()} method instead. */ - private static volatile @Nullable ServiceThread sServiceThread; + @Nullable + private static volatile ServiceThread sServiceThread; } diff --git a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java new file mode 100644 index 000000000000..2a933a8340c6 --- /dev/null +++ b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java @@ -0,0 +1,1042 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.presence; + +import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; +import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; +import static android.companion.DevicePresenceEvent.NO_ASSOCIATION; +import static android.os.Process.ROOT_UID; +import static android.os.Process.SHELL_UID; + +import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; +import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObserveDevicePresenceByUuid; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.TestApi; +import android.annotation.UserIdInt; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.companion.AssociationInfo; +import android.companion.DeviceNotAssociatedException; +import android.companion.DevicePresenceEvent; +import android.companion.ObservingDevicePresenceRequest; +import android.content.Context; +import android.hardware.power.Mode; +import android.os.Binder; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelUuid; +import android.os.PowerManagerInternal; +import android.os.RemoteException; +import android.os.UserManager; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.companion.association.AssociationStore; + +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class responsible for monitoring companion devices' "presence" status (i.e. + * connected/disconnected for Bluetooth devices; nearby or not for BLE devices). + * + * <p> + * Should only be used by + * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} + * to which it provides the following API: + * <ul> + * <li> {@link #onSelfManagedDeviceConnected(int)} + * <li> {@link #onSelfManagedDeviceDisconnected(int)} + * <li> {@link #isDevicePresent(int)} + * </ul> + */ +@SuppressLint("LongLogTag") +public class DevicePresenceProcessor implements AssociationStore.OnChangeListener, + BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback { + static final boolean DEBUG = false; + private static final String TAG = "CDM_DevicePresenceProcessor"; + + @NonNull + private final Context mContext; + @NonNull + private final CompanionAppBinder mCompanionAppBinder; + @NonNull + private final AssociationStore mAssociationStore; + @NonNull + private final ObservableUuidStore mObservableUuidStore; + @NonNull + private final BluetoothCompanionDeviceConnectionListener mBtConnectionListener; + @NonNull + private final BleCompanionDeviceScanner mBleScanner; + @NonNull + private final PowerManagerInternal mPowerManagerInternal; + + // NOTE: Same association may appear in more than one of the following sets at the same time. + // (E.g. self-managed devices that have MAC addresses, could be reported as present by their + // companion applications, while at the same be connected via BT, or detected nearby by BLE + // scanner) + @NonNull + private final Set<Integer> mConnectedBtDevices = new HashSet<>(); + @NonNull + private final Set<Integer> mNearbyBleDevices = new HashSet<>(); + @NonNull + private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>(); + @NonNull + private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>(); + @NonNull + @GuardedBy("mBtDisconnectedDevices") + private final Set<Integer> mBtDisconnectedDevices = new HashSet<>(); + + // A map to track device presence within 10 seconds of Bluetooth disconnection. + // The key is the association ID, and the boolean value indicates if the device + // was detected again within that time frame. + @GuardedBy("mBtDisconnectedDevices") + private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence = + new SparseBooleanArray(); + + // Tracking "simulated" presence. Used for debugging and testing only. + private final @NonNull Set<Integer> mSimulated = new HashSet<>(); + private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper = + new SimulatedDevicePresenceSchedulerHelper(); + + private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler = + new BleDeviceDisappearedScheduler(); + + public DevicePresenceProcessor(@NonNull Context context, + @NonNull CompanionAppBinder companionAppBinder, + UserManager userManager, + @NonNull AssociationStore associationStore, + @NonNull ObservableUuidStore observableUuidStore, + @NonNull PowerManagerInternal powerManagerInternal) { + mContext = context; + mCompanionAppBinder = companionAppBinder; + mAssociationStore = associationStore; + mObservableUuidStore = observableUuidStore; + mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager, + associationStore, mObservableUuidStore, + /* BluetoothCompanionDeviceConnectionListener.Callback */ this); + mBleScanner = new BleCompanionDeviceScanner(associationStore, + /* BleCompanionDeviceScanner.Callback */ this); + mPowerManagerInternal = powerManagerInternal; + } + + /** Initialize {@link DevicePresenceProcessor} */ + public void init(Context context) { + if (DEBUG) Slog.i(TAG, "init()"); + + final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); + if (btAdapter != null) { + mBtConnectionListener.init(btAdapter); + mBleScanner.init(context, btAdapter); + } else { + Slog.w(TAG, "BluetoothAdapter is NOT available."); + } + + mAssociationStore.registerLocalListener(this); + } + + /** + * Process device presence start request. + */ + public void startObservingDevicePresence(ObservingDevicePresenceRequest request, + String packageName, int userId) { + Slog.i(TAG, + "Start observing request=[" + request + "] for userId=[" + userId + "], package=[" + + packageName + "]..."); + final ParcelUuid requestUuid = request.getUuid(); + + if (requestUuid != null) { + enforceCallerCanObserveDevicePresenceByUuid(mContext); + + // If it's already being observed, then no-op. + if (mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) { + Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=[" + + userId + "] is already being observed."); + return; + } + + final ObservableUuid observableUuid = new ObservableUuid(userId, requestUuid, + packageName, System.currentTimeMillis()); + mObservableUuidStore.writeObservableUuid(userId, observableUuid); + } else { + final int associationId = request.getAssociationId(); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + + // If it's already being observed, then no-op. + if (association.isNotifyOnDeviceNearby()) { + Slog.i(TAG, "Associated device id=[" + association.getId() + + "] is already being observed. No-op."); + return; + } + + association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(true) + .build(); + mAssociationStore.updateAssociation(association); + + // Send callback immediately if the device is present. + if (isDevicePresent(associationId)) { + Slog.i(TAG, "Device is already present. Triggering callback."); + if (isBlePresent(associationId)) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); + } else if (isBtConnected(associationId)) { + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); + } else if (isSimulatePresent(associationId)) { + onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_APPEARED); + } + } + } + + Slog.i(TAG, "Registered device presence listener."); + } + + /** + * Process device presence stop request. + */ + public void stopObservingDevicePresence(ObservingDevicePresenceRequest request, + String packageName, int userId) { + Slog.i(TAG, + "Stop observing request=[" + request + "] for userId=[" + userId + "], package=[" + + packageName + "]..."); + + final ParcelUuid requestUuid = request.getUuid(); + + if (requestUuid != null) { + enforceCallerCanObserveDevicePresenceByUuid(mContext); + + if (!mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) { + Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=[" + + userId + "] is already not being observed."); + return; + } + + mObservableUuidStore.removeObservableUuid(userId, requestUuid, packageName); + removeCurrentConnectedUuidDevice(requestUuid); + } else { + final int associationId = request.getAssociationId(); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + + // If it's already being observed, then no-op. + if (!association.isNotifyOnDeviceNearby()) { + Slog.i(TAG, "Associated device id=[" + association.getId() + + "] is already not being observed. No-op."); + return; + } + + association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(false) + .build(); + mAssociationStore.updateAssociation(association); + } + + Slog.i(TAG, "Unregistered device presence listener."); + + // If last listener is unregistered, then unbind application. + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApplication(userId, packageName); + } + } + + /** + * For legacy device presence below Android V. + * + * @deprecated Use {@link #startObservingDevicePresence(ObservingDevicePresenceRequest, String, + * int)} + */ + @Deprecated + public void startObservingDevicePresence(int userId, String packageName, String deviceAddress) + throws RemoteException { + Slog.i(TAG, + "Start observing device=[" + deviceAddress + "] for userId=[" + userId + + "], package=[" + + packageName + "]..."); + + enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null); + + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, deviceAddress); + + if (association == null) { + throw new RemoteException(new DeviceNotAssociatedException("App " + packageName + + " is not associated with device " + deviceAddress + + " for user " + userId)); + } + + startObservingDevicePresence( + new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId()) + .build(), packageName, userId); + } + + /** + * For legacy device presence below Android V. + * + * @deprecated Use {@link #stopObservingDevicePresence(ObservingDevicePresenceRequest, String, + * int)} + */ + @Deprecated + public void stopObservingDevicePresence(int userId, String packageName, String deviceAddress) + throws RemoteException { + Slog.i(TAG, + "Stop observing device=[" + deviceAddress + "] for userId=[" + userId + + "], package=[" + + packageName + "]..."); + + enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null); + + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, deviceAddress); + + if (association == null) { + throw new RemoteException(new DeviceNotAssociatedException("App " + packageName + + " is not associated with device " + deviceAddress + + " for user " + userId)); + } + + stopObservingDevicePresence( + new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId()) + .build(), packageName, userId); + } + + /** + * @return whether the package should be bound (i.e. at least one of the devices associated with + * the package is currently present OR the UUID to be observed by this package is + * currently present). + */ + private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) { + final List<AssociationInfo> packageAssociations = + mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + final List<ObservableUuid> observableUuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + + for (AssociationInfo association : packageAssociations) { + if (!association.shouldBindWhenPresent()) continue; + if (isDevicePresent(association.getId())) return true; + } + + for (ObservableUuid uuid : observableUuids) { + if (isDeviceUuidPresent(uuid.getUuid())) { + return true; + } + } + + return false; + } + + /** + * Bind the system to the app if it's not bound. + * + * Set bindImportant to true when the association is self-managed to avoid the target service + * being killed. + */ + private void bindApplicationIfNeeded(int userId, String packageName, boolean bindImportant) { + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + mCompanionAppBinder.bindCompanionApplication( + userId, packageName, bindImportant, this::onBinderDied); + } else { + Slog.i(TAG, + "UserId=[" + userId + "], packageName=[" + packageName + "] is already bound."); + } + } + + /** + * @return current connected UUID devices. + */ + public Set<ParcelUuid> getCurrentConnectedUuidDevices() { + return mConnectedUuidDevices; + } + + /** + * Remove current connected UUID device. + */ + public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) { + mConnectedUuidDevices.remove(uuid); + } + + /** + * @return whether the associated companion devices is present. I.e. device is nearby (for BLE); + * or devices is connected (for Bluetooth); or reported (by the application) to be + * nearby (for "self-managed" associations). + */ + public boolean isDevicePresent(int associationId) { + return mReportedSelfManagedDevices.contains(associationId) + || mConnectedBtDevices.contains(associationId) + || mNearbyBleDevices.contains(associationId) + || mSimulated.contains(associationId); + } + + /** + * @return whether the current uuid to be observed is present. + */ + public boolean isDeviceUuidPresent(ParcelUuid uuid) { + return mConnectedUuidDevices.contains(uuid); + } + + /** + * @return whether the current device is BT connected and had already reported to the app. + */ + + public boolean isBtConnected(int associationId) { + return mConnectedBtDevices.contains(associationId); + } + + /** + * @return whether the current device in BLE range and had already reported to the app. + */ + public boolean isBlePresent(int associationId) { + return mNearbyBleDevices.contains(associationId); + } + + /** + * @return whether the current device had been already reported by the simulator. + */ + public boolean isSimulatePresent(int associationId) { + return mSimulated.contains(associationId); + } + + /** + * Marks a "self-managed" device as connected. + * + * <p> + * Must ONLY be invoked by the + * {@link com.android.server.companion.CompanionDeviceManagerService + * CompanionDeviceManagerService} + * when an application invokes + * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) + * notifyDeviceAppeared()} + */ + public void onSelfManagedDeviceConnected(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_APPEARED); + } + + /** + * Marks a "self-managed" device as disconnected. + * + * <p> + * Must ONLY be invoked by the + * {@link com.android.server.companion.CompanionDeviceManagerService + * CompanionDeviceManagerService} + * when an application invokes + * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) + * notifyDeviceDisappeared()} + */ + public void onSelfManagedDeviceDisconnected(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_DISAPPEARED); + } + + /** + * Marks a "self-managed" device as disconnected when binderDied. + */ + public void onSelfManagedDeviceReporterBinderDied(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_DISAPPEARED); + } + + @Override + public void onBluetoothCompanionDeviceConnected(int associationId) { + synchronized (mBtDisconnectedDevices) { + // A device is considered reconnected within 10 seconds if a pending BLE lost report is + // followed by a detected Bluetooth connection. + boolean isReconnected = mBtDisconnectedDevices.contains(associationId); + if (isReconnected) { + Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s."); + mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); + } + + Slog.i(TAG, "onBluetoothCompanionDeviceConnected: " + + "associationId( " + associationId + " )"); + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); + + // Stop the BLE scan if all devices report BT connected status and BLE was present. + if (canStopBleScan()) { + mBleScanner.stopScanIfNeeded(); + } + + } + } + + @Override + public void onBluetoothCompanionDeviceDisconnected(int associationId) { + Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected " + + "associationId( " + associationId + " )"); + // Start BLE scanning when the device is disconnected. + mBleScanner.startScan(); + + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED); + // If current device is BLE present but BT is disconnected , means it will be + // potentially out of range later. Schedule BLE disappeared callback. + if (isBlePresent(associationId)) { + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.add(associationId); + } + mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId); + } + } + + + @Override + public void onBleCompanionDeviceFound(int associationId) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); + synchronized (mBtDisconnectedDevices) { + final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId); + if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) { + mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); + } + } + } + + @Override + public void onBleCompanionDeviceLost(int associationId) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); + } + + /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ + @TestApi + public void simulateDeviceEvent(int associationId, int event) { + // IMPORTANT: this API should only be invoked via the + // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to + // make this call are SHELL and ROOT. + // No other caller (including SYSTEM!) should be allowed. + enforceCallerShellOrRoot(); + // Make sure the association exists. + enforceAssociationExists(associationId); + + switch (event) { + case EVENT_BLE_APPEARED: + simulateDeviceAppeared(associationId, event); + break; + case EVENT_BT_CONNECTED: + onBluetoothCompanionDeviceConnected(associationId); + break; + case EVENT_BLE_DISAPPEARED: + simulateDeviceDisappeared(associationId, event); + break; + case EVENT_BT_DISCONNECTED: + onBluetoothCompanionDeviceDisconnected(associationId); + break; + default: + throw new IllegalArgumentException("Event: " + event + "is not supported"); + } + } + + /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ + @TestApi + public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) { + // IMPORTANT: this API should only be invoked via the + // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to + // make this call are SHELL and ROOT. + // No other caller (including SYSTEM!) should be allowed. + enforceCallerShellOrRoot(); + onDevicePresenceEventByUuid(uuid, event); + } + + private void simulateDeviceAppeared(int associationId, int state) { + onDevicePresenceEvent(mSimulated, associationId, state); + mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); + } + + private void simulateDeviceDisappeared(int associationId, int state) { + mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); + onDevicePresenceEvent(mSimulated, associationId, state); + } + + private void enforceAssociationExists(int associationId) { + if (mAssociationStore.getAssociationById(associationId) == null) { + throw new IllegalArgumentException( + "Association with id " + associationId + " does not exist."); + } + } + + private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource, + int associationId, int eventType) { + Slog.i(TAG, + "onDevicePresenceEvent() id=[" + associationId + "], event=[" + eventType + "]..."); + + AssociationInfo association = mAssociationStore.getAssociationById(associationId); + if (association == null) { + Slog.e(TAG, "Association doesn't exist."); + return; + } + + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + final DevicePresenceEvent event = new DevicePresenceEvent(associationId, eventType, null); + + if (eventType == EVENT_BLE_APPEARED) { + synchronized (mBtDisconnectedDevices) { + // If a BLE device is detected within 10 seconds after BT is disconnected, + // flag it as BLE is present. + if (mBtDisconnectedDevices.contains(associationId)) { + Slog.i(TAG, "Device ( " + associationId + " ) is present," + + " do not need to send the callback with event ( " + + EVENT_BLE_APPEARED + " )."); + mBtDisconnectedDevicesBlePresence.append(associationId, true); + } + } + } + + switch (eventType) { + case EVENT_BLE_APPEARED: + case EVENT_BT_CONNECTED: + case EVENT_SELF_MANAGED_APPEARED: + final boolean added = presentDevicesForSource.add(associationId); + if (!added) { + Slog.w(TAG, "The association is already present."); + } + + if (association.shouldBindWhenPresent()) { + bindApplicationIfNeeded(userId, packageName, association.isSelfManaged()); + } else { + return; + } + + if (association.isSelfManaged() || added) { + notifyDevicePresenceEvent(userId, packageName, event); + // Also send the legacy callback. + legacyNotifyDevicePresenceEvent(association, true); + } + break; + case EVENT_BLE_DISAPPEARED: + case EVENT_BT_DISCONNECTED: + case EVENT_SELF_MANAGED_DISAPPEARED: + final boolean removed = presentDevicesForSource.remove(associationId); + if (!removed) { + Slog.w(TAG, "The association is already NOT present."); + } + + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + Slog.e(TAG, "Package is not bound"); + return; + } + + if (association.isSelfManaged() || removed) { + notifyDevicePresenceEvent(userId, packageName, event); + // Also send the legacy callback. + legacyNotifyDevicePresenceEvent(association, false); + } + + // Check if there are other devices associated to the app that are present. + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApplication(userId, packageName); + } + break; + default: + Slog.e(TAG, "Event: " + eventType + " is not supported."); + break; + } + } + + @Override + public void onDevicePresenceEventByUuid(ObservableUuid uuid, int eventType) { + Slog.i(TAG, "onDevicePresenceEventByUuid ObservableUuid=[" + uuid + "], event=[" + eventType + + "]..."); + + final ParcelUuid parcelUuid = uuid.getUuid(); + final String packageName = uuid.getPackageName(); + final int userId = uuid.getUserId(); + final DevicePresenceEvent event = new DevicePresenceEvent(NO_ASSOCIATION, eventType, + parcelUuid); + + switch (eventType) { + case EVENT_BT_CONNECTED: + boolean added = mConnectedUuidDevices.add(parcelUuid); + if (!added) { + Slog.w(TAG, "This device is already connected."); + } + + bindApplicationIfNeeded(userId, packageName, false); + + notifyDevicePresenceEvent(userId, packageName, event); + break; + case EVENT_BT_DISCONNECTED: + final boolean removed = mConnectedUuidDevices.remove(parcelUuid); + if (!removed) { + Slog.w(TAG, "This device is already disconnected."); + return; + } + + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + Slog.e(TAG, "Package is not bound."); + return; + } + + notifyDevicePresenceEvent(userId, packageName, event); + + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApplication(userId, packageName); + } + break; + default: + Slog.e(TAG, "Event: " + eventType + " is not supported"); + break; + } + } + + /** + * Notify device presence event to the app. + * + * @deprecated Use {@link #notifyDevicePresenceEvent(int, String, DevicePresenceEvent)} instead. + */ + @Deprecated + private void legacyNotifyDevicePresenceEvent(AssociationInfo association, + boolean isAppeared) { + Slog.i(TAG, "legacyNotifyDevicePresenceEvent() association=[" + association.toShortString() + + "], isAppeared=[" + isAppeared + "]"); + + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + + final CompanionServiceConnector primaryServiceConnector = + mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName); + if (primaryServiceConnector == null) { + Slog.e(TAG, "Package is not bound."); + return; + } + + if (isAppeared) { + primaryServiceConnector.postOnDeviceAppeared(association); + } else { + primaryServiceConnector.postOnDeviceDisappeared(association); + } + } + + /** + * Notify the device presence event to the app. + */ + private void notifyDevicePresenceEvent(int userId, String packageName, + DevicePresenceEvent event) { + Slog.i(TAG, + "notifyCompanionDevicePresenceEvent userId=[" + userId + "], packageName=[" + + packageName + "], event=[" + event + "]..."); + + final CompanionServiceConnector primaryServiceConnector = + mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName); + + if (primaryServiceConnector == null) { + Slog.e(TAG, "Package is NOT bound."); + return; + } + + primaryServiceConnector.postOnDevicePresenceEvent(event); + } + + /** + * Notify the self-managed device presence event to the app. + */ + public void notifySelfManagedDevicePresenceEvent(int associationId, boolean isAppeared) { + Slog.i(TAG, "notifySelfManagedDeviceAppeared() id=" + associationId); + + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + if (!association.isSelfManaged()) { + throw new IllegalArgumentException("Association id=[" + associationId + + "] is not self-managed."); + } + // AssociationInfo class is immutable: create a new AssociationInfo object with updated + // timestamp. + association = (new AssociationInfo.Builder(association)) + .setLastTimeConnected(System.currentTimeMillis()) + .build(); + mAssociationStore.updateAssociation(association); + + if (isAppeared) { + onSelfManagedDeviceConnected(associationId); + } else { + onSelfManagedDeviceDisconnected(associationId); + } + + final String deviceProfile = association.getDeviceProfile(); + if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { + Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile); + mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, isAppeared); + } + } + + private void onBinderDied(@UserIdInt int userId, @NonNull String packageName, + @NonNull CompanionServiceConnector serviceConnector) { + + boolean isPrimary = serviceConnector.isPrimary(); + Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary); + + // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY. + if (isPrimary) { + final List<AssociationInfo> associations = + mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + + for (AssociationInfo association : associations) { + final String deviceProfile = association.getDeviceProfile(); + if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { + Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); + mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); + break; + } + } + + mCompanionAppBinder.removePackage(userId, packageName); + } + + // Second: schedule rebinding if needed. + final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary); + + if (shouldScheduleRebind) { + mCompanionAppBinder.scheduleRebinding(userId, packageName, serviceConnector); + } + } + + /** + * Check if the system should rebind the self-managed secondary services + * OR non-self-managed services. + */ + private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) { + // Make sure do not schedule rebind for the case ServiceConnector still gets callback after + // app is uninstalled. + boolean stillAssociated = false; + // Make sure to clean up the state for all the associations + // that associate with this package. + boolean shouldScheduleRebind = false; + boolean shouldScheduleRebindForUuid = false; + final List<ObservableUuid> uuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + + for (AssociationInfo ai : + mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) { + final int associationId = ai.getId(); + stillAssociated = true; + if (ai.isSelfManaged()) { + // Do not rebind if primary one is died for selfManaged application. + if (isPrimary && isDevicePresent(associationId)) { + onSelfManagedDeviceReporterBinderDied(associationId); + shouldScheduleRebind = false; + } + // Do not rebind if both primary and secondary services are died for + // selfManaged application. + shouldScheduleRebind = mCompanionAppBinder.isCompanionApplicationBound(userId, + packageName); + } else if (ai.isNotifyOnDeviceNearby()) { + // Always rebind for non-selfManaged devices. + shouldScheduleRebind = true; + } + } + + for (ObservableUuid uuid : uuids) { + if (isDeviceUuidPresent(uuid.getUuid())) { + shouldScheduleRebindForUuid = true; + break; + } + } + + return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid; + } + + /** + * Implements + * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)} + */ + @Override + public void onAssociationRemoved(@NonNull AssociationInfo association) { + final int id = association.getId(); + if (DEBUG) { + Log.i(TAG, "onAssociationRemoved() id=" + id); + Log.d(TAG, " > association=" + association); + } + + mConnectedBtDevices.remove(id); + mNearbyBleDevices.remove(id); + mReportedSelfManagedDevices.remove(id); + mSimulated.remove(id); + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.remove(id); + mBtDisconnectedDevicesBlePresence.delete(id); + } + + // Do NOT call mCallback.onDeviceDisappeared()! + // CompanionDeviceManagerService will know that the association is removed, and will do + // what's needed. + } + + /** + * Return a set of devices that pending to report connectivity + */ + public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() { + synchronized (mBtConnectionListener.mPendingConnectedDevices) { + return mBtConnectionListener.mPendingConnectedDevices; + } + } + + private static void enforceCallerShellOrRoot() { + final int callingUid = Binder.getCallingUid(); + if (callingUid == SHELL_UID || callingUid == ROOT_UID) return; + + throw new SecurityException("Caller is neither Shell nor Root"); + } + + /** + * The BLE scan can be only stopped if all the devices have been reported + * BT connected and BLE presence and are not pending to report BLE lost. + */ + private boolean canStopBleScan() { + for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) { + int id = ai.getId(); + synchronized (mBtDisconnectedDevices) { + if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id) + && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) { + Slog.i(TAG, "The BLE scan cannot be stopped, " + + "device( " + id + " ) is not yet connected " + + "OR the BLE is not current present Or is pending to report BLE lost"); + return false; + } + } + } + return true; + } + + /** + * Dumps system information about devices that are marked as "present". + */ + public void dump(@NonNull PrintWriter out) { + out.append("Companion Device Present: "); + if (mConnectedBtDevices.isEmpty() + && mNearbyBleDevices.isEmpty() + && mReportedSelfManagedDevices.isEmpty()) { + out.append("<empty>\n"); + return; + } else { + out.append("\n"); + } + + out.append(" Connected Bluetooth Devices: "); + if (mConnectedBtDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mConnectedBtDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + + out.append(" Nearby BLE Devices: "); + if (mNearbyBleDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mNearbyBleDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + + out.append(" Self-Reported Devices: "); + if (mReportedSelfManagedDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mReportedSelfManagedDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + } + + private class SimulatedDevicePresenceSchedulerHelper extends Handler { + SimulatedDevicePresenceSchedulerHelper() { + super(Looper.getMainLooper()); + } + + void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { + // First, unschedule if it was scheduled previously. + if (hasMessages(/* what */ associationId)) { + removeMessages(/* what */ associationId); + } + + sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */); + } + + void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { + removeMessages(/* what */ associationId); + } + + @Override + public void handleMessage(@NonNull Message msg) { + final int associationId = msg.what; + if (mSimulated.contains(associationId)) { + onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED); + } + } + } + + private class BleDeviceDisappearedScheduler extends Handler { + BleDeviceDisappearedScheduler() { + super(Looper.getMainLooper()); + } + + void scheduleBleDeviceDisappeared(int associationId) { + if (hasMessages(associationId)) { + removeMessages(associationId); + } + Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " )."); + sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */); + } + + void unScheduleDeviceDisappeared(int associationId) { + if (hasMessages(associationId)) { + Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )"); + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.remove(associationId); + mBtDisconnectedDevicesBlePresence.delete(associationId); + } + + removeMessages(associationId); + } + } + + @Override + public void handleMessage(@NonNull Message msg) { + final int associationId = msg.what; + synchronized (mBtDisconnectedDevices) { + final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get( + associationId); + // If a device hasn't reported after 10 seconds and is not currently present, + // assume BLE is lost and trigger the onDeviceEvent callback with the + // EVENT_BLE_DISAPPEARED event. + if (mBtDisconnectedDevices.contains(associationId) + && !isCurrentPresent) { + Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, " + + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )"); + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); + } + + mBtDisconnectedDevices.remove(associationId); + mBtDisconnectedDevicesBlePresence.delete(associationId); + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java index db15da2922cf..fa0f6bd92acb 100644 --- a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java +++ b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java @@ -300,4 +300,18 @@ public class ObservableUuidStore { return readObservableUuidsFromCache(userId); } } + + /** + * Check if a UUID is being observed by the package. + */ + public boolean isUuidBeingObserved(ParcelUuid uuid, int userId, String packageName) { + final List<ObservableUuid> uuidsBeingObserved = getObservableUuidsForPackage(userId, + packageName); + for (ObservableUuid observableUuid : uuidsBeingObserved) { + if (observableUuid.getUuid().equals(uuid)) { + return true; + } + } + return false; + } } diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 793fb7ff74b1..697ef87b5a12 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -46,7 +46,6 @@ import java.util.concurrent.Future; @SuppressLint("LongLogTag") public class CompanionTransportManager { private static final String TAG = "CDM_CompanionTransportManager"; - private static final boolean DEBUG = false; private boolean mSecureTransportEnabled = true; @@ -137,11 +136,17 @@ public class CompanionTransportManager { } } - public void attachSystemDataTransport(String packageName, int userId, int associationId, - ParcelFileDescriptor fd) { + /** + * Attach transport. + */ + public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) { + Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]..."); + + mAssociationStore.getAssociationWithCallerChecks(associationId); + synchronized (mTransports) { if (mTransports.contains(associationId)) { - detachSystemDataTransport(packageName, userId, associationId); + detachSystemDataTransport(associationId); } // TODO: Implement new API to pass a PSK @@ -149,9 +154,18 @@ public class CompanionTransportManager { notifyOnTransportsChanged(); } + + Slog.i(TAG, "Transport attached."); } - public void detachSystemDataTransport(String packageName, int userId, int associationId) { + /** + * Detach transport. + */ + public void detachSystemDataTransport(int associationId) { + Slog.i(TAG, "Detaching transport for association id=[" + associationId + "]..."); + + mAssociationStore.getAssociationWithCallerChecks(associationId); + synchronized (mTransports) { final Transport transport = mTransports.removeReturnOld(associationId); if (transport == null) { @@ -161,6 +175,8 @@ public class CompanionTransportManager { transport.stop(); notifyOnTransportsChanged(); } + + Slog.i(TAG, "Transport detached."); } private void notifyOnTransportsChanged() { @@ -307,8 +323,7 @@ public class CompanionTransportManager { int associationId = transport.mAssociationId; AssociationInfo association = mAssociationStore.getAssociationById(associationId); if (association != null) { - detachSystemDataTransport(association.getPackageName(), - association.getUserId(), + detachSystemDataTransport( association.getId()); } } diff --git a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java index 2cf1f462a7d1..d7e766eed209 100644 --- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java @@ -39,7 +39,6 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; import android.content.Context; @@ -208,7 +207,7 @@ public final class PermissionsUtils { /** * Require the caller to hold necessary permission to observe device presence by UUID. */ - public static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) { + public static void enforceCallerCanObserveDevicePresenceByUuid(@NonNull Context context) { if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) != PERMISSION_GRANTED) { throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have " @@ -235,23 +234,6 @@ public final class PermissionsUtils { return checkCallerCanManageCompanionDevice(context); } - /** - * Check if CDM can trust the context to process the association. - */ - @Nullable - public static AssociationInfo sanitizeWithCallerChecks(@NonNull Context context, - @Nullable AssociationInfo association) { - if (association == null) return null; - - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - if (!checkCallerCanManageAssociationsForPackage(context, userId, packageName)) { - return null; - } - - return association; - } - private static boolean checkPackage(@UserIdInt int uid, @NonNull String packageName) { try { return getAppOpsService().checkPackage(uid, packageName) == MODE_ALLOWED; diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 133a77df3573..04dd2f3fa288 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -8154,7 +8154,7 @@ public final class ActiveServices { BackgroundStartPrivileges.NONE); @ReasonCode int allowStartFgs = shouldAllowFgsStartForegroundNoBindingCheckLocked( allowWhileInUse, callingPid, callingUid, callingPackage, null /* targetService */, - BackgroundStartPrivileges.NONE); + BackgroundStartPrivileges.NONE, null); if (allowStartFgs == REASON_DENIED) { if (canBindingClientStartFgsLocked(callingUid) != null) { @@ -8410,7 +8410,8 @@ public final class ActiveServices { allowWhileInUse2, clientPid, clientUid, clientPackageName, null /* targetService */, - BackgroundStartPrivileges.NONE); + BackgroundStartPrivileges.NONE, + pr); if (allowStartFgs != REASON_DENIED) { return new Pair<>(allowStartFgs, clientPackageName); } else { @@ -8447,7 +8448,7 @@ public final class ActiveServices { ActivityManagerService.FgsTempAllowListItem tempAllowListReason = r.mInfoTempFgsAllowListReason = mAm.isAllowlistedForFgsStartLOSP(callingUid); int ret = shouldAllowFgsStartForegroundNoBindingCheckLocked(allowWhileInUse, callingPid, - callingUid, callingPackage, r, backgroundStartPrivileges); + callingUid, callingPackage, r, backgroundStartPrivileges, null); // If an app (App 1) is bound by another app (App 2) that could start an FGS, then App 1 // is also allowed to start an FGS. We check all the binding @@ -8503,7 +8504,8 @@ public final class ActiveServices { private @ReasonCode int shouldAllowFgsStartForegroundNoBindingCheckLocked( @ReasonCode int allowWhileInUse, int callingPid, int callingUid, String callingPackage, @Nullable ServiceRecord targetService, - BackgroundStartPrivileges backgroundStartPrivileges) { + BackgroundStartPrivileges backgroundStartPrivileges, + @Nullable ProcessRecord targetRecord) { int ret = allowWhileInUse; if (ret == REASON_DENIED) { @@ -8565,13 +8567,15 @@ public final class ActiveServices { if (ret == REASON_DENIED) { // Flag check: are we disabling SAW FGS background starts? final boolean shouldDisableSaw = Flags.fgsDisableSaw() - && CompatChanges.isChangeEnabled(FGS_BOOT_COMPLETED_RESTRICTIONS, callingUid); + && CompatChanges.isChangeEnabled(FGS_SAW_RESTRICTIONS, callingUid); if (shouldDisableSaw) { - final ProcessRecord processRecord = mAm - .getProcessRecordLocked(targetService.processName, - targetService.appInfo.uid); - if (processRecord != null) { - if (processRecord.mState.hasOverlayUi()) { + if (targetRecord == null) { + synchronized (mAm.mPidsSelfLocked) { + targetRecord = mAm.mPidsSelfLocked.get(callingPid); + } + } + if (targetRecord != null) { + if (targetRecord.mState.hasOverlayUi()) { if (mAm.mAtmInternal.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) { ret = REASON_SYSTEM_ALERT_WINDOW_PERMISSION; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 4364f16ff0e1..4f1a35c3fbd4 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -2656,6 +2656,11 @@ public class ActivityManagerService extends IActivityManager.Stub return mBackgroundLaunchBroadcasts; } + private String getWearRemoteIntentAction() { + return mContext.getResources().getString( + com.android.internal.R.string.config_wearRemoteIntentAction); + } + /** * Ensures that the given package name has an explicit set of allowed associations. * If it does not, give it an empty set. @@ -15213,6 +15218,18 @@ public class ActivityManagerService extends IActivityManager.Stub intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); } + // TODO: b/329211459 - Remove this after background remote intent is fixed. + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) + && getWearRemoteIntentAction().equals(action)) { + final int callerProcState = callerApp != null + ? callerApp.getCurProcState() + : ActivityManager.PROCESS_STATE_NONEXISTENT; + if (ActivityManager.RunningAppProcessInfo.procStateToImportance(callerProcState) + > ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + return ActivityManager.START_CANCELED; + } + } + switch (action) { case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE: UserManagerInternal umInternal = LocalServices.getService( @@ -19630,7 +19647,7 @@ public class ActivityManagerService extends IActivityManager.Stub record.procStateSeqWaitingForNetwork = 0; final long totalTime = SystemClock.uptimeMillis() - startTime; if (totalTime >= mConstants.mNetworkAccessTimeoutMs || DEBUG_NETWORK) { - Slog.w(TAG_NETWORK, "Total time waited for network rules to get updated: " + Slog.wtf(TAG_NETWORK, "Total time waited for network rules to get updated: " + totalTime + ". Uid: " + callingUid + " procStateSeq: " + procStateSeq + " UidRec: " + record + " validateUidRec: " diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index c5073001a672..3d4801b3e9aa 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -831,6 +831,7 @@ public final class AuthSession implements IBinder.DeathRecipient { break; case BiometricPrompt.DISMISSED_REASON_NEGATIVE: + case BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS: mClientReceiver.onDialogDismissed(reason); break; diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 506b4562b43d..62c21cf36f69 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -256,10 +256,10 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> // For BP, BiometricService will add the authToken to Keystore. if (!isBiometricPrompt() && mIsStrongBiometric) { final int result = KeyStore.getInstance().addAuthToken(byteToken); - if (result != KeyStore.NO_ERROR) { + if (result != 0) { Slog.d(TAG, "Error adding auth token : " + result); } else { - Slog.d(TAG, "addAuthToken: " + result); + Slog.d(TAG, "addAuthToken succeeded"); } } else { Slog.d(TAG, "Skipping addAuthToken"); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index fb826c824354..11db18359f23 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -895,7 +895,13 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { for (int i = 0; i < mFaceSensors.size(); i++) { final Sensor sensor = mFaceSensors.valueAt(i); final int sensorId = mFaceSensors.keyAt(i); - PerformanceTracker.getInstanceForSensorId(sensorId).incrementHALDeathCount(); + final PerformanceTracker performanceTracker = PerformanceTracker.getInstanceForSensorId( + sensorId); + if (performanceTracker != null) { + performanceTracker.incrementHALDeathCount(); + } else { + Slog.w(getTag(), "Performance tracker is null. Not counting HAL death."); + } sensor.onBinderDied(); } }); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index c04c47e2d95a..9290f8a48b79 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -954,7 +954,13 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi for (int i = 0; i < mFingerprintSensors.size(); i++) { final Sensor sensor = mFingerprintSensors.valueAt(i); final int sensorId = mFingerprintSensors.keyAt(i); - PerformanceTracker.getInstanceForSensorId(sensorId).incrementHALDeathCount(); + final PerformanceTracker performanceTracker = PerformanceTracker.getInstanceForSensorId( + sensorId); + if (performanceTracker != null) { + performanceTracker.incrementHALDeathCount(); + } else { + Slog.w(getTag(), "Performance tracker is null. Not counting HAL death."); + } sensor.onBinderDied(); } }); diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 851d1978dd98..9950d8ff42ed 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -3471,6 +3471,12 @@ public class DisplayDeviceConfig { throw new RuntimeException("Lux values should be in ascending order in the" + " idle screen refresh rate timeout config"); } + + int timeout = point.getTimeout().intValue(); + if (timeout < 0) { + throw new RuntimeException("The timeout value cannot be negative in" + + " idle screen refresh rate timeout config"); + } previousLux = newLux; } } diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 3b3a03bce524..86fab17e6ae8 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -1103,7 +1103,8 @@ final class LocalDisplayAdapter extends DisplayAdapter { new SurfaceControl.DesiredDisplayModeSpecs(baseSfModeId, mDisplayModeSpecs.allowGroupSwitching, mDisplayModeSpecs.primary, - mDisplayModeSpecs.appRequest))); + mDisplayModeSpecs.appRequest, + mDisplayModeSpecs.mIdleScreenRefreshRateConfig))); } } diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 495ae87fe0b9..572d32e80c12 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -64,6 +64,8 @@ import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.SurfaceControl; +import android.view.SurfaceControl.IdleScreenRefreshRateConfig; import android.view.SurfaceControl.RefreshRateRange; import android.view.SurfaceControl.RefreshRateRanges; @@ -74,6 +76,7 @@ import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.os.BackgroundThread; import com.android.server.LocalServices; import com.android.server.display.DisplayDeviceConfig; +import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.utils.AmbientFilter; @@ -184,6 +187,8 @@ public class DisplayModeDirector { private final boolean mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled; + private final DisplayManagerFlags mDisplayManagerFlags; + private final boolean mDvrrSupported; @@ -206,7 +211,7 @@ public class DisplayModeDirector { .isDisplaysRefreshRatesSynchronizationEnabled(); mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled = displayManagerFlags .isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled(); - + mDisplayManagerFlags = displayManagerFlags; mContext = context; mHandler = new DisplayModeDirectorHandler(handler.getLooper()); mInjector = injector; @@ -374,7 +379,7 @@ public class DisplayModeDirector { final RefreshRateRanges ranges = new RefreshRateRanges(range, range); return new DesiredDisplayModeSpecs(defaultMode.getModeId(), /*allowGroupSwitching */ false, - ranges, ranges); + ranges, ranges, mBrightnessObserver.getIdleScreenRefreshRateConfig()); } boolean modeSwitchingDisabled = @@ -422,7 +427,8 @@ public class DisplayModeDirector { appRequestSummary.maxPhysicalRefreshRate), new RefreshRateRange( appRequestSummary.minRenderFrameRate, - appRequestSummary.maxRenderFrameRate))); + appRequestSummary.maxRenderFrameRate)), + mBrightnessObserver.getIdleScreenRefreshRateConfig()); } } @@ -764,6 +770,16 @@ public class DisplayModeDirector { public boolean allowGroupSwitching; /** + * Represents the idle time of the screen after which the associated display's refresh rate + * is to be reduced to preserve power + * Defaults to null, meaning that the device is not configured to have a timeout based on + * the surrounding conditions + * -1 means that the current conditions require no timeout + */ + @Nullable + public IdleScreenRefreshRateConfig mIdleScreenRefreshRateConfig; + + /** * The primary refresh rate ranges. */ public final RefreshRateRanges primary; @@ -783,11 +799,13 @@ public class DisplayModeDirector { public DesiredDisplayModeSpecs(int baseModeId, boolean allowGroupSwitching, @NonNull RefreshRateRanges primary, - @NonNull RefreshRateRanges appRequest) { + @NonNull RefreshRateRanges appRequest, + @Nullable SurfaceControl.IdleScreenRefreshRateConfig idleScreenRefreshRateConfig) { this.baseModeId = baseModeId; this.allowGroupSwitching = allowGroupSwitching; this.primary = primary; this.appRequest = appRequest; + this.mIdleScreenRefreshRateConfig = idleScreenRefreshRateConfig; } /** @@ -797,9 +815,10 @@ public class DisplayModeDirector { public String toString() { return String.format("baseModeId=%d allowGroupSwitching=%b" + " primary=%s" - + " appRequest=%s", + + " appRequest=%s" + + " idleScreenRefreshRateConfig=%s", baseModeId, allowGroupSwitching, primary.toString(), - appRequest.toString()); + appRequest.toString(), String.valueOf(mIdleScreenRefreshRateConfig)); } /** @@ -830,12 +849,18 @@ public class DisplayModeDirector { desiredDisplayModeSpecs.appRequest)) { return false; } + + if (!Objects.equals(mIdleScreenRefreshRateConfig, + desiredDisplayModeSpecs.mIdleScreenRefreshRateConfig)) { + return false; + } return true; } @Override public int hashCode() { - return Objects.hash(baseModeId, allowGroupSwitching, primary, appRequest); + return Objects.hash(baseModeId, allowGroupSwitching, primary, appRequest, + mIdleScreenRefreshRateConfig); } /** @@ -853,6 +878,14 @@ public class DisplayModeDirector { appRequest.physical.max = other.appRequest.physical.max; appRequest.render.min = other.appRequest.render.min; appRequest.render.max = other.appRequest.render.max; + + if (other.mIdleScreenRefreshRateConfig == null) { + mIdleScreenRefreshRateConfig = null; + } else { + mIdleScreenRefreshRateConfig = + new IdleScreenRefreshRateConfig( + other.mIdleScreenRefreshRateConfig.timeoutMillis); + } } } @@ -1543,12 +1576,20 @@ public class DisplayModeDirector { private float mAmbientLux = -1.0f; private AmbientFilter mAmbientFilter; + /** + * The current timeout configuration. This value is used by surface flinger to track the + * time after which an idle screen's refresh rate is to be reduced. + */ + @Nullable + private SurfaceControl.IdleScreenRefreshRateConfig mIdleScreenRefreshRateConfig; + private float mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; private final Context mContext; private final Injector mInjector; private final Handler mHandler; + private final boolean mVsyncLowLightBlockingVoteEnabled; private final IThermalEventListener.Stub mThermalListener = @@ -1643,6 +1684,11 @@ public class DisplayModeDirector { return mRefreshRateInLowZone; } + @VisibleForTesting + IdleScreenRefreshRateConfig getIdleScreenRefreshRateConfig() { + return mIdleScreenRefreshRateConfig; + } + private void loadLowBrightnessThresholds(@Nullable DisplayDeviceConfig displayDeviceConfig, boolean attemptReadFromFeatureParams) { loadRefreshRateInHighZone(displayDeviceConfig, attemptReadFromFeatureParams); @@ -2381,6 +2427,10 @@ public class DisplayModeDirector { // is interrupted by a new sensor event. mHandler.postDelayed(mInjectSensorEventRunnable, INJECT_EVENTS_INTERVAL_MS); } + + if (mDisplayManagerFlags.isIdleScreenRefreshRateTimeoutEnabled()) { + updateIdleScreenRefreshRate(mAmbientLux); + } } @Override @@ -2440,6 +2490,40 @@ public class DisplayModeDirector { } }; } + + private void updateIdleScreenRefreshRate(float ambientLux) { + List<IdleScreenRefreshRateTimeoutLuxThresholdPoint> + idleScreenRefreshRateTimeoutLuxThresholdPoints; + synchronized (mLock) { + if (mDefaultDisplayDeviceConfig == null || mDefaultDisplayDeviceConfig + .getIdleScreenRefreshRateTimeoutLuxThresholdPoint().isEmpty()) { + // Setting this to null will let surface flinger know that the idle timer is not + // configured in the display configs + mIdleScreenRefreshRateConfig = null; + return; + } + + idleScreenRefreshRateTimeoutLuxThresholdPoints = + mDefaultDisplayDeviceConfig + .getIdleScreenRefreshRateTimeoutLuxThresholdPoint(); + } + int newTimeout = -1; + for (IdleScreenRefreshRateTimeoutLuxThresholdPoint point : + idleScreenRefreshRateTimeoutLuxThresholdPoints) { + int newLux = point.getLux().intValue(); + if (newLux <= ambientLux) { + newTimeout = point.getTimeout().intValue(); + } + } + if (mIdleScreenRefreshRateConfig == null + || newTimeout != mIdleScreenRefreshRateConfig.timeoutMillis) { + mIdleScreenRefreshRateConfig = + new IdleScreenRefreshRateConfig(newTimeout); + synchronized (mLock) { + notifyDesiredDisplayModeSpecsChangedLocked(); + } + } + } } private class UdfpsObserver extends IUdfpsRefreshRateRequestCallback.Stub { diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index a608049cd677..6e991b4db2b1 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -17,6 +17,7 @@ package com.android.server.location; import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.LOCATION_BYPASS; import static android.Manifest.permission.WRITE_SECURE_SETTINGS; import static android.app.compat.CompatChanges.isChangeEnabled; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; @@ -34,6 +35,7 @@ import static android.location.provider.LocationProviderBase.ACTION_NETWORK_PROV import static com.android.server.location.LocationPermissions.PERMISSION_COARSE; import static com.android.server.location.LocationPermissions.PERMISSION_FINE; +import static com.android.server.location.LocationPermissions.PERMISSION_NONE; import static com.android.server.location.eventlog.LocationEventLog.EVENT_LOG; import static java.util.concurrent.TimeUnit.NANOSECONDS; @@ -73,6 +75,7 @@ import android.location.LocationManagerInternal.LocationPackageTagsListener; import android.location.LocationProvider; import android.location.LocationRequest; import android.location.LocationTime; +import android.location.flags.Flags; import android.location.provider.ForwardGeocodeRequest; import android.location.provider.IGeocodeCallback; import android.location.provider.IProviderRequestListener; @@ -776,8 +779,19 @@ public class LocationManagerService extends ILocationManager.Stub implements listenerId); int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(), identity.getPid()); - LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, - PERMISSION_COARSE); + if (Flags.enableLocationBypass()) { + if (permissionLevel == PERMISSION_NONE) { + if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) { + LocationPermissions.enforceLocationPermission( + identity.getUid(), permissionLevel, PERMISSION_COARSE); + } else { + permissionLevel = PERMISSION_FINE; + } + } + } else { + LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, + PERMISSION_COARSE); + } // clients in the system process must have an attribution tag set Preconditions.checkState(identity.getPid() != Process.myPid() || attributionTag != null); @@ -805,8 +819,19 @@ public class LocationManagerService extends ILocationManager.Stub implements listenerId); int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(), identity.getPid()); - LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, - PERMISSION_COARSE); + if (Flags.enableLocationBypass()) { + if (permissionLevel == PERMISSION_NONE) { + if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) { + LocationPermissions.enforceLocationPermission( + identity.getUid(), permissionLevel, PERMISSION_COARSE); + } else { + permissionLevel = PERMISSION_FINE; + } + } + } else { + LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, + PERMISSION_COARSE); + } // clients in the system process should have an attribution tag set if (identity.getPid() == Process.myPid() && attributionTag == null) { @@ -830,8 +855,19 @@ public class LocationManagerService extends ILocationManager.Stub implements AppOpsManager.toReceiverId(pendingIntent)); int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(), identity.getPid()); - LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, - PERMISSION_COARSE); + if (Flags.enableLocationBypass()) { + if (permissionLevel == PERMISSION_NONE) { + if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) { + LocationPermissions.enforceLocationPermission( + identity.getUid(), permissionLevel, PERMISSION_COARSE); + } else { + permissionLevel = PERMISSION_FINE; + } + } + } else { + LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, + PERMISSION_COARSE); + } // clients in the system process must have an attribution tag set Preconditions.checkArgument(identity.getPid() != Process.myPid() || attributionTag != null); @@ -982,8 +1018,19 @@ public class LocationManagerService extends ILocationManager.Stub implements CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag); int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(), identity.getPid()); - LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, - PERMISSION_COARSE); + if (Flags.enableLocationBypass()) { + if (permissionLevel == PERMISSION_NONE) { + if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) { + LocationPermissions.enforceLocationPermission( + identity.getUid(), permissionLevel, PERMISSION_COARSE); + } else { + permissionLevel = PERMISSION_FINE; + } + } + } else { + LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, + PERMISSION_COARSE); + } // clients in the system process must have an attribution tag set Preconditions.checkArgument(identity.getPid() != Process.myPid() || attributionTag != null); diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java index 40e538b02728..542a29ae4172 100644 --- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java +++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java @@ -16,6 +16,7 @@ package com.android.server.location.provider; +import static android.Manifest.permission.LOCATION_BYPASS; import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; import static android.app.AppOpsManager.OP_MONITOR_LOCATION; import static android.app.compat.CompatChanges.isChangeEnabled; @@ -51,6 +52,7 @@ import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.AlarmManager.OnAlarmListener; +import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.PendingIntent; import android.content.Context; @@ -66,6 +68,7 @@ import android.location.LocationRequest; import android.location.LocationResult; import android.location.LocationResult.BadLocationException; import android.location.altitude.AltitudeConverter; +import android.location.flags.Flags; import android.location.provider.IProviderRequestListener; import android.location.provider.ProviderProperties; import android.location.provider.ProviderRequest; @@ -106,6 +109,7 @@ import com.android.server.location.injector.AlarmHelper; import com.android.server.location.injector.AppForegroundHelper; import com.android.server.location.injector.AppForegroundHelper.AppForegroundListener; import com.android.server.location.injector.AppOpsHelper; +import com.android.server.location.injector.EmergencyHelper; import com.android.server.location.injector.Injector; import com.android.server.location.injector.LocationPermissionsHelper; import com.android.server.location.injector.LocationPermissionsHelper.LocationPermissionsListener; @@ -375,8 +379,13 @@ public class LocationProviderManager extends // we cache these values because checking/calculating on the fly is more expensive @GuardedBy("mMultiplexerLock") private boolean mPermitted; + + @GuardedBy("mMultiplexerLock") + private boolean mBypassPermitted; + @GuardedBy("mMultiplexerLock") private boolean mForeground; + @GuardedBy("mMultiplexerLock") private LocationRequest mProviderLocationRequest; @GuardedBy("mMultiplexerLock") @@ -421,8 +430,8 @@ public class LocationProviderManager extends EVENT_LOG.logProviderClientRegistered(mName, getIdentity(), mBaseRequest); // initialization order is important as there are ordering dependencies - mPermitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel, - getIdentity()); + onLocationPermissionsChanged(); + onBypassLocationPermissionsChanged(mEmergencyHelper.isInEmergency(0)); mForeground = mAppForegroundHelper.isAppForeground(getIdentity().getUid()); mProviderLocationRequest = calculateProviderLocationRequest(); mIsUsingHighPower = isUsingHighPower(); @@ -491,7 +500,13 @@ public class LocationProviderManager extends public final boolean isPermitted() { synchronized (mMultiplexerLock) { - return mPermitted; + return mPermitted || mBypassPermitted; + } + } + + public final boolean isOnlyBypassPermitted() { + synchronized (mMultiplexerLock) { + return mBypassPermitted && !mPermitted; } } @@ -562,6 +577,33 @@ public class LocationProviderManager extends } } + boolean onBypassLocationPermissionsChanged(boolean isInEmergency) { + synchronized (mMultiplexerLock) { + boolean bypassPermitted = + Flags.enableLocationBypass() && isInEmergency + && mContext.checkPermission( + LOCATION_BYPASS, mIdentity.getPid(), mIdentity.getUid()) + == PERMISSION_GRANTED; + if (mBypassPermitted != bypassPermitted) { + if (D) { + Log.v( + TAG, + mName + + " provider package " + + getIdentity().getPackageName() + + " bypass permitted = " + + bypassPermitted); + } + + mBypassPermitted = bypassPermitted; + + return true; + } + + return false; + } + } + @GuardedBy("mMultiplexerLock") private boolean onLocationPermissionsChanged() { boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel, @@ -941,8 +983,11 @@ public class LocationProviderManager extends } // note app ops - if (!mAppOpsHelper.noteOpNoThrow(LocationPermissions.asAppOp(getPermissionLevel()), - getIdentity())) { + int op = + Flags.enableLocationBypass() && isOnlyBypassPermitted() + ? AppOpsManager.OP_EMERGENCY_LOCATION + : LocationPermissions.asAppOp(getPermissionLevel()); + if (!mAppOpsHelper.noteOpNoThrow(op, getIdentity())) { if (D) { Log.w(TAG, mName + " provider registration " + getIdentity() + " noteOp denied"); @@ -1292,12 +1337,17 @@ public class LocationProviderManager extends } // lastly - note app ops - if (fineLocationResult != null && !mAppOpsHelper.noteOpNoThrow( - LocationPermissions.asAppOp(getPermissionLevel()), getIdentity())) { - if (D) { - Log.w(TAG, "noteOp denied for " + getIdentity()); + if (fineLocationResult != null) { + int op = + Flags.enableLocationBypass() && isOnlyBypassPermitted() + ? AppOpsManager.OP_EMERGENCY_LOCATION + : LocationPermissions.asAppOp(getPermissionLevel()); + if (!mAppOpsHelper.noteOpNoThrow(op, getIdentity())) { + if (D) { + Log.w(TAG, "noteOp denied for " + getIdentity()); + } + fineLocationResult = null; } - fineLocationResult = null; } if (fineLocationResult != null) { @@ -1399,6 +1449,7 @@ public class LocationProviderManager extends protected final ScreenInteractiveHelper mScreenInteractiveHelper; protected final LocationUsageLogger mLocationUsageLogger; protected final LocationFudger mLocationFudger; + protected final EmergencyHelper mEmergencyHelper; private final PackageResetHelper mPackageResetHelper; private final UserListener mUserChangedListener = this::onUserChanged; @@ -1434,6 +1485,8 @@ public class LocationProviderManager extends this::onLocationPowerSaveModeChanged; private final ScreenInteractiveChangedListener mScreenInteractiveChangedListener = this::onScreenInteractiveChanged; + private final EmergencyHelper.EmergencyStateChangedListener mEmergencyStateChangedListener = + this::onEmergencyStateChanged; private final PackageResetHelper.Responder mPackageResetResponder = new PackageResetHelper.Responder() { @Override @@ -1507,6 +1560,7 @@ public class LocationProviderManager extends mScreenInteractiveHelper = injector.getScreenInteractiveHelper(); mLocationUsageLogger = injector.getLocationUsageLogger(); mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM()); + mEmergencyHelper = injector.getEmergencyHelper(); mPackageResetHelper = injector.getPackageResetHelper(); mProvider = new MockableLocationProvider(mMultiplexerLock); @@ -1757,8 +1811,17 @@ public class LocationProviderManager extends if (location != null) { // lastly - note app ops - if (!mAppOpsHelper.noteOpNoThrow(LocationPermissions.asAppOp(permissionLevel), - identity)) { + int op = + (Flags.enableLocationBypass() + && !mLocationPermissionsHelper.hasLocationPermissions( + permissionLevel, identity) + && mEmergencyHelper.isInEmergency(0) + && mContext.checkPermission( + LOCATION_BYPASS, identity.getPid(), identity.getUid()) + == PERMISSION_GRANTED) + ? AppOpsManager.OP_EMERGENCY_LOCATION + : LocationPermissions.asAppOp(permissionLevel); + if (!mAppOpsHelper.noteOpNoThrow(op, identity)) { return null; } @@ -2069,6 +2132,9 @@ public class LocationProviderManager extends mAppForegroundHelper.addListener(mAppForegroundChangedListener); mLocationPowerSaveModeHelper.addListener(mLocationPowerSaveModeChangedListener); mScreenInteractiveHelper.addListener(mScreenInteractiveChangedListener); + if (Flags.enableLocationBypass()) { + mEmergencyHelper.addOnEmergencyStateChangedListener(mEmergencyStateChangedListener); + } mPackageResetHelper.register(mPackageResetResponder); } @@ -2088,6 +2154,9 @@ public class LocationProviderManager extends mAppForegroundHelper.removeListener(mAppForegroundChangedListener); mLocationPowerSaveModeHelper.removeListener(mLocationPowerSaveModeChangedListener); mScreenInteractiveHelper.removeListener(mScreenInteractiveChangedListener); + if (Flags.enableLocationBypass()) { + mEmergencyHelper.removeOnEmergencyStateChangedListener(mEmergencyStateChangedListener); + } mPackageResetHelper.unregister(mPackageResetResponder); } @@ -2466,6 +2535,12 @@ public class LocationProviderManager extends } } + private void onEmergencyStateChanged() { + boolean inEmergency = mEmergencyHelper.isInEmergency(0); + updateRegistrations( + registration -> registration.onBypassLocationPermissionsChanged(inEmergency)); + } + private void onBackgroundThrottlePackageWhitelistChanged() { updateRegistrations(Registration::onProviderLocationRequestChanged); } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java index fcca94b0611a..178eb717f271 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java @@ -145,6 +145,10 @@ final class MediaRoute2ProviderWatcher { new ComponentName(serviceInfo.packageName, serviceInfo.name), isSelfScanOnlyProvider, mUserId); + Slog.i( + TAG, + "Enabling proxy for MediaRoute2ProviderService: " + + proxy.mComponentName); proxy.start(/* rebindIfDisconnected= */ false); mProxies.add(targetIndex++, proxy); mCallback.onAddProviderService(proxy); @@ -162,6 +166,9 @@ final class MediaRoute2ProviderWatcher { if (targetIndex < mProxies.size()) { for (int i = mProxies.size() - 1; i >= targetIndex; i--) { MediaRoute2ProviderServiceProxy proxy = mProxies.get(i); + Slog.i( + TAG, + "Disabling proxy for MediaRoute2ProviderService: " + proxy.mComponentName); mCallback.onRemoveProviderService(proxy); mProxies.remove(proxy); proxy.stop(); diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 25095edda5d8..22f5332e150c 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -1214,16 +1214,14 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { return false; } final int previousProcState = previousInfo.procState; - if (mBackgroundNetworkRestricted && (previousProcState >= BACKGROUND_THRESHOLD_STATE) - != (newProcState >= BACKGROUND_THRESHOLD_STATE)) { - // Proc-state change crossed BACKGROUND_THRESHOLD_STATE: Network rules for the - // BACKGROUND chain may change. - return true; - } if ((previousProcState <= TOP_THRESHOLD_STATE) - != (newProcState <= TOP_THRESHOLD_STATE)) { - // Proc-state change crossed TOP_THRESHOLD_STATE: Network rules for the - // LOW_POWER_STANDBY chain may change. + || (newProcState <= TOP_THRESHOLD_STATE)) { + // If the proc-state change crossed TOP_THRESHOLD_STATE, network rules for the + // LOW_POWER_STANDBY chain may change, so we need to evaluate the transition. + // In addition, we always process changes when the new process state is + // TOP_THRESHOLD_STATE or below, to avoid situations where the TOP app ends up + // waiting for NPMS to finish processing newProcStateSeq, even when it was + // redundant (b/327303931). return true; } if ((previousProcState <= FOREGROUND_THRESHOLD_STATE) @@ -1232,6 +1230,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // different chains may change. return true; } + if (mBackgroundNetworkRestricted && (previousProcState >= BACKGROUND_THRESHOLD_STATE) + != (newProcState >= BACKGROUND_THRESHOLD_STATE)) { + // Proc-state change crossed BACKGROUND_THRESHOLD_STATE: Network rules for the + // BACKGROUND chain may change. + return true; + } final int networkCapabilities = PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK | PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK; if ((previousInfo.capability & networkCapabilities) diff --git a/services/core/java/com/android/server/net/OWNERS b/services/core/java/com/android/server/net/OWNERS index d0e95dd55b6c..669cdaaf3ab5 100644 --- a/services/core/java/com/android/server/net/OWNERS +++ b/services/core/java/com/android/server/net/OWNERS @@ -4,3 +4,4 @@ file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking jsharkey@android.com sudheersai@google.com yamasani@google.com +suprabh@google.com diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index e80c79a8cffb..9fcdfdd564b6 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -11924,6 +11924,9 @@ public class NotificationManagerService extends SystemService { if (record != null && (record.getSbn().getNotification().flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) { boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND; + + // Lifetime extended notifications don't need to alert on state change. + record.setPostSilently(true); mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(), record, isAppForeground, mPostNotificationTrackerFactory.newTracker(null))); diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java index a25d67ab66af..f3d7dd19ecc2 100644 --- a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java +++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java @@ -24,8 +24,10 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.net.ConnectivityManager; import android.net.IpSecTransformState; import android.net.Network; +import android.net.vcn.Flags; import android.net.vcn.VcnManager; import android.os.Handler; import android.os.HandlerExecutor; @@ -71,6 +73,7 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { @NonNull private final Handler mHandler; @NonNull private final PowerManager mPowerManager; + @NonNull private final ConnectivityManager mConnectivityManager; @NonNull private final Object mCancellationToken = new Object(); @NonNull private final PacketLossCalculator mPacketLossCalculator; @@ -98,6 +101,8 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { mHandler = new Handler(getVcnContext().getLooper()); mPowerManager = getVcnContext().getContext().getSystemService(PowerManager.class); + mConnectivityManager = + getVcnContext().getContext().getSystemService(ConnectivityManager.class); mPacketLossCalculator = deps.getPacketLossCalculator(); @@ -313,6 +318,13 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { } else { logInfo(logMsg); onValidationResultReceivedInternal(true /* isFailed */); + + if (Flags.validateNetworkOnIpsecLoss()) { + // Trigger re-validation of the underlying network; if it fails, the VCN will + // attempt to migrate away. + mConnectivityManager.reportNetworkConnectivity( + getNetwork(), false /* hasConnectivity */); + } } } diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java index 1704aa117a2b..4bacf3b8abe5 100644 --- a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java +++ b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java @@ -203,6 +203,11 @@ public abstract class NetworkMetricMonitor implements AutoCloseable { return mVcnContext; } + @NonNull + public Network getNetwork() { + return mNetwork; + } + // Override methods for AutoCloseable. Subclasses MUST call super when overriding this method @Override public void close() { diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 418998870f16..3f041cb48ee2 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -961,16 +961,37 @@ final class AccessibilityController { populateTransformationMatrix(windowState, matrix); Region touchableRegion = mTempRegion3; windowState.getTouchableRegion(touchableRegion); - Rect touchableFrame = mTempRect1; - touchableRegion.getBounds(touchableFrame); - RectF windowFrame = mTempRectF; - windowFrame.set(touchableFrame); - windowFrame.offset(-windowState.getFrame().left, - -windowState.getFrame().top); - matrix.mapRect(windowFrame); Region windowBounds = mTempRegion2; - windowBounds.set((int) windowFrame.left, (int) windowFrame.top, - (int) windowFrame.right, (int) windowFrame.bottom); + if (Flags.useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds()) { + // For b/323366243, if using the bounds from touchableRegion.getBounds, in + // non-magnifiable windowBounds computation, part of the non-touchableRegion + // may be included into nonMagnifiedBounds. This will make users lose + // the magnification control on mis-included areas. + // Therefore, to prevent the above issue, we change to use the window exact + // touchableRegion in magnificationRegion computation. + // Like the original approach, the touchableRegion is in non-magnified display + // space, so first we need to offset the region by the windowFrames bounds, then + // apply the transform matrix to the region to get the exact region in magnified + // display space. + // TODO: For a long-term plan, since touchable regions provided by WindowState + // doesn't actually reflect the real touchable regions on display, we should + // delete the WindowState dependency and migrate to use the touchableRegion + // from WindowInfoListener data. (b/330653961) + touchableRegion.translate(-windowState.getFrame().left, + -windowState.getFrame().top); + applyMatrixToRegion(matrix, touchableRegion); + windowBounds.set(touchableRegion); + } else { + Rect touchableFrame = mTempRect1; + touchableRegion.getBounds(touchableFrame); + RectF windowFrame = mTempRectF; + windowFrame.set(touchableFrame); + windowFrame.offset(-windowState.getFrame().left, + -windowState.getFrame().top); + matrix.mapRect(windowFrame); + windowBounds.set((int) windowFrame.left, (int) windowFrame.top, + (int) windowFrame.right, (int) windowFrame.bottom); + } // Only update new regions Region portionOfWindowAlreadyAccountedFor = mTempRegion3; portionOfWindowAlreadyAccountedFor.set(mMagnificationRegion); @@ -1066,6 +1087,30 @@ final class AccessibilityController { || windowType == TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY; } + private void applyMatrixToRegion(Matrix matrix, Region region) { + // Since Matrix does not support mapRegion api, so we follow the Matrix#mapRect logic + // to apply the matrix to the given region. + // In Matrix#mapRect, the internal calculation is applying the transform matrix to + // rect's 4 corner points with the below calculation. (see SkMatrix::mapPoints) + // |A B C| |x| Ax+By+C Dx+Ey+F + // |D E F| |y| = |Ax+By+C Dx+Ey+F Gx+Hy+I| = ------- , ------- + // |G H I| |1| Gx+Hy+I Gx+Hy+I + // For magnification usage, the matrix is created from + // WindowState#getTransformationMatrix. We can simplify the matrix calculation to be + // |scale 0 trans_x| |x| + // | 0 scale trans_y| |y| = (scale*x + trans_x, scale*y + trans_y) + // | 0 0 1 | |1| + // So, to follow the simplified matrix computation, we first scale the region with + // matrix.scale, then translate the region with matrix.trans_x and matrix.trans_y. + float[] transformArray = sTempFloats; + matrix.getValues(transformArray); + // For magnification transform matrix, the scale_x and scale_y are equal. + region.scale(transformArray[Matrix.MSCALE_X]); + region.translate( + (int) transformArray[Matrix.MTRANS_X], + (int) transformArray[Matrix.MTRANS_Y]); + } + private void populateWindowsOnScreen(SparseArray<WindowState> outWindows) { mTempLayer = 0; mDisplayContent.forAllWindows((w) -> { diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 59a56de16ce6..19f344996700 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -149,6 +149,10 @@ class ActivityMetricsLogger { private static final int WINDOW_STATE_MULTI_WINDOW = 4; private static final int WINDOW_STATE_INVALID = -1; + // These should match AppStartOccurred.MultiWindowLaunchType in the atoms.proto + private static final int MULTI_WINDOW_LAUNCH_TYPE_UNSPECIFIED = 0; + private static final int MULTI_WINDOW_LAUNCH_TYPE_APP_PAIR = 1; + /** * If a launching activity isn't visible within this duration when the device is sleeping, e.g. * keyguard is locked, its transition info will be dropped. @@ -329,6 +333,8 @@ class ActivityMetricsLogger { @Nullable Runnable mPendingFullyDrawn; /** Non-null if the trace is active. */ @Nullable String mLaunchTraceName; + /** Whether this transition info is for an activity that is a part of multi-window. */ + int mMultiWindowLaunchType = MULTI_WINDOW_LAUNCH_TYPE_UNSPECIFIED; /** @return Non-null if there will be a window drawn event for the launch. */ @Nullable @@ -477,6 +483,7 @@ class ActivityMetricsLogger { final int activityRecordIdHashCode; final boolean relaunched; final long timestampNs; + final int multiWindowLaunchType; private TransitionInfoSnapshot(TransitionInfo info) { this(info, info.mLastLaunchedActivity, INVALID_DELAY); @@ -507,6 +514,7 @@ class ActivityMetricsLogger { this.windowsFullyDrawnDelayMs = windowsFullyDrawnDelayMs; relaunched = info.mRelaunched; timestampNs = info.mLaunchingState.mStartRealtimeNs; + multiWindowLaunchType = info.mMultiWindowLaunchType; } @WaitResult.LaunchState int getLaunchState() { @@ -744,6 +752,10 @@ class ActivityMetricsLogger { return; } + // Look at all other transition infos and mark them as a split pair if they belong to + // adjacent tasks + updateSplitPairLaunches(newInfo); + if (DEBUG_METRICS) Slog.i(TAG, "notifyActivityLaunched successful"); // A new launch sequence has begun. Start tracking it. mTransitionInfoList.add(newInfo); @@ -769,6 +781,36 @@ class ActivityMetricsLogger { } } + /** + * Updates all transition infos including the given {@param info} if they are a part of a + * split pair launch. + */ + private void updateSplitPairLaunches(@NonNull TransitionInfo info) { + final Task launchedActivityTask = info.mLastLaunchedActivity.getTask(); + final Task adjacentToLaunchedTask = launchedActivityTask.getAdjacentTask(); + if (adjacentToLaunchedTask == null) { + // Not a part of a split pair + return; + } + for (int i = mTransitionInfoList.size() - 1; i >= 0; i--) { + final TransitionInfo otherInfo = mTransitionInfoList.get(i); + if (otherInfo == info) { + continue; + } + final Task otherTask = otherInfo.mLastLaunchedActivity.getTask(); + // The adjacent task is the split root in which activities are started + if (otherTask.isDescendantOf(adjacentToLaunchedTask)) { + if (DEBUG_METRICS) { + Slog.i(TAG, "Found adjacent tasks t1=" + launchedActivityTask.mTaskId + + " t2=" + otherTask.mTaskId); + } + // These tasks are adjacent, so mark them as such + info.mMultiWindowLaunchType = MULTI_WINDOW_LAUNCH_TYPE_APP_PAIR; + otherInfo.mMultiWindowLaunchType = MULTI_WINDOW_LAUNCH_TYPE_APP_PAIR; + } + } + } + private void scheduleCheckActivityToBeDrawnIfSleeping(@NonNull ActivityRecord r) { if (r.mDisplayContent.isSleeping()) { // It is unknown whether the activity can be drawn or not, e.g. it depends on the @@ -1168,7 +1210,8 @@ class ActivityMetricsLogger { packageState, false, // is_xr_activity firstLaunch, - 0L /* TODO: stoppedDuration */); + 0L /* TODO: stoppedDuration */, + info.multiWindowLaunchType); // Reset the stopped state to avoid reporting stopped again if (info.processRecord != null) { info.processRecord.setWasStoppedLogged(true); diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 48d78f5e497b..7144445f86d8 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -422,7 +422,6 @@ class BackNavigationController { // Searching previous final ActivityRecord prevActivity = currentTask.getActivity((below) -> !below.finishing, currentActivity, false /*includeBoundary*/, true /*traverseTopToBottom*/); - final TaskFragment currTF = currentActivity.getTaskFragment(); if (currTF != null && currTF.asTask() == null) { // The currentActivity is embedded, search for the candidate previous activities. @@ -431,13 +430,34 @@ class BackNavigationController { outPrevActivities.add(prevActivity); return true; } - if (currTF.getAdjacentTaskFragment() != null) { - // The two TFs are adjacent (visually displayed side-by-side), search if any - // activity below the lowest one - // If companion, those two TF will be closed together. - if (currTF.getCompanionTaskFragment() != null) { + if (currTF.getAdjacentTaskFragment() == null) { + final TaskFragment nextTF = findNextTaskFragment(currentTask, currTF); + if (isSecondCompanionToFirst(currTF, nextTF)) { + // TF is isStacked, search bottom activity from companion TF. + // + // Sample hierarchy: search for underPrevious if any. + // Current TF + // Companion TF (bottomActivityInCompanion) + // Bottom Activity not inside companion TF (underPrevious) + // find bottom activity in Companion TF. + final ActivityRecord bottomActivityInCompanion = nextTF.getActivity( + (below) -> !below.finishing, false /* traverseTopToBottom */); + final ActivityRecord underPrevious = currentTask.getActivity( + (below) -> !below.finishing, bottomActivityInCompanion, + false /*includeBoundary*/, true /*traverseTopToBottom*/); + if (underPrevious != null) { + outPrevActivities.add(underPrevious); + addPreviousAdjacentActivityIfExist(underPrevious, outPrevActivities); + } + return true; + } + } else { + // If adjacent TF has companion to current TF, those two TF will be closed together. + final TaskFragment adjacentTF = currTF.getAdjacentTaskFragment(); + if (isSecondCompanionToFirst(currTF, adjacentTF)) { + // The two TFs are adjacent (visually displayed side-by-side), search if any + // activity below the lowest one. final WindowContainer commonParent = currTF.getParent(); - final TaskFragment adjacentTF = currTF.getAdjacentTaskFragment(); final TaskFragment lowerTF = commonParent.mChildren.indexOf(currTF) < commonParent.mChildren.indexOf(adjacentTF) ? currTF : adjacentTF; @@ -451,25 +471,6 @@ class BackNavigationController { // Unable to predict if no companion, it can only close current activity and make // prev Activity full screened. return false; - } else if (currTF.getCompanionTaskFragment() != null) { - // TF is isStacked, search bottom activity from companion TF. - // - // Sample hierarchy: search for underPrevious if any. - // Current TF - // Companion TF (bottomActivityInCompanion) - // Bottom Activity not inside companion TF (underPrevious) - final TaskFragment companionTF = currTF.getCompanionTaskFragment(); - // find bottom activity in Companion TF. - final ActivityRecord bottomActivityInCompanion = companionTF.getActivity( - (below) -> !below.finishing, false /* traverseTopToBottom */); - final ActivityRecord underPrevious = currentTask.getActivity( - (below) -> !below.finishing, bottomActivityInCompanion, - false /*includeBoundary*/, true /*traverseTopToBottom*/); - if (underPrevious != null) { - outPrevActivities.add(underPrevious); - addPreviousAdjacentActivityIfExist(underPrevious, outPrevActivities); - } - return true; } } @@ -484,6 +485,24 @@ class BackNavigationController { return true; } + private static TaskFragment findNextTaskFragment(@NonNull Task currentTask, + @NonNull TaskFragment topTF) { + final int topIndex = currentTask.mChildren.indexOf(topTF); + if (topIndex <= 0) { + return null; + } + final WindowContainer next = currentTask.mChildren.get(topIndex - 1); + return next.asTaskFragment(); + } + + /** + * Whether the second TF has set companion to first TF. + * When set, the second TF will be removed by organizer if the first TF is removed. + */ + private static boolean isSecondCompanionToFirst(TaskFragment first, TaskFragment second) { + return second != null && second.getCompanionTaskFragment() == first; + } + private static void addPreviousAdjacentActivityIfExist(@NonNull ActivityRecord prevActivity, @NonNull ArrayList<ActivityRecord> outPrevActivities) { final TaskFragment prevTF = prevActivity.getTaskFragment(); @@ -748,6 +767,10 @@ class BackNavigationController { if (!isMonitoringTransition() || targets.isEmpty()) { return; } + if (mAnimationHandler.hasTargetDetached()) { + mNavigationMonitor.cancelBackNavigating("targetDetached"); + return; + } for (int i = targets.size() - 1; i >= 0; --i) { final WindowContainer wc = targets.get(i).mContainer; if (wc.asActivityRecord() == null && wc.asTask() == null @@ -1122,6 +1145,21 @@ class BackNavigationController { || containTarget(openApps, false /* open */)); } + /** + * Check if any animation target is detached, possibly due to app crash. + */ + boolean hasTargetDetached() { + if (!mComposed) { + return false; + } + for (int i = mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) { + if (!mOpenAnimAdaptor.mAdaptors[i].mTarget.isAttached()) { + return true; + } + } + return !mCloseAdaptor.mTarget.isAttached(); + } + @Override public String toString() { return "AnimationTargets{" @@ -1659,6 +1697,10 @@ class BackNavigationController { } private static void restoreLaunchBehind(@NonNull ActivityRecord activity) { + if (!activity.isAttached()) { + // The activity was detached from hierarchy. + return; + } activity.mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp(); // Restore the launch-behind state. diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 46d4ce400053..fe280cbcc205 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -5097,7 +5097,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } finally { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } - prepareSurfaces(); + if (!com.android.window.flags.Flags.removePrepareSurfaceInPlacement()) { + prepareSurfaces(); + } // This should be called after the insets have been dispatched to clients and we have // committed finish drawing windows. diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index dd77a69b0dd9..3f245456d849 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -22,6 +22,7 @@ import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS; @@ -130,6 +131,7 @@ import com.android.internal.statusbar.LetterboxDetails; import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType; import com.android.server.wm.utils.OptPropFactory; import com.android.server.wm.utils.OptPropFactory.OptProp; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.util.ArrayList; @@ -350,7 +352,6 @@ final class LetterboxUiController { isCompatChangeEnabled(OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA); mIsOverrideRespectRequestedOrientationEnabled = isCompatChangeEnabled(OVERRIDE_RESPECT_REQUESTED_ORIENTATION); - } /** Cleans up {@link Letterbox} if it exists.*/ @@ -714,6 +715,25 @@ final class LetterboxUiController { isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION)); } + /** + * Whether activity is eligible for camera compatibility free-form treatment. + * + * <p>The treatment is applied to a fixed-orientation camera activity in free-form windowing + * mode. The treatment letterboxes or pillarboxes the activity to the expected orientation and + * provides changes to the camera and display orientation signals to match those expected on a + * portrait device in that orientation (for example, on a standard phone). + * + * <p>The treatment is enabled when the following conditions are met: + * <ul> + * <li>Property gating the camera compatibility free-form treatment is enabled. + * <li>Activity isn't opted out by the device manufacturer with override. + * </ul> + */ + boolean shouldApplyFreeformTreatmentForCameraCompat() { + return Flags.cameraCompatForFreeform() && !isCompatChangeEnabled( + OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); + } + private boolean isCameraCompatTreatmentActive() { DisplayContent displayContent = mActivityRecord.mDisplayContent; if (displayContent == null) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 0effa6c05801..f09ef9643433 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -154,6 +154,7 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; import static com.android.window.flags.Flags.multiCrop; +import static com.android.window.flags.Flags.setScPropertiesInClient; import android.Manifest; import android.Manifest.permission; @@ -2363,9 +2364,12 @@ public class WindowManagerService extends IWindowManager.Stub updateNonSystemOverlayWindowsVisibilityIfNeeded( win, win.mWinAnimator.getShown()); } - if ((attrChanges & (WindowManager.LayoutParams.PRIVATE_FLAGS_CHANGED)) != 0) { - winAnimator.setColorSpaceAgnosticLocked((win.mAttrs.privateFlags - & WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) != 0); + if (!setScPropertiesInClient()) { + if ((attrChanges & (WindowManager.LayoutParams.PRIVATE_FLAGS_CHANGED)) != 0) { + winAnimator.setColorSpaceAgnosticLocked((win.mAttrs.privateFlags + & WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) + != 0); + } } // See if the DisplayWindowPolicyController wants to keep the activity on the window if (displayContent.mDwpcHelper.hasController() diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 7f7c2493cd68..a242d4242388 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -45,6 +45,7 @@ import static com.android.server.wm.WindowStateAnimatorProto.DRAW_STATE; import static com.android.server.wm.WindowStateAnimatorProto.SURFACE; import static com.android.server.wm.WindowStateAnimatorProto.SYSTEM_DECOR_RECT; import static com.android.window.flags.Flags.secureWindowState; +import static com.android.window.flags.Flags.setScPropertiesInClient; import android.content.Context; import android.graphics.PixelFormat; @@ -311,8 +312,10 @@ class WindowStateAnimator { mSurfaceController = new WindowSurfaceController(attrs.getTitle().toString(), format, flags, this, attrs.type); - mSurfaceController.setColorSpaceAgnostic(w.getPendingTransaction(), - (attrs.privateFlags & LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) != 0); + if (!setScPropertiesInClient()) { + mSurfaceController.setColorSpaceAgnostic(w.getPendingTransaction(), + (attrs.privateFlags & LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) != 0); + } w.setHasSurface(true); // The surface instance is changed. Make sure the input info can be applied to the diff --git a/services/core/jni/com_android_server_am_OomConnection.cpp b/services/core/jni/com_android_server_am_OomConnection.cpp index 49a3ad35649b..054937fc683e 100644 --- a/services/core/jni/com_android_server_am_OomConnection.cpp +++ b/services/core/jni/com_android_server_am_OomConnection.cpp @@ -44,6 +44,12 @@ static MemEventListener memevent_listener(MemEventClient::AMS); * @throws java.lang.RuntimeException */ static jobjectArray android_server_am_OomConnection_waitOom(JNIEnv* env, jobject) { + if (!memevent_listener.ok()) { + memevent_listener.deregisterAllEvents(); + jniThrowRuntimeException(env, "Failed to initialize memevents listener"); + return nullptr; + } + if (!memevent_listener.registerEvent(MEM_EVENT_OOM_KILL)) { memevent_listener.deregisterAllEvents(); jniThrowRuntimeException(env, "listener failed to register to OOM events"); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 740ffc90d785..5842dacbd8f6 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -1547,6 +1547,47 @@ public final class DisplayPowerControllerTest { } @Test + public void testOffloadBlocker_turnON_screenOnBlocked() { + // set up. + int initState = Display.STATE_OFF; + mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); + mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); + // start with OFF. + when(mHolder.displayPowerState.getScreenState()).thenReturn(initState); + DisplayPowerRequest dpr = new DisplayPowerRequest(); + dpr.policy = DisplayPowerRequest.POLICY_OFF; + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + // go to ON. + dpr.policy = DisplayPowerRequest.POLICY_BRIGHT; + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + verify(mDisplayOffloadSession).blockScreenOn(any(Runnable.class)); + } + + @Test + public void testOffloadBlocker_turnOFF_screenOnNotBlocked() { + // set up. + int initState = Display.STATE_ON; + mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); + // start with ON. + when(mHolder.displayPowerState.getScreenState()).thenReturn(initState); + DisplayPowerRequest dpr = new DisplayPowerRequest(); + dpr.policy = DisplayPowerRequest.POLICY_BRIGHT; + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + // go to OFF. + dpr.policy = DisplayPowerRequest.POLICY_OFF; + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + verify(mDisplayOffloadSession, never()).blockScreenOn(any(Runnable.class)); + } + + @Test public void testBrightnessFromOffload() { when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true); mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index 14de527aa1f7..7fd96c57c215 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -49,6 +49,7 @@ import android.os.Looper; import android.view.Display; import android.view.DisplayAddress; import android.view.SurfaceControl; +import android.view.SurfaceControl.IdleScreenRefreshRateConfig; import android.view.SurfaceControl.RefreshRateRange; import android.view.SurfaceControl.RefreshRateRanges; @@ -830,18 +831,20 @@ public class LocalDisplayAdapterTest { .get() .getModeId(); + IdleScreenRefreshRateConfig + idleScreenRefreshRateConfig = new SurfaceControl.IdleScreenRefreshRateConfig(500); displayDevice.setDesiredDisplayModeSpecsLocked( new DisplayModeDirector.DesiredDisplayModeSpecs( /*baseModeId*/ baseModeId, /*allowGroupSwitching*/ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, idleScreenRefreshRateConfig )); waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); verify(mSurfaceControlProxy).setDesiredDisplayModeSpecs(display.token, new SurfaceControl.DesiredDisplayModeSpecs( /* baseModeId */ 0, /* allowGroupSwitching */ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, idleScreenRefreshRateConfig )); // Change the display @@ -862,12 +865,13 @@ public class LocalDisplayAdapterTest { baseModeId = displayDevice.getDisplayDeviceInfoLocked().supportedModes[0].getModeId(); + idleScreenRefreshRateConfig = new SurfaceControl.IdleScreenRefreshRateConfig(600); // The traversal request will call setDesiredDisplayModeSpecsLocked on the display device displayDevice.setDesiredDisplayModeSpecsLocked( new DisplayModeDirector.DesiredDisplayModeSpecs( /*baseModeId*/ baseModeId, /*allowGroupSwitching*/ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, idleScreenRefreshRateConfig )); waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); @@ -877,7 +881,7 @@ public class LocalDisplayAdapterTest { new SurfaceControl.DesiredDisplayModeSpecs( /* baseModeId */ 2, /* allowGroupSwitching */ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, idleScreenRefreshRateConfig )); } @@ -1319,7 +1323,8 @@ public class LocalDisplayAdapterTest { new SurfaceControl.DesiredDisplayModeSpecs( /* defaultMode */ 0, /* allowGroupSwitching */ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, + new IdleScreenRefreshRateConfig(100) ); private FakeDisplay(int port) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index 3eced7fa025c..3a59c84b636c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -75,6 +75,8 @@ import android.util.SparseArray; import android.util.TypedValue; import android.view.Display; import android.view.DisplayInfo; +import android.view.SurfaceControl; +import android.view.SurfaceControl.IdleScreenRefreshRateConfig; import android.view.SurfaceControl.RefreshRateRange; import android.view.SurfaceControl.RefreshRateRanges; @@ -91,6 +93,7 @@ import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.TestUtils; +import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.mode.DisplayModeDirector.BrightnessObserver; import com.android.server.display.mode.DisplayModeDirector.DesiredDisplayModeSpecs; @@ -112,6 +115,7 @@ import org.mockito.Mockito; import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -1405,6 +1409,81 @@ public class DisplayModeDirectorTest { } @Test + public void testIdleScreenTimeOnLuxChanges() throws Exception { + DisplayModeDirector director = + createDirectorFromRefreshRateArray(new float[] {60.f, 90.f, 120.f}, 0); + setPeakRefreshRate(120 /*fps*/); + director.getSettingsObserver().setDefaultRefreshRate(120); + director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); + + // Set the DisplayDeviceConfig + DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class); + when(ddcMock.getDefaultHighBlockingZoneRefreshRate()).thenReturn(90); + when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new float[] { 200 }); + when(ddcMock.getHighAmbientBrightnessThresholds()).thenReturn(new float[] { 8000 }); + when(ddcMock.getDefaultLowBlockingZoneRefreshRate()).thenReturn(90); + when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new float[] {}); + when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new float[] {}); + + director.defaultDisplayDeviceUpdated(ddcMock); // set the ddc + + Sensor lightSensor = createLightSensor(); + SensorManager sensorManager = createMockSensorManager(lightSensor); + director.start(sensorManager); + + // Get the sensor listener so that we can give it new light sensor events + ArgumentCaptor<SensorEventListener> listenerCaptor = + ArgumentCaptor.forClass(SensorEventListener.class); + verify(sensorManager, Mockito.timeout(TimeUnit.SECONDS.toMillis(1))) + .registerListener( + listenerCaptor.capture(), + eq(lightSensor), + anyInt(), + any(Handler.class)); + SensorEventListener sensorListener = listenerCaptor.getValue(); + + // Disable the idle screen flag + when(mDisplayManagerFlags.isIdleScreenRefreshRateTimeoutEnabled()) + .thenReturn(false); + + // Sensor reads 5 lux, with idleScreenRefreshRate timeout not configured + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 5)); + waitForIdleSync(); + assertEquals(null, director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + // Enable the idle screen flag + when(mDisplayManagerFlags.isIdleScreenRefreshRateTimeoutEnabled()) + .thenReturn(true); + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 8)); + waitForIdleSync(); + assertEquals(null, director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + // Configure DDC with idle screen timeout + when(ddcMock.getIdleScreenRefreshRateTimeoutLuxThresholdPoint()) + .thenReturn(List.of(getIdleScreenRefreshRateTimeoutLuxThresholdPoint(6, 1000), + getIdleScreenRefreshRateTimeoutLuxThresholdPoint(100, 800))); + + // Sensor reads 5 lux + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 5)); + waitForIdleSync(); + assertEquals(new SurfaceControl.IdleScreenRefreshRateConfig(-1), + director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + // Sensor reads 50 lux + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 50)); + waitForIdleSync(); + assertEquals(new IdleScreenRefreshRateConfig(1000), + director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + // Sensor reads 200 lux + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 200)); + waitForIdleSync(); + assertEquals(new SurfaceControl.IdleScreenRefreshRateConfig(800), + director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + } + + @Test public void testLockFpsForHighZoneWithThermalCondition() throws Exception { // First, configure brightness zones or DMD won't register for sensor data. final FakeDeviceConfig config = mInjector.getDeviceConfig(); @@ -1440,11 +1519,11 @@ public class DisplayModeDirectorTest { // Get the display listener so that we can send it new brightness events ArgumentCaptor<DisplayListener> displayListenerCaptor = - ArgumentCaptor.forClass(DisplayListener.class); + ArgumentCaptor.forClass(DisplayListener.class); verify(mInjector).registerDisplayListener(displayListenerCaptor.capture(), any(Handler.class), eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED - | DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS)); + | DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS)); DisplayListener displayListener = displayListenerCaptor.getValue(); // Get the sensor listener so that we can give it new light sensor events @@ -3746,4 +3825,14 @@ public class DisplayModeDirectorTest { } } } + + private IdleScreenRefreshRateTimeoutLuxThresholdPoint + getIdleScreenRefreshRateTimeoutLuxThresholdPoint(int lux, int timeout) { + IdleScreenRefreshRateTimeoutLuxThresholdPoint + idleScreenRefreshRateTimeoutLuxThresholdPoint = + new IdleScreenRefreshRateTimeoutLuxThresholdPoint(); + idleScreenRefreshRateTimeoutLuxThresholdPoint.setLux(BigInteger.valueOf(lux)); + idleScreenRefreshRateTimeoutLuxThresholdPoint.setTimeout(BigInteger.valueOf(timeout)); + return idleScreenRefreshRateTimeoutLuxThresholdPoint; + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/PackageManagerBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/PackageManagerBackupAgentTest.java index 20e198c192bf..822dacf0404e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/PackageManagerBackupAgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/PackageManagerBackupAgentTest.java @@ -16,14 +16,19 @@ package com.android.server.backup; -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.content.pm.SigningDetails; +import android.content.pm.SigningInfo; import android.os.Build; import android.os.ParcelFileDescriptor; import android.platform.test.annotations.Presubmit; @@ -38,6 +43,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.io.BufferedOutputStream; import java.io.DataOutputStream; @@ -51,22 +58,30 @@ import java.util.Optional; public class PackageManagerBackupAgentTest { private static final String EXISTING_PACKAGE_NAME = "com.android.wallpaperbackup"; + private static final int EXISTING_PACKAGE_VERSION = 1; + private static final int USER_ID = 0; @Rule public TemporaryFolder folder = new TemporaryFolder(); - private PackageManager mPackageManager; + @Mock private PackageManager mPackageManager; + private PackageManagerBackupAgent mPackageManagerBackupAgent; private ImmutableList<PackageInfo> mPackages; private File mBackupData, mOldState, mNewState; @Before public void setUp() throws Exception { - mPackageManager = getApplicationContext().getPackageManager(); + MockitoAnnotations.initMocks(this); PackageInfo existingPackageInfo = - mPackageManager.getPackageInfoAsUser( - EXISTING_PACKAGE_NAME, PackageManager.GET_SIGNING_CERTIFICATES, USER_ID); + createPackage(EXISTING_PACKAGE_NAME, EXISTING_PACKAGE_VERSION); + Signature sig = new Signature(new byte[256]); + existingPackageInfo.signingInfo = + new SigningInfo(new SigningDetails(new Signature[] {sig}, 1, null, null)); + when(mPackageManager.getPackageInfoAsUser(eq(EXISTING_PACKAGE_NAME), anyInt(), anyInt())) + .thenReturn(existingPackageInfo); + mPackages = ImmutableList.of(existingPackageInfo); mPackageManagerBackupAgent = new PackageManagerBackupAgent(mPackageManager, mPackages, USER_ID); diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java index ca730910943b..7f1a0bb1a5da 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java @@ -110,7 +110,7 @@ public class TestInjector implements Injector { } @Override - public EmergencyHelper getEmergencyHelper() { + public FakeEmergencyHelper getEmergencyHelper() { return mEmergencyHelper; } diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java index 32878b3e199f..09282646ff68 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java @@ -16,6 +16,9 @@ package com.android.server.location.provider; +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.LOCATION_BYPASS; import static android.app.AppOpsManager.OP_FINE_LOCATION; import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; import static android.app.AppOpsManager.OP_MONITOR_LOCATION; @@ -1170,6 +1173,63 @@ public class LocationProviderManagerTest { } @Test + public void testProviderRequest_IgnoreLocationSettings_LocationBypass() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LOCATION_BYPASS); + + doReturn(PackageManager.PERMISSION_GRANTED) + .when(mContext) + .checkPermission(LOCATION_BYPASS, IDENTITY.getPid(), IDENTITY.getUid()); + mInjector.getLocationPermissionsHelper() + .revokePermission(IDENTITY.getPackageName(), ACCESS_FINE_LOCATION); + mInjector.getLocationPermissionsHelper() + .revokePermission(IDENTITY.getPackageName(), ACCESS_COARSE_LOCATION); + mInjector + .getSettingsHelper() + .setIgnoreSettingsAllowlist( + new PackageTagsList.Builder().add(IDENTITY.getPackageName()).build()); + + ILocationListener listener = createMockLocationListener(); + LocationRequest request = + new LocationRequest.Builder(1) + .setLocationSettingsIgnored(true) + .setWorkSource(WORK_SOURCE) + .build(); + mManager.registerLocationRequest(request, IDENTITY, PERMISSION_FINE, listener); + + assertThat(mProvider.getRequest().isActive()).isFalse(); + } + + @Test + public void testProviderRequest_IgnoreLocationSettings_LocationBypass_EmergencyCall() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LOCATION_BYPASS); + + doReturn(PackageManager.PERMISSION_GRANTED) + .when(mContext) + .checkPermission(LOCATION_BYPASS, IDENTITY.getPid(), IDENTITY.getUid()); + mInjector.getLocationPermissionsHelper() + .revokePermission(IDENTITY.getPackageName(), ACCESS_FINE_LOCATION); + mInjector.getLocationPermissionsHelper() + .revokePermission(IDENTITY.getPackageName(), ACCESS_COARSE_LOCATION); + mInjector.getEmergencyHelper().setInEmergency(true); + mInjector + .getSettingsHelper() + .setIgnoreSettingsAllowlist( + new PackageTagsList.Builder().add(IDENTITY.getPackageName()).build()); + + ILocationListener listener = createMockLocationListener(); + LocationRequest request = + new LocationRequest.Builder(1) + .setLocationSettingsIgnored(true) + .setWorkSource(WORK_SOURCE) + .build(); + mManager.registerLocationRequest(request, IDENTITY, PERMISSION_FINE, listener); + + assertThat(mProvider.getRequest().isActive()).isTrue(); + assertThat(mProvider.getRequest().getIntervalMillis()).isEqualTo(1); + assertThat(mProvider.getRequest().isLocationSettingsIgnored()).isTrue(); + } + + @Test public void testProviderRequest_BackgroundThrottle_IgnoreLocationSettings() { mInjector.getSettingsHelper().setIgnoreSettingsAllowlist( new PackageTagsList.Builder().add( diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 49583ef5194b..a852677c2ed1 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -1318,6 +1318,28 @@ public class BiometricServiceTest { } @Test + public void testDismissedReasonMoreOptions_whilePaused_invokeHalCancel() throws Exception { + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, null /* authenticators */); + + mBiometricService.mAuthSession.mSensorReceiver.onError( + SENSOR_ID_FACE, + getCookieForCurrentSession(mBiometricService.mAuthSession), + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, + 0 /* vendorCode */); + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( + BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS, + null /* credentialAttestation */); + waitForIdle(); + + verify(mReceiver1).onDialogDismissed( + eq(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS)); + verify(mBiometricService.mSensors.get(0).impl) + .cancelAuthenticationFromService(any(), any(), anyLong()); + } + + @Test public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception { when(mContext.getResources().getString(anyInt())).thenReturn("test string"); diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java index 124970758fa5..3cab75b5d320 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java @@ -2315,10 +2315,11 @@ public class NetworkPolicyManagerServiceTest { } waitForUidEventHandlerIdle(); try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { - // Doesn't cross any other threshold. + // Doesn't cross any threshold, but changes below TOP_THRESHOLD_STATE should always + // be processed callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE - 1, testProcStateSeq++, PROCESS_CAPABILITY_NONE); - assertFalse(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); + assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); } waitForUidEventHandlerIdle(); } @@ -2349,21 +2350,21 @@ public class NetworkPolicyManagerServiceTest { int testProcStateSeq = 0; try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { // First callback for uid. - callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + callOnUidStatechanged(UID_A, FOREGROUND_THRESHOLD_STATE, testProcStateSeq++, PROCESS_CAPABILITY_NONE); assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); } waitForUidEventHandlerIdle(); try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { // The same process-state with one network capability added. - callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + callOnUidStatechanged(UID_A, FOREGROUND_THRESHOLD_STATE, testProcStateSeq++, PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK); assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); } waitForUidEventHandlerIdle(); try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { // The same process-state with another network capability added. - callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + callOnUidStatechanged(UID_A, FOREGROUND_THRESHOLD_STATE, testProcStateSeq++, PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK | PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK); assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); @@ -2371,11 +2372,21 @@ public class NetworkPolicyManagerServiceTest { waitForUidEventHandlerIdle(); try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { // The same process-state with all capabilities, but no change in network capabilities. - callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + callOnUidStatechanged(UID_A, FOREGROUND_THRESHOLD_STATE, testProcStateSeq++, PROCESS_CAPABILITY_ALL); assertFalse(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); } waitForUidEventHandlerIdle(); + + callAndWaitOnUidStateChanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + PROCESS_CAPABILITY_ALL); + try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { + // No change in capabilities, but TOP_THRESHOLD_STATE change should always be processed. + callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + PROCESS_CAPABILITY_ALL); + assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); + } + waitForUidEventHandlerIdle(); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 99ab40569b70..06a4ac932e8e 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -5862,6 +5862,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(captor.getValue().getNotification().flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); + assertThat(captor.getValue().shouldPostSilently()).isTrue(); } @Test @@ -8603,6 +8604,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(captor.getValue().getNotification().flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); + assertThat(captor.getValue().shouldPostSilently()).isTrue(); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 82e557115608..beafeec20bb5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -3416,6 +3416,7 @@ public class ActivityRecordTests extends WindowTestsBase { // Remove window during transition, so it is requested to hide, but won't be committed until // the transition is finished. app.mActivityRecord.onRemovedFromDisplay(); + app.mActivityRecord.prepareSurfaces(); assertTrue(mDisplayContent.mClosingApps.contains(app.mActivityRecord)); assertFalse(app.mActivityRecord.isVisibleRequested()); @@ -3433,6 +3434,7 @@ public class ActivityRecordTests extends WindowTestsBase { public void testInClosingAnimation_visibilityCommitted_hideSurface() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); makeWindowVisibleAndDrawn(app); + app.mActivityRecord.prepareSurfaces(); // Put the activity in close transition. mDisplayContent.mOpeningApps.clear(); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index b9e87dc6efce..363ae141e512 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -291,13 +291,22 @@ public class BackNavigationControllerTests extends WindowTestsBase { assertTrue(predictable); outPrevActivities.clear(); - // Stacked + companion => predict for previous task + // Stacked + top companion to bottom but bottom didn't => predict for previous activity tf2.setCompanionTaskFragment(tf1); predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, outPrevActivities); - assertTrue(outPrevActivities.isEmpty()); + assertTrue(outPrevActivities.contains(prevAr)); assertTrue(predictable); tf2.setCompanionTaskFragment(null); + outPrevActivities.clear(); + + // Stacked + next companion to top => predict for previous task + tf1.setCompanionTaskFragment(tf2); + predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, + outPrevActivities); + assertTrue(outPrevActivities.isEmpty()); + assertTrue(predictable); + tf1.setCompanionTaskFragment(null); // Adjacent + no companion => unable to predict // TF1 | TF2 @@ -314,11 +323,13 @@ public class BackNavigationControllerTests extends WindowTestsBase { // Adjacent + companion => predict for previous task tf1.setCompanionTaskFragment(tf2); - tf2.setCompanionTaskFragment(tf1); predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, outPrevActivities); assertTrue(outPrevActivities.isEmpty()); assertTrue(predictable); + tf1.setCompanionTaskFragment(null); + + tf2.setCompanionTaskFragment(tf1); predictable = BackNavigationController.getAnimatablePrevActivities(task, prevAr, outPrevActivities); assertTrue(outPrevActivities.isEmpty()); @@ -361,18 +372,27 @@ public class BackNavigationControllerTests extends WindowTestsBase { tf3.setAdjacentTaskFragment(null); final TaskFragment tf4 = createTaskFragmentWithActivity(task); - // Stacked + companion => predict for previous activity below companion. + // Stacked + next companion to top => predict for previous activity below companion. // Tf4 // TF3 // TF2 // TF1 - tf4.setCompanionTaskFragment(tf3); tf3.setCompanionTaskFragment(tf4); topAr = tf4.getTopMostActivity(); predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, outPrevActivities); assertTrue(outPrevActivities.contains(tf2.getTopMostActivity())); assertTrue(predictable); + outPrevActivities.clear(); + tf3.setCompanionTaskFragment(null); + + // Stacked + top companion to next but next one didn't => predict for previous activity. + tf4.setCompanionTaskFragment(tf3); + topAr = tf4.getTopMostActivity(); + predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr, + outPrevActivities); + assertTrue(outPrevActivities.contains(tf3.getTopMostActivity())); + assertTrue(predictable); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java index 0a59ae159390..137ef5ed6849 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyInsetsTests.java @@ -29,6 +29,8 @@ import android.view.DisplayInfo; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; + import org.junit.Rule; import org.junit.Test; import org.junit.rules.ErrorCollector; @@ -142,12 +144,20 @@ public class DisplayPolicyInsetsTests extends DisplayPolicyTestsBase { private void verifyStableInsets(DisplayInfo di, int left, int top, int right, int bottom) { + if (Flags.insetsDecoupledConfiguration()) { + // TODO: update the verification to match the new behavior. + return; + } mErrorCollector.checkThat("stableInsets", getStableInsets(di), equalTo(new Rect(left, top, right, bottom))); } private void verifyNonDecorInsets(DisplayInfo di, int left, int top, int right, int bottom) { + if (Flags.insetsDecoupledConfiguration()) { + // TODO: update the verification to match the new behavior. + return; + } mErrorCollector.checkThat("nonDecorInsets", getNonDecorInsets(di), equalTo(new Rect(left, top, right, bottom))); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 4e4bbfe6371d..5aabea38bf5b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -21,6 +21,7 @@ import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP; import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION; import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS; @@ -63,6 +64,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.server.wm.LetterboxUiController.MIN_COUNT_TO_IGNORE_REQUEST_IN_LOOP; import static com.android.server.wm.LetterboxUiController.SET_ORIENTATION_REQUEST_COUNTER_TIMEOUT_MS; +import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -101,11 +103,11 @@ import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; - /** +/** * Test class for {@link LetterboxUiControllerTest}. * * Build/Install/Run: - * atest WmTests:LetterboxUiControllerTest + * atest WmTests:LetterboxUiControllerTest */ @SmallTest @Presubmit @@ -466,10 +468,48 @@ public class LetterboxUiControllerTest extends WindowTestsBase { assertTrue(mController.shouldForceRotateForCameraCompat()); } + // shouldApplyFreeformTreatmentForCameraCompat + + @Test + public void testShouldApplyCameraCompatFreeformTreatment_flagIsDisabled_returnsFalse() { + mSetFlagsRule.disableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM); + + assertFalse(mController.shouldApplyFreeformTreatmentForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) + public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() { + mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM); + + assertFalse(mController.shouldApplyFreeformTreatmentForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) + public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() + throws Exception { + mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldApplyFreeformTreatmentForCameraCompat()); + } + + @Test + public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() + throws Exception { + mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldApplyFreeformTreatmentForCameraCompat()); + } + @Test public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() { final InsetsSource taskbar = new InsetsSource(/*id=*/ 0, - WindowInsets.Type.navigationBars()); + WindowInsets.Type.navigationBars()); taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER); final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar); final Rect opaqueBounds = new Rect(0, 0, 500, 300); @@ -726,7 +766,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { throws Exception { mDisplayContent.setIgnoreOrientationRequest(false); assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded( - /* candidate */ SCREEN_ORIENTATION_PORTRAIT)); + /* candidate */ SCREEN_ORIENTATION_PORTRAIT)); } @Test @@ -736,7 +776,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { prepareActivityThatShouldApplyUserMinAspectRatioOverride(); assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded( - /* candidate */ SCREEN_ORIENTATION_PORTRAIT)); + /* candidate */ SCREEN_ORIENTATION_PORTRAIT)); } @Test @@ -859,6 +899,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded( /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED)); } + @Test public void testOverrideOrientationIfNeeded_respectOrientationRequestOverUserFullScreen() { spyOn(mController); @@ -1380,7 +1421,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { private void mockThatProperty(String propertyName, boolean value) throws Exception { Property property = new Property(propertyName, /* value */ value, /* packageName */ "", - /* className */ ""); + /* className */ ""); PackageManager pm = mWm.mContext.getPackageManager(); spyOn(pm); doReturn(property).when(pm).getProperty(eq(propertyName), anyString()); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java index ad7b9e69282e..96c3ed5767f6 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java @@ -184,7 +184,7 @@ abstract class DetectorSession { private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool(); // TODO: This may need to be a Handler(looper) final ScheduledExecutorService mScheduledExecutorService; - private final AppOpsManager mAppOpsManager; + final AppOpsManager mAppOpsManager; final HotwordAudioStreamCopier mHotwordAudioStreamCopier; final AtomicBoolean mUpdateStateAfterStartFinished = new AtomicBoolean(false); final IHotwordRecognitionStatusCallback mCallback; @@ -201,7 +201,7 @@ abstract class DetectorSession { /** Identity used for attributing app ops when delivering data to the Interactor. */ @Nullable - private final Identity mVoiceInteractorIdentity; + final Identity mVoiceInteractorIdentity; @GuardedBy("mLock") ParcelFileDescriptor mCurrentAudioSink; @GuardedBy("mLock") @@ -926,7 +926,7 @@ abstract class DetectorSession { * @param permission The identifier of the permission we want to check. * @param reason The reason why we're requesting the permission, for auditing purposes. */ - private static void enforcePermissionForDataDelivery(@NonNull Context context, + protected static void enforcePermissionForDataDelivery(@NonNull Context context, @NonNull Identity identity, @NonNull String permission, @NonNull String reason) { final int status = PermissionUtil.checkPermissionForDataDelivery(context, identity, permission, reason); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java index aef8e6fabc9b..0a660658338d 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java @@ -16,6 +16,10 @@ package com.android.server.voiceinteraction; +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.RECORD_AUDIO; +import static android.app.AppOpsManager.OP_CAMERA; +import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.service.voice.VisualQueryDetectionServiceFailure.ERROR_CODE_ILLEGAL_ATTENTION_STATE; import static android.service.voice.VisualQueryDetectionServiceFailure.ERROR_CODE_ILLEGAL_STREAMING_STATE; @@ -24,6 +28,7 @@ import android.annotation.Nullable; import android.content.Context; import android.media.AudioFormat; import android.media.permission.Identity; +import android.os.Binder; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; @@ -58,6 +63,14 @@ import java.util.concurrent.ScheduledExecutorService; final class VisualQueryDetectorSession extends DetectorSession { private static final String TAG = "VisualQueryDetectorSession"; + + private static final String VISUAL_QUERY_DETECTION_AUDIO_OP_MESSAGE = + "Providing query detection result from VisualQueryDetectionService to " + + "VoiceInteractionService"; + + private static final String VISUAL_QUERY_DETECTION_CAMERA_OP_MESSAGE = + "Providing query detection result from VisualQueryDetectionService to " + + "VoiceInteractionService"; private IVisualQueryDetectionAttentionListener mAttentionListener; private boolean mEgressingData; private boolean mQueryStreaming; @@ -172,6 +185,22 @@ final class VisualQueryDetectorSession extends DetectorSession { "Cannot stream queries without attention signals.")); return; } + try { + enforcePermissionsForVisualQueryDelivery(RECORD_AUDIO, OP_RECORD_AUDIO, + VISUAL_QUERY_DETECTION_AUDIO_OP_MESSAGE); + } catch (SecurityException e) { + Slog.w(TAG, "Ignoring #onQueryDetected due to a SecurityException", e); + try { + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot stream queries without audio permission.")); + } catch (RemoteException e1) { + notifyOnDetectorRemoteException(); + throw e1; + } + return; + } mQueryStreaming = true; callback.onQueryDetected(partialQuery); Slog.i(TAG, "Egressed from visual query detection process."); @@ -202,6 +231,48 @@ final class VisualQueryDetectorSession extends DetectorSession { + "enabling the setting.")); return; } + + // Show camera icon if visual only accessibility data egresses + if (partialResult.getAccessibilityDetectionData() != null) { + try { + enforcePermissionsForVisualQueryDelivery(CAMERA, OP_CAMERA, + VISUAL_QUERY_DETECTION_CAMERA_OP_MESSAGE); + } catch (SecurityException e) { + Slog.w(TAG, "Ignoring #onQueryDetected due to a SecurityException", e); + try { + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot stream visual only accessibility data " + + "without camera permission.")); + } catch (RemoteException e1) { + notifyOnDetectorRemoteException(); + throw e1; + } + return; + } + } + + // Show microphone icon if text query egresses + if (!partialResult.getPartialQuery().isEmpty()) { + try { + enforcePermissionsForVisualQueryDelivery(RECORD_AUDIO, OP_RECORD_AUDIO, + VISUAL_QUERY_DETECTION_AUDIO_OP_MESSAGE); + } catch (SecurityException e) { + Slog.w(TAG, "Ignoring #onQueryDetected due to a SecurityException", e); + try { + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot stream queries without audio permission.")); + } catch (RemoteException e1) { + notifyOnDetectorRemoteException(); + throw e1; + } + return; + } + } + mQueryStreaming = true; callback.onResultDetected(partialResult); Slog.i(TAG, "Egressed from visual query detection process."); @@ -280,6 +351,20 @@ final class VisualQueryDetectorSession extends DetectorSession { mEnableAccessibilityDataEgress = enable; } + void enforcePermissionsForVisualQueryDelivery(String permission, int op, String msg) { + Binder.withCleanCallingIdentity(() -> { + synchronized (mLock) { + enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity, + permission, msg); + mAppOpsManager.noteOpNoThrow( + op, mVoiceInteractorIdentity.uid, + mVoiceInteractorIdentity.packageName, + mVoiceInteractorIdentity.attributionTag, + msg); + } + }); + } + @SuppressWarnings("GuardedBy") public void dumpLocked(String prefix, PrintWriter pw) { super.dumpLocked(prefix, pw); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index a047b97d8592..69c47d43e2b8 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -15021,16 +15021,15 @@ public class TelephonyManager { */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @FlaggedApi(android.permission.flags.Flags.FLAG_GET_EMERGENCY_ROLE_HOLDER_API_ENABLED) - @NonNull + @Nullable @SystemApi public String getEmergencyAssistancePackageName() { if (!isEmergencyAssistanceEnabled() || !isVoiceCapable()) { throw new IllegalStateException("isEmergencyAssistanceEnabled() is false or device" + " not voice capable."); } - String emergencyRole = mContext.getSystemService(RoleManager.class) + return mContext.getSystemService(RoleManager.class) .getEmergencyRoleHolder(mContext.getUserId()); - return Objects.requireNonNull(emergencyRole, "Emergency role holder must not be null"); } /** diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt index 0c60f284a35b..ffed4087acff 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt @@ -47,7 +47,7 @@ constructor( val bound = gameView.getVisibleBounds() return uiDevice.swipe( bound.centerX(), - bound.top, + 0, bound.centerX(), bound.centerY(), SWIPE_STEPS diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java index 1d7be2f4f039..5107943c3528 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java @@ -333,6 +333,7 @@ public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase { public void testHandleLossRate_validationFail() throws Exception { checkHandleLossRate( 22, true /* isLastStateExpectedToUpdate */, true /* isCallbackExpected */); + verify(mConnectivityManager).reportNetworkConnectivity(mNetwork, false); } @Test diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java index 381c57496878..444208edc473 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.content.Context; +import android.net.ConnectivityManager; import android.net.IpSecConfig; import android.net.IpSecTransform; import android.net.LinkProperties; @@ -33,12 +34,14 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.TelephonyNetworkSpecifier; import android.net.vcn.FeatureFlags; +import android.net.vcn.Flags; import android.os.Handler; import android.os.IPowerManager; import android.os.IThermalService; import android.os.ParcelUuid; import android.os.PowerManager; import android.os.test.TestLooper; +import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.TelephonyManager; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; @@ -46,6 +49,7 @@ import com.android.server.vcn.VcnContext; import com.android.server.vcn.VcnNetworkProvider; import org.junit.Before; +import org.junit.Rule; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -53,6 +57,8 @@ import java.util.Set; import java.util.UUID; public abstract class NetworkEvaluationTestBase { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + protected static final String SSID = "TestWifi"; protected static final String SSID_OTHER = "TestWifiOther"; protected static final String PLMN_ID = "123456"; @@ -103,6 +109,7 @@ public abstract class NetworkEvaluationTestBase { @Mock protected FeatureFlags mFeatureFlags; @Mock protected android.net.platform.flags.FeatureFlags mCoreNetFeatureFlags; @Mock protected TelephonySubscriptionSnapshot mSubscriptionSnapshot; + @Mock protected ConnectivityManager mConnectivityManager; @Mock protected TelephonyManager mTelephonyManager; @Mock protected IPowerManager mPowerManagerService; @@ -114,6 +121,8 @@ public abstract class NetworkEvaluationTestBase { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mSetFlagsRule.enableFlags(Flags.FLAG_VALIDATE_NETWORK_ON_IPSEC_LOSS); + when(mNetwork.getNetId()).thenReturn(-1); mTestLooper = new TestLooper(); @@ -130,6 +139,12 @@ public abstract class NetworkEvaluationTestBase { doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled(); setupSystemService( + mContext, + mConnectivityManager, + Context.CONNECTIVITY_SERVICE, + ConnectivityManager.class); + + setupSystemService( mContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class); when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager); when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID); diff --git a/wifi/java/src/android/net/wifi/WifiBlobStore.java b/wifi/java/src/android/net/wifi/WifiBlobStore.java new file mode 100644 index 000000000000..8bfaae72f932 --- /dev/null +++ b/wifi/java/src/android/net/wifi/WifiBlobStore.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.wifi; + +import com.android.internal.net.ConnectivityBlobStore; + +/** + * Database blob store for Wifi. + * @hide + */ +public class WifiBlobStore extends ConnectivityBlobStore { + private static final String DB_NAME = "WifiBlobStore.db"; + private static WifiBlobStore sInstance; + private WifiBlobStore() { + super(DB_NAME); + } + + /** Returns an instance of WifiBlobStore. */ + public static WifiBlobStore getInstance() { + if (sInstance == null) { + sInstance = new WifiBlobStore(); + } + return sInstance; + } +} diff --git a/wifi/java/src/android/net/wifi/WifiKeystore.java b/wifi/java/src/android/net/wifi/WifiKeystore.java index 1cda0326bf6c..a06d0eeade72 100644 --- a/wifi/java/src/android/net/wifi/WifiKeystore.java +++ b/wifi/java/src/android/net/wifi/WifiKeystore.java @@ -18,12 +18,17 @@ package android.net.wifi; import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.os.Binder; import android.os.Process; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.security.legacykeystore.ILegacyKeystore; import android.util.Log; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + /** * This class allows the storage and retrieval of non-standard Wifi certificate blobs. * @hide @@ -34,7 +39,7 @@ public final class WifiKeystore { private static final String TAG = "WifiKeystore"; private static final String LEGACY_KEYSTORE_SERVICE_NAME = "android.security.legacykeystore"; - private static ILegacyKeystore getService() { + private static ILegacyKeystore getLegacyKeystore() { return ILegacyKeystore.Stub.asInterface( ServiceManager.checkService(LEGACY_KEYSTORE_SERVICE_NAME)); } @@ -54,13 +59,18 @@ public final class WifiKeystore { @SystemApi @SuppressLint("UnflaggedApi") public static boolean put(@NonNull String alias, @NonNull byte[] blob) { + // ConnectivityBlobStore uses the calling uid as a key into the DB. + // Clear identity to ensure that callers from system apps and the Wifi framework + // are able to access the same values. + final long identity = Binder.clearCallingIdentity(); try { Log.i(TAG, "put blob. alias " + alias); - getService().put(alias, Process.WIFI_UID, blob); - return true; + return WifiBlobStore.getInstance().put(alias, blob); } catch (Exception e) { Log.e(TAG, "Failed to put blob.", e); return false; + } finally { + Binder.restoreCallingIdentity(identity); } } @@ -69,23 +79,31 @@ public final class WifiKeystore { * @param alias Name of the blob to retrieve. * @return The unstructured blob, that is the blob that was stored using * {@link android.net.wifi.WifiKeystore#put}. - * Returns null if no blob was found. + * Returns empty byte[] if no blob was found. * @hide */ @SystemApi @SuppressLint("UnflaggedApi") public static @NonNull byte[] get(@NonNull String alias) { + final long identity = Binder.clearCallingIdentity(); try { Log.i(TAG, "get blob. alias " + alias); - return getService().get(alias, Process.WIFI_UID); + byte[] blob = WifiBlobStore.getInstance().get(alias); + if (blob != null) { + return blob; + } + Log.i(TAG, "Searching for blob in Legacy Keystore"); + return getLegacyKeystore().get(alias, Process.WIFI_UID); } catch (ServiceSpecificException e) { if (e.errorCode != ILegacyKeystore.ERROR_ENTRY_NOT_FOUND) { Log.e(TAG, "Failed to get blob.", e); } } catch (Exception e) { Log.e(TAG, "Failed to get blob.", e); + } finally { + Binder.restoreCallingIdentity(identity); } - return null; + return new byte[0]; } /** @@ -97,17 +115,27 @@ public final class WifiKeystore { @SystemApi @SuppressLint("UnflaggedApi") public static boolean remove(@NonNull String alias) { + boolean blobStoreSuccess = false; + boolean legacyKsSuccess = false; + final long identity = Binder.clearCallingIdentity(); try { - getService().remove(alias, Process.WIFI_UID); - return true; + Log.i(TAG, "remove blob. alias " + alias); + blobStoreSuccess = WifiBlobStore.getInstance().remove(alias); + // Legacy Keystore will throw an exception if the alias is not found. + getLegacyKeystore().remove(alias, Process.WIFI_UID); + legacyKsSuccess = true; } catch (ServiceSpecificException e) { if (e.errorCode != ILegacyKeystore.ERROR_ENTRY_NOT_FOUND) { Log.e(TAG, "Failed to remove blob.", e); } } catch (Exception e) { Log.e(TAG, "Failed to remove blob.", e); + } finally { + Binder.restoreCallingIdentity(identity); } - return false; + Log.i(TAG, "Removal status: wifiBlobStore=" + blobStoreSuccess + + ", legacyKeystore=" + legacyKsSuccess); + return blobStoreSuccess || legacyKsSuccess; } /** @@ -119,14 +147,24 @@ public final class WifiKeystore { @SystemApi @SuppressLint("UnflaggedApi") public static @NonNull String[] list(@NonNull String prefix) { + final long identity = Binder.clearCallingIdentity(); try { - final String[] aliases = getService().list(prefix, Process.WIFI_UID); - for (int i = 0; i < aliases.length; ++i) { - aliases[i] = aliases[i].substring(prefix.length()); + // Aliases from WifiBlobStore will be pre-trimmed. + final String[] blobStoreAliases = WifiBlobStore.getInstance().list(prefix); + final String[] legacyAliases = getLegacyKeystore().list(prefix, Process.WIFI_UID); + for (int i = 0; i < legacyAliases.length; ++i) { + legacyAliases[i] = legacyAliases[i].substring(prefix.length()); } - return aliases; + // Deduplicate aliases before returning. + Set<String> uniqueAliases = new HashSet<>(); + uniqueAliases.addAll(Arrays.asList(blobStoreAliases)); + uniqueAliases.addAll(Arrays.asList(legacyAliases)); + String[] uniqueAliasArray = new String[uniqueAliases.size()]; + return uniqueAliases.toArray(uniqueAliasArray); } catch (Exception e) { Log.e(TAG, "Failed to list blobs.", e); + } finally { + Binder.restoreCallingIdentity(identity); } return new String[0]; } |