diff options
202 files changed, 5081 insertions, 1497 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 35ab5f014815..443a6c0e91e6 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -4135,7 +4135,6 @@ package android.window { method @NonNull public static String typeToString(int); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.window.BackNavigationInfo> CREATOR; - field public static final String KEY_TRIGGER_BACK = "TriggerBack"; field public static final int TYPE_CALLBACK = 4; // 0x4 field public static final int TYPE_CROSS_ACTIVITY = 2; // 0x2 field public static final int TYPE_CROSS_TASK = 3; // 0x3 diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index dbde7d20f0d8..6ff1bfc5291c 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -2583,6 +2583,7 @@ public class Notification implements Parcelable this.when = System.currentTimeMillis(); if (updateRankingTime()) { creationTime = when; + extras.putBoolean(EXTRA_SHOW_WHEN, true); } else { this.creationTime = System.currentTimeMillis(); } @@ -2598,6 +2599,7 @@ public class Notification implements Parcelable { if (updateRankingTime()) { creationTime = when; + extras.putBoolean(EXTRA_SHOW_WHEN, true); } new Builder(context) .setWhen(when) @@ -2630,6 +2632,7 @@ public class Notification implements Parcelable this.when = when; if (updateRankingTime()) { creationTime = when; + extras.putBoolean(EXTRA_SHOW_WHEN, true); } else { this.creationTime = System.currentTimeMillis(); } @@ -4382,14 +4385,16 @@ public class Notification implements Parcelable /** * Add a timestamp pertaining to the notification (usually the time the event occurred). * - * For apps targeting {@link android.os.Build.VERSION_CODES#N} and above, this time is not - * shown anymore by default and must be opted into by using - * {@link android.app.Notification.Builder#setShowWhen(boolean)} - * * @see Notification#when */ @NonNull public Builder setWhen(long when) { + if (updateRankingTime()) { + // don't show a timestamp that's decades old + if (mN.extras.getBoolean(EXTRA_SHOW_WHEN, true) && when == 0) { + return this; + } + } mN.when = when; return this; } @@ -4397,8 +4402,6 @@ public class Notification implements Parcelable /** * Control whether the timestamp set with {@link #setWhen(long) setWhen} is shown * in the content view. - * For apps targeting {@link android.os.Build.VERSION_CODES#N} and above, this defaults to - * {@code false}. For earlier apps, the default is {@code true}. */ @NonNull public Builder setShowWhen(boolean show) { diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java index f21e11ab35cc..c7b0be7553c2 100644 --- a/core/java/android/app/admin/DeviceAdminReceiver.java +++ b/core/java/android/app/admin/DeviceAdminReceiver.java @@ -244,6 +244,10 @@ public class DeviceAdminReceiver extends BroadcastReceiver { * {@link android.app.admin.DevicePolicyManager#isProfileOwnerApp}. You will generally handle * this in {@link DeviceAdminReceiver#onProfileProvisioningComplete}. * + * <p>The intent for this action may include the following extras: + * <ul> + * <li>{@link DevicePolicyManager#EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE} + * * @see DevicePolicyManager#ACTION_PROVISIONING_SUCCESSFUL */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) @@ -801,6 +805,9 @@ public class DeviceAdminReceiver extends BroadcastReceiver { * {@link DevicePolicyManager#ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the same * application. * + * <p>The {@code Intent} may include any of the extras specified for + * {@link #ACTION_PROFILE_PROVISIONING_COMPLETE}. + * * @param context The running context as per {@link #onReceive}. * @param intent The received intent as per {@link #onReceive}. */ diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index ba91be9e9e6c..9058713ecfa1 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -192,32 +192,134 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.function.Consumer; -// TODO(b/172376923) - add CarDevicePolicyManager examples below (or remove reference to it). /** - * Public interface for managing policies enforced on a device. Most clients of this class must be - * registered with the system as a <a href="{@docRoot}guide/topics/admin/device-admin.html">device - * administrator</a>. Additionally, a device administrator may be registered as either a profile or - * device owner. A given method is accessible to all device administrators unless the documentation - * for that method specifies that it is restricted to either device or profile owners. Any - * application calling an api may only pass as an argument a device administrator component it - * owns. Otherwise, a {@link SecurityException} will be thrown. + * Manages device policy and restrictions applied to the user of the device or + * apps running on the device. * - * <p><b>Note: </b>on - * {@link android.content.pm.PackageManager#FEATURE_AUTOMOTIVE automotive builds}, some methods can - * throw an {@link UnsafeStateException} exception (for example, if the vehicle is moving), so - * callers running on automotive builds should always check for that exception, otherwise they - * might crash. + * <p>This class contains three types of methods: + * <ol><li>Those aimed at <a href="#managingapps">managing apps</a> + * <li>Those aimed at the <a href="#roleholder">Device Policy Management Role Holder</a> + * <li>Those aimed at <a href="#querying">apps which wish to respect device policy</a> + * </ol> * - * <div class="special reference"> - * <h3>Developer Guides</h3> - * <p> - * For more information about managing policies for device administration, read the <a href= - * "{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a> developer - * guide. </div> + * <p>The intended caller for each API is indicated in its Javadoc. + * + * <p id="managingapps"><b>Managing Apps</b> + * <p>Apps can be made capable of setting device policy ("Managing Apps") either by + * being set as a <a href="#deviceadmin">Device Administrator</a>, being set as a + * <a href="#devicepolicycontroller">Device Policy Controller</a>, or by holding the + * appropriate <a href="#permissions">Permissions</a>. + * + * <p id="deviceadmin">A <b>Device Administrator</b> is an app which is able to enforce device + * policies that it has declared in its device admin XML file. An app can prompt the user to give it + * device administator privileges using the {@link #ACTION_ADD_DEVICE_ADMIN} action. + * + * <p>For more information about Device Administration, read the + * <a href="{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a> + * developer guide. + * + * <p id="devicepolicycontroller">Through <a href="#managed_provisioning">Managed Provisioning</a>, + * Device Administrator apps can also be recognised as <b> + Device Policy Controllers</b>. Device Policy Controllers can be one of + * two types: + * <ul> + * <li>A <i id="deviceowner">Device Owner</i>, which only ever exists on the + * {@link UserManager#isSystemUser System User} or {@link UserManager#isMainUser Main User}, is + * the most powerful type of Device Policy Controller and can affect policy across the device. + * <li>A <i id="profileowner">Profile Owner<i>, which can exist on any user, can + * affect policy on the user it is on, and when it is running on + * {@link UserManager#isProfile a profile} has + * <a href="#profile-on-parent">limited</a> ability to affect policy on its + * {@link UserManager#getProfileParent parent}. + * </ul> + * + * <p>Additional capabilities can be provided to Device Policy Controllers in + * the following circumstances: + * <ul> + * <li>A Profile Owner on an <a href="#organization-owned">organization owned</a> device has access + * to additional abilities, both <a href="#profile-on-parent-organization-owned">affecting policy on the profile's</a> + * {@link UserManager#getProfileParent parent} and also the profile itself. + * <li>A Profile Owner running on the {@link UserManager#isSystemUser System User} has access to + * additional capabilities which affect the {@link UserManager#isSystemUser System User} and + * also the whole device. + * <li>A Profile Owner running on an <a href="#affiliated">affiliated</a> user has + * capabilities similar to that of a <a href="#deviceowner">Device Owner</a> + * </ul> + * + * <p>For more information, see <a href="{@docRoot}work/dpc/build-dpc">Building a Device Policy + * Controller</a>. + * + * <p><a href="#permissions">Permissions</a> are generally only given to apps + * fulfilling particular key roles on the device (such as managing {@link DeviceLockManager +device locks}). + * + * <p id="roleholder"><b>Device Policy Management Role Holder</b> + * <p>One app on the device fulfills the {@link RoleManager#ROLE_DEVICE_POLICY_MANAGEMENT Device +Policy Management Role} and is trusted with managing the overall state of + * Device Policy. This has access to much more powerful methods than + * <a href="#managingapps">managing apps</a>. + * + * <p id="querying"><b>Querying Device Policy</b> + * <p>In most cases, regular apps do not need to concern themselves with device + * policy, and restrictions will be enforced automatically. There are some cases + * where an app may wish to query device policy to provide a better user + * experience. Only a small number of policies allow apps to query them directly. + * These APIs will typically have no special required permissions. + * + * <p id="managedprovisioning"><b>Managed Provisioning</b> + * <p>Managed Provisioning is the process of recognising an app as a + * <a href="#deviceowner">Device Owner</a> or <a href="#profileowner">Profile Owner</a>. It + * involves presenting education and consent screens to the user to ensure they + * are aware of the capabilities this grants the <a href="#devicepolicycontroller">Device Policy + * Controller</a> + * + * <p>For more information on provisioning, see <a href="{@docRoot}work/dpc/build-dpc">Building a + * Device Policy Controller</a>. + * + * <p id="managed_profile">A <b>Managed Profile</b> enables data separation. For example to use + * a device both for personal and corporate usage. The managed profile and its + * {@link UserManager#getProfileParent parent} share a launcher. + * + * <p id="affiliated"><b>Affiliation</b> + * <p>Using the {@link #setAffiliationIds} method, a + * <a href="#deviceowner">Device Owner</a> can set a list of affiliation ids for the + * {@link UserManager#isSystemUser System User}. Any <a href="#profileowner">Profile Owner</a> on + * the same device can also call {@link #setAffiliationIds} to set affiliation ids + * for the {@link UserManager user} it is on. When there is the same ID + * present in both lists, the user is said to be "affiliated" and we can refer to + * the <a href="#profileowner">Profile Owner</a> as a "profile owner on an affiliated + * user" or an "affiliated profile owner". + * + * Becoming affiliated grants the <a href="#profileowner">Profile Owner</a> capabilities similar to + * that of the <a href="#deviceowner">Device Owner</a>. It also allows use of the + * {@link #bindDeviceAdminServiceAsUser} APIs for direct communication between the + * <a href="#deviceowner">Device Owner</a> and + * affiliated <a href="#profileowner">Profile Owners</a>. + * + * <p id="organization-owned"><b>Organization Owned</b></p> + * An organization owned device is one which is not owned by the person making use of the device and + * is instead owned by an organization such as their employer or education provider. These devices + * are recognised as being organization owned either by the presence of a + * <a href="#deviceowner">device owner</a> or of a + * {@link #isOrganizationOwnedDeviceWithManagedProfile profile which has a profile owner is marked + * as organization owned}. + * + * <p id="profile-on-parent-organization-owned">Profile owners running on an + * <a href="organization-owned">organization owned</a> device can exercise additional capabilities + * using the {@link #getParentProfileInstance(ComponentName)} API which apply to the parent user. + * Each API will indicate if it is usable in this way. + * + * <p id="automotive"><b>Android Automotive</b> + * <p>On {@link android.content.pm.PackageManager#FEATURE_AUTOMOTIVE + * "Android Automotive builds"}, some methods can throw + * {@link UnsafeStateException "an exception"} if an action is unsafe (for example, if the vehicle + * is moving). Callers running on + * {@link android.content.pm.PackageManager#FEATURE_AUTOMOTIVE + * "Android Automotive builds"} should always check for this exception. */ + @SystemService(Context.DEVICE_POLICY_SERVICE) @RequiresFeature(PackageManager.FEATURE_DEVICE_ADMIN) -@SuppressLint("UseIcu") public class DevicePolicyManager { /** @hide */ @@ -257,7 +359,7 @@ public class DevicePolicyManager { * Fetch the current value of mService. This is used in the binder cache lambda * expressions. */ - private final IDevicePolicyManager getService() { + private IDevicePolicyManager getService() { return mService; } @@ -265,7 +367,7 @@ public class DevicePolicyManager { * Fetch the current value of mParentInstance. This is used in the binder cache * lambda expressions. */ - private final boolean isParentInstance() { + private boolean isParentInstance() { return mParentInstance; } @@ -273,7 +375,7 @@ public class DevicePolicyManager { * Fetch the current value of mContext. This is used in the binder cache lambda * expressions. */ - private final Context getContext() { + private Context getContext() { return mContext; } @@ -284,39 +386,80 @@ public class DevicePolicyManager { } /** - * Activity action: Starts the provisioning flow which sets up a managed profile. - * - * <p>A managed profile allows data separation for example for the usage of a - * device as a personal and corporate device. The user which provisioning is started from and - * the managed profile share a launcher. - * - * <p>This intent will typically be sent by a mobile device management application (MDM). - * Provisioning adds a managed profile and sets the MDM as the profile owner who has full - * control over the profile. + * Activity action: Starts the provisioning flow which sets up a + * <a href="#managed-profile">managed profile</a>. * * <p>It is possible to check if provisioning is allowed or not by querying the method * {@link #isProvisioningAllowed(String)}. * - * <p>In version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this intent must contain the - * extra {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}. - * As of {@link android.os.Build.VERSION_CODES#M}, it should contain the extra - * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME} instead, although specifying only - * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} is still supported. - * - * <p>The intent may also contain the following extras: - * <ul> - * <li>{@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE}, optional </li> - * <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}, optional, supported from - * {@link android.os.Build.VERSION_CODES#N}</li> - * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}, optional</li> - * <li>{@link #EXTRA_PROVISIONING_LOGO_URI}, optional</li> - * <li>{@link #EXTRA_PROVISIONING_SKIP_USER_CONSENT}, optional</li> - * <li>{@link #EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION}, optional</li> - * <li>{@link #EXTRA_PROVISIONING_DISCLAIMERS}, optional</li> - * </ul> - * - * <p>When managed provisioning has completed, broadcasts are sent to the application specified - * in the provisioning intent. The + * <p>The intent may contain the following extras: + * + * <table> + * <thead> + * <tr> + * <th>Extra</th> + * <th></th> + * <th>Supported Versions</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE}</td> + * <td colspan="2"></td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}</td> + * <td></td> + * <td>{@link android.os.Build.VERSION_CODES#N}+</td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}</td> + * <td colspan="2"></td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_LOGO_URI}</td> + * <td colspan="2"></td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_SKIP_USER_CONSENT}</td> + * <td colspan="2"><b>Can only be used by an existing device owner trying to create a + * managed profile</b></td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION}</td> + * <td colspan="2"></td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_DISCLAIMERS}</td> + * <td colspan="2"></td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME}</td> + * <td> + * <b>Required if {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME} is not + * specified. Must match the package name of the calling application.</b> + * </td> + * <td>{@link android.os.Build.VERSION_CODES#LOLLIPOP}+</td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}</td> + * <td> + * <b>Required if {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} is not + * specified. Package name must match the package name of the calling + * application.</b> + * </td> + * <td>{@link android.os.Build.VERSION_CODES#M}+</td> + * </tr> + * <tr> + * <td>{@link #EXTRA_PROVISIONING_ALLOW_OFFLINE}</td> + * <td colspan="2">On {@link android.os.Build.VERSION_CODES#TIRAMISU}+, when set to + * true this will <b>force</b> offline provisioning instead of allowing it</td> + * </tr> + * </tbody> + * </table> + * + * <p>When <a href="#managedprovisioning">managed provisioning</a> has completed, broadcasts + * are sent to the application specified in the provisioning intent. The * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} broadcast is sent in the * managed profile and the {@link #ACTION_MANAGED_PROFILE_PROVISIONED} broadcast is sent in * the primary profile. @@ -325,25 +468,25 @@ public class DevicePolicyManager { * completed, along with the above broadcast, activity intent * {@link #ACTION_PROVISIONING_SUCCESSFUL} will also be sent to the profile owner. * - * <p>If provisioning fails, the managedProfile is removed so the device returns to its + * <p>If provisioning fails, the managed profile is removed so the device returns to its * previous state. * * <p>If launched with {@link android.app.Activity#startActivityForResult(Intent, int)} a - * result code of {@link android.app.Activity#RESULT_OK} implies that the synchronous part of + * result code of {@link android.app.Activity#RESULT_OK} indicates that the synchronous part of * the provisioning flow was successful, although this doesn't guarantee the full flow will - * succeed. Conversely a result code of {@link android.app.Activity#RESULT_CANCELED} implies - * that the user backed-out of provisioning, or some precondition for provisioning wasn't met. + * succeed. Conversely a result code of {@link android.app.Activity#RESULT_CANCELED} indicates + * that the user backed-out of provisioning or some precondition for provisioning wasn't met. * - * <p>If a device policy management role holder (DPMRH) updater is present on the device, an - * internet connection attempt must be made prior to launching this intent. If internet - * connection could not be established, provisioning will fail unless {@link + * <p>If a <a href="#roleholder">device policy management role holder</a> updater is present on + * the device, an internet connection attempt must be made prior to launching this intent. If + * an internet connection can not be established, provisioning will fail unless {@link * #EXTRA_PROVISIONING_ALLOW_OFFLINE} is explicitly set to {@code true}, in which case - * provisioning will continue without using the DPMRH. If an internet connection has been - * established, the DPMRH updater will be launched, which will update the DPMRH if it's not - * present on the device, or if it's present and not valid. - * - * <p>If a DPMRH is present on the device and valid, the provisioning flow will be deferred to - * it. + * provisioning will continue without using the + * <a href="#roleholder">device policy management role holder</a>. If an internet connection + * has been established, the <a href="#roleholder">device policy management role holder</a> + * updater will be launched, which may update the + * <a href="#roleholder">device policy management role holder</a> before continuing + * provisioning. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_PROVISION_MANAGED_PROFILE @@ -822,31 +965,25 @@ public class DevicePolicyManager { "android.app.extra.FORCE_UPDATE_ROLE_HOLDER"; /** - * A boolean extra indicating whether offline provisioning is allowed. - * - * <p>For the online provisioning flow, there will be an attempt to download and install - * the latest version of the device policy management role holder. The platform will then - * delegate provisioning to the device policy management role holder via role holder-specific - * provisioning actions. - * - * <p>For the offline provisioning flow, the provisioning flow will always be handled by - * the platform. + * A boolean extra indicating whether offline provisioning should be used. * - * <p>If this extra is set to {@code false}, the provisioning flow will enforce that an - * internet connection is established, which will start the online provisioning flow. If an - * internet connection cannot be established, provisioning will fail. + * <p>The default value is {@code false}. * - * <p>If this extra is set to {@code true}, the provisioning flow will still try to connect to - * the internet, but if it fails it will start the offline provisioning flow. + * <p>Usually during the <a href="#managedprovisioning">provisioning flow</a>, there will be + * an attempt to download and install the latest version of the <a href="#roleholder">device + * policy management role holder</a>. The platform will then + * delegate provisioning to the <a href="#roleholder">device + * * policy management role holder</a>. * - * <p>For T if this extra is set to {@code true}, the provisioning flow will be forced through - * the platform and there will be no attempt to download and install the device policy - * management role holder. + * <p>When this extra is set to {@code true}, the + * <a href="#managedprovisioning">provisioning flow</a> will always be handled by the platform + * and the <a href="#roleholder">device policy management role holder</a>'s part skipped. * - * <p>The default value is {@code false}. - * - * <p>This extra is respected when provided via the provisioning intent actions such as {@link - * #ACTION_PROVISION_MANAGED_PROFILE}. + * <p>On Android versions prior to {@link Build.VERSION_CODES#TIRAMISU}, when this extra is + * {@code false}, the <a href="#managedprovisioning">provisioning flow</a> will enforce that an + * internet connection is established, or otherwise fail. When this extra is {@code true}, a + * connection will still be attempted but when it cannot be established provisioning will + * continue offline. */ public static final String EXTRA_PROVISIONING_ALLOW_OFFLINE = "android.app.extra.PROVISIONING_ALLOW_OFFLINE"; @@ -1057,64 +1194,40 @@ public class DevicePolicyManager { public static final long DEFAULT_STRONG_AUTH_TIMEOUT_MS = 72 * 60 * 60 * 1000; // 72h /** - * A {@link android.os.Parcelable} extra of type {@link android.os.PersistableBundle} that - * allows a mobile device management application or NFC programmer application which starts - * managed provisioning to pass data to the management application instance after provisioning. + * A {@link android.os.Parcelable} extra of type {@link android.os.PersistableBundle} that is + * passed directly to the <a href="#devicepolicycontroller">Device Policy Controller</a> + * after <a href="#managed-provisioning">provisioning</a>. + * * <p> - * If used with {@link #ACTION_PROVISION_MANAGED_PROFILE} it can be used by the application that - * sends the intent to pass data to itself on the newly created profile. - * If used with {@link #ACTION_PROVISION_MANAGED_DEVICE} it allows passing data to the same - * instance of the app on the primary user. * Starting from {@link android.os.Build.VERSION_CODES#M}, if used with * {@link #MIME_TYPE_PROVISIONING_NFC} as part of NFC managed device provisioning, the NFC * message should contain a stringified {@link java.util.Properties} instance, whose string * properties will be converted into a {@link android.os.PersistableBundle} and passed to the * management application after provisioning. - * - * <p>Admin apps will receive this extra in their {@link #ACTION_GET_PROVISIONING_MODE} and - * {@link #ACTION_ADMIN_POLICY_COMPLIANCE} intent handlers. Additionally, {@link - * #ACTION_GET_PROVISIONING_MODE} may also return this extra which will then be sent over to - * {@link #ACTION_ADMIN_POLICY_COMPLIANCE}, alongside the original values that were passed to - * {@link #ACTION_GET_PROVISIONING_MODE}. - * - * <p> - * In both cases the application receives the data in - * {@link DeviceAdminReceiver#onProfileProvisioningComplete} via an intent with the action - * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE}. The bundle is not changed - * during the managed provisioning. */ public static final String EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE = "android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE"; /** - * A String extra holding the package name of the mobile device management application that - * will be set as the profile owner or device owner. + * A String extra holding the package name of the application that + * will be set as <a href="#devicepolicycontroller">Device Policy Controller</a>. * - * <p>If an application starts provisioning directly via an intent with action - * {@link #ACTION_PROVISION_MANAGED_PROFILE} this package has to match the package name of the - * application that started provisioning. The package will be set as profile owner in that case. + * <p>When this extra is set, the application must have exactly one + * {@link DeviceAdminReceiver device admin receiver}. This receiver will be set as the + * <a href="#devicepolicycontroller">Device Policy Controller</a>. * - * <p>This package is set as device owner when device owner provisioning is started by an NFC - * message containing an NFC record with MIME type {@link #MIME_TYPE_PROVISIONING_NFC}. - * - * <p> When this extra is set, the application must have exactly one device admin receiver. - * This receiver will be set as the profile or device owner and active admin. - * - * @see DeviceAdminReceiver - * @deprecated Use {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}. This extra is still - * supported, but only if there is only one device admin receiver in the package that requires - * the permission {@link android.Manifest.permission#BIND_DEVICE_ADMIN}. + * @deprecated Use {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME}. */ @Deprecated public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME = "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME"; /** - * A ComponentName extra indicating the device admin receiver of the mobile device management - * application that will be set as the profile owner or device owner and active admin. + * A ComponentName extra indicating the {@link DeviceAdminReceiver device admin receiver} of + * the application that will be set as the <a href="#devicepolicycontroller"> + * Device Policy Controller</a>. * * <p>If an application starts provisioning directly via an intent with action - * {@link #ACTION_PROVISION_MANAGED_PROFILE} or * {@link #ACTION_PROVISION_MANAGED_DEVICE} the package name of this * component has to match the package name of the application that started provisioning. * @@ -1123,35 +1236,28 @@ public class DevicePolicyManager { * message containing an NFC record with MIME type * {@link #MIME_TYPE_PROVISIONING_NFC}. For the NFC record, the component name must be * flattened to a string, via {@link ComponentName#flattenToShortString()}. - * - * @see DeviceAdminReceiver */ public static final String EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME = "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME"; /** * An {@link android.accounts.Account} extra holding the account to migrate during managed - * profile provisioning. If the account supplied is present in the primary user, it will be - * copied, along with its credentials to the managed profile and removed from the primary user. + * profile provisioning. * - * Use with {@link #ACTION_PROVISION_MANAGED_PROFILE}, with managed account provisioning, or - * return as an extra to the intent result from the {@link #ACTION_GET_PROVISIONING_MODE} - * activity. + * <p>If the account supplied is present in the user, it will be copied, along with its + * credentials to the managed profile and removed from the user. */ - public static final String EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE = "android.app.extra.PROVISIONING_ACCOUNT_TO_MIGRATE"; /** - * Boolean extra to indicate that the migrated account should be kept. This is used in - * conjunction with {@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE}. If it's set to {@code true}, - * the account will not be removed from the primary user after it is migrated to the newly - * created user or profile. + * Boolean extra to indicate that the + * {@link #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE migrated account} should be kept. * - * <p> Defaults to {@code false} + * <p>If it's set to {@code true}, the account will not be removed from the user after it is + * migrated to the newly created user or profile. * - * <p> Use with {@link #ACTION_PROVISION_MANAGED_PROFILE} or set as an extra to the - * intent result of the {@link #ACTION_GET_PROVISIONING_MODE} activity. + * <p>Defaults to {@code false} * * @see #EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE */ @@ -1590,17 +1696,14 @@ public class DevicePolicyManager { "android.app.action.PROVISIONING_SUCCESSFUL"; /** - * A boolean extra indicating whether device encryption can be skipped as part of device owner - * or managed profile provisioning. + * A boolean extra indicating whether device encryption can be skipped as part of + * <a href="#managed-provisioning>provisioning</a>. * * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC} or an intent with action * {@link #ACTION_PROVISION_MANAGED_DEVICE} that starts device owner provisioning. * * <p>From {@link android.os.Build.VERSION_CODES#N} onwards, this is also supported for an * intent with action {@link #ACTION_PROVISION_MANAGED_PROFILE}. - * - * <p>This extra can also be returned by the admin app when performing the admin-integrated - * provisioning flow as a result of the {@link #ACTION_GET_PROVISIONING_MODE} activity. */ public static final String EXTRA_PROVISIONING_SKIP_ENCRYPTION = "android.app.extra.PROVISIONING_SKIP_ENCRYPTION"; @@ -1608,23 +1711,22 @@ public class DevicePolicyManager { /** * A {@link Uri} extra pointing to a logo image. This image will be shown during the * provisioning. If this extra is not passed, a default image will be shown. - * <h5>The following URI schemes are accepted:</h5> + * + * <p><b>The following URI schemes are accepted:</b> * <ul> * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li> * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li> * </ul> * - * <p> It is the responsibility of the caller to provide an image with a reasonable + * <p>It is the responsibility of the caller to provide an image with a reasonable * pixel density for the device. * - * <p> If a content: URI is passed, the intent should have the flag + * <p>If a content: URI is passed, the intent should also have the flag * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and the uri should be added to the - * {@link android.content.ClipData} of the intent too. + * {@link android.content.ClipData} of the intent. * - * <p>Use in an intent with action {@link #ACTION_PROVISION_MANAGED_PROFILE} or - * {@link #ACTION_PROVISION_MANAGED_DEVICE} - * - * @deprecated Logo customization is no longer supported in the provisioning flow. + * @deprecated Logo customization is no longer supported in the + * <a href="#managedprovisioning">provisioning flow</a>. */ @Deprecated public static final String EXTRA_PROVISIONING_LOGO_URI = @@ -1632,7 +1734,8 @@ public class DevicePolicyManager { /** * A {@link Bundle}[] extra consisting of list of disclaimer headers and disclaimer contents. - * Each {@link Bundle} must have both {@link #EXTRA_PROVISIONING_DISCLAIMER_HEADER} + * + * <p>Each {@link Bundle} must have both {@link #EXTRA_PROVISIONING_DISCLAIMER_HEADER} * as disclaimer header, and {@link #EXTRA_PROVISIONING_DISCLAIMER_CONTENT} as disclaimer * content. * @@ -1653,20 +1756,21 @@ public class DevicePolicyManager { /** * A String extra of localized disclaimer header. * - * <p> The extra is typically the company name of mobile device management application (MDM) + * <p>The extra is typically the company name of mobile device management application (MDM) * or the organization name. * - * <p> Use in Bundle {@link #EXTRA_PROVISIONING_DISCLAIMERS} - * - * <p> System app, i.e. application with {@link ApplicationInfo#FLAG_SYSTEM}, can also insert a - * disclaimer by declaring an application-level meta-data in {@code AndroidManifest.xml}. - * Must use it with {@link #EXTRA_PROVISIONING_DISCLAIMER_CONTENT}. Here is the example: + * <p>{@link ApplicationInfo#FLAG_SYSTEM System apps} can also insert a disclaimer by declaring + * an application-level meta-data in {@code AndroidManifest.xml}. * + * <p>For example: * <pre> * <meta-data * android:name="android.app.extra.PROVISIONING_DISCLAIMER_HEADER" * android:resource="@string/disclaimer_header" * /></pre> + * + * <p>This must be accompanied with another extra using the key + * {@link #EXTRA_PROVISIONING_DISCLAIMER_CONTENT}. */ public static final String EXTRA_PROVISIONING_DISCLAIMER_HEADER = "android.app.extra.PROVISIONING_DISCLAIMER_HEADER"; @@ -1680,35 +1784,35 @@ public class DevicePolicyManager { * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})</li> * </ul> * - * <p> Styled text is supported in the disclaimer content. The content is parsed by - * {@link android.text.Html#fromHtml(String)} and displayed in a - * {@link android.widget.TextView}. + * <p>Styled text is supported. This is parsed by {@link android.text.Html#fromHtml(String)} + * and displayed in a {@link android.widget.TextView}. * - * <p> If a <code>content:</code> URI is passed, URI is passed, the intent should have the flag - * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and the uri should be added to the - * {@link android.content.ClipData} of the intent too. + * <p>If a <code>content:</code> URI is passed, the intent should also have the + * flag {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and the uri should be added to the + * {@link android.content.ClipData} of the intent. * - * <p> Use in Bundle {@link #EXTRA_PROVISIONING_DISCLAIMERS} - * - * <p> System app, i.e. application with {@link ApplicationInfo#FLAG_SYSTEM}, can also insert a + * <p>{@link ApplicationInfo#FLAG_SYSTEM System apps} can also insert a * disclaimer by declaring an application-level meta-data in {@code AndroidManifest.xml}. - * Must use it with {@link #EXTRA_PROVISIONING_DISCLAIMER_HEADER}. Here is the example: + * + * <p>For example: * * <pre> * <meta-data * android:name="android.app.extra.PROVISIONING_DISCLAIMER_CONTENT" * android:resource="@string/disclaimer_content" * /></pre> + * + * <p>This must be accompanied with another extra using the key + * {@link #EXTRA_PROVISIONING_DISCLAIMER_HEADER}. */ public static final String EXTRA_PROVISIONING_DISCLAIMER_CONTENT = "android.app.extra.PROVISIONING_DISCLAIMER_CONTENT"; /** - * A boolean extra indicating if the user consent steps from the provisioning flow should be - * skipped. If unspecified, defaults to {@code false}. + * A boolean extra indicating if the user consent steps from the + * <a href="#managed-provisioning">provisioning flow</a> should be skipped. * - * It can only be used by an existing device owner trying to create a managed profile via - * {@link #ACTION_PROVISION_MANAGED_PROFILE}. Otherwise it is ignored. + * <p>If unspecified, defaults to {@code false}. * * @deprecated this extra is no longer relevant as device owners cannot create managed profiles */ diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java index a51a74075298..d9a18d785537 100644 --- a/core/java/android/ddm/DdmHandleHello.java +++ b/core/java/android/ddm/DdmHandleHello.java @@ -42,12 +42,6 @@ public class DdmHandleHello extends DdmHandle { private static DdmHandleHello mInstance = new DdmHandleHello(); - private static final String[] FRAMEWORK_FEATURES = new String[] { - "opengl-tracing", - "view-hierarchy", - "support_boot_stages" - }; - /* singleton, do not instantiate */ private DdmHandleHello() {} @@ -193,22 +187,25 @@ public class DdmHandleHello extends DdmHandle { if (false) Log.v("ddm-heap", "Got feature list request"); - int size = 4 + 4 * (vmFeatures.length + FRAMEWORK_FEATURES.length); - for (int i = vmFeatures.length-1; i >= 0; i--) + String[] fmFeatures = Debug.getFeatureList(); + int size = 4 + 4 * (vmFeatures.length + fmFeatures.length); + for (int i = vmFeatures.length - 1; i >= 0; i--) { size += vmFeatures[i].length() * 2; - for (int i = FRAMEWORK_FEATURES.length-1; i>= 0; i--) - size += FRAMEWORK_FEATURES[i].length() * 2; + } + for (int i = fmFeatures.length - 1; i >= 0; i--) { + size += fmFeatures[i].length() * 2; + } ByteBuffer out = ByteBuffer.allocate(size); out.order(ChunkHandler.CHUNK_ORDER); - out.putInt(vmFeatures.length + FRAMEWORK_FEATURES.length); + out.putInt(vmFeatures.length + fmFeatures.length); for (int i = vmFeatures.length-1; i >= 0; i--) { out.putInt(vmFeatures[i].length()); putString(out, vmFeatures[i]); } - for (int i = FRAMEWORK_FEATURES.length-1; i >= 0; i--) { - out.putInt(FRAMEWORK_FEATURES[i].length()); - putString(out, FRAMEWORK_FEATURES[i]); + for (int i = fmFeatures.length - 1; i >= 0; i--) { + out.putInt(fmFeatures[i].length()); + putString(out, fmFeatures[i]); } return new Chunk(CHUNK_FEAT, out); diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index f785cca4e9f4..a55398ac9752 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -110,6 +110,12 @@ public final class Debug private static final String DEFAULT_TRACE_BODY = "dmtrace"; private static final String DEFAULT_TRACE_EXTENSION = ".trace"; + private static final String[] FRAMEWORK_FEATURES = new String[] { + "opengl-tracing", + "view-hierarchy", + "support_boot_stages", + }; + /** * This class is used to retrieved various statistics about the memory mappings for this * process. The returned info is broken down by dalvik, native, and other. All results are in kB. @@ -1106,6 +1112,17 @@ public final class Debug } /** + * Returns an array of strings that identify Framework features. This is + * used by DDMS to determine what sorts of operations the Framework can + * perform. + * + * @hide + */ + public static String[] getFeatureList() { + return FRAMEWORK_FEATURES; + } + + /** * Change the JDWP port. * * @deprecated no longer needed or useful diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java index 17d14042e468..7dc151d8f9ee 100644 --- a/core/java/android/view/PointerIcon.java +++ b/core/java/android/view/PointerIcon.java @@ -288,7 +288,7 @@ public final class PointerIcon implements Parcelable { if (bitmap == null) { throw new IllegalArgumentException("bitmap must not be null"); } - validateHotSpot(bitmap, hotSpotX, hotSpotY); + validateHotSpot(bitmap, hotSpotX, hotSpotY, false /* isScaled */); PointerIcon icon = new PointerIcon(TYPE_CUSTOM); icon.mBitmap = bitmap; @@ -521,7 +521,9 @@ public final class PointerIcon implements Parcelable { BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; final Bitmap bitmap = getBitmapFromDrawable(bitmapDrawable); - validateHotSpot(bitmap, hotSpotX, hotSpotY); + // The bitmap and hotspot are loaded from the context, which means it is implicitly scaled + // to the current display density, so treat this as a scaled icon when verifying hotspot. + validateHotSpot(bitmap, hotSpotX, hotSpotY, true /* isScaled */); // Set the properties now that we have successfully loaded the icon. mBitmap = bitmap; mHotSpotX = hotSpotX; @@ -535,11 +537,16 @@ public final class PointerIcon implements Parcelable { + ", hotspotX=" + mHotSpotX + ", hotspotY=" + mHotSpotY + "}"; } - private static void validateHotSpot(Bitmap bitmap, float hotSpotX, float hotSpotY) { - if (hotSpotX < 0 || hotSpotX >= bitmap.getWidth()) { + private static void validateHotSpot(Bitmap bitmap, float hotSpotX, float hotSpotY, + boolean isScaled) { + // Be more lenient when checking the hotspot for scaled icons to account for the restriction + // that bitmaps must have an integer size. + if (hotSpotX < 0 || (isScaled ? (int) hotSpotX > bitmap.getWidth() + : hotSpotX >= bitmap.getWidth())) { throw new IllegalArgumentException("x hotspot lies outside of the bitmap area"); } - if (hotSpotY < 0 || hotSpotY >= bitmap.getHeight()) { + if (hotSpotY < 0 || (isScaled ? (int) hotSpotY > bitmap.getHeight() + : hotSpotY >= bitmap.getHeight())) { throw new IllegalArgumentException("y hotspot lies outside of the bitmap area"); } } diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java index 4816f35e6a07..b1cf8340cc25 100644 --- a/core/java/android/window/BackNavigationInfo.java +++ b/core/java/android/window/BackNavigationInfo.java @@ -72,8 +72,17 @@ public final class BackNavigationInfo implements Parcelable { /** * Key to access the boolean value passed in {#mOnBackNavigationDone} result bundle * that represents if back navigation has been triggered. + * @hide + */ + public static final String KEY_NAVIGATION_FINISHED = "NavigationFinished"; + + /** + * Key to access the boolean value passed in {#mOnBackNavigationDone} result bundle + * that represents if back gesture has been triggered. + * @hide */ - public static final String KEY_TRIGGER_BACK = "TriggerBack"; + public static final String KEY_GESTURE_FINISHED = "GestureFinished"; + /** * Defines the type of back destinations a back even can lead to. This is used to define the @@ -192,7 +201,21 @@ public final class BackNavigationInfo implements Parcelable { public void onBackNavigationFinished(boolean triggerBack) { if (mOnBackNavigationDone != null) { Bundle result = new Bundle(); - result.putBoolean(KEY_TRIGGER_BACK, triggerBack); + result.putBoolean(KEY_NAVIGATION_FINISHED, triggerBack); + mOnBackNavigationDone.sendResult(result); + } + } + + /** + * Callback to be called when the back gesture is finished in order to notify the server that + * it can ask app to start rendering. + * @hide + * @param triggerBack Boolean indicating if back gesture has been triggered. + */ + public void onBackGestureFinished(boolean triggerBack) { + if (mOnBackNavigationDone != null) { + Bundle result = new Bundle(); + result.putBoolean(KEY_GESTURE_FINISHED, triggerBack); mOnBackNavigationDone.sendResult(result); } } diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java index 93297e64c621..89327fe358f5 100644 --- a/core/java/android/window/TaskFragmentCreationParams.java +++ b/core/java/android/window/TaskFragmentCreationParams.java @@ -18,10 +18,13 @@ package android.window; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WindowingMode; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.TestApi; +import android.content.pm.ActivityInfo.ScreenOrientation; import android.graphics.Rect; import android.os.IBinder; import android.os.Parcel; @@ -101,11 +104,20 @@ public final class TaskFragmentCreationParams implements Parcelable { */ private final boolean mAllowTransitionWhenEmpty; + /** + * The override orientation for the TaskFragment. This is effective only for a system organizer. + * The value is ignored otherwise. Default to {@code SCREEN_ORIENTATION_UNSPECIFIED}. + * + * @see TaskFragmentOrganizer#registerOrganizer(boolean) + */ + private final @ScreenOrientation int mOverrideOrientation; + private TaskFragmentCreationParams( @NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect initialRelativeBounds, @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken, - @Nullable IBinder pairedActivityToken, boolean allowTransitionWhenEmpty) { + @Nullable IBinder pairedActivityToken, boolean allowTransitionWhenEmpty, + @ScreenOrientation int overrideOrientation) { if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) { throw new IllegalArgumentException("pairedPrimaryFragmentToken and" + " pairedActivityToken should not be set at the same time."); @@ -118,6 +130,7 @@ public final class TaskFragmentCreationParams implements Parcelable { mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken; mPairedActivityToken = pairedActivityToken; mAllowTransitionWhenEmpty = allowTransitionWhenEmpty; + mOverrideOrientation = overrideOrientation; } @NonNull @@ -168,6 +181,11 @@ public final class TaskFragmentCreationParams implements Parcelable { return mAllowTransitionWhenEmpty; } + /** @hide */ + public @ScreenOrientation int getOverrideOrientation() { + return mOverrideOrientation; + } + private TaskFragmentCreationParams(Parcel in) { mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in); mFragmentToken = in.readStrongBinder(); @@ -177,6 +195,7 @@ public final class TaskFragmentCreationParams implements Parcelable { mPairedPrimaryFragmentToken = in.readStrongBinder(); mPairedActivityToken = in.readStrongBinder(); mAllowTransitionWhenEmpty = in.readBoolean(); + mOverrideOrientation = in.readInt(); } /** @hide */ @@ -190,6 +209,7 @@ public final class TaskFragmentCreationParams implements Parcelable { dest.writeStrongBinder(mPairedPrimaryFragmentToken); dest.writeStrongBinder(mPairedActivityToken); dest.writeBoolean(mAllowTransitionWhenEmpty); + dest.writeInt(mOverrideOrientation); } @NonNull @@ -217,6 +237,7 @@ public final class TaskFragmentCreationParams implements Parcelable { + " pairedFragmentToken=" + mPairedPrimaryFragmentToken + " pairedActivityToken=" + mPairedActivityToken + " allowTransitionWhenEmpty=" + mAllowTransitionWhenEmpty + + " overrideOrientation=" + mOverrideOrientation + "}"; } @@ -252,6 +273,8 @@ public final class TaskFragmentCreationParams implements Parcelable { private boolean mAllowTransitionWhenEmpty; + private @ScreenOrientation int mOverrideOrientation = SCREEN_ORIENTATION_UNSPECIFIED; + public Builder(@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) { mOrganizer = organizer; @@ -330,12 +353,28 @@ public final class TaskFragmentCreationParams implements Parcelable { return this; } + /** + * Sets the override orientation for the TaskFragment. This is effective only for a system + * organizer. The value is ignored otherwise. Default to + * {@code SCREEN_ORIENTATION_UNSPECIFIED}. + * + * @see TaskFragmentOrganizer#registerOrganizer(boolean) + * + * @hide + */ + @RequiresPermission(value = android.Manifest.permission.MANAGE_ACTIVITY_TASKS) + @NonNull + public Builder setOverrideOrientation(@ScreenOrientation int overrideOrientation) { + mOverrideOrientation = overrideOrientation; + return this; + } + /** Constructs the options to create TaskFragment with. */ @NonNull public TaskFragmentCreationParams build() { return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken, mInitialRelativeBounds, mWindowingMode, mPairedPrimaryFragmentToken, - mPairedActivityToken, mAllowTransitionWhenEmpty); + mPairedActivityToken, mAllowTransitionWhenEmpty, mOverrideOrientation); } } } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index bcbac9319887..47a4052df95c 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -36,6 +36,8 @@ import android.view.ImeBackAnimationController; import androidx.annotation.VisibleForTesting; +import com.android.internal.annotations.GuardedBy; + import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -75,14 +77,17 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { @Nullable private ImeBackAnimationController mImeBackAnimationController; + @GuardedBy("mLock") /** Convenience hashmap to quickly decide if a callback has been added. */ private final HashMap<OnBackInvokedCallback, Integer> mAllCallbacks = new HashMap<>(); /** Holds all callbacks by priorities. */ @VisibleForTesting + @GuardedBy("mLock") public final TreeMap<Integer, ArrayList<OnBackInvokedCallback>> mOnBackInvokedCallbacks = new TreeMap<>(); private Checker mChecker; + private final Object mLock = new Object(); public WindowOnBackInvokedDispatcher(@NonNull Context context) { mChecker = new Checker(context); @@ -94,20 +99,24 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { */ public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window, @Nullable ImeBackAnimationController imeBackAnimationController) { - mWindowSession = windowSession; - mWindow = window; - mImeBackAnimationController = imeBackAnimationController; - if (!mAllCallbacks.isEmpty()) { - setTopOnBackInvokedCallback(getTopCallback()); + synchronized (mLock) { + mWindowSession = windowSession; + mWindow = window; + mImeBackAnimationController = imeBackAnimationController; + if (!mAllCallbacks.isEmpty()) { + setTopOnBackInvokedCallback(getTopCallback()); + } } } /** Detaches the dispatcher instance from its window. */ public void detachFromWindow() { - clear(); - mWindow = null; - mWindowSession = null; - mImeBackAnimationController = null; + synchronized (mLock) { + clear(); + mWindow = null; + mWindowSession = null; + mImeBackAnimationController = null; + } } // TODO: Take an Executor for the callback to run on. @@ -125,65 +134,71 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { */ public void registerOnBackInvokedCallbackUnchecked( @NonNull OnBackInvokedCallback callback, @Priority int priority) { - if (mImeDispatcher != null) { - mImeDispatcher.registerOnBackInvokedCallback(priority, callback); - return; - } - if (!mOnBackInvokedCallbacks.containsKey(priority)) { - mOnBackInvokedCallbacks.put(priority, new ArrayList<>()); - } - if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) { - callback = mImeBackAnimationController; - } - ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); + synchronized (mLock) { + if (mImeDispatcher != null) { + mImeDispatcher.registerOnBackInvokedCallback(priority, callback); + return; + } + if (!mOnBackInvokedCallbacks.containsKey(priority)) { + mOnBackInvokedCallbacks.put(priority, new ArrayList<>()); + } + if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) { + callback = mImeBackAnimationController; + } + ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); - // If callback has already been added, remove it and re-add it. - if (mAllCallbacks.containsKey(callback)) { - if (DEBUG) { - Log.i(TAG, "Callback already added. Removing and re-adding it."); + // If callback has already been added, remove it and re-add it. + if (mAllCallbacks.containsKey(callback)) { + if (DEBUG) { + Log.i(TAG, "Callback already added. Removing and re-adding it."); + } + Integer prevPriority = mAllCallbacks.get(callback); + mOnBackInvokedCallbacks.get(prevPriority).remove(callback); } - Integer prevPriority = mAllCallbacks.get(callback); - mOnBackInvokedCallbacks.get(prevPriority).remove(callback); - } - OnBackInvokedCallback previousTopCallback = getTopCallback(); - callbacks.add(callback); - mAllCallbacks.put(callback, priority); - if (previousTopCallback == null - || (previousTopCallback != callback - && mAllCallbacks.get(previousTopCallback) <= priority)) { - setTopOnBackInvokedCallback(callback); + OnBackInvokedCallback previousTopCallback = getTopCallback(); + callbacks.add(callback); + mAllCallbacks.put(callback, priority); + if (previousTopCallback == null + || (previousTopCallback != callback + && mAllCallbacks.get(previousTopCallback) <= priority)) { + setTopOnBackInvokedCallback(callback); + } } } @Override public void unregisterOnBackInvokedCallback(@NonNull OnBackInvokedCallback callback) { - if (mImeDispatcher != null) { - mImeDispatcher.unregisterOnBackInvokedCallback(callback); - return; - } - if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) { - callback = mImeBackAnimationController; - } - if (!mAllCallbacks.containsKey(callback)) { - if (DEBUG) { - Log.i(TAG, "Callback not found. returning..."); + synchronized (mLock) { + if (mImeDispatcher != null) { + mImeDispatcher.unregisterOnBackInvokedCallback(callback); + return; + } + if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) { + callback = mImeBackAnimationController; + } + if (!mAllCallbacks.containsKey(callback)) { + if (DEBUG) { + Log.i(TAG, "Callback not found. returning..."); + } + return; + } + OnBackInvokedCallback previousTopCallback = getTopCallback(); + Integer priority = mAllCallbacks.get(callback); + ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); + callbacks.remove(callback); + if (callbacks.isEmpty()) { + mOnBackInvokedCallbacks.remove(priority); + } + mAllCallbacks.remove(callback); + // Re-populate the top callback to WM if the removed callback was previously the top + // one. + if (previousTopCallback == callback) { + // We should call onBackCancelled() when an active callback is removed from + // dispatcher. + sendCancelledIfInProgress(callback); + setTopOnBackInvokedCallback(getTopCallback()); } - return; - } - OnBackInvokedCallback previousTopCallback = getTopCallback(); - Integer priority = mAllCallbacks.get(callback); - ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); - callbacks.remove(callback); - if (callbacks.isEmpty()) { - mOnBackInvokedCallbacks.remove(priority); - } - mAllCallbacks.remove(callback); - // Re-populate the top callback to WM if the removed callback was previously the top one. - if (previousTopCallback == callback) { - // We should call onBackCancelled() when an active callback is removed from dispatcher. - sendCancelledIfInProgress(callback); - setTopOnBackInvokedCallback(getTopCallback()); } } @@ -191,15 +206,21 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { * Indicates if the dispatcher is actively dispatching to a callback. */ public boolean isDispatching() { - return mIsDispatching; + synchronized (mLock) { + return mIsDispatching; + } } private void onStartDispatching() { - mIsDispatching = true; + synchronized (mLock) { + mIsDispatching = true; + } } private void onStopDispatching() { - mIsDispatching = false; + synchronized (mLock) { + mIsDispatching = false; + } } private void sendCancelledIfInProgress(@NonNull OnBackInvokedCallback callback) { @@ -223,27 +244,29 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { /** Clears all registered callbacks on the instance. */ public void clear() { - if (mImeDispatcher != null) { - mImeDispatcher.clear(); - mImeDispatcher = null; - } - if (!mAllCallbacks.isEmpty()) { - OnBackInvokedCallback topCallback = getTopCallback(); - if (topCallback != null) { - sendCancelledIfInProgress(topCallback); - } else { - // Should not be possible - Log.e(TAG, "There is no topCallback, even if mAllCallbacks is not empty"); + synchronized (mLock) { + if (mImeDispatcher != null) { + mImeDispatcher.clear(); + mImeDispatcher = null; + } + if (!mAllCallbacks.isEmpty()) { + OnBackInvokedCallback topCallback = getTopCallback(); + if (topCallback != null) { + sendCancelledIfInProgress(topCallback); + } else { + // Should not be possible + Log.e(TAG, "There is no topCallback, even if mAllCallbacks is not empty"); + } + // Clear binder references in WM. + setTopOnBackInvokedCallback(null); } - // Clear binder references in WM. - setTopOnBackInvokedCallback(null); - } - // We should also stop running animations since all callbacks have been removed. - // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler. - Handler.getMain().post(mProgressAnimator::reset); - mAllCallbacks.clear(); - mOnBackInvokedCallbacks.clear(); + // We should also stop running animations since all callbacks have been removed. + // note: mSpring.skipToEnd(), in ProgressAnimator.reset(), requires the main handler. + Handler.getMain().post(mProgressAnimator::reset); + mAllCallbacks.clear(); + mOnBackInvokedCallbacks.clear(); + } } private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) { @@ -275,13 +298,15 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { } public OnBackInvokedCallback getTopCallback() { - if (mAllCallbacks.isEmpty()) { - return null; - } - for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) { - ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); - if (!callbacks.isEmpty()) { - return callbacks.get(callbacks.size() - 1); + synchronized (mLock) { + if (mAllCallbacks.isEmpty()) { + return null; + } + for (Integer priority : mOnBackInvokedCallbacks.descendingKeySet()) { + ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority); + if (!callbacks.isEmpty()) { + return callbacks.get(callbacks.size() - 1); + } } } return null; @@ -315,16 +340,18 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { public void dump(String prefix, PrintWriter writer) { String innerPrefix = prefix + " "; writer.println(prefix + "WindowOnBackDispatcher:"); - if (mAllCallbacks.isEmpty()) { - writer.println(prefix + "<None>"); - return; - } + synchronized (mLock) { + if (mAllCallbacks.isEmpty()) { + writer.println(prefix + "<None>"); + return; + } - writer.println(innerPrefix + "Top Callback: " + getTopCallback()); - writer.println(innerPrefix + "Callbacks: "); - mAllCallbacks.forEach((callback, priority) -> { - writer.println(innerPrefix + " Callback: " + callback + " Priority=" + priority); - }); + writer.println(innerPrefix + "Top Callback: " + getTopCallback()); + writer.println(innerPrefix + "Callbacks: "); + mAllCallbacks.forEach((callback, priority) -> { + writer.println(innerPrefix + " Callback: " + callback + " Priority=" + priority); + }); + } } static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub { diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index e531bcbaa215..06ae11fee847 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -135,7 +135,7 @@ public class AccessibilityShortcutController { DialogStatus.SHOWN, }) /** Denotes the user shortcut type. */ - @interface DialogStatus { + public @interface DialogStatus { int NOT_SHOWN = 0; int SHOWN = 1; } diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java index cb1abf13c109..bdb33c4b151c 100644 --- a/core/java/com/android/internal/view/menu/ListMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java @@ -16,12 +16,10 @@ package com.android.internal.view.menu; -import android.app.AppGlobals; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.text.TextFlags; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -61,8 +59,6 @@ public class ListMenuItemView extends LinearLayout private int mMenuType; - private boolean mUseNewContextMenu; - private LayoutInflater mInflater; private boolean mForceShowIcon; @@ -89,10 +85,6 @@ public class ListMenuItemView extends LinearLayout a.recycle(); b.recycle(); - - mUseNewContextMenu = AppGlobals.getIntCoreSetting( - TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU, - TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT ? 1 : 0) != 0; } public ListMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) { @@ -289,9 +281,7 @@ public class ListMenuItemView extends LinearLayout private void insertIconView() { LayoutInflater inflater = getInflater(); - mIconView = (ImageView) inflater.inflate( - mUseNewContextMenu ? com.android.internal.R.layout.list_menu_item_fixed_size_icon : - com.android.internal.R.layout.list_menu_item_icon, + mIconView = (ImageView) inflater.inflate(com.android.internal.R.layout.list_menu_item_icon, this, false); addContentView(mIconView, 0); } diff --git a/core/java/com/android/internal/view/menu/StandardMenuPopup.java b/core/java/com/android/internal/view/menu/StandardMenuPopup.java index 1979e4fe7a90..36828f2dadca 100644 --- a/core/java/com/android/internal/view/menu/StandardMenuPopup.java +++ b/core/java/com/android/internal/view/menu/StandardMenuPopup.java @@ -16,24 +16,26 @@ package com.android.internal.view.menu; +import android.app.AppGlobals; import android.content.Context; import android.content.res.Resources; import android.os.Parcelable; +import android.text.TextFlags; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.View.OnKeyListener; -import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.AdapterView.OnItemClickListener; import android.widget.FrameLayout; import android.widget.ListView; import android.widget.MenuPopupWindow; import android.widget.PopupWindow; -import android.widget.TextView; -import android.widget.AdapterView.OnItemClickListener; import android.widget.PopupWindow.OnDismissListener; +import android.widget.TextView; import java.util.Objects; @@ -44,6 +46,8 @@ import java.util.Objects; final class StandardMenuPopup extends MenuPopup implements OnDismissListener, OnItemClickListener, MenuPresenter, OnKeyListener { private static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout; + private static final int ITEM_LAYOUT_MATERIAL = + com.android.internal.R.layout.popup_menu_item_layout_material; private final Context mContext; @@ -53,6 +57,7 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On private final int mPopupMaxWidth; private final int mPopupStyleAttr; private final int mPopupStyleRes; + // The popup window is final in order to couple its lifecycle to the lifecycle of the // StandardMenuPopup. private final MenuPopupWindow mPopup; @@ -114,10 +119,15 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On public StandardMenuPopup(Context context, MenuBuilder menu, View anchorView, int popupStyleAttr, int popupStyleRes, boolean overflowOnly) { mContext = Objects.requireNonNull(context); + boolean useNewContextMenu = AppGlobals.getIntCoreSetting( + TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU, + TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT ? 1 : 0) != 0; + mMenu = menu; mOverflowOnly = overflowOnly; final LayoutInflater inflater = LayoutInflater.from(context); - mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly, ITEM_LAYOUT); + mAdapter = new MenuAdapter(menu, inflater, mOverflowOnly, + useNewContextMenu ? ITEM_LAYOUT_MATERIAL : ITEM_LAYOUT); mPopupStyleAttr = popupStyleAttr; mPopupStyleRes = popupStyleRes; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c694426a5aa4..ab714ad7d807 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -581,6 +581,7 @@ <protected-broadcast android:name="android.app.action.KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED" /> <protected-broadcast android:name="NotificationManagerService.TIMEOUT" /> + <protected-broadcast android:name="com.android.server.notification.TimeToLiveHelper" /> <protected-broadcast android:name="NotificationHistoryDatabase.CLEANUP" /> <protected-broadcast android:name="ScheduleConditionProvider.EVALUATE" /> <protected-broadcast android:name="EventConditionProvider.EVALUATE" /> diff --git a/core/res/res/layout/list_menu_item_icon.xml b/core/res/res/layout/list_menu_item_icon.xml index a30be6a13db6..d8514608e8dd 100644 --- a/core/res/res/layout/list_menu_item_icon.xml +++ b/core/res/res/layout/list_menu_item_icon.xml @@ -20,7 +20,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="8dip" - android:layout_marginEnd="-8dip" + android:layout_marginEnd="8dip" android:layout_marginTop="8dip" android:layout_marginBottom="8dip" android:scaleType="centerInside" diff --git a/core/res/res/layout/popup_menu_item_layout_material.xml b/core/res/res/layout/popup_menu_item_layout_material.xml new file mode 100644 index 000000000000..e20ead62032c --- /dev/null +++ b/core/res/res/layout/popup_menu_item_layout_material.xml @@ -0,0 +1,91 @@ +<?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. + + Forked from the popup_menu_item_layout.xml for material support. When you edit this file, you + may also need to update that file. +--> + +<com.android.internal.view.menu.ListMenuItemView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minWidth="196dip" + android:orientation="vertical" > + + <ImageView + android:id="@+id/group_divider" + android:layout_width="match_parent" + android:layout_height="1dip" + android:layout_marginTop="4dip" + android:layout_marginBottom="4dip" + android:background="@drawable/list_divider_material" /> + + <LinearLayout + android:id="@+id/content" + android:layout_width="match_parent" + android:layout_height="?attr/dropdownListPreferredItemHeight" + android:paddingEnd="16dip" + android:duplicateParentState="true" > + + <!-- Icon will be inserted here. --> + + <!-- The title and summary have some gap between them, + and this 'group' should be centered vertically. --> + <RelativeLayout + android:layout_width="0dip" + android:layout_weight="1" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:duplicateParentState="true"> + + <TextView + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_alignParentStart="true" + android:textAppearance="?attr/textAppearanceLargePopupMenu" + android:singleLine="true" + android:duplicateParentState="true" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:textAlignment="viewStart" /> + + <TextView + android:id="@+id/shortcut" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/title" + android:layout_alignParentStart="true" + android:textAppearance="?attr/textAppearanceSmallPopupMenu" + android:singleLine="true" + android:duplicateParentState="true" + android:textAlignment="viewStart" /> + + </RelativeLayout> + + <ImageView + android:id="@+id/submenuarrow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="8dp" + android:scaleType="center" + android:visibility="gone" /> + + <!-- Checkbox, and/or radio button will be inserted here. --> + + </LinearLayout> + +</com.android.internal.view.menu.ListMenuItemView> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2115f64a60b3..a622d36edc6a 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7015,4 +7015,9 @@ <!-- Frame rate compatibility value for Wallpaper FRAME_RATE_COMPATIBILITY_MIN (102) is used by default for lower power consumption --> <integer name="config_wallpaperFrameRateCompatibility">102</integer> + + <!-- Min time in milliseconds to complete an emergency gesture for it count. + If the gesture is completed faster than this, we assume it's not performed by human and the + event gets ignored. --> + <integer name="config_defaultMinEmergencyGestureTapDurationMillis">200</integer> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 9e0954093eb2..c4033f2d680a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1527,6 +1527,7 @@ <java-symbol type="layout" name="number_picker" /> <java-symbol type="layout" name="permissions_package_list_item" /> <java-symbol type="layout" name="popup_menu_item_layout" /> + <java-symbol type="layout" name="popup_menu_item_layout_material" /> <java-symbol type="layout" name="popup_menu_header_item_layout" /> <java-symbol type="layout" name="remote_views_adapter_default_loading_view" /> <java-symbol type="layout" name="search_bar" /> @@ -5401,4 +5402,6 @@ <!-- Frame rate compatibility value for Wallpaper --> <java-symbol type="integer" name="config_wallpaperFrameRateCompatibility" /> + + <java-symbol type="integer" name="config_defaultMinEmergencyGestureTapDurationMillis" /> </resources> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index 404e8731d5c5..04e90baaff3a 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -154,6 +154,12 @@ android_app { "android.test.runner", "org.apache.http.legacy", ], + uses_libs: [ + "android.test.runner", + ], + optional_uses_libs: [ + "org.apache.http.legacy", + ], sdk_version: "core_platform", } diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java index 2f2215fd51a2..d1d7c145680f 100644 --- a/keystore/java/android/security/KeyStore.java +++ b/keystore/java/android/security/KeyStore.java @@ -16,8 +16,6 @@ package android.security; -import android.compat.annotation.UnsupportedAppUsage; - /** * This class provides some constants and helper methods related to Android's Keystore service. * This class was originally much larger, but its functionality was superseded by other classes. @@ -30,11 +28,4 @@ public class KeyStore { // Used for UID field to indicate the calling UID. public static final int UID_SELF = -1; - - private static final KeyStore KEY_STORE = new KeyStore(); - - @UnsupportedAppUsage - public static KeyStore getInstance() { - return KEY_STORE; - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 73b2656d596a..d3fe4f82daf7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -837,6 +837,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // The next callback should be {@link #onBackAnimationFinished}. if (mCurrentTracker.getTriggerBack()) { + // notify gesture finished + mBackNavigationInfo.onBackGestureFinished(true); dispatchOrAnimateOnBackInvoked(mActiveCallback, mCurrentTracker); } else { tryDispatchOnBackCancelled(mActiveCallback); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java index a32b435ff99e..4988a9481d21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java @@ -28,6 +28,7 @@ import android.view.RemoteAnimationTarget; import android.window.IBackAnimationRunner; import android.window.IOnBackInvokedCallback; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.Cuj.CujType; import com.android.wm.shell.common.InteractionJankMonitorUtils; @@ -108,7 +109,8 @@ public class BackAnimationRunner { } } - private boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) { + @VisibleForTesting + boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) { return apps.length > 0 && mCujType != NO_CUJ; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 9c623bd5b76f..65169e36a225 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -16,7 +16,7 @@ package com.android.wm.shell.back; -import static android.window.BackNavigationInfo.KEY_TRIGGER_BACK; +import static android.window.BackNavigationInfo.KEY_NAVIGATION_FINISHED; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -596,6 +596,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Set up the monitoring objects. doNothing().when(runner).onAnimationStart(anyInt(), any(), any(), any(), any()); + doReturn(false).when(animationRunner).shouldMonitorCUJ(any()); doReturn(runner).when(animationRunner).getRunner(); doReturn(callback).when(animationRunner).getCallback(); @@ -677,7 +678,7 @@ public class BackAnimationControllerTest extends ShellTestCase { @Override public void onResult(@Nullable Bundle result) { mBackNavigationDone = true; - mTriggerBack = result.getBoolean(KEY_TRIGGER_BACK); + mTriggerBack = result.getBoolean(KEY_NAVIGATION_FINISHED); } } } diff --git a/media/java/android/media/session/ParcelableListBinder.java b/media/java/android/media/session/ParcelableListBinder.java index bbf1e0889b68..d78828462b1e 100644 --- a/media/java/android/media/session/ParcelableListBinder.java +++ b/media/java/android/media/session/ParcelableListBinder.java @@ -45,6 +45,7 @@ public class ParcelableListBinder<T extends Parcelable> extends Binder { private static final int END_OF_PARCEL = 0; private static final int ITEM_CONTINUED = 1; + private final Class<T> mListElementsClass; private final Consumer<List<T>> mConsumer; private final Object mLock = new Object(); @@ -61,9 +62,11 @@ public class ParcelableListBinder<T extends Parcelable> extends Binder { /** * Creates an instance. * + * @param listElementsClass the class of the list elements. * @param consumer a consumer that consumes the list received */ - public ParcelableListBinder(@NonNull Consumer<List<T>> consumer) { + public ParcelableListBinder(Class<T> listElementsClass, @NonNull Consumer<List<T>> consumer) { + mListElementsClass = listElementsClass; mConsumer = consumer; } @@ -83,7 +86,13 @@ public class ParcelableListBinder<T extends Parcelable> extends Binder { mCount = data.readInt(); } while (i < mCount && data.readInt() != END_OF_PARCEL) { - mList.add(data.readParcelable(null)); + Object object = data.readParcelable(null); + if (mListElementsClass.isAssignableFrom(object.getClass())) { + // Checking list items are of compaitible types to validate against malicious + // apps calling it directly via reflection with non compilable items. + // See b/317048338 for more details + mList.add((T) object); + } i++; } if (i >= mCount) { diff --git a/packages/CredentialManager/wear/res/drawable/passkey_icon.xml b/packages/CredentialManager/wear/res/drawable/passkey_icon.xml deleted file mode 100644 index be366bf2a255..000000000000 --- a/packages/CredentialManager/wear/res/drawable/passkey_icon.xml +++ /dev/null @@ -1,21 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:pathData="M23,10.5H17V13.5H23V10.5Z" - android:fillColor="#188038"/> - <path - android:pathData="M6.5,17.5C3.5,17.5 1,15 1,12C1,9 3.5,6.5 6.5,6.5C9.5,6.5 12,9 12,12C12,15 9.5,17.5 6.5,17.5ZM6.5,9.5C5.1,9.5 4,10.6 4,12C4,13.4 5.1,14.5 6.5,14.5C7.9,14.5 9,13.4 9,12C9,10.6 7.9,9.5 6.5,9.5Z" - android:fillColor="#4285F4"/> - <path - android:pathData="M21,13.5H19H17V16.5H19V15.5C19,14.9 19.4,14.5 20,14.5C20.6,14.5 21,14.9 21,15.5V16.5H23V13.5H21Z" - android:fillColor="#34A853"/> - <path - android:pathData="M11.8,10.5H8.5C8.8,10.9 9,11.4 9,12C9,12.6 8.8,13.1 8.5,13.5H11.8C11.9,13 12,12.5 12,12C12,11.5 11.9,11 11.8,10.5Z" - android:fillColor="#EA4335"/> - <path - android:pathData="M17,10.5H11.8C11.9,11 12,11.5 12,12C12,12.5 11.9,13 11.8,13.5H17V10.5Z" - android:fillColor="#FBBC04"/> -</vector> diff --git a/packages/CredentialManager/wear/res/values-watch/donottranslate.xml b/packages/CredentialManager/wear/res/values-watch/donottranslate.xml new file mode 100644 index 000000000000..c3ab3cbb1f27 --- /dev/null +++ b/packages/CredentialManager/wear/res/values-watch/donottranslate.xml @@ -0,0 +1,31 @@ +<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- font-family-device-default is expected to be preloaded in the font_customization.xml(/vendor/<OEM>/products/<PRODUCT>/fonts/fonts_customization.xml)--> + <!-- Falls back to system default when font-family-device-default doesn't exist --> + <string name="wear_material_compose_display_1_font_family">font-family-device-default</string> + <string name="wear_material_compose_display_2_font_family">font-family-device-default</string> + <string name="wear_material_compose_display_3_font_family">font-family-device-default</string> + <string name="wear_material_compose_title_1_font_family">font-family-medium-device-default</string> + <string name="wear_material_compose_title_2_font_family">font-family-medium-device-default</string> + <string name="wear_material_compose_title_3_font_family">font-family-device-default</string> + <string name="wear_material_compose_body_1_font_family">font-family-text-device-default</string> + <string name="wear_material_compose_body_2_font_family">font-family-text-device-default</string> + <string name="wear_material_compose_button_font_family">font-family-text-medium-device-default</string> + <string name="wear_material_compose_caption_1_font_family">font-family-text-medium-device-default</string> + <string name="wear_material_compose_caption_2_font_family">font-family-text-medium-device-default</string> + <string name="wear_material_compose_caption_3_font_family">font-family-text-medium-device-default</string> +</resources> diff --git a/packages/CredentialManager/wear/res/values/overlayable.xml b/packages/CredentialManager/wear/res/values/overlayable.xml new file mode 100644 index 000000000000..5b9d37259b98 --- /dev/null +++ b/packages/CredentialManager/wear/res/values/overlayable.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <overlayable name="CredentialSelectorStyles"> + <policy type="product|system|vendor|odm|oem"> + <!--START WEAR SPECIFIC FONT STRINGS --> + <item type="string" name="wear_material_compose_display_1_font_family" /> + <item type="string" name="wear_material_compose_display_2_font_family" /> + <item type="string" name="wear_material_compose_display_3_font_family" /> + <item type="string" name="wear_material_compose_title_1_font_family" /> + <item type="string" name="wear_material_compose_title_2_font_family" /> + <item type="string" name="wear_material_compose_title_3_font_family" /> + <item type="string" name="wear_material_compose_body_1_font_family" /> + <item type="string" name="wear_material_compose_body_2_font_family" /> + <item type="string" name="wear_material_compose_button_font_family" /> + <item type="string" name="wear_material_compose_caption_1_font_family" /> + <item type="string" name="wear_material_compose_caption_2_font_family" /> + <item type="string" name="wear_material_compose_caption_3_font_family" /> + <!--END WEAR SPECIFIC FONT STRINGS --> + + </policy> + + </overlayable> + +</resources> diff --git a/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt index 3422d3dc4d94..6c145631a39e 100644 --- a/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt +++ b/packages/CredentialManager/wear/robotests/src/com/android/credentialmanager/CredentialSelectorUiStateGetMapperTest.kt @@ -65,29 +65,29 @@ class CredentialSelectorUiStateGetMapperTest { isLastUnlocked = true ) - val passkeyCredentialEntryInfo = + private val passkeyCredentialEntryInfo = createCredentialEntryInfo(credentialType = CredentialType.PASSKEY, userName = "userName") - val unknownCredentialEntryInfo = + private val unknownCredentialEntryInfo = createCredentialEntryInfo(credentialType = CredentialType.UNKNOWN, userName = "userName2") - val passwordCredentialEntryInfo = + private val passwordCredentialEntryInfo = createCredentialEntryInfo(credentialType = CredentialType.PASSWORD, userName = "userName") - val recentlyUsedPasskeyCredential = + private val recentlyUsedPasskeyCredential = createCredentialEntryInfo(credentialType = CredentialType.PASSKEY, lastUsedTimeMillis = 2L, userName = "userName") - val recentlyUsedPasswordCredential = + private val recentlyUsedPasswordCredential = createCredentialEntryInfo(credentialType = CredentialType.PASSWORD, lastUsedTimeMillis = 2L, userName = "userName") - val credentialList1 = listOf( + private val credentialList1 = listOf( passkeyCredentialEntryInfo, passwordCredentialEntryInfo ) - val credentialList2 = listOf( + private val credentialList2 = listOf( passkeyCredentialEntryInfo, passwordCredentialEntryInfo, recentlyUsedPasskeyCredential, @@ -118,11 +118,12 @@ class CredentialSelectorUiStateGetMapperTest { unknownCredentialEntryInfo)))).toGet(isPrimary = true) assertThat(getCredentialUiState).isEqualTo( - CredentialSelectorUiState.Get.SingleEntryPerAccount( + CredentialSelectorUiState.Get.MultipleEntryPrimaryScreen( sortedEntries = listOf( passkeyCredentialEntryInfo, // userName unknownCredentialEntryInfo // userName2 ), + icon = mDrawable, authenticationEntryList = listOf(authenticationEntryInfo) )) // prefer passkey from account 1, then unknown from account 2 } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 0fe35e695047..652e62cb26b4 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -21,7 +21,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels -import androidx.wear.compose.material.MaterialTheme +import com.android.credentialmanager.ui.theme.WearCredentialSelectorTheme import com.android.credentialmanager.ui.WearApp import com.google.android.horologist.annotations.ExperimentalHorologistApi import dagger.hilt.android.AndroidEntryPoint @@ -36,7 +36,7 @@ class CredentialSelectorActivity : Hilt_CredentialSelectorActivity() { super.onCreate(savedInstanceState) setTheme(android.R.style.Theme_DeviceDefault) setContent { - MaterialTheme { + WearCredentialSelectorTheme { WearApp( flowEngine = viewModel, onCloseApp = { finish() }, diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index b7fa33e9372f..36085684db57 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -86,7 +86,7 @@ class CredentialSelectorViewModel @Inject constructor( when (uiState.value) { is Get.MultipleEntry -> isPrimaryScreen.value = true is Create, Close, is Cancel, Idle -> shouldClose.value = true - is Get.SingleEntry, is Get.SingleEntryPerAccount -> cancel() + is Get.SingleEntry, is Get.MultipleEntryPrimaryScreen -> cancel() } } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt index c05fc93b8223..b2f55c108317 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager import android.content.Intent +import android.graphics.drawable.Drawable import androidx.activity.result.IntentSenderRequest import androidx.compose.runtime.Composable import com.android.credentialmanager.model.EntryInfo @@ -71,14 +72,14 @@ sealed class CredentialSelectorUiState { /** Getting credential UI state when there is only one credential available. */ data class SingleEntry(val entry: CredentialEntryInfo) : Get() /** - * Getting credential UI state when there is only one account while with multiple - * credentials, with different types(eg, passkey vs password) or providers. + * Getting credential UI state on primary screen when there is are multiple accounts. */ - data class SingleEntryPerAccount( + data class MultipleEntryPrimaryScreen( + val icon: Drawable?, val sortedEntries: List<CredentialEntryInfo>, val authenticationEntryList: List<AuthenticationEntryInfo>, ) : Get() - /** Getting credential UI state when there are multiple accounts available. */ + /** Getting credential UI state on secondary screen when there are multiple accounts available. */ data class MultipleEntry( val accounts: List<PerUserNameEntries>, val actionEntryList: List<ActionEntryInfo>, diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt index 018db6899f6e..a75aeaff0c48 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt @@ -29,7 +29,7 @@ import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.android.credentialmanager.CredentialSelectorUiState -import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntryPerAccount +import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntryPrimaryScreen import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry import com.android.credentialmanager.FlowEngine @@ -95,7 +95,7 @@ fun WearApp( scrollable(Screen.MultipleCredentialsScreenFold.route) { MultiCredentialsFoldScreen( - credentialSelectorUiState = (remember { uiState } as SingleEntryPerAccount), + credentialSelectorUiState = (remember { uiState } as MultipleEntryPrimaryScreen), columnState = it.columnState, flowEngine = flowEngine, ) @@ -124,7 +124,6 @@ fun WearApp( handleGetNavigation( navController = navController, state = state, - onCloseApp = onCloseApp, selectEntry = selectEntry ) } @@ -147,7 +146,6 @@ fun WearApp( private fun handleGetNavigation( navController: NavController, state: CredentialSelectorUiState.Get, - onCloseApp: () -> Unit, selectEntry: (entry: EntryInfo, isAutoSelected: Boolean) -> Unit, ) { when (state) { @@ -169,7 +167,7 @@ private fun handleGetNavigation( } } - is SingleEntryPerAccount -> { + is MultipleEntryPrimaryScreen -> { navController.navigateToMultipleCredentialsFoldScreen() } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt index 18c9f3102409..c641d7f9f48f 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/CredentialsScreenChip.kt @@ -32,7 +32,6 @@ import androidx.core.graphics.drawable.toBitmap import androidx.wear.compose.material.ChipColors import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.style.TextAlign import androidx.wear.compose.material.ChipDefaults import com.android.credentialmanager.R @@ -80,8 +79,7 @@ fun CredentialsScreenChip( icon: Drawable? = null, isAuthenticationEntryLocked: Boolean = false, modifier: Modifier = Modifier, - colors: ChipColors = - ChipDefaults.chipColors(backgroundColor = colorResource(R.color.wear_material_almond)), + colors: ChipColors = ChipDefaults.primaryChipColors(), ) { val labelParam: (@Composable RowScope.() -> Unit) = { @@ -168,11 +166,9 @@ fun ContinueChip(onClick: () -> Unit) { WearButtonText( text = stringResource(R.string.dialog_continue_button), textAlign = TextAlign.Center, - color = colorResource(R.color.wear_material_almond_dark), ) }, - colors = - ChipDefaults.chipColors(backgroundColor = colorResource(R.color.wear_material_almond)), + colors = ChipDefaults.primaryChipColors(), ) } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt index 437a699abcee..0afef5eba85e 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/SignInHeader.kt @@ -56,6 +56,6 @@ fun SignInHeader( text = title, ) - Spacer(modifier = Modifier.size(12.dp)) + Spacer(modifier = Modifier.size(8.dp)) } } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt index e7a854f2a4d4..22f6bf0f37ee 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/components/Texts.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.android.compose.theme.LocalAndroidColorScheme import androidx.wear.compose.material.MaterialTheme as WearMaterialTheme @Composable @@ -34,7 +33,7 @@ fun WearTitleText(text: String, modifier: Modifier = Modifier) { Text( modifier = modifier.wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.onSurface, + color = WearMaterialTheme.colors.onSurface, textAlign = TextAlign.Center, style = WearMaterialTheme.typography.title3, ) @@ -45,7 +44,7 @@ fun WearDisplayNameText(text: String, modifier: Modifier = Modifier) { Text( modifier = modifier.wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.onSurface, + color = WearMaterialTheme.colors.onSurfaceVariant, textAlign = TextAlign.Center, overflow = TextOverflow.Ellipsis, maxLines = 2, @@ -56,28 +55,30 @@ fun WearDisplayNameText(text: String, modifier: Modifier = Modifier) { @Composable fun WearUsernameText( text: String, + textAlign: TextAlign = TextAlign.Center, modifier: Modifier = Modifier, onTextLayout: (TextLayoutResult) -> Unit = {}, ) { Text( modifier = modifier.padding(start = 8.dp, end = 8.dp).wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.onSurfaceVariant, + color = WearMaterialTheme.colors.onSurfaceVariant, style = WearMaterialTheme.typography.caption1, overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, + textAlign = textAlign, maxLines = 2, onTextLayout = onTextLayout, ) } +// used for primary label in button @Composable fun WearButtonText( text: String, textAlign: TextAlign, maxLines: Int = 1, modifier: Modifier = Modifier, - color: Color = LocalAndroidColorScheme.current.onSurface, + color: Color = WearMaterialTheme.colors.onSurface, onTextLayout: (TextLayoutResult) -> Unit = {}, ) { Text( @@ -101,8 +102,8 @@ fun WearSecondaryLabel( Text( modifier = modifier.wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.onSurfaceVariant, - style = WearMaterialTheme.typography.button, + color = WearMaterialTheme.colors.onSurfaceVariant, + style = WearMaterialTheme.typography.caption1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Start, maxLines = 1, diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt index 7a936b603ec1..04175335b9db 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt @@ -16,6 +16,7 @@ package com.android.credentialmanager.ui.mappers +import android.graphics.drawable.Drawable import com.android.credentialmanager.model.Request import com.android.credentialmanager.CredentialSelectorUiState import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry.PerUserNameEntries @@ -35,10 +36,19 @@ fun Request.Get.toGet(isPrimary: Boolean): CredentialSelectorUiState.Get { entry = accounts[0].value.minWith(comparator) ) } else { - CredentialSelectorUiState.Get.SingleEntryPerAccount( - sortedEntries = accounts.map { - it.value.minWith(comparator) - }.sortedWith(comparator), + val sortedEntries = accounts.map { + it.value.minWith(comparator) + }.sortedWith(comparator) + + var icon: Drawable? = null + // provide icon if all entries have the same provider + if (sortedEntries.all {it.providerId == sortedEntries[0].providerId}) { + icon = providerInfos[0].icon + } + + CredentialSelectorUiState.Get.MultipleEntryPrimaryScreen( + sortedEntries = sortedEntries, + icon = icon, authenticationEntryList = providerInfos.flatMap { it.authenticationEntryList } ) } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt index a545e48eec0f..fb81e736171b 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFlattenScreen.kt @@ -27,7 +27,7 @@ import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry import com.android.credentialmanager.FlowEngine import com.android.credentialmanager.R import com.android.credentialmanager.common.ui.components.WearButtonText -import com.android.credentialmanager.common.ui.components.WearDisplayNameText +import com.android.credentialmanager.common.ui.components.WearSecondaryLabel import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.ui.components.CredentialsScreenChipSpacer import com.google.android.horologist.annotations.ExperimentalHorologistApi @@ -64,10 +64,9 @@ fun MultiCredentialsFlattenScreen( credentialSelectorUiState.accounts.forEach { userNameEntries -> item { - WearDisplayNameText( + WearSecondaryLabel( text = userNameEntries.userName, - modifier = Modifier.padding(top = 16.dp, bottom = 8.dp, start = 14.dp, - end = 14.dp) + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp) ) } @@ -86,9 +85,9 @@ fun MultiCredentialsFlattenScreen( } } item { - WearDisplayNameText( + WearSecondaryLabel( text = stringResource(R.string.provider_list_title), - modifier = Modifier.padding(top = 12.dp, bottom = 8.dp, start = 14.dp, end = 14.dp) + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp) ) } credentialSelectorUiState.actionEntryList.forEach { actionEntry -> diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt index acf4eca64c0b..7addc74aecd0 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt @@ -48,7 +48,7 @@ import com.android.credentialmanager.ui.components.CredentialsScreenChipSpacer @OptIn(ExperimentalHorologistApi::class) @Composable fun MultiCredentialsFoldScreen( - credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntryPerAccount, + credentialSelectorUiState: CredentialSelectorUiState.Get.MultipleEntryPrimaryScreen, columnState: ScalingLazyColumnState, flowEngine: FlowEngine, ) { @@ -61,29 +61,32 @@ fun MultiCredentialsFoldScreen( val credentials = credentialSelectorUiState.sortedEntries item { var title = stringResource(R.string.choose_sign_in_title) - if (credentials.all{ it.credentialType == CredentialType.PASSKEY }) { + + if (credentials.isEmpty()) { + title = stringResource(R.string.choose_sign_in_title) + } else if (credentials.all{ it.credentialType == CredentialType.PASSKEY }) { title = stringResource(R.string.choose_passkey_title) } else if (credentials.all { it.credentialType == CredentialType.PASSWORD }) { title = stringResource(R.string.choose_password_title) } SignInHeader( - icon = null, + icon = credentialSelectorUiState.icon, title = title, ) } credentials.forEach { credential: CredentialEntryInfo -> - item { - CredentialsScreenChip( - label = credential.userName, - onClick = { selectEntry(credential, false) }, - secondaryLabel = credential.credentialTypeDisplayName, - icon = credential.icon, - ) - CredentialsScreenChipSpacer() - } + item { + CredentialsScreenChip( + label = credential.userName, + onClick = { selectEntry(credential, false) }, + secondaryLabel = credential.credentialTypeDisplayName, + icon = credential.icon, + ) + CredentialsScreenChipSpacer() } + } credentialSelectorUiState.authenticationEntryList.forEach { authenticationEntryInfo -> item { @@ -93,10 +96,13 @@ fun MultiCredentialsFoldScreen( CredentialsScreenChipSpacer() } } + + item { + Spacer(modifier = Modifier.size(8.dp)) + } + item { - Spacer(modifier = Modifier.size(12.dp)) SignInOptionsChip { flowEngine.openSecondaryScreen() } - CredentialsScreenChipSpacer() } item { DismissChip { flowEngine.cancel() } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt index de7c1f19e193..03608a48beb6 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt @@ -61,10 +61,18 @@ fun SinglePasskeyScreen( ) }, accountContent = { - AccountRow( - primaryText = checkNotNull(entry.displayName), + val displayName = entry.displayName + if (displayName == null || + entry.displayName.equals(entry.userName, ignoreCase = true)) { + AccountRow( + primaryText = entry.userName, + ) + } else { + AccountRow( + primaryText = displayName, secondaryText = entry.userName, ) + } }, columnState = columnState, modifier = Modifier.padding(horizontal = 10.dp) diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt index 884d9f6e5e16..34d6e977533e 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt @@ -59,14 +59,15 @@ fun SignInWithProviderScreen( }, accountContent = { val displayName = entry.displayName - if (displayName != null) { + if (displayName == null || + entry.displayName.equals(entry.userName, ignoreCase = true)) { AccountRow( - primaryText = displayName, - secondaryText = entry.userName, + primaryText = entry.userName, ) } else { AccountRow( - primaryText = entry.userName, + primaryText = displayName, + secondaryText = entry.userName, ) } }, diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/theme/WearCredentialSelectorTheme.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/theme/WearCredentialSelectorTheme.kt new file mode 100644 index 000000000000..ee0ba7ba2274 --- /dev/null +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/theme/WearCredentialSelectorTheme.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.credentialmanager.ui.theme + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.annotation.StringRes +import androidx.wear.compose.material.Colors +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.DeviceFontFamilyName +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.wear.compose.material.Typography +import androidx.wear.compose.material.MaterialTheme +import com.android.credentialmanager.R +import androidx.compose.ui.graphics.Color + +/** The Material 3 Theme Wrapper for Supporting RRO. */ +@Composable +fun WearCredentialSelectorTheme(content: @Composable () -> Unit) { + val context = LocalContext.current + val colors = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + overlayColors(context) + .copy(error = MaterialTheme.colors.error, onError = MaterialTheme.colors.onError) + } else { + MaterialTheme.colors + } + MaterialTheme(colors = colors, typography = deviceDefaultTypography(context), content = content) +} + +/** + * Creates a dynamic color maps that can be overlaid. 100 - Lightest shade; 0 - Darkest Shade; In + * wear we only support dark theme for the time being. Thus the fill colors and variants are dark + * and anything on top is light. We will use this custom redirection until wear compose material + * supports color scheming. + * + * The mapping is best case match on wear material color tokens from + * /android/clockwork/common/wearable/wearmaterial/color/res/values/color-tokens.xml + * + * @param context The context required to get system resource data. + */ +@RequiresApi(Build.VERSION_CODES.S) +internal fun overlayColors(context: Context): Colors { + val tonalPalette = dynamicTonalPalette(context) + return Colors( + background = Color.Black, + onBackground = Color.White, + primary = tonalPalette.primary90, + primaryVariant = tonalPalette.primary80, + onPrimary = tonalPalette.primary10, + secondary = tonalPalette.tertiary90, + secondaryVariant = tonalPalette.tertiary60, + onSecondary = tonalPalette.tertiary10, + surface = tonalPalette.neutral20, + onSurface = tonalPalette.neutral95, + onSurfaceVariant = tonalPalette.neutralVariant80, + ) +} + +private fun fontFamily(context: Context, @StringRes id: Int): FontFamily { + val typefaceName = context.resources.getString(id) + val font = Font(familyName = DeviceFontFamilyName(typefaceName)) + return FontFamily(font) +} + +/* + Only customizes font family. The material 3 roles to 2.5 are mapped to the best case matching of + google3/java/com/google/android/wearable/libraries/compose/theme/GoogleMaterialTheme.kt +*/ +internal fun deviceDefaultTypography(context: Context): Typography { + val defaultTypography = Typography() + return Typography( + display1 = + defaultTypography.display1.copy( + fontFamily = + fontFamily(context, R.string.wear_material_compose_display_1_font_family) + ), + display2 = + defaultTypography.display2.copy( + fontFamily = + fontFamily(context, R.string.wear_material_compose_display_2_font_family) + ), + display3 = + defaultTypography.display1.copy( + fontFamily = + fontFamily(context, R.string.wear_material_compose_display_3_font_family) + ), + title1 = + defaultTypography.title1.copy( + fontFamily = fontFamily(context, R.string.wear_material_compose_title_1_font_family) + ), + title2 = + defaultTypography.title2.copy( + fontFamily = fontFamily(context, R.string.wear_material_compose_title_2_font_family) + ), + title3 = + defaultTypography.title3.copy( + fontFamily = fontFamily(context, R.string.wear_material_compose_title_3_font_family) + ), + body1 = + defaultTypography.body1.copy( + fontFamily = fontFamily(context, R.string.wear_material_compose_body_1_font_family) + ), + body2 = + defaultTypography.body2.copy( + fontFamily = fontFamily(context, R.string.wear_material_compose_body_2_font_family) + ), + button = + defaultTypography.button.copy( + fontFamily = fontFamily(context, R.string.wear_material_compose_button_font_family) + ), + caption1 = + defaultTypography.caption1.copy( + fontFamily = + fontFamily(context, R.string.wear_material_compose_caption_1_font_family) + ), + caption2 = + defaultTypography.caption2.copy( + fontFamily = + fontFamily(context, R.string.wear_material_compose_caption_2_font_family) + ), + caption3 = + defaultTypography.caption3.copy( + fontFamily = + fontFamily(context, R.string.wear_material_compose_caption_3_font_family) + ), + ) +}
\ No newline at end of file diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/theme/WearCredentialTonalPalette.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/theme/WearCredentialTonalPalette.kt new file mode 100644 index 000000000000..1d6ed33e65e2 --- /dev/null +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/theme/WearCredentialTonalPalette.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.ui.theme + +import android.R +import android.content.Context +import android.os.Build +import androidx.annotation.ColorRes +import androidx.annotation.DoNotInline +import androidx.annotation.RequiresApi + +import androidx.compose.ui.graphics.Color + +/** + * Tonal Palette structure in Material. + * + * A tonal palette is comprised of 5 tonal ranges. Each tonal range includes the 13 stops, or tonal + * swatches. + * + * Tonal range names are: + * - Neutral (N) + * - Neutral variant (NV) + * - Primary (P) + * - Secondary (S) + * - Tertiary (T) + */ +internal class WearCredentialSelectorTonalPalette( + // The neutral tonal range. + val neutral100: Color, + val neutral99: Color, + val neutral95: Color, + val neutral90: Color, + val neutral80: Color, + val neutral70: Color, + val neutral60: Color, + val neutral50: Color, + val neutral40: Color, + val neutral30: Color, + val neutral20: Color, + val neutral10: Color, + val neutral0: Color, + + // The neutral variant tonal range, sometimes called "neutral 2" + val neutralVariant100: Color, + val neutralVariant99: Color, + val neutralVariant95: Color, + val neutralVariant90: Color, + val neutralVariant80: Color, + val neutralVariant70: Color, + val neutralVariant60: Color, + val neutralVariant50: Color, + val neutralVariant40: Color, + val neutralVariant30: Color, + val neutralVariant20: Color, + val neutralVariant10: Color, + val neutralVariant0: Color, + + // The primary tonal range, also known as accent 1 + val primary100: Color, + val primary99: Color, + val primary95: Color, + val primary90: Color, + val primary80: Color, + val primary70: Color, + val primary60: Color, + val primary50: Color, + val primary40: Color, + val primary30: Color, + val primary20: Color, + val primary10: Color, + val primary0: Color, + + // The Secondary tonal range, also know as accent 2 + val secondary100: Color, + val secondary99: Color, + val secondary95: Color, + val secondary90: Color, + val secondary80: Color, + val secondary70: Color, + val secondary60: Color, + val secondary50: Color, + val secondary40: Color, + val secondary30: Color, + val secondary20: Color, + val secondary10: Color, + val secondary0: Color, + + // The tertiary tonal range, also known as accent 3 + val tertiary100: Color, + val tertiary99: Color, + val tertiary95: Color, + val tertiary90: Color, + val tertiary80: Color, + val tertiary70: Color, + val tertiary60: Color, + val tertiary50: Color, + val tertiary40: Color, + val tertiary30: Color, + val tertiary20: Color, + val tertiary10: Color, + val tertiary0: Color, +) +/** Dynamic colors for wear compose material to support resource overlay. */ +@RequiresApi(Build.VERSION_CODES.S) +// TODO: once we have proper support for this on Wear 6+, we will do something similar to +// https://source.corp.google.com/h/android/platform/superproject/+/androidx-main:frameworks/support/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DynamicTonalPalette.android.kt;l=307-362?q=dynamicTonalPalette&sq=repo:android%2Fplatform%2Fsuperproject%20b:androidx-main +// Tracking Bug: b/270720571 +internal fun dynamicTonalPalette(context: Context) = + WearCredentialSelectorTonalPalette( + // The neutral tonal range from the generated dynamic color palette. + neutral100 = ColorResourceHelper.getColor(context, R.color.system_neutral1_0), + neutral99 = ColorResourceHelper.getColor(context, R.color.system_neutral1_10), + neutral95 = ColorResourceHelper.getColor(context, R.color.system_neutral1_50), + neutral90 = ColorResourceHelper.getColor(context, R.color.system_neutral1_100), + neutral80 = ColorResourceHelper.getColor(context, R.color.system_neutral1_200), + neutral70 = ColorResourceHelper.getColor(context, R.color.system_neutral1_300), + neutral60 = ColorResourceHelper.getColor(context, R.color.system_neutral1_400), + neutral50 = ColorResourceHelper.getColor(context, R.color.system_neutral1_500), + neutral40 = ColorResourceHelper.getColor(context, R.color.system_neutral1_600), + neutral30 = ColorResourceHelper.getColor(context, R.color.system_neutral1_700), + neutral20 = ColorResourceHelper.getColor(context, R.color.system_neutral1_800), + neutral10 = ColorResourceHelper.getColor(context, R.color.system_neutral1_900), + neutral0 = ColorResourceHelper.getColor(context, R.color.system_neutral1_1000), + + // The neutral variant tonal range, sometimes called "neutral 2", from the + // generated dynamic color palette. + neutralVariant100 = ColorResourceHelper.getColor(context, R.color.system_neutral2_0), + neutralVariant99 = ColorResourceHelper.getColor(context, R.color.system_neutral2_10), + neutralVariant95 = ColorResourceHelper.getColor(context, R.color.system_neutral2_50), + neutralVariant90 = ColorResourceHelper.getColor(context, R.color.system_neutral2_100), + neutralVariant80 = ColorResourceHelper.getColor(context, R.color.system_neutral2_200), + neutralVariant70 = ColorResourceHelper.getColor(context, R.color.system_neutral2_300), + neutralVariant60 = ColorResourceHelper.getColor(context, R.color.system_neutral2_400), + neutralVariant50 = ColorResourceHelper.getColor(context, R.color.system_neutral2_500), + neutralVariant40 = ColorResourceHelper.getColor(context, R.color.system_neutral2_600), + neutralVariant30 = ColorResourceHelper.getColor(context, R.color.system_neutral2_700), + neutralVariant20 = ColorResourceHelper.getColor(context, R.color.system_neutral2_800), + neutralVariant10 = ColorResourceHelper.getColor(context, R.color.system_neutral2_900), + neutralVariant0 = ColorResourceHelper.getColor(context, R.color.system_neutral2_1000), + + // The primary tonal range from the generated dynamic color palette. + primary100 = ColorResourceHelper.getColor(context, R.color.system_accent1_0), + primary99 = ColorResourceHelper.getColor(context, R.color.system_accent1_10), + primary95 = ColorResourceHelper.getColor(context, R.color.system_accent1_50), + primary90 = ColorResourceHelper.getColor(context, R.color.system_accent1_100), + primary80 = ColorResourceHelper.getColor(context, R.color.system_accent1_200), + primary70 = ColorResourceHelper.getColor(context, R.color.system_accent1_300), + primary60 = ColorResourceHelper.getColor(context, R.color.system_accent1_400), + primary50 = ColorResourceHelper.getColor(context, R.color.system_accent1_500), + primary40 = ColorResourceHelper.getColor(context, R.color.system_accent1_600), + primary30 = ColorResourceHelper.getColor(context, R.color.system_accent1_700), + primary20 = ColorResourceHelper.getColor(context, R.color.system_accent1_800), + primary10 = ColorResourceHelper.getColor(context, R.color.system_accent1_900), + primary0 = ColorResourceHelper.getColor(context, R.color.system_accent1_1000), + + // The secondary tonal range from the generated dynamic color palette. + secondary100 = ColorResourceHelper.getColor(context, R.color.system_accent2_0), + secondary99 = ColorResourceHelper.getColor(context, R.color.system_accent2_10), + secondary95 = ColorResourceHelper.getColor(context, R.color.system_accent2_50), + secondary90 = ColorResourceHelper.getColor(context, R.color.system_accent2_100), + secondary80 = ColorResourceHelper.getColor(context, R.color.system_accent2_200), + secondary70 = ColorResourceHelper.getColor(context, R.color.system_accent2_300), + secondary60 = ColorResourceHelper.getColor(context, R.color.system_accent2_400), + secondary50 = ColorResourceHelper.getColor(context, R.color.system_accent2_500), + secondary40 = ColorResourceHelper.getColor(context, R.color.system_accent2_600), + secondary30 = ColorResourceHelper.getColor(context, R.color.system_accent2_700), + secondary20 = ColorResourceHelper.getColor(context, R.color.system_accent2_800), + secondary10 = ColorResourceHelper.getColor(context, R.color.system_accent2_900), + secondary0 = ColorResourceHelper.getColor(context, R.color.system_accent2_1000), + + // The tertiary tonal range from the generated dynamic color palette. + tertiary100 = ColorResourceHelper.getColor(context, R.color.system_accent3_0), + tertiary99 = ColorResourceHelper.getColor(context, R.color.system_accent3_10), + tertiary95 = ColorResourceHelper.getColor(context, R.color.system_accent3_50), + tertiary90 = ColorResourceHelper.getColor(context, R.color.system_accent3_100), + tertiary80 = ColorResourceHelper.getColor(context, R.color.system_accent3_200), + tertiary70 = ColorResourceHelper.getColor(context, R.color.system_accent3_300), + tertiary60 = ColorResourceHelper.getColor(context, R.color.system_accent3_400), + tertiary50 = ColorResourceHelper.getColor(context, R.color.system_accent3_500), + tertiary40 = ColorResourceHelper.getColor(context, R.color.system_accent3_600), + tertiary30 = ColorResourceHelper.getColor(context, R.color.system_accent3_700), + tertiary20 = ColorResourceHelper.getColor(context, R.color.system_accent3_800), + tertiary10 = ColorResourceHelper.getColor(context, R.color.system_accent3_900), + tertiary0 = ColorResourceHelper.getColor(context, R.color.system_accent3_1000), + ) + +private object ColorResourceHelper { + @DoNotInline + fun getColor(context: Context, @ColorRes id: Int): Color { + return Color(context.resources.getColor(id, context.theme)) + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 4640de304ed8..fbbed921de1d 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1673,6 +1673,8 @@ <string name="accessibility_phone_two_bars">Phone two bars.</string> <!-- Content description of the phone signal when it is three bars for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_phone_three_bars">Phone three bars.</string> + <!-- Content description of the phone signal when it is four bars for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> + <string name="accessibility_phone_four_bars">Phone four bars.</string> <!-- Content description of the phone signal when it is full for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="accessibility_phone_signal_full">Phone signal full.</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java b/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java index ce466dfbf19c..9073d281c4a9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java +++ b/packages/SettingsLib/src/com/android/settingslib/AccessibilityContentDescriptions.java @@ -33,6 +33,59 @@ public class AccessibilityContentDescriptions { R.string.accessibility_phone_signal_full }; + /** + * @param level int in range [0-4] that describes the signal level + * @return the appropriate content description for that signal strength, or 0 if the param is + * invalid + */ + public static int getDescriptionForLevel(int level) { + if (level > 4 || level < 0) { + return 0; + } + + return PHONE_SIGNAL_STRENGTH[level]; + } + + public static final int[] PHONE_SIGNAL_STRENGTH_INFLATED = { + PHONE_SIGNAL_STRENGTH_NONE, + R.string.accessibility_phone_one_bar, + R.string.accessibility_phone_two_bars, + R.string.accessibility_phone_three_bars, + R.string.accessibility_phone_four_bars, + R.string.accessibility_phone_signal_full + }; + + /** + * @param level int in range [0-5] that describes the inflated signal level + * @return the appropriate content description for that signal strength, or 0 if the param is + * invalid + */ + public static int getDescriptionForInflatedLevel(int level) { + if (level > 5 || level < 0) { + return 0; + } + + return PHONE_SIGNAL_STRENGTH_INFLATED[level]; + } + + /** + * @param level int in range [0-5] that describes the inflated signal level + * @param numberOfLevels one of (4, 5) that describes the default number of levels, or the + * inflated number of levels. The level param should be relative to the + * number of levels. This won't do any inflation. + * @return the appropriate content description for that signal strength, or 0 if the param is + * invalid + */ + public static int getDescriptionForLevel(int level, int numberOfLevels) { + if (numberOfLevels == 5) { + return getDescriptionForLevel(level); + } else if (numberOfLevels == 6) { + return getDescriptionForInflatedLevel(level); + } else { + return 0; + } + } + public static final int[] DATA_CONNECTION_STRENGTH = { R.string.accessibility_no_data, R.string.accessibility_data_one_bar, diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index 8137e408ba39..14ebc3907c04 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -32,6 +32,16 @@ flag { } flag { + name: "floating_menu_narrow_target_content_observer" + namespace: "accessibility" + description: "stops the FAB from monitoring enabled services to trigger target content changes." + bug: "331740049" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "floating_menu_overlaps_nav_bars_flag" namespace: "accessibility" description: "Adjusts bounds to allow the floating menu to render on top of navigation bars." diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index b105a4e3b05a..c979d053617a 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -477,10 +477,13 @@ flag { } flag { - name: "screenshot_private_profile" + name: "screenshot_private_profile_behavior_fix" namespace: "systemui" description: "Private profile support for screenshots" bug: "327613051" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { @@ -770,4 +773,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "dream_input_session_pilfer_once" + namespace: "systemui" + description: "Pilfer at most once per input session" + bug: "324600132" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt index fc511e12ec54..e15d315f9a0a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ButtonComponent.kt @@ -69,9 +69,9 @@ class ButtonComponent( role = Role.Button contentDescription = label }, - color = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.tertiaryContainer, shape = RoundedCornerShape(28.dp), - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, onClick = onClick, ) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt index 780e3f2de4c8..b2351c492fc1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt @@ -66,8 +66,8 @@ class ToggleButtonComponent( val colors = if (viewModel.isChecked) { ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, ) } else { ButtonDefaults.buttonColors( 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 c74331477229..51e206470389 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 @@ -30,9 +30,11 @@ 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.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -119,6 +121,7 @@ fun VolumePanelRadioButtonBar( ) { for (itemIndex in items.indices) { val item = items[itemIndex] + val isSelected = itemIndex == scope.selectedIndex Row( modifier = Modifier.height(48.dp) @@ -126,7 +129,7 @@ fun VolumePanelRadioButtonBar( .semantics { item.contentDescription?.let { contentDescription = it } role = Role.Switch - selected = itemIndex == scope.selectedIndex + selected = isSelected } .clickable( interactionSource = null, @@ -137,7 +140,11 @@ fun VolumePanelRadioButtonBar( verticalAlignment = Alignment.CenterVertically, ) { if (item.icon !== Empty) { - with(items[itemIndex]) { icon() } + CompositionLocalProvider( + LocalContentColor provides colors.getIconColor(isSelected) + ) { + with(items[itemIndex]) { icon() } + } } } } @@ -163,7 +170,10 @@ fun VolumePanelRadioButtonBar( ) { val item = items[itemIndex] if (item.icon !== Empty) { - with(items[itemIndex]) { label() } + val textColor = colors.getLabelColor(itemIndex == scope.selectedIndex) + CompositionLocalProvider(LocalContentColor provides textColor) { + with(items[itemIndex]) { label() } + } } } } @@ -265,8 +275,22 @@ data class VolumePanelRadioButtonBarColors( val indicatorColor: Color, /** Color of the indicator background. */ val indicatorBackgroundColor: Color, + /** Color of the icon. */ + val iconColor: Color, + /** Color of the icon when it's selected. */ + val selectedIconColor: Color, + /** Color of the label. */ + val labelColor: Color, + /** Color of the label when it's selected. */ + val selectedLabelColor: Color, ) +private fun VolumePanelRadioButtonBarColors.getIconColor(selected: Boolean): Color = + if (selected) selectedIconColor else iconColor + +private fun VolumePanelRadioButtonBarColors.getLabelColor(selected: Boolean): Color = + if (selected) selectedLabelColor else labelColor + object VolumePanelRadioButtonBarDefaults { val DefaultIndicatorBackgroundPadding = 8.dp @@ -283,12 +307,20 @@ object VolumePanelRadioButtonBarDefaults { */ @Composable fun defaultColors( - indicatorColor: Color = MaterialTheme.colorScheme.primaryContainer, + indicatorColor: Color = MaterialTheme.colorScheme.tertiaryContainer, indicatorBackgroundColor: Color = MaterialTheme.colorScheme.surface, + iconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + selectedIconColor: Color = MaterialTheme.colorScheme.onTertiaryContainer, + labelColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + selectedLabelColor: Color = MaterialTheme.colorScheme.onSurface, ): VolumePanelRadioButtonBarColors = VolumePanelRadioButtonBarColors( indicatorColor = indicatorColor, indicatorBackgroundColor = indicatorBackgroundColor, + iconColor = iconColor, + selectedIconColor = selectedIconColor, + labelColor = labelColor, + selectedLabelColor = selectedLabelColor, ) } 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 9a98bdeec8f1..f377fa6276a0 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 @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.spatialaudio.ui.composable import androidx.compose.foundation.basicMarquee +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -29,7 +30,6 @@ import androidx.compose.ui.text.style.TextAlign import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon -import com.android.systemui.common.ui.compose.toColor import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.volume.panel.component.popup.ui.composable.VolumePanelPopup @@ -88,18 +88,13 @@ constructor( isSelected = buttonViewModel.button.isChecked, onItemSelected = { viewModel.setEnabled(buttonViewModel.model) }, contentDescription = label, - icon = { - Icon( - icon = buttonViewModel.button.icon, - tint = buttonViewModel.iconColor.toColor(), - ) - }, + icon = { Icon(icon = buttonViewModel.button.icon) }, label = { Text( modifier = Modifier.basicMarquee(), text = label, style = MaterialTheme.typography.labelMedium, - color = buttonViewModel.labelColor.toColor(), + color = LocalContentColor.current, textAlign = TextAlign.Center, maxLines = 2 ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index a54d005c990a..a3467f2ab78e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -107,7 +107,7 @@ fun ColumnVolumeSliders( } } transition.AnimatedVisibility( - visible = { it }, + visible = { it || !isExpandable }, enter = expandVertically(animationSpec = tween(durationMillis = EXPAND_DURATION_MILLIS)), exit = @@ -122,7 +122,7 @@ fun ColumnVolumeSliders( val sliderState by sliderViewModel.slider.collectAsState() transition.AnimatedVisibility( modifier = Modifier.padding(top = 16.dp), - visible = { it }, + visible = { it || !isExpandable }, enter = enterTransition(index = index, totalCount = viewModels.size), exit = exitTransition(index = index, totalCount = viewModels.size) ) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 228d29259038..9f5ab3c0e284 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -61,7 +61,8 @@ fun VolumeSlider( modifier = modifier.clearAndSetSemantics { if (!state.isEnabled) disabled() - contentDescription = state.label + contentDescription = + state.disabledMessage?.let { "${state.label}, $it" } ?: state.label // provide a not animated value to the a11y because it fails to announce the // settled value when it changes rapidly. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 5bb36a0acbdf..256687b56f4e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -389,6 +389,61 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(isAnimationEnabled).isTrue() } + @Test + fun onPinButtonClicked_whenInputSameLengthAsHintedPin_ignoresClick() = + testScope.runTest { + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) + val hintedPinLength by collectLastValue(underTest.hintedPinLength) + assertThat(hintedPinLength).isEqualTo(FakeAuthenticationRepository.HINTING_PIN_LENGTH) + lockDeviceAndOpenPinBouncer() + + repeat(FakeAuthenticationRepository.HINTING_PIN_LENGTH - 1) { repetition -> + underTest.onPinButtonClicked(repetition + 1) + runCurrent() + } + kosmos.fakeAuthenticationRepository.pauseCredentialChecking() + // If credential checking were not paused, this would check the credentials and succeed. + underTest.onPinButtonClicked(FakeAuthenticationRepository.HINTING_PIN_LENGTH) + runCurrent() + + // This one should be ignored because the user has already entered a number of digits + // that's equal to the length of the hinting PIN length. It should result in a PIN + // that's exactly the same length as the hinting PIN length. + underTest.onPinButtonClicked(FakeAuthenticationRepository.HINTING_PIN_LENGTH + 1) + runCurrent() + + assertThat(pin) + .isEqualTo( + buildList { + repeat(FakeAuthenticationRepository.HINTING_PIN_LENGTH) { index -> + add(index + 1) + } + } + ) + + kosmos.fakeAuthenticationRepository.unpauseCredentialChecking() + runCurrent() + assertThat(pin).isEmpty() + } + + @Test + fun onPinButtonClicked_whenPinNotHinted_doesNotIgnoreClick() = + testScope.runTest { + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(false) + val hintedPinLength by collectLastValue(underTest.hintedPinLength) + assertThat(hintedPinLength).isNull() + lockDeviceAndOpenPinBouncer() + + repeat(FakeAuthenticationRepository.HINTING_PIN_LENGTH + 1) { repetition -> + underTest.onPinButtonClicked(repetition + 1) + runCurrent() + } + + assertThat(pin).hasSize(FakeAuthenticationRepository.HINTING_PIN_LENGTH + 1) + } + private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer diff --git a/packages/SystemUI/res/drawable/shelf_action_chip_background.xml b/packages/SystemUI/res/drawable/shelf_action_chip_background.xml new file mode 100644 index 000000000000..63600beff126 --- /dev/null +++ b/packages/SystemUI/res/drawable/shelf_action_chip_background.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:color="@color/overlay_button_ripple"> + <item android:id="@android:id/background"> + <shape android:shape="rectangle"> + <solid android:color="?androidprv:attr/materialColorSecondary"/> + <corners android:radius="10000dp"/> <!-- fully-rounded radius --> + </shape> + </item> +</ripple> diff --git a/packages/SystemUI/res/drawable/shelf_action_chip_container_background.xml b/packages/SystemUI/res/drawable/shelf_action_chip_container_background.xml new file mode 100644 index 000000000000..bb8cece9203b --- /dev/null +++ b/packages/SystemUI/res/drawable/shelf_action_chip_container_background.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <solid android:color="?androidprv:attr/materialColorSurfaceBright"/> + <corners android:radius="10000dp"/> <!-- fully-rounded radius --> +</shape> diff --git a/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml b/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml new file mode 100644 index 000000000000..a5b44e564157 --- /dev/null +++ b/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<shape xmlns:android = "http://schemas.android.com/apk/res/android"> + <size + android:width = "@dimen/overlay_action_chip_margin_start" + android:height = "0dp"/> +</shape> diff --git a/packages/CredentialManager/wear/res/values/colors.xml b/packages/SystemUI/res/drawable/shelf_action_container_clipping_shape.xml index bf10bb3d7178..76779f9f1b2c 100644 --- a/packages/CredentialManager/wear/res/values/colors.xml +++ b/packages/SystemUI/res/drawable/shelf_action_container_clipping_shape.xml @@ -14,8 +14,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - -<resources> - <color name="wear_material_almond">#FFFCF7EB</color> - <color name="wear_material_almond_dark">#FF262523</color> -</resources>
\ No newline at end of file +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <!-- We don't actually draw anything, just expressing the shape for clipping. --> + <solid android:color="#0000"/> + <corners android:radius="10000dp"/> <!-- fully-rounded radius --> +</shape> diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml index 13355f374dd6..76d10ccb8a25 100644 --- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml +++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml @@ -47,7 +47,7 @@ android:layout_marginBottom="@dimen/bluetooth_dialog_layout_margin" android:ellipsize="end" android:gravity="center_vertical|center_horizontal" - android:maxLines="1" + android:maxLines="2" android:text="@string/quick_settings_bluetooth_tile_subtitle" android:textAppearance="@style/TextAppearance.Dialog.Body.Message" app:layout_constraintEnd_toEndOf="parent" @@ -256,6 +256,24 @@ app:constraint_referenced_ids="pair_new_device_button,bluetooth_auto_on_toggle_info_text" /> <Button + android:id="@+id/audio_sharing_button" + style="@style/Widget.Dialog.Button.BorderButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="9dp" + android:layout_marginBottom="@dimen/dialog_bottom_padding" + android:layout_marginEnd="@dimen/dialog_side_padding" + android:layout_marginStart="@dimen/dialog_side_padding" + android:ellipsize="end" + android:maxLines="1" + android:text="@string/quick_settings_bluetooth_audio_sharing_button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@+id/barrier" + app:layout_constraintVertical_bias="1" + android:visibility="gone" /> + + <Button android:id="@+id/done_button" style="@style/Widget.Dialog.Button" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml index 1eb05bfd602d..e3c5a7d03d2e 100644 --- a/packages/SystemUI/res/layout/qs_panel.xml +++ b/packages/SystemUI/res/layout/qs_panel.xml @@ -36,8 +36,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/transparent" - android:focusable="true" - android:accessibilityTraversalBefore="@android:id/edit" + android:focusable="false" + android:importantForAccessibility="yes" android:clipToPadding="false" android:clipChildren="false"> diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml index eeb64bd8460e..6a5b999f5444 100644 --- a/packages/SystemUI/res/layout/screenshot_shelf.xml +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -20,39 +20,37 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> - <ImageView + <FrameLayout android:id="@+id/actions_container_background" android:visibility="gone" - android:layout_height="0dp" - android:layout_width="0dp" - android:elevation="4dp" - android:background="@drawable/action_chip_container_background" - android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - android:layout_marginBottom="@dimen/screenshot_shelf_vertical_margin" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container" - app:layout_constraintBottom_toTopOf="@id/guideline"/> - <HorizontalScrollView - android:id="@+id/actions_container" - android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" - android:paddingHorizontal="@dimen/overlay_action_container_padding_end" - android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:layout_width="wrap_content" android:elevation="4dp" - android:scrollbars="none" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintWidth_percent="1.0" - app:layout_constraintWidth_max="wrap" + android:background="@drawable/shelf_action_chip_container_background" + android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginBottom="@dimen/screenshot_shelf_vertical_margin" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> - <LinearLayout - android:id="@+id/screenshot_actions" + app:layout_constraintBottom_toTopOf="@id/guideline" + > + <HorizontalScrollView + android:id="@+id/actions_container" android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - </HorizontalScrollView> + android:layout_height="wrap_content" + android:layout_marginVertical="@dimen/overlay_action_container_padding_vertical" + android:layout_marginHorizontal="@dimen/overlay_action_chip_margin_start" + android:background="@drawable/shelf_action_container_clipping_shape" + android:clipToOutline="true" + android:scrollbars="none"> + <LinearLayout + android:id="@+id/screenshot_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:showDividers="middle" + android:divider="@drawable/shelf_action_chip_divider" + android:animateLayoutChanges="true" + /> + </HorizontalScrollView> + </FrameLayout> <View android:id="@+id/screenshot_preview_border" android:layout_width="0dp" @@ -66,7 +64,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/screenshot_preview" app:layout_constraintEnd_toEndOf="@id/screenshot_preview" - app:layout_constraintBottom_toTopOf="@id/actions_container"/> + app:layout_constraintBottom_toTopOf="@id/actions_container_background"/> <ImageView android:id="@+id/screenshot_preview" android:layout_width="@dimen/overlay_x_scale" diff --git a/packages/SystemUI/res/layout/shelf_action_chip.xml b/packages/SystemUI/res/layout/shelf_action_chip.xml new file mode 100644 index 000000000000..709c80d07088 --- /dev/null +++ b/packages/SystemUI/res/layout/shelf_action_chip.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. + --> +<com.android.systemui.screenshot.OverlayActionChip + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/overlay_action_chip" + android:theme="@style/FloatingOverlay" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:alpha="0.0"> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingVertical="@dimen/overlay_action_chip_padding_vertical" + android:background="@drawable/shelf_action_chip_background" + android:gravity="center"> + <ImageView + android:id="@+id/overlay_action_chip_icon" + android:tint="?androidprv:attr/materialColorOnSecondary" + android:layout_width="@dimen/overlay_action_chip_icon_size" + android:layout_height="@dimen/overlay_action_chip_icon_size"/> + <TextView + android:id="@+id/overlay_action_chip_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@*android:string/config_headlineFontFamilyMedium" + android:textSize="@dimen/overlay_action_chip_text_size" + android:textColor="?androidprv:attr/materialColorOnSecondary"/> + </LinearLayout> +</com.android.systemui.screenshot.OverlayActionChip> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index af661aa172c7..f60f6c7af289 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -678,6 +678,8 @@ <string name="turn_on_bluetooth">Use Bluetooth</string> <!-- QuickSettings: Bluetooth dialog device connected default summary [CHAR LIMIT=NONE]--> <string name="quick_settings_bluetooth_device_connected">Connected</string> + <!-- QuickSettings: Bluetooth dialog device in audio sharing default summary [CHAR LIMIT=50]--> + <string name="quick_settings_bluetooth_device_audio_sharing">Audio Sharing</string> <!-- QuickSettings: Bluetooth dialog device saved default summary [CHAR LIMIT=NONE]--> <string name="quick_settings_bluetooth_device_saved">Saved</string> <!-- QuickSettings: Accessibility label to disconnect a device [CHAR LIMIT=NONE]--> @@ -687,9 +689,13 @@ <!-- QuickSettings: Bluetooth auto on tomorrow [CHAR LIMIT=NONE]--> <string name="turn_on_bluetooth_auto_tomorrow">Automatically turn on again tomorrow</string> <!-- QuickSettings: Bluetooth auto on info text when disabled [CHAR LIMIT=NONE]--> - <string name="turn_on_bluetooth_auto_info_disabled">Features like Quick Share, Find My Device, and device location use Bluetooth</string> + <string name="turn_on_bluetooth_auto_info_disabled">Features like Quick Share and Find My Device use Bluetooth</string> <!-- QuickSettings: Bluetooth auto on info text when enabled [CHAR LIMIT=NONE]--> - <string name="turn_on_bluetooth_auto_info_enabled">Bluetooth will turn on tomorrow at 5 AM</string> + <string name="turn_on_bluetooth_auto_info_enabled">Bluetooth will turn on tomorrow morning</string> + <!-- QuickSettings: Bluetooth dialog audio sharing button text [CHAR LIMIT=50]--> + <string name="quick_settings_bluetooth_audio_sharing_button">Audio Sharing</string> + <!-- QuickSettings: Bluetooth dialog audio sharing button text when sharing audio [CHAR LIMIT=50]--> + <string name="quick_settings_bluetooth_audio_sharing_button_sharing">Sharing Audio</string> <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]--> <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string> diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensor.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensor.kt index a2b119833474..f07dce5e3482 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensor.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/FingerprintSensor.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.shared.model +import android.graphics.Rect import android.hardware.fingerprint.FingerprintSensorPropertiesInternal /** Fingerprint sensor property. Represents [FingerprintSensorPropertiesInternal]. */ @@ -23,12 +24,23 @@ data class FingerprintSensor( val sensorId: Int, val sensorStrength: SensorStrength, val maxEnrollmentsPerUser: Int, - val sensorType: FingerprintSensorType + val sensorType: FingerprintSensorType, + val sensorBounds: Rect, + val sensorRadius: Int, ) /** Convert [FingerprintSensorPropertiesInternal] to corresponding [FingerprintSensor] */ fun FingerprintSensorPropertiesInternal.toFingerprintSensor(): FingerprintSensor { val sensorStrength: SensorStrength = this.sensorStrength.toSensorStrength() val sensorType: FingerprintSensorType = this.sensorType.toSensorType() - return FingerprintSensor(this.sensorId, sensorStrength, this.maxEnrollmentsPerUser, sensorType) + val sensorBounds: Rect = this.location.rect + val sensorRadius = this.location.sensorRadius + return FingerprintSensor( + this.sensorId, + sensorStrength, + this.maxEnrollmentsPerUser, + sensorType, + sensorBounds, + sensorRadius, + ) } diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt index 476daac5ff00..0f3d28586588 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt @@ -33,3 +33,10 @@ fun Int.toSensorStrength(): SensorStrength = SensorProperties.STRENGTH_STRONG -> SensorStrength.STRONG else -> throw IllegalArgumentException("Invalid SensorStrength value: $this") } +/** Convert [SensorStrength] to corresponding [Int] */ +fun SensorStrength.toInt(): Int = + when (this) { + SensorStrength.CONVENIENCE -> SensorProperties.STRENGTH_CONVENIENCE + SensorStrength.WEAK -> SensorProperties.STRENGTH_WEAK + SensorStrength.STRONG -> SensorProperties.STRENGTH_STRONG + } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java index 7e96e48545ea..615363da073a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java @@ -72,6 +72,7 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp private boolean mEndAnimationCanceled = false; @MagnificationState private int mState = STATE_DISABLED; + private Runnable mOnAnimationEndRunnable; WindowMagnificationAnimationController(@UiContext Context context) { this(context, newValueAnimator(context.getResources())); @@ -303,12 +304,7 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp return; } - // If the animation is playing backwards, mStartSpec will be the final spec we would - // like to reach. - AnimationSpec spec = isReverse ? mStartSpec : mEndSpec; - mController.updateWindowMagnificationInternal( - spec.mScale, spec.mCenterX, spec.mCenterY, - mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); + mOnAnimationEndRunnable.run(); if (mState == STATE_DISABLING) { mController.deleteWindowMagnification(); @@ -333,6 +329,10 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp public void onAnimationRepeat(Animator animation) { } + void setOnAnimationEndRunnable(Runnable runnable) { + mOnAnimationEndRunnable = runnable; + } + private void sendAnimationCallback(boolean success) { if (mAnimationCallback != null) { try { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index a847c3d510b1..9837e369bc91 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -260,6 +260,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mContext = context; mHandler = handler; mAnimationController = animationController; + mAnimationController.setOnAnimationEndRunnable(() -> { + if (Flags.createWindowlessWindowMagnifier()) { + notifySourceBoundsChanged(); + } + }); mAnimationController.setWindowMagnificationController(this); mWindowMagnifierCallback = callback; mSysUiState = sysUiState; @@ -1051,11 +1056,15 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold // Notify source bounds change when the magnifier is not animating. if (!mAnimationController.isAnimating()) { - mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds); + notifySourceBoundsChanged(); } } } + private void notifySourceBoundsChanged() { + mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds); + } + /** * Updates the position of {@link mSurfaceControlViewHost} and layout params of MirrorView based * on the position and size of {@link #mMagnificationFrame}. diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java index 1018f70c7f60..eb840f1f4c90 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java @@ -244,11 +244,13 @@ class MenuInfoRepository { mSecureSettings.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), /* notifyForDescendants */ false, mMenuTargetFeaturesContentObserver, UserHandle.USER_CURRENT); - mSecureSettings.registerContentObserverForUser( - mSecureSettings.getUriFor(ENABLED_ACCESSIBILITY_SERVICES), - /* notifyForDescendants */ false, - mMenuTargetFeaturesContentObserver, - UserHandle.USER_CURRENT); + if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) { + mSecureSettings.registerContentObserverForUser( + mSecureSettings.getUriFor(ENABLED_ACCESSIBILITY_SERVICES), + /* notifyForDescendants */ false, + mMenuTargetFeaturesContentObserver, + UserHandle.USER_CURRENT); + } mSecureSettings.registerContentObserverForUser( mSecureSettings.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE), /* notifyForDescendants */ false, mMenuSizeContentObserver, @@ -263,8 +265,10 @@ class MenuInfoRepository { UserHandle.USER_CURRENT); mContext.registerComponentCallbacks(mComponentCallbacks); - mAccessibilityManager.addAccessibilityServicesStateChangeListener( - mA11yServicesStateChangeListener); + if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) { + mAccessibilityManager.addAccessibilityServicesStateChangeListener( + mA11yServicesStateChangeListener); + } } void unregisterObserversAndCallbacks() { @@ -273,8 +277,10 @@ class MenuInfoRepository { mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver); mContext.unregisterComponentCallbacks(mComponentCallbacks); - mAccessibilityManager.removeAccessibilityServicesStateChangeListener( - mA11yServicesStateChangeListener); + if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) { + mAccessibilityManager.removeAccessibilityServicesStateChangeListener( + mA11yServicesStateChangeListener); + } } interface OnSettingsContentsChanged { diff --git a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS new file mode 100644 index 000000000000..b65d29c6a0bb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS @@ -0,0 +1,9 @@ +# Bug component: 1495344 + +dupin@google.com +linyuh@google.com +pauldpong@google.com +praveenj@google.com +vicliang@google.com +mfolkerts@google.com +yuklimko@google.com diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt new file mode 100644 index 000000000000..e44f0543fc87 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bluetooth.qsdialog + +import androidx.annotation.StringRes +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn + +internal sealed class AudioSharingButtonState { + object Gone : AudioSharingButtonState() + data class Visible(@StringRes val resId: Int) : AudioSharingButtonState() +} + +/** Holds business logic for the audio sharing state. */ +@SysUISingleton +internal class AudioSharingInteractor +@Inject +constructor( + private val localBluetoothManager: LocalBluetoothManager?, + bluetoothStateInteractor: BluetoothStateInteractor, + deviceItemInteractor: DeviceItemInteractor, + @Application private val coroutineScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + /** Flow representing the update of AudioSharingButtonState. */ + internal val audioSharingButtonStateUpdate: Flow<AudioSharingButtonState> = + combine( + bluetoothStateInteractor.bluetoothStateUpdate, + deviceItemInteractor.deviceItemUpdate + ) { bluetoothState, deviceItem -> + getButtonState(bluetoothState, deviceItem) + } + .flowOn(backgroundDispatcher) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(replayExpirationMillis = 0), + initialValue = AudioSharingButtonState.Gone + ) + + private fun getButtonState( + bluetoothState: Boolean, + deviceItem: List<DeviceItem> + ): AudioSharingButtonState { + return when { + // Don't show button when bluetooth is off + !bluetoothState -> AudioSharingButtonState.Gone + // Show sharing audio when broadcasting + BluetoothUtils.isBroadcasting(localBluetoothManager) -> + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button_sharing + ) + // When not broadcasting, don't show button if there's connected source in any device + deviceItem.any { + BluetoothUtils.hasConnectedBroadcastSource( + it.cachedBluetoothDevice, + localBluetoothManager + ) + } -> AudioSharingButtonState.Gone + // Show audio sharing when there's a connected LE audio device + deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } -> + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button + ) + else -> AudioSharingButtonState.Gone + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt index 94d7af74f1dd..17f9e634ec62 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt @@ -25,12 +25,17 @@ import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLoggin import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext /** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */ @SysUISingleton @@ -40,9 +45,10 @@ constructor( private val localBluetoothManager: LocalBluetoothManager?, private val logger: BluetoothTileDialogLogger, @Application private val coroutineScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, ) { - internal val bluetoothStateUpdate: StateFlow<Boolean?> = + internal val bluetoothStateUpdate: StateFlow<Boolean> = conflatedCallbackFlow { val listener = object : BluetoothCallback { @@ -64,16 +70,22 @@ constructor( localBluetoothManager?.eventManager?.registerCallback(listener) awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) } } + .onStart { emit(isBluetoothEnabled()) } + .flowOn(backgroundDispatcher) .stateIn( coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), - initialValue = null + initialValue = false ) - internal var isBluetoothEnabled: Boolean - get() = localBluetoothManager?.bluetoothAdapter?.isEnabled == true - set(value) { - if (isBluetoothEnabled != value) { + suspend fun isBluetoothEnabled(): Boolean = + withContext(backgroundDispatcher) { + localBluetoothManager?.bluetoothAdapter?.isEnabled == true + } + + suspend fun setBluetoothEnabled(value: Boolean) { + withContext(backgroundDispatcher) { + if (isBluetoothEnabled() != value) { localBluetoothManager?.bluetoothAdapter?.apply { if (value) enable() else disable() logger.logBluetoothState( @@ -83,6 +95,7 @@ constructor( } } } + } companion object { private const val TAG = "BtStateInteractor" diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index c7d171d5b804..dd8c0df387dc 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -27,6 +27,7 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction +import android.widget.Button import android.widget.ImageView import android.widget.ProgressBar import android.widget.Switch @@ -59,7 +60,6 @@ class BluetoothTileDialogDelegate internal constructor( @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties, @Assisted private val cachedContentHeight: Int, - @Assisted private val bluetoothToggleInitialValue: Boolean, @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback, @Assisted private val dismissListener: Runnable, @Main private val mainDispatcher: CoroutineDispatcher, @@ -69,8 +69,7 @@ internal constructor( private val systemuiDialogFactory: SystemUIDialog.Factory, ) : SystemUIDialog.Delegate { - private val mutableBluetoothStateToggle: MutableStateFlow<Boolean> = - MutableStateFlow(bluetoothToggleInitialValue) + private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null) internal val bluetoothStateToggle get() = mutableBluetoothStateToggle.asStateFlow() @@ -99,7 +98,6 @@ internal constructor( fun create( initialUiProperties: BluetoothTileDialogViewModel.UiProperties, cachedContentHeight: Int, - bluetoothEnabled: Boolean, dialogCallback: BluetoothTileDialogCallback, dimissListener: Runnable ): BluetoothTileDialogDelegate @@ -130,6 +128,9 @@ internal constructor( getPairNewDeviceButton(dialog).setOnClickListener { bluetoothTileDialogCallback.onPairNewDeviceClicked(it) } + getAudioSharingButtonView(dialog).setOnClickListener { + bluetoothTileDialogCallback.onAudioSharingButtonClicked(it) + } getScrollViewContent(dialog).apply { minimumHeight = resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId) @@ -211,9 +212,19 @@ internal constructor( getAutoOnToggleInfoTextView(dialog).text = dialog.context.getString(infoResId) } + internal fun onAudioSharingButtonUpdated( + dialog: SystemUIDialog, + visibility: Int, + label: String? + ) { + getAudioSharingButtonView(dialog).apply { + this.visibility = visibility + label?.let { text = it } + } + } + private fun setupToggle(dialog: SystemUIDialog) { val toggleView = getToggleView(dialog) - toggleView.isChecked = bluetoothToggleInitialValue toggleView.setOnCheckedChangeListener { view, isChecked -> mutableBluetoothStateToggle.value = isChecked view.apply { @@ -259,6 +270,10 @@ internal constructor( return dialog.requireViewById(R.id.bluetooth_auto_on_toggle) } + private fun getAudioSharingButtonView(dialog: SystemUIDialog): Button { + return dialog.requireViewById(R.id.audio_sharing_button) + } + private fun getAutoOnToggleView(dialog: SystemUIDialog): View { return dialog.requireViewById(R.id.bluetooth_auto_on_toggle_layout) } @@ -412,6 +427,8 @@ internal constructor( const val ACTION_PREVIOUSLY_CONNECTED_DEVICE = "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE" const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS" + const val ACTION_AUDIO_SHARING = + "com.google.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS" const val DISABLED_ALPHA = 0.3f const val ENABLED_ALPHA = 1f const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt index add1647143d8..b592b8ed4332 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -30,9 +30,12 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent @UiEvent(doc = "Connected device clicked to active") CONNECTED_DEVICE_SET_ACTIVE(1499), @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500), @UiEvent(doc = "Active device clicked to disconnect") ACTIVE_DEVICE_DISCONNECT(1507), + @UiEvent(doc = "Audio sharing device clicked, do nothing") AUDIO_SHARING_DEVICE_CLICKED(1699), @UiEvent(doc = "Connected other device clicked to disconnect") CONNECTED_OTHER_DEVICE_DISCONNECT(1508), - @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617); + @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617), + @UiEvent(doc = "The audio sharing button is clicked") + BLUETOOTH_AUDIO_SHARING_BUTTON_CLICKED(1700); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index e65b65710f94..eb919e3ca36b 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -28,9 +28,11 @@ import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger +import com.android.settingslib.bluetooth.BluetoothUtils import com.android.systemui.Prefs import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE @@ -61,6 +63,7 @@ constructor( private val deviceItemInteractor: DeviceItemInteractor, private val bluetoothStateInteractor: BluetoothStateInteractor, private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor, + private val audioSharingInteractor: AudioSharingInteractor, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, private val uiEventLogger: UiEventLogger, @@ -119,7 +122,8 @@ constructor( dialog, it.take(MAX_DEVICE_ITEM_ENTRY), showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY, - showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled + showPairNewDevice = + bluetoothStateInteractor.isBluetoothEnabled() ) animateProgressBar(dialog, false) } @@ -142,10 +146,25 @@ constructor( } .launchIn(this) + if (BluetoothUtils.isAudioSharingEnabled()) { + audioSharingInteractor.audioSharingButtonStateUpdate + .onEach { + if (it is AudioSharingButtonState.Visible) { + dialogDelegate.onAudioSharingButtonUpdated( + dialog, + VISIBLE, + context.getString(it.resId) + ) + } else { + dialogDelegate.onAudioSharingButtonUpdated(dialog, GONE, null) + } + } + .launchIn(this) + } + // bluetoothStateUpdate is emitted when bluetooth on/off state is changed, re-fetch // the device item list. bluetoothStateInteractor.bluetoothStateUpdate - .filterNotNull() .onEach { dialogDelegate.onBluetoothStateUpdated( dialog, @@ -165,9 +184,10 @@ constructor( // bluetoothStateToggle is emitted when user toggles the bluetooth state switch, // send the new value to the bluetoothStateInteractor and animate the progress bar. dialogDelegate.bluetoothStateToggle + .filterNotNull() .onEach { dialogDelegate.animateProgressBar(dialog, true) - bluetoothStateInteractor.isBluetoothEnabled = it + bluetoothStateInteractor.setBluetoothEnabled(it) } .launchIn(this) @@ -222,11 +242,10 @@ constructor( return bluetoothDialogDelegateFactory.create( UiProperties.build( - bluetoothStateInteractor.isBluetoothEnabled, + bluetoothStateInteractor.isBluetoothEnabled(), isAutoOnToggleFeatureAvailable() ), cachedContentHeight, - bluetoothStateInteractor.isBluetoothEnabled, this@BluetoothTileDialogViewModel, { cancelJob() } ) @@ -256,6 +275,11 @@ constructor( startSettingsActivity(Intent(ACTION_PAIR_NEW_DEVICE), view) } + override fun onAudioSharingButtonClicked(view: View) { + uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUDIO_SHARING_BUTTON_CLICKED) + startSettingsActivity(Intent(ACTION_AUDIO_SHARING), view) + } + private fun cancelJob() { job?.cancel() job = null @@ -312,4 +336,5 @@ interface BluetoothTileDialogCallback { fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) fun onSeeAllClicked(view: View) fun onPairNewDeviceClicked(view: View) + fun onAudioSharingButtonClicked(view: View) } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt index dc5efefdfb16..0ea98d14bca3 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt @@ -37,6 +37,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice enum class DeviceItemType { ACTIVE_MEDIA_BLUETOOTH_DEVICE, + AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, AVAILABLE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE, SAVED_BLUETOOTH_DEVICE, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index f04ba75ca3ef..49d0847ab0c7 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -21,13 +21,16 @@ import android.content.Context import android.media.AudioManager import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.flags.Flags +import com.android.settingslib.flags.Flags.enableLeAudioSharing import com.android.systemui.res.R private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on private val backgroundOff = R.drawable.bluetooth_tile_dialog_bg_off private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy private val connected = R.string.quick_settings_bluetooth_device_connected +private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing private val saved = R.string.quick_settings_bluetooth_device_saved private val actionAccessibilityLabelActivate = R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate @@ -39,35 +42,81 @@ internal abstract class DeviceItemFactory { abstract fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager, ): Boolean abstract fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem + + companion object { + @JvmStatic + fun createDeviceItem( + context: Context, + cachedDevice: CachedBluetoothDevice, + type: DeviceItemType, + connectionSummary: String, + background: Int, + actionAccessibilityLabel: String + ): DeviceItem { + return DeviceItem( + type = type, + cachedBluetoothDevice = cachedDevice, + deviceName = cachedDevice.name, + connectionSummary = connectionSummary, + iconWithDescription = + BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { + Pair(it.first, it.second) + }, + background = background, + isEnabled = !cachedDevice.isBusy, + actionAccessibilityLabel = actionAccessibilityLabel + ) + } + } } internal open class ActiveMediaDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager ): Boolean { return BluetoothUtils.isActiveMediaDevice(cachedDevice) && BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager) } override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { - return DeviceItem( - type = DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedDevice, - deviceName = cachedDevice.name, - connectionSummary = cachedDevice.connectionSummary ?: "", - iconWithDescription = - BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p -> - Pair(p.first, p.second) - }, - background = backgroundOn, - isEnabled = !cachedDevice.isBusy, - actionAccessibilityLabel = context.getString(actionAccessibilityLabelDisconnect), + return createDeviceItem( + context, + cachedDevice, + DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, + cachedDevice.connectionSummary ?: "", + backgroundOn, + context.getString(actionAccessibilityLabelDisconnect) + ) + } +} + +internal class AudioSharingMediaDeviceItemFactory( + private val localBluetoothManager: LocalBluetoothManager? +) : DeviceItemFactory() { + override fun isFilterMatched( + context: Context, + cachedDevice: CachedBluetoothDevice, + audioManager: AudioManager + ): Boolean { + return enableLeAudioSharing() && + BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager) + } + + override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { + return createDeviceItem( + context, + cachedDevice, + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } + ?: context.getString(audioSharing), + if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn, + "" ) } } @@ -76,7 +125,7 @@ internal class ActiveHearingDeviceItemFactory : ActiveMediaDeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager ): Boolean { return BluetoothUtils.isActiveMediaDevice(cachedDevice) && BluetoothUtils.isAvailableHearingDevice(cachedDevice) @@ -87,27 +136,21 @@ internal open class AvailableMediaDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager ): Boolean { return !BluetoothUtils.isActiveMediaDevice(cachedDevice) && BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager) } - // TODO(b/298124674): move create() to the abstract class to reduce duplicate code override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { - return DeviceItem( - type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedDevice, - deviceName = cachedDevice.name, - connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } - ?: context.getString(connected), - iconWithDescription = - BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p -> - Pair(p.first, p.second) - }, - background = if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, - isEnabled = !cachedDevice.isBusy, - actionAccessibilityLabel = context.getString(actionAccessibilityLabelActivate), + return createDeviceItem( + context, + cachedDevice, + DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, + cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } + ?: context.getString(connected), + if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, + context.getString(actionAccessibilityLabelActivate) ) } } @@ -116,7 +159,7 @@ internal class AvailableHearingDeviceItemFactory : ActiveMediaDeviceItemFactory( override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager ): Boolean { return !BluetoothUtils.isActiveMediaDevice(cachedDevice) && BluetoothUtils.isAvailableHearingDevice(cachedDevice) @@ -127,32 +170,25 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager ): Boolean { return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { - !BluetoothUtils.isExclusivelyManagedBluetoothDevice( - context, - cachedDevice.getDevice() - ) && BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) + !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) && + BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) } else { BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) } } override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { - return DeviceItem( - type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedDevice, - deviceName = cachedDevice.name, - connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } - ?: context.getString(connected), - iconWithDescription = - BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p -> - Pair(p.first, p.second) - }, - background = if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, - isEnabled = !cachedDevice.isBusy, - actionAccessibilityLabel = context.getString(actionAccessibilityLabelDisconnect), + return createDeviceItem( + context, + cachedDevice, + DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, + cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } + ?: context.getString(connected), + if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, + context.getString(actionAccessibilityLabelDisconnect) ) } } @@ -161,32 +197,26 @@ internal open class SavedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager ): Boolean { return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { - !BluetoothUtils.isExclusivelyManagedBluetoothDevice( - context, - cachedDevice.getDevice() - ) && cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected + !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) && + cachedDevice.bondState == BluetoothDevice.BOND_BONDED && + !cachedDevice.isConnected } else { cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected } } override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { - return DeviceItem( - type = DeviceItemType.SAVED_BLUETOOTH_DEVICE, - cachedBluetoothDevice = cachedDevice, - deviceName = cachedDevice.name, - connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } - ?: context.getString(saved), - iconWithDescription = - BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p -> - Pair(p.first, p.second) - }, - background = if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, - isEnabled = !cachedDevice.isBusy, - actionAccessibilityLabel = context.getString(actionAccessibilityLabelActivate), + return createDeviceItem( + context, + cachedDevice, + DeviceItemType.SAVED_BLUETOOTH_DEVICE, + cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } + ?: context.getString(saved), + if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, + context.getString(actionAccessibilityLabelActivate) ) } } @@ -195,7 +225,7 @@ internal class SavedHearingDeviceItemFactory : SavedDeviceItemFactory() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager ): Boolean { return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { !BluetoothUtils.isExclusivelyManagedBluetoothDevice( diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt index 4e28cafb5004..66e593b94b21 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt @@ -113,6 +113,7 @@ constructor( private var deviceItemFactoryList: List<DeviceItemFactory> = listOf( ActiveMediaDeviceItemFactory(), + AudioSharingMediaDeviceItemFactory(localBluetoothManager), AvailableMediaDeviceItemFactory(), ConnectedDeviceItemFactory(), SavedDeviceItemFactory() @@ -121,6 +122,7 @@ constructor( private var displayPriority: List<DeviceItemType> = listOf( DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, DeviceItemType.SAVED_BLUETOOTH_DEVICE, @@ -177,6 +179,9 @@ constructor( disconnect() uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT) } + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED) + } DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { setActive() uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index 12cac9251b25..4c2380c5e4db 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -135,8 +135,11 @@ class PinBouncerViewModel( onIntentionalUserInput() - mutablePinInput.value = pinInput.append(input) - tryAuthenticate(useAutoConfirm = true) + val maxInputLength = hintedPinLength.value ?: Int.MAX_VALUE + if (pinInput.getPin().size < maxInputLength) { + mutablePinInput.value = pinInput.append(input) + tryAuthenticate(useAutoConfirm = true) + } } /** Notifies that the user clicked the backspace button. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java index e1d03392044a..cddba04b5a7c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/InputSession.java @@ -16,7 +16,6 @@ package com.android.systemui.dreams.touch; -import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.INPUT_SESSION_NAME; import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.PILFER_ON_GESTURE_CONSUME; import android.os.Looper; @@ -24,7 +23,8 @@ import android.view.Choreographer; import android.view.GestureDetector; import android.view.MotionEvent; -import com.android.systemui.settings.DisplayTracker; +import com.android.systemui.Flags; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputMonitorCompat; @@ -42,26 +42,34 @@ public class InputSession { private final InputChannelCompat.InputEventReceiver mInputEventReceiver; private final GestureDetector mGestureDetector; + // Pilfering is a destructive operation. Once pilfering starts, the all events will be captured + // by the associated monitor. We track whether we're pilfering since initiating pilfering + // requires reaching out to the InputManagerService, which can be a heavy operation. This is + // especially costly if this is happening on a continuous stream of motion events. + private boolean mPilfering; + /** * Default session constructor. - * @param sessionName The session name that will be applied to the underlying - * {@link InputMonitorCompat}. + * @param inputMonitor Input monitor to track input events. + * @param gestureDetector Gesture detector for detecting gestures. * @param inputEventListener A listener to receive input events. - * @param gestureListener A listener to receive gesture events. + * @param choreographer Choreographer to use with the input receiver. + * @param looper Looper to use with the input receiver * @param pilferOnGestureConsume Whether touch events should be pilfered after a gesture has * been consumed. */ @Inject - public InputSession(@Named(INPUT_SESSION_NAME) String sessionName, + public InputSession( + InputMonitorCompat inputMonitor, + GestureDetector gestureDetector, InputChannelCompat.InputEventListener inputEventListener, - GestureDetector.OnGestureListener gestureListener, - DisplayTracker displayTracker, + Choreographer choreographer, + @Main Looper looper, @Named(PILFER_ON_GESTURE_CONSUME) boolean pilferOnGestureConsume) { - mInputMonitor = new InputMonitorCompat(sessionName, displayTracker.getDefaultDisplayId()); - mGestureDetector = new GestureDetector(gestureListener); + mInputMonitor = inputMonitor; + mGestureDetector = gestureDetector; - mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(), - Choreographer.getInstance(), + mInputEventReceiver = mInputMonitor.getInputReceiver(looper, choreographer, ev -> { // Process event. Since sometimes input may be a prerequisite for some // gesture logic, process input first. @@ -69,7 +77,9 @@ public class InputSession { if (ev instanceof MotionEvent && mGestureDetector.onTouchEvent((MotionEvent) ev) - && pilferOnGestureConsume) { + && pilferOnGestureConsume + && !(mPilfering && Flags.dreamInputSessionPilferOnce())) { + mPilfering = true; mInputMonitor.pilferPointers(); } }); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java index ad59a2e2b5c3..0b145211cd45 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionComponent.java @@ -24,16 +24,18 @@ import android.view.GestureDetector; import com.android.systemui.dreams.touch.InputSession; import com.android.systemui.shared.system.InputChannelCompat; -import javax.inject.Named; - import dagger.BindsInstance; import dagger.Subcomponent; +import javax.inject.Named; + /** * {@link InputSessionComponent} generates {@link InputSession} with specific instances bound for * the session name and whether touches should be pilfered when consumed. */ -@Subcomponent +@Subcomponent( + modules = { InputSessionModule.class } +) public interface InputSessionComponent { /** * Generates {@link InputSessionComponent}. diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionModule.java new file mode 100644 index 000000000000..dfab666d5f59 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/InputSessionModule.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.touch.dagger; + +import static com.android.systemui.dreams.touch.dagger.DreamTouchModule.INPUT_SESSION_NAME; + +import android.view.GestureDetector; + +import com.android.systemui.settings.DisplayTracker; +import com.android.systemui.shared.system.InputMonitorCompat; + +import dagger.Module; +import dagger.Provides; + +import javax.inject.Named; + + +/** + * Module for providing dependencies to {@link com.android.systemui.dreams.touch.InputSession}. + */ +@Module +public interface InputSessionModule { + /** */ + @Provides + static InputMonitorCompat providesInputMonitorCompat(@Named(INPUT_SESSION_NAME) String name, + DisplayTracker displayTracker) { + return new InputMonitorCompat(name, displayTracker.getDefaultDisplayId()); + } + + /** */ + @Provides + static GestureDetector providesGestureDetector( + android.view.GestureDetector.OnGestureListener gestureListener) { + return new GestureDetector(gestureListener); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt index a6b01e748246..44f767aa321e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt @@ -19,7 +19,7 @@ package com.android.systemui.screenshot.policy import android.content.ComponentName import android.content.Context import android.os.Process -import com.android.systemui.Flags.screenshotPrivateProfile +import com.android.systemui.Flags.screenshotPrivateProfileBehaviorFix import com.android.systemui.SystemUIService import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -72,7 +72,7 @@ interface ScreenshotPolicyModule { displayContentRepoProvider: Provider<DisplayContentRepository>, policyListProvider: Provider<List<CapturePolicy>>, ): ScreenshotRequestProcessor { - return if (screenshotPrivateProfile()) { + return if (screenshotPrivateProfileBehaviorFix()) { PolicyRequestProcessor( background = background, capture = imageCapture, 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 d9a51029d346..5f835b3697a1 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 @@ -95,7 +95,7 @@ object ScreenshotShelfViewBinder { // mean that the new action must be inserted here. val actionButton = layoutInflater.inflate( - R.layout.overlay_action_chip, + R.layout.shelf_action_chip, actionsContainer, false ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 8b7b348ede76..79e6a0aa9c8c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -67,6 +67,8 @@ import com.android.systemui.util.time.SystemClock; import dagger.Lazy; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -77,8 +79,6 @@ import java.util.Set; import javax.inject.Inject; -import kotlinx.coroutines.ExperimentalCoroutinesApi; - /** * Controller which coordinates all the biometric unlocking actions with the UI. */ @@ -183,6 +183,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private final SystemClock mSystemClock; private final boolean mOrderUnlockAndWake; private final Lazy<SelectedUserInteractor> mSelectedUserInteractor; + private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; private long mLastFpFailureUptimeMillis; private int mNumConsecutiveFpFailures; @@ -323,6 +324,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mOrderUnlockAndWake = resources.getBoolean( com.android.internal.R.bool.config_orderUnlockAndWake); mSelectedUserInteractor = selectedUserInteractor; + mKeyguardTransitionInteractor = keyguardTransitionInteractor; javaAdapter.alwaysCollectFlow( keyguardTransitionInteractor.getStartedKeyguardTransitionStep(), this::consumeTransitionStepOnStartedKeyguardState); @@ -665,7 +667,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } if (isKeyguardShowing) { if ((mKeyguardViewController.primaryBouncerIsOrWillBeShowing() - || mKeyguardBypassController.getAltBouncerShowing()) && unlockingAllowed) { + || mKeyguardTransitionInteractor.getCurrentState() + == KeyguardState.ALTERNATE_BOUNCER) && unlockingAllowed) { return MODE_DISMISS_BOUNCER; } else if (unlockingAllowed && bypass) { return MODE_UNLOCK_COLLAPSING; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt index 3b2930f78d19..f4e3eab8593d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.pipeline.mobile.data.model import android.os.PersistableBundle import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL -import android.telephony.CarrierConfigManager.KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT import android.telephony.CarrierConfigManager.KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL import androidx.annotation.VisibleForTesting import kotlinx.coroutines.flow.MutableStateFlow @@ -43,11 +42,10 @@ import kotlinx.coroutines.flow.asStateFlow * using the default config for logging purposes. * * NOTE to add new keys to be tracked: - * 1. Define a new `private val` wrapping the key using [BooleanCarrierConfig] or [IntCarrierConfig] - * 2. Define a public `val` exposing the wrapped flow using [BooleanCarrierConfig.config] or - * [IntCarrierConfig.config] - * 3. Add the new wrapped public flow to the list of tracked configs, so they are properly updated - * when a new carrier config comes down + * 1. Define a new `private val` wrapping the key using [BooleanCarrierConfig] + * 2. Define a public `val` exposing the wrapped flow using [BooleanCarrierConfig.config] + * 3. Add the new [BooleanCarrierConfig] to the list of tracked configs, so they are properly + * updated when a new carrier config comes down */ class SystemUiCarrierConfig internal constructor( @@ -68,16 +66,10 @@ internal constructor( /** Flow tracking the [KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL] config */ val showOperatorNameInStatusBar: StateFlow<Boolean> = showOperatorName.config - private val satelliteHysteresisSeconds = - IntCarrierConfig(KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT, defaultConfig) - /** Flow tracking the [KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT] config */ - val satelliteConnectionHysteresisSeconds: StateFlow<Int> = satelliteHysteresisSeconds.config - private val trackedConfigs = listOf( inflateSignalStrength, showOperatorName, - satelliteHysteresisSeconds, ) /** Ingest a new carrier config, and switch all of the tracked keys over to the new values */ @@ -98,19 +90,15 @@ internal constructor( override fun toString(): String = trackedConfigs.joinToString { it.toString() } } -interface CarrierConfig { - fun update(config: PersistableBundle) -} - /** Extracts [key] from the carrier config, and stores it in a flow */ private class BooleanCarrierConfig( val key: String, defaultConfig: PersistableBundle, -) : CarrierConfig { +) { private val _configValue = MutableStateFlow(defaultConfig.getBoolean(key)) val config = _configValue.asStateFlow() - override fun update(config: PersistableBundle) { + fun update(config: PersistableBundle) { _configValue.value = config.getBoolean(key) } @@ -118,20 +106,3 @@ private class BooleanCarrierConfig( return "$key=${config.value}" } } - -/** Extracts [key] from the carrier config, and stores it in a flow */ -private class IntCarrierConfig( - val key: String, - defaultConfig: PersistableBundle, -) : CarrierConfig { - private val _configValue = MutableStateFlow(defaultConfig.getInt(key)) - val config = _configValue.asStateFlow() - - override fun update(config: PersistableBundle) { - _configValue.value = config.getInt(key) - } - - override fun toString(): String { - return "$key=${config.value}" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 8f00b43e79f8..22785979f3ae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -44,6 +44,9 @@ interface MobileConnectionRepository { /** The carrierId for this connection. See [TelephonyManager.getSimCarrierId] */ val carrierId: StateFlow<Int> + /** Reflects the value from the carrier config INFLATE_SIGNAL_STRENGTH for this connection */ + val inflateSignalStrength: StateFlow<Boolean> + /** * The table log buffer created for this connection. Will have the name "MobileConnectionLog * [subId]" @@ -141,9 +144,6 @@ interface MobileConnectionRepository { */ val hasPrioritizedNetworkCapabilities: StateFlow<Boolean> - /** Duration in seconds of the hysteresis to use when losing satellite connection. */ - val satelliteConnectionHysteresisSeconds: StateFlow<Int> - /** * True if this connection is in emergency callback mode. * diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt index af34a57c7242..83d5f2b5d325 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.F import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** @@ -67,6 +68,17 @@ class DemoMobileConnectionRepository( ) .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierId.value) + private val _inflateSignalStrength: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val inflateSignalStrength = + _inflateSignalStrength + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = "inflate", + _inflateSignalStrength.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _inflateSignalStrength.value) + private val _isEmergencyOnly = MutableStateFlow(false) override val isEmergencyOnly = _isEmergencyOnly @@ -191,7 +203,16 @@ class DemoMobileConnectionRepository( .logDiffsForTable(tableLogBuffer, columnPrefix = "", _resolvedNetworkType.value) .stateIn(scope, SharingStarted.WhileSubscribed(), _resolvedNetworkType.value) - override val numberOfLevels = MutableStateFlow(MobileConnectionRepository.DEFAULT_NUM_LEVELS) + override val numberOfLevels = + _inflateSignalStrength + .map { shouldInflate -> + if (shouldInflate) { + DEFAULT_NUM_LEVELS + 1 + } else { + DEFAULT_NUM_LEVELS + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS) override val dataEnabled = MutableStateFlow(true) @@ -206,8 +227,6 @@ class DemoMobileConnectionRepository( override val hasPrioritizedNetworkCapabilities = MutableStateFlow(false) - override val satelliteConnectionHysteresisSeconds = MutableStateFlow(0) - override suspend fun isInEcmMode(): Boolean = false /** @@ -226,8 +245,7 @@ class DemoMobileConnectionRepository( _carrierId.value = event.carrierId ?: INVALID_SUBSCRIPTION_ID - numberOfLevels.value = - if (event.inflateStrength) DEFAULT_NUM_LEVELS + 1 else DEFAULT_NUM_LEVELS + _inflateSignalStrength.value = event.inflateStrength cdmaRoaming.value = event.roaming _isRoaming.value = event.roaming @@ -258,7 +276,6 @@ class DemoMobileConnectionRepository( carrierName.value = NetworkNameModel.SubscriptionDerived(CARRIER_MERGED_NAME) // TODO(b/276943904): is carrierId a thing with carrier merged networks? _carrierId.value = INVALID_SUBSCRIPTION_ID - numberOfLevels.value = event.numberOfLevels cdmaRoaming.value = false _primaryLevel.value = event.level _cdmaLevel.value = event.level diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt index 2bc3bcbc8bf5..a532e6227451 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -165,6 +165,7 @@ class CarrierMergedConnectionRepository( override val isRoaming = MutableStateFlow(false).asStateFlow() override val carrierId = MutableStateFlow(INVALID_SUBSCRIPTION_ID).asStateFlow() + override val inflateSignalStrength = MutableStateFlow(false).asStateFlow() override val isEmergencyOnly = MutableStateFlow(false).asStateFlow() override val operatorAlphaShort = MutableStateFlow(null).asStateFlow() override val isInService = MutableStateFlow(true).asStateFlow() @@ -186,9 +187,6 @@ class CarrierMergedConnectionRepository( */ override val hasPrioritizedNetworkCapabilities = MutableStateFlow(false).asStateFlow() - /** Non-applicable to carrier merged connections. */ - override val satelliteConnectionHysteresisSeconds = MutableStateFlow(0).asStateFlow() - override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled override suspend fun isInEcmMode(): Boolean = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt index b085d8046b12..41559b2c1455 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod +import android.util.IndentingPrintWriter import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer @@ -24,6 +25,7 @@ import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -291,6 +293,21 @@ class FullMobileConnectionRepository( ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value) + override val inflateSignalStrength = + activeRepo + .flatMapLatest { it.inflateSignalStrength } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = "inflate", + initialValue = activeRepo.value.inflateSignalStrength.value, + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.inflateSignalStrength.value + ) + override val numberOfLevels = activeRepo .flatMapLatest { it.numberOfLevels } @@ -334,17 +351,29 @@ class FullMobileConnectionRepository( activeRepo.value.hasPrioritizedNetworkCapabilities.value, ) - override val satelliteConnectionHysteresisSeconds = - activeRepo - .flatMapLatest { it.satelliteConnectionHysteresisSeconds } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.satelliteConnectionHysteresisSeconds.value - ) - override suspend fun isInEcmMode(): Boolean = activeRepo.value.isInEcmMode() + fun dump(pw: PrintWriter) { + val ipw = IndentingPrintWriter(pw, " ") + + ipw.println("MobileConnectionRepository[$subId]") + ipw.increaseIndent() + + ipw.println("carrierMerged=${_isCarrierMerged.value}") + + ipw.print("Type (cellular or carrier merged): ") + when (activeRepo.value) { + is CarrierMergedConnectionRepository -> ipw.println("Carrier merged") + is MobileConnectionRepositoryImpl -> ipw.println("Cellular") + } + + ipw.increaseIndent() + ipw.println("Provider: ${activeRepo.value}") + ipw.decreaseIndent() + + ipw.decreaseIndent() + } + class Factory @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 5ab2ae899370..b3885d247949 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -302,8 +302,10 @@ class MobileConnectionRepositoryImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), UnknownNetworkType) + override val inflateSignalStrength = systemUiCarrierConfig.shouldInflateSignalStrength + override val numberOfLevels = - systemUiCarrierConfig.shouldInflateSignalStrength + inflateSignalStrength .map { shouldInflate -> if (shouldInflate) { DEFAULT_NUM_LEVELS + 1 @@ -448,9 +450,6 @@ class MobileConnectionRepositoryImpl( .flowOn(bgDispatcher) .stateIn(scope, SharingStarted.WhileSubscribed(), false) - override val satelliteConnectionHysteresisSeconds: StateFlow<Int> = - systemUiCarrierConfig.satelliteConnectionHysteresisSeconds - class Factory @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index a455db2e67ce..5d91ef323ead 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -26,17 +26,20 @@ import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager +import android.util.IndentingPrintWriter import androidx.annotation.VisibleForTesting import com.android.internal.telephony.PhoneConstants import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.Dumpable import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dump.DumpManager import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.res.R @@ -52,6 +55,8 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.android.systemui.util.kotlin.pairwise +import java.io.PrintWriter +import java.lang.ref.WeakReference import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -97,8 +102,12 @@ constructor( wifiRepository: WifiRepository, private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, -) : MobileConnectionsRepository { - private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> = + private val dumpManager: DumpManager, +) : MobileConnectionsRepository, Dumpable { + + // TODO(b/333912012): for now, we are never invalidating the cache. We can do better though + private var subIdRepositoryCache: + MutableMap<Int, WeakReference<FullMobileConnectionRepository>> = mutableMapOf() private val defaultNetworkName = @@ -109,6 +118,10 @@ constructor( private val networkNameSeparator: String = context.getString(R.string.status_bar_network_name_separator) + init { + dumpManager.registerNormalDumpable("MobileConnectionsRepository", this) + } + private val carrierMergedSubId: StateFlow<Int?> = combine( wifiRepository.wifiNetwork, @@ -283,8 +296,10 @@ constructor( getOrCreateRepoForSubId(subId) private fun getOrCreateRepoForSubId(subId: Int) = - subIdRepositoryCache[subId] - ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } + subIdRepositoryCache[subId]?.get() + ?: createRepositoryForSubId(subId).also { + subIdRepositoryCache[subId] = WeakReference(it) + } override val mobileIsDefault: StateFlow<Boolean> = connectivityRepository.defaultConnections @@ -374,9 +389,8 @@ constructor( } private fun updateRepos(newInfos: List<SubscriptionModel>) { - dropUnusedReposFromCache(newInfos) subIdRepositoryCache.forEach { (subId, repo) -> - repo.setIsCarrierMerged(isCarrierMerged(subId)) + repo.get()?.setIsCarrierMerged(isCarrierMerged(subId)) } } @@ -384,13 +398,6 @@ constructor( return subId == carrierMergedSubId.value } - private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) { - // Remove any connection repository from the cache that isn't in the new set of IDs. They - // will get garbage collected once their subscribers go away - subIdRepositoryCache = - subIdRepositoryCache.filter { checkSub(it.key, newInfos) }.toMutableMap() - } - /** * True if the checked subId is in the list of current subs or the active mobile data subId * @@ -422,6 +429,22 @@ constructor( profileClass = profileClass, ) + override fun dump(pw: PrintWriter, args: Array<String>) { + val ipw = IndentingPrintWriter(pw, " ") + ipw.println("Connection cache:") + + ipw.increaseIndent() + subIdRepositoryCache.entries.forEach { (subId, repo) -> + ipw.println("$subId: ${repo.get()}") + } + ipw.decreaseIndent() + + ipw.println("Connections (${subIdRepositoryCache.size} total):") + ipw.increaseIndent() + subIdRepositoryCache.values.forEach { it.get()?.dump(ipw) } + ipw.decreaseIndent() + } + companion object { private const val LOGGING_PREFIX = "Repo" } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 9d194cfca350..ed9e4056535f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -35,25 +35,18 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIc import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel -import com.android.systemui.util.kotlin.pairwiseBy -import kotlin.time.DurationUnit -import kotlin.time.toDuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch interface MobileIconInteractor { /** The table log created for this connection */ @@ -269,43 +262,6 @@ class MobileIconInteractorImpl( MutableStateFlow(false).asStateFlow() } - private val hysteresisActive = MutableStateFlow(false) - - private val isNonTerrestrialWithHysteresis: StateFlow<Boolean> = - combine(isNonTerrestrial, hysteresisActive) { isNonTerrestrial, hysteresisActive -> - if (hysteresisActive) { - true - } else { - isNonTerrestrial - } - } - .logDiffsForTable( - tableLogBuffer = tableLogBuffer, - columnName = "isNonTerrestrialWithHysteresis", - columnPrefix = "", - initialValue = Flags.carrierEnabledSatelliteFlag(), - ) - .stateIn(scope, SharingStarted.Eagerly, Flags.carrierEnabledSatelliteFlag()) - - private val lostSatelliteConnection = - isNonTerrestrial.pairwiseBy { old, new -> hysteresisActive.value = old && !new } - - init { - scope.launch { lostSatelliteConnection.collect() } - scope.launch { - hysteresisActive.collectLatest { - if (it) { - delay( - connectionRepository.satelliteConnectionHysteresisSeconds.value.toDuration( - DurationUnit.SECONDS - ) - ) - hysteresisActive.value = false - } - } - } - } - override val isRoaming: StateFlow<Boolean> = combine( connectionRepository.carrierNetworkChangeActive, @@ -367,8 +323,11 @@ class MobileIconInteractorImpl( combine( level, isInService, - ) { level, isInService -> - if (isInService) level else 0 + connectionRepository.inflateSignalStrength, + ) { level, isInService, inflate -> + if (isInService) { + if (inflate) level + 1 else level + } else 0 } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) @@ -404,7 +363,7 @@ class MobileIconInteractorImpl( showExclamationMark.value, carrierNetworkChangeActive.value, ) - isNonTerrestrialWithHysteresis + isNonTerrestrial .flatMapLatest { ntn -> if (ntn) { satelliteIcon diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index eda5c44b5c2f..103b0e3a6f27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel -import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH +import com.android.settingslib.AccessibilityContentDescriptions import com.android.systemui.Flags.statusBarStaticInoutIndicators import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon @@ -50,7 +50,7 @@ interface MobileIconViewModelCommon { /** True if this view should be visible at all. */ val isVisible: StateFlow<Boolean> val icon: Flow<SignalIconModel> - val contentDescription: Flow<ContentDescription> + val contentDescription: Flow<ContentDescription?> val roaming: Flow<Boolean> /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ val networkTypeIcon: Flow<Icon.Resource?> @@ -123,7 +123,7 @@ class MobileIconViewModel( override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon } - override val contentDescription: Flow<ContentDescription> = + override val contentDescription: Flow<ContentDescription?> = vmProvider.flatMapLatest { it.contentDescription } override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming } @@ -206,12 +206,26 @@ private class CellularIconViewModel( override val icon: Flow<SignalIconModel> = iconInteractor.signalLevelIcon - override val contentDescription: Flow<ContentDescription> = run { - val initial = ContentDescription.Resource(PHONE_SIGNAL_STRENGTH[0]) + override val contentDescription: Flow<ContentDescription?> = iconInteractor.signalLevelIcon - .map { ContentDescription.Resource(PHONE_SIGNAL_STRENGTH[it.level]) } - .stateIn(scope, SharingStarted.WhileSubscribed(), initial) - } + .map { + // We expect the signal icon to be cellular here since this is the cellular vm + if (it !is SignalIconModel.Cellular) { + null + } else { + val resId = + AccessibilityContentDescriptions.getDescriptionForLevel( + it.level, + it.numberOfLevels + ) + if (resId != 0) { + ContentDescription.Resource(resId) + } else { + null + } + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) private val showNetworkTypeIcon: Flow<Boolean> = combine( diff --git a/packages/SystemUI/src/com/android/systemui/volume/Util.java b/packages/SystemUI/src/com/android/systemui/volume/Util.java index 7edb5a55a9ae..df19013ce2c6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/Util.java +++ b/packages/SystemUI/src/com/android/systemui/volume/Util.java @@ -17,6 +17,7 @@ package com.android.systemui.volume; import android.media.AudioManager; +import android.util.MathUtils; import android.view.View; /** @@ -46,4 +47,27 @@ class Util extends com.android.settingslib.volume.Util { if (v == null || (v.getVisibility() == View.VISIBLE) == vis) return; v.setVisibility(vis ? View.VISIBLE : View.GONE); } + + /** + * Translates a value from one range to another. + * + * ``` + * Given: currentValue=3, currentRange=[0, 8], targetRange=[0, 100] + * Result: 37.5 + * ``` + */ + public static float translateToRange(float value, + float valueRangeStart, + float valueRangeEnd, + float targetRangeStart, + float targetRangeEnd) { + float currentRangeLength = valueRangeEnd - valueRangeStart; + float targetRangeLength = targetRangeEnd - targetRangeStart; + if (currentRangeLength == 0f || targetRangeLength == 0f) { + return targetRangeStart; + } + float valueFraction = (value - valueRangeStart) / currentRangeLength; + return MathUtils.constrain(targetRangeStart + valueFraction * targetRangeLength, + targetRangeStart, targetRangeEnd); + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 1688b0b51b41..22455417b647 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -137,13 +137,13 @@ import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; import com.android.systemui.volume.ui.navigation.VolumeNavigator; -import dagger.Lazy; - import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import dagger.Lazy; + /** * Visual presentation of the volume dialog. * @@ -166,6 +166,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private static final int DRAWER_ANIMATION_DURATION_SHORT = 175; private static final int DRAWER_ANIMATION_DURATION = 250; + private static final int DISPLAY_RANGE_MULTIPLIER = 100; /** Shows volume dialog show animation. */ private static final String TYPE_SHOW = "show"; @@ -826,12 +827,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, writer.print(" mSilentMode: "); writer.println(mSilentMode); } - private static int getImpliedLevel(SeekBar seekBar, int progress) { - final int m = seekBar.getMax(); - final int n = m / 100 - 1; - final int level = progress == 0 ? 0 - : progress == m ? (m / 100) : (1 + (int) ((progress / (float) m) * n)); - return level; + private static int getVolumeFromProgress(StreamState state, SeekBar seekBar, int progress) { + return (int) Util.translateToRange(progress, seekBar.getMin(), seekBar.getMax(), + state.levelMin, state.levelMax); + } + + private static int getProgressFromVolume(StreamState state, SeekBar seekBar, int volume) { + return (int) Util.translateToRange(volume, state.levelMin, state.levelMax, seekBar.getMin(), + seekBar.getMax()); } @SuppressLint("InflateParams") @@ -854,6 +857,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, addSliderHapticsToRow(row); row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); row.number = row.view.findViewById(R.id.volume_number); + row.slider.setAccessibilityDelegate( + new VolumeDialogSeekBarAccessibilityDelegate(DISPLAY_RANGE_MULTIPLIER)); row.anim = null; @@ -1916,12 +1921,12 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, : false; // update slider max - final int max = ss.levelMax * 100; + final int max = ss.levelMax * DISPLAY_RANGE_MULTIPLIER; if (max != row.slider.getMax()) { row.slider.setMax(max); } // update slider min - final int min = ss.levelMin * 100; + final int min = ss.levelMin * DISPLAY_RANGE_MULTIPLIER; if (min != row.slider.getMin()) { row.slider.setMin(min); } @@ -2069,7 +2074,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, return; // don't update if user is sliding } final int progress = row.slider.getProgress(); - final int level = getImpliedLevel(row.slider, progress); + final int level = getVolumeFromProgress(row.ss, row.slider, progress); final boolean rowVisible = row.view.getVisibility() == VISIBLE; final boolean inGracePeriod = (SystemClock.uptimeMillis() - row.userAttempt) < USER_ATTEMPT_GRACE_PERIOD; @@ -2085,7 +2090,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, return; // don't clamp if visible } } - final int newProgress = vlevel * 100; + final int newProgress = getProgressFromVolume(row.ss, row.slider, vlevel); if (progress != newProgress) { if (mShowing && rowVisible) { // animate! @@ -2530,13 +2535,13 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, + " onProgressChanged " + progress + " fromUser=" + fromUser); if (!fromUser) return; if (mRow.ss.levelMin > 0) { - final int minProgress = mRow.ss.levelMin * 100; + final int minProgress = getProgressFromVolume(mRow.ss, seekBar, mRow.ss.levelMin); if (progress < minProgress) { seekBar.setProgress(minProgress); progress = minProgress; } } - final int userLevel = getImpliedLevel(seekBar, progress); + final int userLevel = getVolumeFromProgress(mRow.ss, seekBar, progress); if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) { mRow.userAttempt = SystemClock.uptimeMillis(); if (mRow.requestedLevel != userLevel) { @@ -2569,7 +2574,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, } mRow.tracking = false; mRow.userAttempt = SystemClock.uptimeMillis(); - final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); + final int userLevel = getVolumeFromProgress(mRow.ss, seekBar, seekBar.getProgress()); Events.writeEvent(Events.EVENT_TOUCH_LEVEL_DONE, mRow.stream, userLevel); if (mRow.ss.level != userLevel) { mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow), diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogSeekBarAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogSeekBarAccessibilityDelegate.kt new file mode 100644 index 000000000000..cd31a9531db6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogSeekBarAccessibilityDelegate.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume + +import android.os.Bundle +import android.view.View +import android.view.View.AccessibilityDelegate +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.SeekBar +import com.android.internal.R + +class VolumeDialogSeekBarAccessibilityDelegate( + private val accessibilityStep: Int, +) : AccessibilityDelegate() { + + override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { + require(host is SeekBar) { "This class only works with the SeekBar" } + val seekBar: SeekBar = host + if ( + action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD || + action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD + ) { + var increment = accessibilityStep + if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + increment = -increment + } + + return super.performAccessibilityAction( + host, + R.id.accessibilityActionSetProgress, + Bundle().apply { + putFloat( + AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE, + (seekBar.progress + increment).coerceIn(seekBar.min, seekBar.max).toFloat(), + ) + }, + ) + } + return super.performAccessibilityAction(host, action, args) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioButtonViewModel.kt index 9f9275baf4f9..e5c5a655c73e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioButtonViewModel.kt @@ -16,13 +16,10 @@ package com.android.systemui.volume.panel.component.spatial.ui.viewmodel -import com.android.systemui.common.shared.model.Color import com.android.systemui.volume.panel.component.button.ui.viewmodel.ToggleButtonViewModel import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioEnabledModel data class SpatialAudioButtonViewModel( val model: SpatialAudioEnabledModel, val button: ToggleButtonViewModel, - val iconColor: Color, - val labelColor: Color, ) 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 4ecdd46163f9..b5e9ed27d664 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 @@ -18,7 +18,6 @@ package com.android.systemui.volume.panel.component.spatial.ui.viewmodel import android.content.Context import com.android.internal.logging.UiEventLogger -import com.android.systemui.common.shared.model.Color import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R @@ -79,26 +78,7 @@ constructor( val isChecked = isEnabled == currentIsEnabled val buttonViewModel: ToggleButtonViewModel = isEnabled.toViewModel(isChecked) - SpatialAudioButtonViewModel( - button = buttonViewModel, - model = isEnabled, - iconColor = - Color.Attribute( - if (isChecked) { - com.android.internal.R.attr.materialColorOnPrimaryContainer - } else { - com.android.internal.R.attr.materialColorOnSurfaceVariant - } - ), - labelColor = - Color.Attribute( - if (isChecked) { - com.android.internal.R.attr.materialColorOnSurface - } else { - com.android.internal.R.attr.materialColorOnSurfaceVariant - } - ), - ) + SpatialAudioButtonViewModel(button = buttonViewModel, model = isEnabled) } } .stateIn(scope, SharingStarted.Eagerly, emptyList()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index e0764205c85a..44207a0c434b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -477,9 +477,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { }); // Verify the method is called in - // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate} once and - // {@link Animator.AnimatorListener#onAnimationEnd} once in {@link ValueAnimator#end()} - verify(mSpyController, times(2)).updateWindowMagnificationInternal( + // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate} once + verify(mSpyController).updateWindowMagnificationInternal( mScaleCaptor.capture(), mCenterXCaptor.capture(), mCenterYCaptor.capture(), mOffsetXCaptor.capture(), mOffsetYCaptor.capture()); @@ -594,10 +593,10 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { final float expectedY = (int) (windowBounds.exactCenterY() + expectedOffset - defaultMagnificationWindowSize / 2); - // This is called 5 times when (1) first creating WindowlessMirrorWindow (2) SurfaceView is + // This is called 4 times when (1) first creating WindowlessMirrorWindow (2) SurfaceView is // created and we place the mirrored content as a child of the SurfaceView - // (3) the animation starts (4) the animation updates (5) the animation ends - verify(mTransaction, times(5)) + // (3) the animation starts (4) the animation updates + verify(mTransaction, times(4)) .setPosition(any(SurfaceControl.class), eq(expectedX), eq(expectedY)); } @@ -788,9 +787,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { waitForIdleSync(); // Verify the method is called in - // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate} once and - // {@link Animator.AnimatorListener#onAnimationEnd} once in {@link ValueAnimator#end()} - verify(mSpyController, times(2)).updateWindowMagnificationInternal( + // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate} once + verify(mSpyController).updateWindowMagnificationInternal( mScaleCaptor.capture(), mCenterXCaptor.capture(), mCenterYCaptor.capture(), mOffsetXCaptor.capture(), mOffsetYCaptor.capture()); @@ -832,10 +830,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { deleteWindowMagnificationAndWaitAnimating(mWaitAnimationDuration, mAnimationCallback2); // Verify the method is called in - // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate} once and - // {@link Animator.AnimatorListener#onAnimationEnd} once when running the animation at - // the final duration time. - verify(mSpyController, times(2)).updateWindowMagnificationInternal( + // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate} once + verify(mSpyController).updateWindowMagnificationInternal( mScaleCaptor.capture(), mCenterXCaptor.capture(), mCenterYCaptor.capture(), mOffsetXCaptor.capture(), mOffsetYCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java index a88654bdbecc..01e4d58b68c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java @@ -46,6 +46,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; @@ -459,6 +460,7 @@ public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiT final float targetCenterX = sourceBoundsCaptor.getValue().exactCenterX() + 10; final float targetCenterY = sourceBoundsCaptor.getValue().exactCenterY() + 10; + reset(mWindowMagnifierCallback); mInstrumentation.runOnMainSync(() -> { mWindowMagnificationController.moveWindowMagnifierToPosition( targetCenterX, targetCenterY, mAnimationCallback); @@ -491,6 +493,7 @@ public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiT final float centerX = sourceBoundsCaptor.getValue().exactCenterX(); final float centerY = sourceBoundsCaptor.getValue().exactCenterY(); + reset(mWindowMagnifierCallback); mInstrumentation.runOnMainSync(() -> { mWindowMagnificationController.moveWindowMagnifierToPosition( centerX + 10, centerY + 10, mAnimationCallback); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt new file mode 100644 index 000000000000..8a1a08249856 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bluetooth.qsdialog + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.settingslib.bluetooth.BluetoothUtils +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.res.R +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class AudioSharingInteractorTest : SysuiTestCase() { + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val bluetoothState = MutableStateFlow(false) + private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow() + @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice + @Mock private lateinit var localBluetoothManager: LocalBluetoothManager + @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor + @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor + @Mock private lateinit var deviceItem: DeviceItem + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var audioSharingInteractor: AudioSharingInteractor + + @Before + fun setUp() { + mockitoSession = + mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() + whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState) + whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate) + audioSharingInteractor = + AudioSharingInteractor( + localBluetoothManager, + bluetoothStateInteractor, + deviceItemInteractor, + testScope.backgroundScope, + testDispatcher, + ) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + fun testButtonStateUpdate_bluetoothOff_returnGone() { + testScope.runTest { + val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + + assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + } + } + + @Test + fun testButtonStateUpdate_noDevice_returnGone() { + testScope.runTest { + val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + bluetoothState.value = true + runCurrent() + + assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + } + } + + @Test + fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() { + testScope.runTest { + whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(true) + + val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + bluetoothState.value = true + deviceItemUpdate.emit(listOf()) + runCurrent() + + assertThat(actual) + .isEqualTo( + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button_sharing + ) + ) + } + } + + @Test + fun testButtonStateUpdate_hasSource_returnGone() { + testScope.runTest { + whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false) + whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) + whenever( + BluetoothUtils.hasConnectedBroadcastSource( + cachedBluetoothDevice, + localBluetoothManager + ) + ) + .thenReturn(true) + + val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + bluetoothState.value = true + deviceItemUpdate.emit(listOf(deviceItem)) + runCurrent() + + assertThat(actual).isEqualTo(AudioSharingButtonState.Gone) + } + } + + @Test + fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() { + testScope.runTest { + whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false) + whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) + whenever( + BluetoothUtils.hasConnectedBroadcastSource( + cachedBluetoothDevice, + localBluetoothManager + ) + ) + .thenReturn(false) + whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true) + + val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate) + bluetoothState.value = true + deviceItemUpdate.emit(listOf(deviceItem)) + runCurrent() + + assertThat(actual) + .isEqualTo( + AudioSharingButtonState.Visible( + R.string.quick_settings_bluetooth_audio_sharing_button + ) + ) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt index a8f82eda51c7..6fe7d86faab8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt @@ -23,6 +23,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothAdapter import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before @@ -41,7 +42,8 @@ import org.mockito.junit.MockitoRule @TestableLooper.RunWithLooper(setAsMainLooper = true) class BluetoothStateInteractorTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() - private val testScope = TestScope() + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) private lateinit var bluetoothStateInteractor: BluetoothStateInteractor @@ -52,7 +54,12 @@ class BluetoothStateInteractorTest : SysuiTestCase() { @Before fun setUp() { bluetoothStateInteractor = - BluetoothStateInteractor(localBluetoothManager, logger, testScope.backgroundScope) + BluetoothStateInteractor( + localBluetoothManager, + logger, + testScope.backgroundScope, + testDispatcher + ) `when`(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter) } @@ -61,7 +68,7 @@ class BluetoothStateInteractorTest : SysuiTestCase() { testScope.runTest { `when`(bluetoothAdapter.isEnabled).thenReturn(true) - assertThat(bluetoothStateInteractor.isBluetoothEnabled).isTrue() + assertThat(bluetoothStateInteractor.isBluetoothEnabled()).isTrue() } } @@ -70,7 +77,7 @@ class BluetoothStateInteractorTest : SysuiTestCase() { testScope.runTest { `when`(bluetoothAdapter.isEnabled).thenReturn(false) - assertThat(bluetoothStateInteractor.isBluetoothEnabled).isFalse() + assertThat(bluetoothStateInteractor.isBluetoothEnabled()).isFalse() } } @@ -79,7 +86,7 @@ class BluetoothStateInteractorTest : SysuiTestCase() { testScope.runTest { `when`(bluetoothAdapter.isEnabled).thenReturn(false) - bluetoothStateInteractor.isBluetoothEnabled = true + bluetoothStateInteractor.setBluetoothEnabled(true) verify(bluetoothAdapter).enable() verify(logger) .logBluetoothState(BluetoothStateStage.BLUETOOTH_STATE_VALUE_SET, true.toString()) @@ -91,7 +98,7 @@ class BluetoothStateInteractorTest : SysuiTestCase() { testScope.runTest { `when`(bluetoothAdapter.isEnabled).thenReturn(false) - bluetoothStateInteractor.isBluetoothEnabled = false + bluetoothStateInteractor.setBluetoothEnabled(false) verify(bluetoothAdapter, never()).enable() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt index 12dfe97649d3..62c98b05cbd5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt @@ -110,7 +110,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { BluetoothTileDialogDelegate( uiProperties, CONTENT_HEIGHT, - ENABLED, bluetoothTileDialogCallback, {}, dispatcher, @@ -211,7 +210,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { BluetoothTileDialogDelegate( uiProperties, CONTENT_HEIGHT, - ENABLED, bluetoothTileDialogCallback, {}, dispatcher, @@ -267,7 +265,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { BluetoothTileDialogDelegate( BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), cachedHeight, - ENABLED, bluetoothTileDialogCallback, {}, dispatcher, @@ -291,7 +288,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { BluetoothTileDialogDelegate( BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), MATCH_PARENT, - ENABLED, bluetoothTileDialogCallback, {}, dispatcher, @@ -315,7 +311,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { BluetoothTileDialogDelegate( BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), MATCH_PARENT, - ENABLED, bluetoothTileDialogCallback, {}, dispatcher, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt index 6d99c5b62e9b..b05d9591d8a8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt @@ -52,7 +52,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.anyBoolean @@ -74,7 +73,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor - @Mock private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor + @Mock private lateinit var audioSharingInteractor: AudioSharingInteractor @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor @@ -92,6 +91,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var localBluetoothManager: LocalBluetoothManager + @Mock private lateinit var bluetoothTileDialogLogger: BluetoothTileDialogLogger + @Mock private lateinit var mBluetoothTileDialogDelegateDelegateFactory: BluetoothTileDialogDelegate.Factory @@ -115,7 +116,12 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { bluetoothTileDialogViewModel = BluetoothTileDialogViewModel( deviceItemInteractor, - bluetoothStateInteractor, + BluetoothStateInteractor( + localBluetoothManager, + bluetoothTileDialogLogger, + testScope.backgroundScope, + dispatcher + ), // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor. BluetoothAutoOnInteractor( BluetoothAutoOnRepository( @@ -125,6 +131,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { dispatcher ) ), + audioSharingInteractor, mDialogTransitionAnimator, activityStarter, uiEventLogger, @@ -135,20 +142,9 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { mBluetoothTileDialogDelegateDelegateFactory ) whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow()) - whenever(bluetoothStateInteractor.bluetoothStateUpdate) - .thenReturn(MutableStateFlow(null).asStateFlow()) whenever(deviceItemInteractor.deviceItemUpdateRequest) .thenReturn(MutableStateFlow(Unit).asStateFlow()) - whenever(bluetoothStateInteractor.isBluetoothEnabled).thenReturn(true) - whenever( - mBluetoothTileDialogDelegateDelegateFactory.create( - any(), - anyInt(), - ArgumentMatchers.anyBoolean(), - any(), - any() - ) - ) + whenever(mBluetoothTileDialogDelegateDelegateFactory.create(any(), anyInt(), any(), any())) .thenReturn(bluetoothTileDialogDelegate) whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog) whenever(sysuiDialog.context).thenReturn(mContext) @@ -159,6 +155,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0)) whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle) .thenReturn(getMutableStateFlow(false)) + whenever(audioSharingInteractor.audioSharingButtonStateUpdate) + .thenReturn(getMutableStateFlow(AudioSharingButtonState.Gone)) } @Test @@ -201,15 +199,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { } @Test - fun testShowDialog_withBluetoothStateValue() { - testScope.runTest { - bluetoothTileDialogViewModel.showDialog(null) - - verify(bluetoothStateInteractor).bluetoothStateUpdate - } - } - - @Test fun testStartSettingsActivity_activityLaunched_dialogDismissed() { testScope.runTest { whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt index eb735cbfec47..daf4a3cbb9de 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * 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. @@ -281,7 +281,7 @@ class DeviceItemInteractorTest : SysuiTestCase() { override fun isFilterMatched( context: Context, cachedDevice: CachedBluetoothDevice, - audioManager: AudioManager? + audioManager: AudioManager ) = isFilterMatchFunc(cachedDevice) override fun create(context: Context, cachedDevice: CachedBluetoothDevice) = deviceItem diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/InputSessionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/InputSessionTest.java new file mode 100644 index 000000000000..8685384bb243 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/InputSessionTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.touch; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.EnableFlags; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Choreographer; +import android.view.GestureDetector; +import android.view.InputEvent; +import android.view.MotionEvent; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.Flags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.shared.system.InputMonitorCompat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +/** + * A test suite for exercising {@link InputSession}. + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper() +public class InputSessionTest extends SysuiTestCase { + @Mock + InputMonitorCompat mInputMonitor; + + @Mock + GestureDetector mGestureDetector; + + @Mock + InputChannelCompat.InputEventListener mInputEventListener; + + TestableLooper mLooper; + + @Mock + Choreographer mChoreographer; + + @Mock + InputChannelCompat.InputEventReceiver mInputEventReceiver; + + InputSession mSession; + + InputChannelCompat.InputEventListener mEventListener; + + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mLooper = TestableLooper.get(this); + } + + private void createSession(boolean pilfer) { + when(mInputMonitor.getInputReceiver(any(), any(), any())) + .thenReturn(mInputEventReceiver); + mSession = new InputSession(mInputMonitor, mGestureDetector, + mInputEventListener, mChoreographer, mLooper.getLooper(), pilfer); + final ArgumentCaptor<InputChannelCompat.InputEventListener> listenerCaptor = + ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); + verify(mInputMonitor).getInputReceiver(any(), any(), listenerCaptor.capture()); + mEventListener = listenerCaptor.getValue(); + } + + /** + * Ensures consumed motion events are pilfered when option is set. + */ + @Test + public void testPilferOnMotionEventGestureConsume() { + createSession(true); + final MotionEvent event = Mockito.mock(MotionEvent.class); + when(mGestureDetector.onTouchEvent(event)).thenReturn(true); + mEventListener.onInputEvent(event); + verify(mInputEventListener).onInputEvent(eq(event)); + verify(mInputMonitor).pilferPointers(); + } + + /** + * Ensures consumed motion events are not pilfered when option is not set. + */ + @Test + public void testNoPilferOnMotionEventGestureConsume() { + createSession(false); + final MotionEvent event = Mockito.mock(MotionEvent.class); + when(mGestureDetector.onTouchEvent(event)).thenReturn(true); + mEventListener.onInputEvent(event); + verify(mInputEventListener).onInputEvent(eq(event)); + verify(mInputMonitor, never()).pilferPointers(); + } + + /** + * Ensures input events are never pilfered. + */ + @Test + public void testNoPilferOnInputEvent() { + createSession(true); + final InputEvent event = Mockito.mock(InputEvent.class); + mEventListener.onInputEvent(event); + verify(mInputEventListener).onInputEvent(eq(event)); + verify(mInputMonitor, never()).pilferPointers(); + } + + @Test + @EnableFlags(Flags.FLAG_DREAM_INPUT_SESSION_PILFER_ONCE) + public void testPilferOnce() { + createSession(true); + final MotionEvent event = Mockito.mock(MotionEvent.class); + when(mGestureDetector.onTouchEvent(event)).thenReturn(true); + mEventListener.onInputEvent(event); + mEventListener.onInputEvent(event); + verify(mInputEventListener, times(2)).onInputEvent(eq(event)); + verify(mInputMonitor, times(1)).pilferPointers(); + } + + /** + * Ensures components are properly disposed. + */ + @Test + public void testDispose() { + createSession(true); + mSession.dispose(); + verify(mInputMonitor).dispose(); + verify(mInputEventReceiver).dispose(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 50f81ff13825..e9ec3236a06c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -131,6 +131,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { private SelectedUserInteractor mSelectedUserInteractor; @Mock private BiometricUnlockInteractor mBiometricUnlockInteractor; + @Mock + private KeyguardTransitionInteractor mKeyguardTransitionInteractor; private final FakeSystemClock mSystemClock = new FakeSystemClock(); private BiometricUnlockController mBiometricUnlockController; @@ -167,7 +169,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { () -> mSelectedUserInteractor, mBiometricUnlockInteractor, mock(JavaAdapter.class), - mock(KeyguardTransitionInteractor.class) + mKeyguardTransitionInteractor ); biometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); biometricUnlockController.addListener(mBiometricUnlockEventsListener); @@ -374,6 +376,24 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void onBiometricAuthenticated_whenFaceOnAlternateBouncer_dismissBouncer() { + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + when(mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()).thenReturn(false); + when(mKeyguardTransitionInteractor.getCurrentState()) + .thenReturn(KeyguardState.ALTERNATE_BOUNCER); + // the value of isStrongBiometric doesn't matter here since we only care about the returned + // value of isUnlockingWithBiometricAllowed() + mBiometricUnlockController.onBiometricAuthenticated(UserHandle.USER_CURRENT, + BiometricSourceType.FACE, true /* isStrongBiometric */); + + verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false)); + assertThat(mBiometricUnlockController.getMode()) + .isEqualTo(BiometricUnlockController.MODE_DISMISS_BOUNCER); + assertThat(mBiometricUnlockController.getBiometricType()) + .isEqualTo(BiometricSourceType.FACE); + } + + @Test public void onBiometricAuthenticated_whenBypassOnBouncer_dismissBouncer() { reset(mKeyguardBypassController); when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 9b6940e14415..598b12ccdc38 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -137,6 +137,7 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { wifiRepository, mock(), mock(), + mock(), ) demoRepo = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt index c13e830afac7..3c13906dbd43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod import android.net.ConnectivityManager +import android.os.PersistableBundle import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET @@ -99,6 +100,9 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { ) ) + // Use a real config, with no overrides + private val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_ID, PersistableBundle()) + private lateinit var mobileRepo: FakeMobileConnectionRepository private lateinit var carrierMergedRepo: FakeMobileConnectionRepository @@ -680,10 +684,6 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { telephonyManager: TelephonyManager, ): MobileConnectionRepositoryImpl { whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID) - val systemUiCarrierConfigMock: SystemUiCarrierConfig = mock() - whenever(systemUiCarrierConfigMock.satelliteConnectionHysteresisSeconds) - .thenReturn(MutableStateFlow(0)) - val realRepo = MobileConnectionRepositoryImpl( SUB_ID, @@ -693,7 +693,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { SEP, connectivityManager, telephonyManager, - systemUiCarrierConfig = systemUiCarrierConfigMock, + systemUiCarrierConfig = systemUiCarrierConfig, fakeBroadcastDispatcher, mobileMappingsProxy = mock(), testDispatcher, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index f761bcfe63d6..9d1411625a8f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -1030,6 +1030,26 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test + fun inflateSignalStrength_usesCarrierConfig() = + testScope.runTest { + val latest by collectLastValue(underTest.inflateSignalStrength) + + assertThat(latest).isEqualTo(false) + + systemUiCarrierConfig.processNewCarrierConfig( + configWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true) + ) + + assertThat(latest).isEqualTo(true) + + systemUiCarrierConfig.processNewCarrierConfig( + configWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false) + ) + + assertThat(latest).isEqualTo(false) + } + + @Test fun isAllowedDuringAirplaneMode_alwaysFalse() = testScope.runTest { val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 07abd275d1ce..b7a3b300a460 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -80,6 +80,7 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString @@ -229,6 +230,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { wifiRepository, fullConnectionFactory, updateMonitor, + mock(), ) testScope.runCurrent() @@ -529,6 +531,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + @Ignore("b/333912012") fun testConnectionCache_clearsInvalidSubscriptions() = testScope.runTest { collectLastValue(underTest.subscriptions) @@ -553,6 +556,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + @Ignore("b/333912012") fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = testScope.runTest { collectLastValue(underTest.subscriptions) @@ -581,6 +585,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { /** Regression test for b/261706421 */ @Test + @Ignore("b/333912012") fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = testScope.runTest { collectLastValue(underTest.subscriptions) @@ -604,6 +609,54 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun testConnectionsCache_keepsReposCached() = + testScope.runTest { + // Collect subscriptions to start the job + collectLastValue(underTest.subscriptions) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1_1 = underTest.getRepoForSubId(SUB_1_ID) + + // All subscriptions disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) + getSubscriptionCallback().onSubscriptionsChanged() + + // Sub1 comes back + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1_2 = underTest.getRepoForSubId(SUB_1_ID) + + assertThat(repo1_1).isSameInstanceAs(repo1_2) + } + + @Test + fun testConnectionsCache_doesNotDropReferencesThatHaveBeenRealized() = + testScope.runTest { + // Collect subscriptions to start the job + collectLastValue(underTest.subscriptions) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Client grabs a reference to a repository, but doesn't keep it around + underTest.getRepoForSubId(SUB_1_ID) + + // All subscriptions disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + + assertThat(repo1).isNotNull() + } + + @Test fun testConnectionRepository_invalidSubId_doesNotThrow() = testScope.runTest { underTest.getRepoForSubId(SUB_1_ID) @@ -1063,7 +1116,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { airplaneModeRepository, wifiRepository, fullConnectionFactory, - updateMonitor + updateMonitor, + mock(), ) val latest by collectLastValue(underTest.defaultDataSubRatConfig) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index c49fcf88ecaa..dfe80233918a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -43,15 +43,12 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlin.time.Duration.Companion.seconds -import kotlin.time.DurationUnit import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -181,6 +178,22 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test + fun inflateSignalStrength_arbitrarilyAddsOneToTheReportedLevel() = + testScope.runTest { + connectionRepository.inflateSignalStrength.value = false + val latest by collectLastValue(underTest.signalLevelIcon) + + connectionRepository.primaryLevel.value = 4 + assertThat(latest!!.level).isEqualTo(4) + + connectionRepository.inflateSignalStrength.value = true + connectionRepository.primaryLevel.value = 4 + + // when INFLATE_SIGNAL_STRENGTH is true, we add 1 to the reported signal level + assertThat(latest!!.level).isEqualTo(5) + } + + @Test fun iconGroup_three_g() = testScope.runTest { connectionRepository.resolvedNetworkType.value = @@ -678,32 +691,6 @@ class MobileIconInteractorTest : SysuiTestCase() { assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java) } - @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) - @Test - fun satBasedIcon_hasHysteresisWhenDisabled() = - testScope.runTest { - val latest by collectLastValue(underTest.signalLevelIcon) - - val hysteresisDuration = 5.seconds - connectionRepository.satelliteConnectionHysteresisSeconds.value = - hysteresisDuration.toInt(DurationUnit.SECONDS) - - connectionRepository.isNonTerrestrial.value = true - - assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java) - - // Disable satellite - connectionRepository.isNonTerrestrial.value = false - - // Satellite icon should still be visible - assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java) - - // Wait for the icon to change - advanceTimeBy(hysteresisDuration) - - assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java) - } - private fun createInteractor( overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() ) = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 83d0fe8f9c4b..cec41557f344 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupReposi import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn @@ -279,6 +280,76 @@ class MobileIconViewModelTest : SysuiTestCase() { } @Test + fun contentDescription_nonInflated_invalidLevelIsNull() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = false + repository.setAllLevels(-1) + assertThat(latest).isNull() + + repository.setAllLevels(100) + assertThat(latest).isNull() + } + + @Test + fun contentDescription_inflated_invalidLevelIsNull() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = true + repository.numberOfLevels.value = 6 + repository.setAllLevels(-2) + assertThat(latest).isNull() + + repository.setAllLevels(100) + assertThat(latest).isNull() + } + + @Test + fun contentDescription_nonInflated_testABunchOfLevelsForNull() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + + repository.inflateSignalStrength.value = false + repository.numberOfLevels.value = 5 + + // -1 and 5 are out of the bounds for non-inflated content descriptions + for (i in -1..5) { + repository.setAllLevels(i) + when (i) { + -1, + 5 -> assertWithMessage("Level $i is expected to be null").that(latest).isNull() + else -> + assertWithMessage("Level $i is expected not to be null") + .that(latest) + .isNotNull() + } + } + } + + @Test + fun contentDescription_inflated_testABunchOfLevelsForNull() = + testScope.runTest { + val latest by collectLastValue(underTest.contentDescription) + repository.inflateSignalStrength.value = true + repository.numberOfLevels.value = 6 + // -1 and 6 are out of the bounds for inflated content descriptions + // Note that the interactor adds 1 to the reported level, hence the -2 to 5 range + for (i in -2..5) { + repository.setAllLevels(i) + when (i) { + -2, + 5 -> assertWithMessage("Level $i is expected to be null").that(latest).isNull() + else -> + assertWithMessage("Level $i is not expected to be null") + .that(latest) + .isNotNull() + } + } + } + + @Test fun networkType_dataEnabled_groupIsRepresented() = testScope.runTest { val expected = diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/UtilTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/UtilTest.java index fb82b8fed76c..483dc0c9c974 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/UtilTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/UtilTest.java @@ -15,14 +15,14 @@ */ package com.android.systemui.volume; +import static com.google.common.truth.Truth.assertThat; + import android.media.MediaMetadata; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import junit.framework.Assert; - import org.junit.Test; @SmallTest @@ -30,11 +30,59 @@ public class UtilTest extends SysuiTestCase { @Test public void testMediaMetadataToString_null() { - Assert.assertEquals(null, Util.mediaMetadataToString(null)); + assertThat(Util.mediaMetadataToString(null)).isNull(); } @Test public void testMediaMetadataToString_notNull() { - Assert.assertNotNull(Util.mediaMetadataToString(new MediaMetadata.Builder().build())); + assertThat(Util.mediaMetadataToString(new MediaMetadata.Builder().build())).isNotNull(); + } + + @Test + public void translateToRange_translatesStartToStart() { + assertThat( + (int) Util.translateToRange( + /* value= */ 0, + /* valueRangeStart= */ 0, + /* valueRangeEnd= */ 7, + /* targetRangeStart= */ 0, + /* targetRangeEnd= */700) + ).isEqualTo(0); + } + + @Test + public void translateToRange_translatesValueToValue() { + assertThat( + (int) Util.translateToRange( + /* value= */ 4, + /* valueRangeStart= */ 0, + /* valueRangeEnd= */ 7, + /* targetRangeStart= */ 0, + /* targetRangeEnd= */700) + ).isEqualTo(400); + } + + @Test + public void translateToRange_translatesEndToEnd() { + assertThat( + (int) Util.translateToRange( + /* value= */ 7, + /* valueRangeStart= */ 0, + /* valueRangeEnd= */ 7, + /* targetRangeStart= */ 0, + /* targetRangeEnd= */700) + ).isEqualTo(700); + } + + @Test + public void translateToRange_returnsStartForEmptyRange() { + assertThat( + (int) Util.translateToRange( + /* value= */ 7, + /* valueRangeStart= */ 7, + /* valueRangeEnd= */ 7, + /* targetRangeStart= */ 700, + /* targetRangeEnd= */700) + ).isEqualTo(700); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index ed2fb2c0cfc9..3b468aa011ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -248,6 +248,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState(); ss.name = STREAMS.get(i); ss.level = 1; + ss.levelMin = 0; + ss.levelMax = 25; state.states.append(i, ss); } return state; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt index a6dd3cd7d30a..219794f3ad18 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt @@ -32,6 +32,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.currentTime @@ -68,6 +70,8 @@ class FakeAuthenticationRepository( var lockoutStartedReportCount = 0 + private val credentialCheckingMutex = Mutex(locked = false) + override suspend fun getAuthenticationMethod(): AuthenticationMethodModel { return authenticationMethod.value } @@ -124,30 +128,32 @@ class FakeAuthenticationRepository( override suspend fun checkCredential( credential: LockscreenCredential ): AuthenticationResultModel { - val expectedCredential = credentialOverride ?: getExpectedCredential(securityMode) - val isSuccessful = - when { - credential.type != getCurrentCredentialType(securityMode) -> false - credential.type == LockPatternUtils.CREDENTIAL_TYPE_PIN -> - credential.isPin && credential.matches(expectedCredential) - credential.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD -> - credential.isPassword && credential.matches(expectedCredential) - credential.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN -> - credential.isPattern && credential.matches(expectedCredential) - else -> error("Unexpected credential type ${credential.type}!") + return credentialCheckingMutex.withLock { + val expectedCredential = credentialOverride ?: getExpectedCredential(securityMode) + val isSuccessful = + when { + credential.type != getCurrentCredentialType(securityMode) -> false + credential.type == LockPatternUtils.CREDENTIAL_TYPE_PIN -> + credential.isPin && credential.matches(expectedCredential) + credential.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD -> + credential.isPassword && credential.matches(expectedCredential) + credential.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN -> + credential.isPattern && credential.matches(expectedCredential) + else -> error("Unexpected credential type ${credential.type}!") + } + + val failedAttempts = _failedAuthenticationAttempts.value + if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { + AuthenticationResultModel( + isSuccessful = isSuccessful, + lockoutDurationMs = 0, + ) + } else { + AuthenticationResultModel( + isSuccessful = false, + lockoutDurationMs = LOCKOUT_DURATION_MS, + ) } - - val failedAttempts = _failedAuthenticationAttempts.value - return if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { - AuthenticationResultModel( - isSuccessful = isSuccessful, - lockoutDurationMs = 0, - ) - } else { - AuthenticationResultModel( - isSuccessful = false, - lockoutDurationMs = LOCKOUT_DURATION_MS, - ) } } @@ -155,6 +161,23 @@ class FakeAuthenticationRepository( _isPinEnhancedPrivacyEnabled.value = isEnabled } + /** + * Pauses any future credential checking. The test must call [unpauseCredentialChecking] to + * flush the accumulated credential checks. + */ + suspend fun pauseCredentialChecking() { + credentialCheckingMutex.lock() + } + + /** + * Unpauses future credential checking, if it was paused using [pauseCredentialChecking]. This + * doesn't flush any pending coroutine jobs; the test code may still choose to do that using + * `runCurrent`. + */ + fun unpauseCredentialChecking() { + credentialCheckingMutex.unlock() + } + private fun getExpectedCredential(securityMode: SecurityMode): List<Any> { return when (val credentialType = getCurrentCredentialType(securityMode)) { LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index 2d5a3612ff6a..eb2d6c0f5405 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -31,6 +31,7 @@ class FakeMobileConnectionRepository( override val tableLogBuffer: TableLogBuffer, ) : MobileConnectionRepository { override val carrierId = MutableStateFlow(UNKNOWN_CARRIER_ID) + override val inflateSignalStrength: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isEmergencyOnly = MutableStateFlow(false) override val isRoaming = MutableStateFlow(false) override val operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null) @@ -63,8 +64,6 @@ class FakeMobileConnectionRepository( override val hasPrioritizedNetworkCapabilities = MutableStateFlow(false) - override val satelliteConnectionHysteresisSeconds = MutableStateFlow(0) - private var isInEcmMode: Boolean = false override suspend fun isInEcmMode(): Boolean = isInEcmMode diff --git a/proto/src/am_capabilities.proto b/proto/src/am_capabilities.proto index d97bf816b150..fc9f7a4590bd 100644 --- a/proto/src/am_capabilities.proto +++ b/proto/src/am_capabilities.proto @@ -7,6 +7,16 @@ message Capability { string name = 1; } +message VMCapability { + string name = 1; +} + +message FrameworkCapability { + string name = 1; +} + message Capabilities { repeated Capability values = 1; + repeated VMCapability vm_capabilities = 2; + repeated FrameworkCapability framework_capabilities = 3; } diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index bfa1c7bb99d0..8ab2e0fa6379 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -56,6 +56,13 @@ flag { } flag { + name: "enable_hardware_shortcut_disables_warning" + namespace: "accessibility" + description: "When the user purposely enables the hardware shortcut, preemptively disables the first-time warning message." + bug: "287065325" +} + +flag { name: "enable_magnification_joystick" namespace: "accessibility" description: "Whether to enable joystick controls for magnification" diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index e64e500c9b65..ccf9a90b5964 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4273,6 +4273,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } if (shortcutType == UserShortcutType.HARDWARE) { skipVolumeShortcutDialogTimeoutRestriction(userId); + if (com.android.server.accessibility.Flags.enableHardwareShortcutDisablesWarning()) { + persistIntToSetting( + userId, + Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, + AccessibilityShortcutController.DialogStatus.SHOWN + ); + } } else if (shortcutType == UserShortcutType.SOFTWARE) { // Update the A11y FAB size to large when the Magnification shortcut is // enabled and the user hasn't changed the floating button size diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index 23373f1df63c..afeafa4b6373 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -302,7 +302,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController if (Flags.interceptIntentsBeforeApplyingPolicy()) { if (mIntentListenerCallback != null && intent != null && mIntentListenerCallback.shouldInterceptIntent(intent)) { - Slog.d(TAG, "Virtual device intercepting intent"); + logActivityLaunchBlocked("Virtual device intercepting intent"); return false; } if (!canContainActivity(activityInfo, windowingMode, launchingFromDisplayId, @@ -318,7 +318,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController } if (mIntentListenerCallback != null && intent != null && mIntentListenerCallback.shouldInterceptIntent(intent)) { - Slog.d(TAG, "Virtual device intercepting intent"); + logActivityLaunchBlocked("Virtual device intercepting intent"); return false; } } @@ -331,15 +331,17 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController boolean isNewTask) { // Mirror displays cannot contain activities. if (waitAndGetIsMirrorDisplay()) { - Slog.d(TAG, "Mirror virtual displays cannot contain activities."); + logActivityLaunchBlocked("Mirror virtual displays cannot contain activities."); return false; } if (!isWindowingModeSupported(windowingMode)) { - Slog.d(TAG, "Virtual device doesn't support windowing mode " + windowingMode); + logActivityLaunchBlocked( + "Virtual device doesn't support windowing mode " + windowingMode); return false; } if ((activityInfo.flags & FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES) == 0) { - Slog.d(TAG, "Virtual device requires android:canDisplayOnRemoteDevices=true"); + logActivityLaunchBlocked( + "Activity requires android:canDisplayOnRemoteDevices=true"); return false; } final UserHandle activityUser = @@ -350,11 +352,11 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController return true; } if (!activityUser.isSystem() && !mAllowedUsers.contains(activityUser)) { - Slog.d(TAG, "Virtual device launch disallowed from user " + activityUser); + logActivityLaunchBlocked("Activity launch disallowed from user " + activityUser); return false; } if (!activityMatchesDisplayCategory(activityInfo)) { - Slog.d(TAG, "The activity's required display category '" + logActivityLaunchBlocked("The activity's required display category '" + activityInfo.requiredDisplayCategory + "' not found on virtual display with the following categories: " + mDisplayCategories); @@ -363,7 +365,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController synchronized (mGenericWindowPolicyControllerLock) { if (!isAllowedByPolicy(mActivityLaunchAllowedByDefault, mActivityPolicyExemptions, activityComponent)) { - Slog.d(TAG, "Virtual device launch disallowed by policy: " + logActivityLaunchBlocked("Activity launch disallowed by policy: " + activityComponent); return false; } @@ -371,7 +373,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController if (isNewTask && launchingFromDisplayId != DEFAULT_DISPLAY && !isAllowedByPolicy(mCrossTaskNavigationAllowedByDefault, mCrossTaskNavigationExemptions, activityComponent)) { - Slog.d(TAG, "Virtual device cross task navigation disallowed by policy: " + logActivityLaunchBlocked("Cross task navigation disallowed by policy: " + activityComponent); return false; } @@ -380,12 +382,18 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController // based on FLAG_STREAM_PERMISSIONS if (mPermissionDialogComponent != null && mPermissionDialogComponent.equals(activityComponent)) { + logActivityLaunchBlocked("Permission dialog not allowed on virtual device"); return false; } return true; } + private void logActivityLaunchBlocked(String reason) { + Slog.d(TAG, "Virtual device activity launch disallowed on display " + + waitAndGetDisplayId() + ", reason: " + reason); + } + @Override @SuppressWarnings("AndroidFrameworkRequiresPermission") public boolean keepActivityOnWindowFlagsChanged(ActivityInfo activityInfo, int windowFlags, diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index 1741593ba671..ccc44a41759b 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -16,6 +16,8 @@ package com.android.server; +import static com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis; + import android.app.ActivityManager; import android.app.StatusBarManager; import android.content.BroadcastReceiver; @@ -70,12 +72,6 @@ public class GestureLauncherService extends SystemService { */ @VisibleForTesting static final long CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS = 300; - /** - * Min time in milliseconds to complete the emergency gesture for it count. If the gesture is - * completed faster than this, we assume it's not performed by human and the - * event gets ignored. - */ - @VisibleForTesting static final int EMERGENCY_GESTURE_TAP_DETECTION_MIN_TIME_MS = 200; /** * Interval in milliseconds in which the power button must be depressed in succession to be @@ -570,7 +566,8 @@ public class GestureLauncherService extends SystemService { long emergencyGestureTapDetectionMinTimeMs = Settings.Global.getInt( mContext.getContentResolver(), Settings.Global.EMERGENCY_GESTURE_TAP_DETECTION_MIN_TIME_MS, - EMERGENCY_GESTURE_TAP_DETECTION_MIN_TIME_MS); + mContext.getResources().getInteger( + config_defaultMinEmergencyGestureTapDurationMillis)); if (emergencyGestureSpentTime <= emergencyGestureTapDetectionMinTimeMs) { Slog.i(TAG, "Emergency gesture detected but it's too fast. Gesture time: " + emergencyGestureSpentTime + " ms"); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 8022eb37fce7..5ec6b7200191 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -4437,8 +4437,16 @@ public class ActivityManagerService extends IActivityManager.Stub final boolean clearPendingIntentsForStoppedApp = (android.content.pm.Flags.stayStopped() && packageStateStopped); if (packageName == null || uninstalling || clearPendingIntentsForStoppedApp) { + final int cancelReason; + if (packageName == null) { + cancelReason = PendingIntentRecord.CANCEL_REASON_USER_STOPPED; + } else if (uninstalling) { + cancelReason = PendingIntentRecord.CANCEL_REASON_OWNER_UNINSTALLED; + } else { + cancelReason = PendingIntentRecord.CANCEL_REASON_OWNER_FORCE_STOPPED; + } didSomething |= mPendingIntentController.removePendingIntentsForPackage( - packageName, userId, appId, doit); + packageName, userId, appId, doit, cancelReason); } if (doit) { @@ -10167,7 +10175,11 @@ public class ActivityManagerService extends IActivityManager.Stub } final int callingUid = Binder.getCallingUid(); - mProcessList.getAppStartInfoTracker().addStartInfoCompleteListener(listener, callingUid); + mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid, userId, true, + ALLOW_NON_FULL, "addApplicationStartInfoCompleteListener", null); + + mProcessList.getAppStartInfoTracker().addStartInfoCompleteListener(listener, + UserHandle.getUid(userId, UserHandle.getAppId(callingUid))); } @@ -10182,13 +10194,30 @@ public class ActivityManagerService extends IActivityManager.Stub } final int callingUid = Binder.getCallingUid(); - mProcessList.getAppStartInfoTracker().removeStartInfoCompleteListener(listener, callingUid, - true); + mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid, userId, true, + ALLOW_NON_FULL, "removeApplicationStartInfoCompleteListener", null); + + mProcessList.getAppStartInfoTracker().removeStartInfoCompleteListener(listener, + UserHandle.getUid(userId, UserHandle.getAppId(callingUid)), true); } @Override public void addStartInfoTimestamp(int key, long timestampNs, int userId) { enforceNotIsolatedCaller("addStartInfoTimestamp"); + + // For the simplification, we don't support USER_ALL nor USER_CURRENT here. + if (userId == UserHandle.USER_ALL || userId == UserHandle.USER_CURRENT) { + throw new IllegalArgumentException("Unsupported userId"); + } + + final int callingUid = Binder.getCallingUid(); + mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid, userId, true, + ALLOW_NON_FULL, "addStartInfoTimestamp", null); + + final String packageName = Settings.getPackageNameForUid(mContext, callingUid); + + mProcessList.getAppStartInfoTracker().addTimestampToStart(packageName, + UserHandle.getUid(userId, UserHandle.getAppId(callingUid)), timestampNs, key); } @Override diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index e70722ca6579..372ec45763bf 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -97,6 +97,7 @@ import android.opengl.GLES10; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.Debug; import android.os.IProgressListener; import android.os.ParcelFileDescriptor; import android.os.RemoteCallback; @@ -125,6 +126,8 @@ import com.android.server.LocalServices; import com.android.server.am.LowMemDetector.MemFactor; import com.android.server.am.nano.Capabilities; import com.android.server.am.nano.Capability; +import com.android.server.am.nano.FrameworkCapability; +import com.android.server.am.nano.VMCapability; import com.android.server.compat.PlatformCompat; import com.android.server.pm.UserManagerInternal; import com.android.server.utils.Slogf; @@ -443,6 +446,22 @@ final class ActivityManagerShellCommand extends ShellCommand { capabilities.values[i] = cap; } + String[] vmCapabilities = Debug.getVmFeatureList(); + capabilities.vmCapabilities = new VMCapability[vmCapabilities.length]; + for (int i = 0; i < vmCapabilities.length; i++) { + VMCapability cap = new VMCapability(); + cap.name = vmCapabilities[i]; + capabilities.vmCapabilities[i] = cap; + } + + String[] fmCapabilities = Debug.getFeatureList(); + capabilities.frameworkCapabilities = new FrameworkCapability[fmCapabilities.length]; + for (int i = 0; i < fmCapabilities.length; i++) { + FrameworkCapability cap = new FrameworkCapability(); + cap.name = fmCapabilities[i]; + capabilities.frameworkCapabilities[i] = cap; + } + try { getRawOutputStream().write(Capabilities.toByteArray(capabilities)); } catch (IOException e) { @@ -452,10 +471,16 @@ final class ActivityManagerShellCommand extends ShellCommand { } else { // Unfortunately we don't have protobuf text format capabilities here. // Fallback to line separated list instead for text parser. - pw.println("Format: 1"); + pw.println("Format: 2"); for (String capability : CAPABILITIES) { pw.println(capability); } + for (String capability : Debug.getVmFeatureList()) { + pw.println("vm:" + capability); + } + for (String capability : Debug.getFeatureList()) { + pw.println("framework:" + capability); + } } return 0; } diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java index 8b1300b641a9..ef015ee9d743 100644 --- a/services/core/java/com/android/server/am/AppRestrictionController.java +++ b/services/core/java/com/android/server/am/AppRestrictionController.java @@ -2153,9 +2153,12 @@ public final class AppRestrictionController { mRestrictionSettings.update(pkgName, uid, level, reason, subReason); } - if (!allowUpdateBucket || curBucket == STANDBY_BUCKET_EXEMPTED) { + if (!android.app.Flags.appRestrictionsApi() + && (!allowUpdateBucket || curBucket == STANDBY_BUCKET_EXEMPTED)) { return; } + + boolean doItNow = true; if (level >= RESTRICTION_LEVEL_RESTRICTED_BUCKET && curLevel < RESTRICTION_LEVEL_RESTRICTED_BUCKET) { // Moving the app standby bucket to restricted in the meanwhile. @@ -2168,7 +2171,6 @@ public final class AppRestrictionController { && (mConstantsObserver.mBgAutoRestrictedBucket || level == RESTRICTION_LEVEL_RESTRICTED_BUCKET)) { // restrict the app if it hasn't done so. - boolean doIt = true; synchronized (mSettingsLock) { final int index = mActiveUids.indexOfKey(uid, pkgName); if (index >= 0) { @@ -2182,14 +2184,16 @@ public final class AppRestrictionController { logAppBackgroundRestrictionInfo(pkgName, uid, curLevel, level, localTrackerInfo, localReason); }); - doIt = false; + doItNow = false; } } - if (doIt) { + if (doItNow) { appStandbyInternal.restrictApp(pkgName, UserHandle.getUserId(uid), reason, subReason); - logAppBackgroundRestrictionInfo(pkgName, uid, curLevel, level, trackerInfo, - reason); + if (!android.app.Flags.appRestrictionsApi()) { + logAppBackgroundRestrictionInfo(pkgName, uid, curLevel, level, trackerInfo, + reason); + } } } } else if (curLevel >= RESTRICTION_LEVEL_RESTRICTED_BUCKET @@ -2204,6 +2208,13 @@ public final class AppRestrictionController { appStandbyInternal.maybeUnrestrictApp(pkgName, UserHandle.getUserId(uid), prevReason & REASON_MAIN_MASK, prevReason & REASON_SUB_MASK, reason, subReason); + if (!android.app.Flags.appRestrictionsApi()) { + logAppBackgroundRestrictionInfo(pkgName, uid, curLevel, level, trackerInfo, + reason); + } + } + + if (doItNow && android.app.Flags.appRestrictionsApi()) { logAppBackgroundRestrictionInfo(pkgName, uid, curLevel, level, trackerInfo, reason); } diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index ddf1d5f5ab71..0728ea8e5604 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -464,7 +464,7 @@ public final class AppStartInfoTracker { addTimestampToStart(app.info.packageName, app.uid, timeNs, key); } - private void addTimestampToStart(String packageName, int uid, long timeNs, int key) { + void addTimestampToStart(String packageName, int uid, long timeNs, int key) { synchronized (mLock) { AppStartInfoContainer container = mData.get(packageName, uid); if (container == null) { diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java index fb0d6957129a..f3361203c44a 100644 --- a/services/core/java/com/android/server/am/PendingIntentController.java +++ b/services/core/java/com/android/server/am/PendingIntentController.java @@ -22,6 +22,8 @@ import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_MU; import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_MU; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_CANCELED; +import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_SUPERSEDED; import android.annotation.Nullable; import android.app.Activity; @@ -54,6 +56,7 @@ import com.android.internal.util.RingBuffer; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.AlarmManagerInternal; import com.android.server.LocalServices; +import com.android.server.am.PendingIntentRecord.CancellationReason; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.SafeActivityOptions; @@ -191,7 +194,7 @@ public class PendingIntentController { } return rec; } - makeIntentSenderCanceled(rec); + makeIntentSenderCanceled(rec, CANCEL_REASON_SUPERSEDED); mIntentSenderRecords.remove(key); decrementUidStatLocked(rec); } @@ -206,7 +209,7 @@ public class PendingIntentController { } boolean removePendingIntentsForPackage(String packageName, int userId, int appId, - boolean doIt) { + boolean doIt, @CancellationReason int cancelReason) { boolean didSomething = false; synchronized (mLock) { @@ -256,7 +259,7 @@ public class PendingIntentController { } didSomething = true; it.remove(); - makeIntentSenderCanceled(pir); + makeIntentSenderCanceled(pir, cancelReason); decrementUidStatLocked(pir); if (pir.key.activity != null) { final Message m = PooledLambda.obtainMessage( @@ -289,13 +292,14 @@ public class PendingIntentController { } catch (RemoteException e) { throw new SecurityException(e); } - cancelIntentSender(rec, true); + cancelIntentSender(rec, true, CANCEL_REASON_OWNER_CANCELED); } } - public void cancelIntentSender(PendingIntentRecord rec, boolean cleanActivity) { + public void cancelIntentSender(PendingIntentRecord rec, boolean cleanActivity, + @CancellationReason int cancelReason) { synchronized (mLock) { - makeIntentSenderCanceled(rec); + makeIntentSenderCanceled(rec, cancelReason); mIntentSenderRecords.remove(rec.key); decrementUidStatLocked(rec); if (cleanActivity && rec.key.activity != null) { @@ -359,8 +363,10 @@ public class PendingIntentController { } } - private void makeIntentSenderCanceled(PendingIntentRecord rec) { + private void makeIntentSenderCanceled(PendingIntentRecord rec, + @CancellationReason int cancelReason) { rec.canceled = true; + rec.cancelReason = cancelReason; final RemoteCallbackList<IResultReceiver> callbacks = rec.detachCancelListenersLocked(); if (callbacks != null) { final Message m = PooledLambda.obtainMessage( diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 95e130ed1194..da45a7727faf 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -16,11 +16,13 @@ package com.android.server.am; +import static android.app.ActivityManager.PROCESS_STATE_TOP; import static android.app.ActivityManager.START_SUCCESS; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; +import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.app.ActivityManager; @@ -51,11 +53,15 @@ import android.util.ArraySet; import android.util.Slog; import android.util.TimeUtils; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.modules.expresslog.Counter; import com.android.server.wm.SafeActivityOptions; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.Objects; @@ -71,12 +77,35 @@ public final class PendingIntentRecord extends IIntentSender.Stub { public static final int FLAG_BROADCAST_SENDER = 1 << 1; public static final int FLAG_SERVICE_SENDER = 1 << 2; + public static final int CANCEL_REASON_NULL = 0; + public static final int CANCEL_REASON_USER_STOPPED = 1 << 0; + public static final int CANCEL_REASON_OWNER_UNINSTALLED = 1 << 1; + public static final int CANCEL_REASON_OWNER_FORCE_STOPPED = 1 << 2; + public static final int CANCEL_REASON_OWNER_CANCELED = 1 << 3; + public static final int CANCEL_REASON_HOSTING_ACTIVITY_DESTROYED = 1 << 4; + public static final int CANCEL_REASON_SUPERSEDED = 1 << 5; + public static final int CANCEL_REASON_ONE_SHOT_SENT = 1 << 6; + + @IntDef({ + CANCEL_REASON_NULL, + CANCEL_REASON_USER_STOPPED, + CANCEL_REASON_OWNER_UNINSTALLED, + CANCEL_REASON_OWNER_FORCE_STOPPED, + CANCEL_REASON_OWNER_CANCELED, + CANCEL_REASON_HOSTING_ACTIVITY_DESTROYED, + CANCEL_REASON_SUPERSEDED, + CANCEL_REASON_ONE_SHOT_SENT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CancellationReason {} + final PendingIntentController controller; final Key key; final int uid; public final WeakReference<PendingIntentRecord> ref; boolean sent = false; boolean canceled = false; + @CancellationReason int cancelReason = CANCEL_REASON_NULL; /** * Map IBinder to duration specified as Pair<Long, Integer>, Long is allowlist duration in * milliseconds, Integer is allowlist type defined at @@ -419,12 +448,22 @@ public final class PendingIntentRecord extends IIntentSender.Stub { SafeActivityOptions mergedOptions = null; synchronized (controller.mLock) { if (canceled) { + if (cancelReason == CANCEL_REASON_OWNER_FORCE_STOPPED + && controller.mAmInternal.getUidProcessState(callingUid) + == PROCESS_STATE_TOP) { + Counter.logIncrementWithUid( + "app.value_force_stop_cancelled_pi_sent_from_top_per_caller", + callingUid); + Counter.logIncrementWithUid( + "app.value_force_stop_cancelled_pi_sent_from_top_per_owner", + uid); + } return ActivityManager.START_CANCELED; } sent = true; if ((key.flags & PendingIntent.FLAG_ONE_SHOT) != 0) { - controller.cancelIntentSender(this, true); + controller.cancelIntentSender(this, true, CANCEL_REASON_ONE_SHOT_SENT); } finalIntent = key.requestIntent != null ? new Intent(key.requestIntent) : new Intent(); @@ -687,6 +726,21 @@ public final class PendingIntentRecord extends IIntentSender.Stub { } } + @VisibleForTesting + static String cancelReasonToString(@CancellationReason int cancelReason) { + return switch (cancelReason) { + case CANCEL_REASON_NULL -> "NULL"; + case CANCEL_REASON_USER_STOPPED -> "USER_STOPPED"; + case CANCEL_REASON_OWNER_UNINSTALLED -> "OWNER_UNINSTALLED"; + case CANCEL_REASON_OWNER_FORCE_STOPPED -> "OWNER_FORCE_STOPPED"; + case CANCEL_REASON_OWNER_CANCELED -> "OWNER_CANCELED"; + case CANCEL_REASON_HOSTING_ACTIVITY_DESTROYED -> "HOSTING_ACTIVITY_DESTROYED"; + case CANCEL_REASON_SUPERSEDED -> "SUPERSEDED"; + case CANCEL_REASON_ONE_SHOT_SENT -> "ONE_SHOT_SENT"; + default -> "UNKNOWN"; + }; + } + public void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.print("uid="); pw.print(uid); pw.print(" packageName="); pw.print(key.packageName); @@ -707,7 +761,8 @@ public final class PendingIntentRecord extends IIntentSender.Stub { } if (sent || canceled) { pw.print(prefix); pw.print("sent="); pw.print(sent); - pw.print(" canceled="); pw.println(canceled); + pw.print(" canceled="); pw.print(canceled); + pw.print(" cancelReason="); pw.println(cancelReasonToString(cancelReason)); } if (mAllowlistDuration != null) { pw.print(prefix); diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index be39778372ca..83fa34490f32 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -248,6 +248,7 @@ public class AppOpsService extends IAppOpsService.Stub { Process.ROOT_UID, Process.PHONE_UID, Process.BLUETOOTH_UID, + Process.AUDIOSERVER_UID, Process.NFC_UID, Process.NETWORK_STACK_UID, Process.SHELL_UID}; diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java index 6f3526fb07e9..dbd47d00718f 100644 --- a/services/core/java/com/android/server/appop/HistoricalRegistry.java +++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java @@ -54,13 +54,13 @@ import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.AtomicDirectory; -import com.android.internal.os.BackgroundThread; import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; import com.android.internal.util.function.pooled.PooledLambda; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.FgThread; +import com.android.server.IoThread; import org.xmlpull.v1.XmlPullParserException; @@ -669,7 +669,7 @@ final class HistoricalRegistry { } private void clearHistoryOnDiskDLocked() { - BackgroundThread.getHandler().removeMessages(MSG_WRITE_PENDING_HISTORY); + IoThread.getHandler().removeMessages(MSG_WRITE_PENDING_HISTORY); synchronized (mInMemoryLock) { mCurrentHistoricalOps = null; mNextPersistDueTimeMillis = System.currentTimeMillis(); @@ -745,7 +745,7 @@ final class HistoricalRegistry { private void persistPendingHistory(@NonNull List<HistoricalOps> pendingWrites) { synchronized (mOnDiskLock) { - BackgroundThread.getHandler().removeMessages(MSG_WRITE_PENDING_HISTORY); + IoThread.getHandler().removeMessages(MSG_WRITE_PENDING_HISTORY); if (pendingWrites.isEmpty()) { return; } @@ -767,7 +767,7 @@ final class HistoricalRegistry { final Message message = PooledLambda.obtainMessage( HistoricalRegistry::persistPendingHistory, HistoricalRegistry.this); message.what = MSG_WRITE_PENDING_HISTORY; - BackgroundThread.getHandler().sendMessage(message); + IoThread.getHandler().sendMessage(message); mPendingWrites.offerFirst(ops); } diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java index c7b60da2fc51..dd6433d98553 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java @@ -19,11 +19,13 @@ package com.android.server.inputmethod; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.content.Context; import android.content.pm.UserInfo; import android.os.Handler; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.DirectBootAwareness; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; @@ -67,7 +69,7 @@ final class AdditionalSubtypeMapRepository { AdditionalSubtypeUtils.save(map, inputMethodMap, userId); } - static void initialize(@NonNull Handler handler) { + static void initialize(@NonNull Handler handler, @NonNull Context context) { final UserManagerInternal userManagerInternal = LocalServices.getService(UserManagerInternal.class); handler.post(() -> { @@ -79,8 +81,16 @@ final class AdditionalSubtypeMapRepository { handler.post(() -> { synchronized (ImfLock.class) { if (!sPerUserMap.contains(userId)) { - sPerUserMap.put(userId, - AdditionalSubtypeUtils.load(userId)); + final AdditionalSubtypeMap additionalSubtypeMap = + AdditionalSubtypeUtils.load(userId); + sPerUserMap.put(userId, additionalSubtypeMap); + final InputMethodSettings settings = + InputMethodManagerService + .queryInputMethodServicesInternal(context, + userId, + additionalSubtypeMap, + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, settings); } } }); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 03a85c40ef31..e41b47f8f00e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -869,9 +869,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. if (!mSystemReady) { return; } - mSettings = queryInputMethodServicesInternal(mContext, mSettings.getUserId(), - AdditionalSubtypeMapRepository.get(mSettings.getUserId()), - DirectBootAwareness.AUTO); + for (int userId : mUserManagerInternal.getUserIds()) { + final InputMethodSettings settings = queryInputMethodServicesInternal( + mContext, + userId, + AdditionalSubtypeMapRepository.get(userId), + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, settings); + if (userId == mSettings.getUserId()) { + mSettings = settings; + } + } postInputMethodSettingUpdatedLocked(true /* resetDefaultEnabledIme */); // If the locale is changed, needs to reset the default ime resetDefaultImeLocked(mContext); @@ -1118,12 +1126,15 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap, settings.getMethodMap()); } - - if (!isCurrentUser) { + if (isCurrentUser + && !(additionalSubtypeChanged || shouldRebuildInputMethodListLocked())) { return; } - if (!(additionalSubtypeChanged || shouldRebuildInputMethodListLocked())) { + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + userId, newAdditionalSubtypeMap, DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, newSettings); + if (!isCurrentUser) { return; } mSettings = queryInputMethodServicesInternal(mContext, userId, @@ -1281,21 +1292,22 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. void onUnlockUser(@UserIdInt int userId) { synchronized (ImfLock.class) { - final int currentUserId = mSettings.getUserId(); if (DEBUG) { - Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + currentUserId); - } - if (userId != currentUserId) { - return; + Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + + mSettings.getUserId()); } if (!mSystemReady) { return; } - mSettings = queryInputMethodServicesInternal(mContext, userId, - AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO); - // We need to rebuild IMEs. - postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); - updateInputMethodsFromSettingsLocked(true /* enabledChanged */); + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, newSettings); + if (mSettings.getUserId() == userId) { + mSettings = newSettings; + // We need to rebuild IMEs. + postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); + updateInputMethodsFromSettingsLocked(true /* enabledChanged */); + } } } @@ -1361,12 +1373,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mShowOngoingImeSwitcherForPhones = false; - AdditionalSubtypeMapRepository.initialize(mHandler); + // InputMethodSettingsRepository should be initialized before buildInputMethodListLocked + InputMethodSettingsRepository.initialize(mHandler, mContext); + AdditionalSubtypeMapRepository.initialize(mHandler, mContext); final int userId = mActivityManagerInternal.getCurrentUserId(); - // mSettings should be created before buildInputMethodListLocked - mSettings = InputMethodSettings.createEmptyMap(userId); + mSettings = InputMethodSettingsRepository.get(userId); mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(context, @@ -1529,8 +1542,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // and user switch would not happen at that time. resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_USER); - mSettings = queryInputMethodServicesInternal(mContext, newUserId, - AdditionalSubtypeMapRepository.get(newUserId), DirectBootAwareness.AUTO); + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + newUserId, AdditionalSubtypeMapRepository.get(newUserId), DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(newUserId, newSettings); + mSettings = newSettings; postInputMethodSettingUpdatedLocked(initialUserSwitch /* resetDefaultEnabledIme */); if (TextUtils.isEmpty(mSettings.getSelectedInputMethod())) { // This is the first time of the user switch and @@ -1612,9 +1627,11 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final String defaultImiId = mSettings.getSelectedInputMethod(); final boolean imeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId); - mSettings = queryInputMethodServicesInternal(mContext, currentUserId, - AdditionalSubtypeMapRepository.get(mSettings.getUserId()), + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + currentUserId, AdditionalSubtypeMapRepository.get(currentUserId), DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(currentUserId, newSettings); + mSettings = newSettings; postInputMethodSettingUpdatedLocked( !imeSelectedOnBoot /* resetDefaultEnabledIme */); updateFromSettingsLocked(true); @@ -4076,22 +4093,20 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); final boolean isCurrentUser = (mSettings.getUserId() == userId); - final InputMethodSettings settings = isCurrentUser - ? mSettings - : queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - DirectBootAwareness.AUTO); + final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); final var newAdditionalSubtypeMap = settings.getNewAdditionalSubtypeMap( imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); if (additionalSubtypeMap != newAdditionalSubtypeMap) { AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap, settings.getMethodMap()); + final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, + userId, AdditionalSubtypeMapRepository.get(userId), + DirectBootAwareness.AUTO); + InputMethodSettingsRepository.put(userId, newSettings); if (isCurrentUser) { final long ident = Binder.clearCallingIdentity(); try { - mSettings = queryInputMethodServicesInternal(mContext, - mSettings.getUserId(), - AdditionalSubtypeMapRepository.get(mSettings.getUserId()), - DirectBootAwareness.AUTO); + mSettings = newSettings; postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); } finally { Binder.restoreCallingIdentity(ident); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java new file mode 100644 index 000000000000..60b9a4cfe840 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.inputmethod; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.Handler; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.DirectBootAwareness; +import com.android.server.LocalServices; +import com.android.server.pm.UserManagerInternal; + +final class InputMethodSettingsRepository { + @GuardedBy("ImfLock.class") + @NonNull + private static final SparseArray<InputMethodSettings> sPerUserMap = new SparseArray<>(); + + /** + * Not intended to be instantiated. + */ + private InputMethodSettingsRepository() { + } + + @NonNull + @GuardedBy("ImfLock.class") + static InputMethodSettings get(@UserIdInt int userId) { + final InputMethodSettings obj = sPerUserMap.get(userId); + if (obj != null) { + return obj; + } + return InputMethodSettings.createEmptyMap(userId); + } + + @GuardedBy("ImfLock.class") + static void put(@UserIdInt int userId, @NonNull InputMethodSettings obj) { + sPerUserMap.put(userId, obj); + } + + static void initialize(@NonNull Handler handler, @NonNull Context context) { + final UserManagerInternal userManagerInternal = + LocalServices.getService(UserManagerInternal.class); + handler.post(() -> { + userManagerInternal.addUserLifecycleListener( + new UserManagerInternal.UserLifecycleListener() { + @Override + public void onUserRemoved(UserInfo user) { + final int userId = user.id; + handler.post(() -> { + synchronized (ImfLock.class) { + sPerUserMap.remove(userId); + } + }); + } + }); + synchronized (ImfLock.class) { + for (int userId : userManagerInternal.getUserIds()) { + final InputMethodSettings settings = + InputMethodManagerService.queryInputMethodServicesInternal( + context, + userId, + AdditionalSubtypeMapRepository.get(userId), + DirectBootAwareness.AUTO); + sPerUserMap.put(userId, settings); + } + } + }); + } +} diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 194ab04817ec..3d6855547bcd 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -1458,14 +1458,10 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde @Override public IBinder getBinderForSetQueue() throws RemoteException { return new ParcelableListBinder<QueueItem>( + QueueItem.class, (list) -> { - // Checking list items are instanceof QueueItem to validate against - // malicious apps calling it directly via reflection with non compilable - // items. See b/317048338 for more details - List<QueueItem> sanitizedQueue = - list.stream().filter(it -> it instanceof QueueItem).toList(); synchronized (mLock) { - mQueue = sanitizedQueue; + mQueue = list; } mHandler.post(MessageHandler.MSG_UPDATE_QUEUE); }); diff --git a/services/core/java/com/android/server/notification/NotificationManagerPrivate.java b/services/core/java/com/android/server/notification/NotificationManagerPrivate.java index 2cc63ebfc962..a3a91e26c5cc 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerPrivate.java +++ b/services/core/java/com/android/server/notification/NotificationManagerPrivate.java @@ -25,4 +25,6 @@ import android.annotation.Nullable; interface NotificationManagerPrivate { @Nullable NotificationRecord getNotificationByKey(String key); + + void timeoutNotification(String key); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index b14242ef8e08..8075ae0e4d61 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -139,7 +139,6 @@ import static android.service.notification.NotificationListenerService.TRIM_FULL import static android.service.notification.NotificationListenerService.TRIM_LIGHT; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.contentprotection.flags.Flags.rapidClearNotificationsByListenerAppOpEnabled; - import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES; @@ -238,6 +237,7 @@ import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.DeadObjectException; import android.os.DeviceIdleManager; import android.os.Environment; import android.os.Handler; @@ -305,7 +305,6 @@ import android.view.Display; import android.view.accessibility.AccessibilityManager; import android.widget.RemoteViews; import android.widget.Toast; - import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -357,9 +356,7 @@ import com.android.server.utils.quota.MultiRateLimiter; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.BackgroundActivityStartCallback; import com.android.server.wm.WindowManagerInternal; - import libcore.io.IoUtils; - import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParserException; @@ -521,7 +518,7 @@ public class NotificationManagerService extends SystemService { /** * Apps that post custom toasts in the background will have those blocked. Apps can * still post toasts created with - * {@link android.widget.Toast#makeText(Context, CharSequence, int)} and its variants while + * {@link Toast#makeText(Context, CharSequence, int)} and its variants while * in the background. */ @ChangeId @@ -556,7 +553,7 @@ public class NotificationManagerService extends SystemService { /** * Rate limit showing toasts, on a per package basis. * - * It limits the number of {@link android.widget.Toast#show()} calls to prevent overburdening + * It limits the number of {@link Toast#show()} calls to prevent overburdening * the user with too many toasts in a limited time. Any attempt to show more toasts than allowed * in a certain time frame will result in the toast being discarded. */ @@ -580,9 +577,9 @@ public class NotificationManagerService extends SystemService { static final long ENFORCE_NO_CLEAR_FLAG_ON_MEDIA_NOTIFICATION = 264179692L; /** - * App calls to {@link android.app.NotificationManager#setInterruptionFilter} and - * {@link android.app.NotificationManager#setNotificationPolicy} manage DND through the - * creation and activation of an implicit {@link android.app.AutomaticZenRule}. + * App calls to {@link NotificationManager#setInterruptionFilter} and + * {@link NotificationManager#setNotificationPolicy} manage DND through the + * creation and activation of an implicit {@link AutomaticZenRule}. */ @ChangeId @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) @@ -624,6 +621,8 @@ public class NotificationManagerService extends SystemService { private PowerManager mPowerManager; private PostNotificationTrackerFactory mPostNotificationTrackerFactory; + private LockPatternUtils mLockUtils; + final IBinder mForegroundToken = new Binder(); @VisibleForTesting WorkerHandler mHandler; @@ -709,6 +708,7 @@ public class NotificationManagerService extends SystemService { private NotificationHistoryManager mHistoryManager; protected SnoozeHelper mSnoozeHelper; + private TimeToLiveHelper mTtlHelper; private GroupHelper mGroupHelper; private int mAutoGroupAtCount; private boolean mIsTelevision; @@ -734,6 +734,8 @@ public class NotificationManagerService extends SystemService { // Broadcast intent receiver for notification permissions review-related intents private ReviewNotificationPermissionsReceiver mReviewNotificationPermissionsReceiver; + private AppOpsManager.OnOpChangedListener mAppOpsListener; + static class Archive { final SparseArray<Boolean> mEnabled; final int mBufferSize; @@ -779,7 +781,7 @@ public class NotificationManagerService extends SystemService { public StatusBarNotification[] getArray(UserManager um, int count, boolean includeSnoozed) { ArrayList<Integer> currentUsers = new ArrayList<>(); - currentUsers.add(UserHandle.USER_ALL); + currentUsers.add(USER_ALL); Binder.withCleanCallingIdentity(() -> { for (int user : um.getProfileIds(ActivityManager.getCurrentUser(), false)) { currentUsers.add(user); @@ -902,14 +904,14 @@ public class NotificationManagerService extends SystemService { @VisibleForTesting boolean isDNDMigrationDone(int userId) { - return Settings.Secure.getIntForUser(getContext().getContentResolver(), - Settings.Secure.DND_CONFIGS_MIGRATED, 0, userId) == 1; + return Secure.getIntForUser(getContext().getContentResolver(), + Secure.DND_CONFIGS_MIGRATED, 0, userId) == 1; } @VisibleForTesting void setDNDMigrationDone(int userId) { - Settings.Secure.putIntForUser(getContext().getContentResolver(), - Settings.Secure.DND_CONFIGS_MIGRATED, 1, userId); + Secure.putIntForUser(getContext().getContentResolver(), + Secure.DND_CONFIGS_MIGRATED, 1, userId); } protected void migrateDefaultNAS() { @@ -936,15 +938,15 @@ public class NotificationManagerService extends SystemService { @VisibleForTesting void setNASMigrationDone(int baseUserId) { for (int profileId : mUm.getProfileIds(baseUserId, false)) { - Settings.Secure.putIntForUser(getContext().getContentResolver(), - Settings.Secure.NAS_SETTINGS_UPDATED, 1, profileId); + Secure.putIntForUser(getContext().getContentResolver(), + Secure.NAS_SETTINGS_UPDATED, 1, profileId); } } @VisibleForTesting boolean isNASMigrationDone(int userId) { - return (Settings.Secure.getIntForUser(getContext().getContentResolver(), - Settings.Secure.NAS_SETTINGS_UPDATED, 0, userId) == 1); + return (Secure.getIntForUser(getContext().getContentResolver(), + Secure.NAS_SETTINGS_UPDATED, 0, userId) == 1); } boolean isProfileUser(UserInfo userInfo) { @@ -1097,7 +1099,7 @@ public class NotificationManagerService extends SystemService { mSnoozeHelper.readXml(parser, System.currentTimeMillis()); } if (LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_TAG.equals(parser.getName())) { - if (forRestore && userId != UserHandle.USER_SYSTEM) { + if (forRestore && userId != USER_SYSTEM) { continue; } mLockScreenAllowSecureNotifications = parser.getAttributeBoolean(null, @@ -1141,7 +1143,7 @@ public class NotificationManagerService extends SystemService { InputStream infile = null; try { infile = mPolicyFile.openRead(); - readPolicyXml(infile, false /*forRestore*/, UserHandle.USER_ALL); + readPolicyXml(infile, false /*forRestore*/, USER_ALL); // We re-load the default dnd packages to allow the newly added and denined. final boolean isWatch = mPackageManagerClient.hasSystemFeature( @@ -1191,7 +1193,7 @@ public class NotificationManagerService extends SystemService { } try { - writePolicyXml(stream, false /*forBackup*/, UserHandle.USER_ALL); + writePolicyXml(stream, false /*forBackup*/, USER_ALL); mPolicyFile.finishWrite(stream); } catch (IOException e) { Slog.w(TAG, "Failed to save policy file, restoring backup", e); @@ -1220,7 +1222,7 @@ public class NotificationManagerService extends SystemService { mAssistants.writeXml(out, forBackup, userId); mSnoozeHelper.writeXml(out); mConditionProviders.writeXml(out, forBackup, userId); - if (!forBackup || userId == UserHandle.USER_SYSTEM) { + if (!forBackup || userId == USER_SYSTEM) { writeSecureNotificationsPolicy(out); } out.endTag(null, TAG_NOTIFICATION_POLICY); @@ -1273,7 +1275,7 @@ public class NotificationManagerService extends SystemService { StatusBarNotification sbn = r.getSbn(); cancelNotification(callingUid, callingPid, sbn.getPackageName(), sbn.getTag(), - sbn.getId(), Notification.FLAG_AUTO_CANCEL, + sbn.getId(), FLAG_AUTO_CANCEL, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_BUBBLE, false, r.getUserId(), REASON_CLICK, nv.rank, nv.count, null); nv.recycle(); @@ -1761,9 +1763,50 @@ public class NotificationManagerService extends SystemService { }; - NotificationManagerPrivate mNotificationManagerPrivate = key -> { - synchronized (mNotificationLock) { - return mNotificationsByKey.get(key); + NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() { + @Nullable + @Override + public NotificationRecord getNotificationByKey(String key) { + synchronized (mNotificationLock) { + return mNotificationsByKey.get(key); + } + } + + @Override + @FlaggedApi(Flags.FLAG_ALL_NOTIFS_NEED_TTL) + public void timeoutNotification(String key) { + boolean foundNotification = false; + int uid = 0; + int pid = 0; + String packageName = null; + String tag = null; + int id = 0; + int userId = 0; + + synchronized (mNotificationLock) { + NotificationRecord record = findNotificationByKeyLocked(key); + if (record != null) { + foundNotification = true; + uid = record.getUid(); + pid = record.getSbn().getInitialPid(); + packageName = record.getSbn().getPackageName(); + tag = record.getSbn().getTag(); + id = record.getSbn().getId(); + userId = record.getUserId(); + } + } + if (foundNotification) { + if (lifetimeExtensionRefactor()) { + cancelNotification(uid, pid, packageName, tag, id, 0, + FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB + | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, + true, userId, REASON_TIMEOUT, null); + } else { + cancelNotification(uid, pid, packageName, tag, id, 0, + FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB, + true, userId, REASON_TIMEOUT, null); + } + } } }; @@ -1893,7 +1936,7 @@ public class NotificationManagerService extends SystemService { || action.equals(Intent.ACTION_PACKAGES_UNSUSPENDED) || action.equals(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED)) { int changeUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_ALL); + USER_ALL); String pkgList[] = null; int uidList[] = null; boolean removingPackage = queryRemove && @@ -1942,7 +1985,7 @@ public class NotificationManagerService extends SystemService { try { final int enabled = mPackageManager.getApplicationEnabledSetting( pkgName, - changeUserId != UserHandle.USER_ALL ? changeUserId : + changeUserId != USER_ALL ? changeUserId : USER_SYSTEM); if (enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED || enabled == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { @@ -2055,22 +2098,22 @@ public class NotificationManagerService extends SystemService { private final class SettingsObserver extends ContentObserver { private final Uri NOTIFICATION_BADGING_URI - = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BADGING); + = Secure.getUriFor(Secure.NOTIFICATION_BADGING); private final Uri NOTIFICATION_BUBBLES_URI - = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BUBBLES); + = Secure.getUriFor(Secure.NOTIFICATION_BUBBLES); private final Uri NOTIFICATION_RATE_LIMIT_URI = Settings.Global.getUriFor(Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE); private final Uri NOTIFICATION_HISTORY_ENABLED - = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_HISTORY_ENABLED); + = Secure.getUriFor(Secure.NOTIFICATION_HISTORY_ENABLED); private final Uri NOTIFICATION_SHOW_MEDIA_ON_QUICK_SETTINGS_URI = Settings.Global.getUriFor(Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS); private final Uri LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS - = Settings.Secure.getUriFor( - Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); + = Secure.getUriFor( + Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); private final Uri LOCK_SCREEN_SHOW_NOTIFICATIONS - = Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS); + = Secure.getUriFor(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS); private final Uri SHOW_NOTIFICATION_SNOOZE - = Settings.Secure.getUriFor(Settings.Secure.SHOW_NOTIFICATION_SNOOZE); + = Secure.getUriFor(Secure.SHOW_NOTIFICATION_SNOOZE); SettingsObserver(Handler handler) { super(handler); @@ -2079,27 +2122,31 @@ public class NotificationManagerService extends SystemService { void observe() { ContentResolver resolver = getContext().getContentResolver(); resolver.registerContentObserver(NOTIFICATION_BADGING_URI, - false, this, UserHandle.USER_ALL); + false, this, USER_ALL); resolver.registerContentObserver(NOTIFICATION_RATE_LIMIT_URI, - false, this, UserHandle.USER_ALL); + false, this, USER_ALL); resolver.registerContentObserver(NOTIFICATION_BUBBLES_URI, - false, this, UserHandle.USER_ALL); + false, this, USER_ALL); resolver.registerContentObserver(NOTIFICATION_HISTORY_ENABLED, - false, this, UserHandle.USER_ALL); + false, this, USER_ALL); resolver.registerContentObserver(NOTIFICATION_SHOW_MEDIA_ON_QUICK_SETTINGS_URI, - false, this, UserHandle.USER_ALL); + false, this, USER_ALL); resolver.registerContentObserver(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, - false, this, UserHandle.USER_ALL); + false, this, USER_ALL); resolver.registerContentObserver(LOCK_SCREEN_SHOW_NOTIFICATIONS, - false, this, UserHandle.USER_ALL); + false, this, USER_ALL); resolver.registerContentObserver(SHOW_NOTIFICATION_SNOOZE, - false, this, UserHandle.USER_ALL); + false, this, USER_ALL); update(null); } + void destroy() { + getContext().getContentResolver().unregisterContentObserver(this); + } + @Override public void onChange(boolean selfChange, Uri uri, int userId) { update(uri); } @@ -2131,7 +2178,7 @@ public class NotificationManagerService extends SystemService { mPreferencesHelper.updateLockScreenShowNotifications(); } if (SHOW_NOTIFICATION_SNOOZE.equals(uri)) { - final boolean snoozeEnabled = Settings.Secure.getIntForUser(resolver, + final boolean snoozeEnabled = Secure.getIntForUser(resolver, Secure.SHOW_NOTIFICATION_SNOOZE, 0, UserHandle.USER_CURRENT) != 0; if (!snoozeEnabled) { @@ -2144,8 +2191,8 @@ public class NotificationManagerService extends SystemService { ContentResolver resolver = getContext().getContentResolver(); if (uri == null || NOTIFICATION_HISTORY_ENABLED.equals(uri)) { mArchive.updateHistoryEnabled(userId, - Settings.Secure.getIntForUser(resolver, - Settings.Secure.NOTIFICATION_HISTORY_ENABLED, 0, + Secure.getIntForUser(resolver, + Secure.NOTIFICATION_HISTORY_ENABLED, 0, userId) == 1); // note: this setting is also handled in NotificationHistoryManager } @@ -2229,6 +2276,11 @@ public class NotificationManagerService extends SystemService { } @VisibleForTesting + void setLockPatternUtils(LockPatternUtils lockUtils) { + mLockUtils = lockUtils; + } + + @VisibleForTesting ShortcutHelper getShortcutHelper() { return mShortcutHelper; } @@ -2400,7 +2452,7 @@ public class NotificationManagerService extends SystemService { getContext().sendBroadcastAsUser( new Intent(ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL) .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT), - UserHandle.ALL, permission.MANAGE_NOTIFICATIONS); + UserHandle.ALL, android.Manifest.permission.MANAGE_NOTIFICATIONS); synchronized (mNotificationLock) { updateInterruptionFilterLocked(); } @@ -2465,6 +2517,9 @@ public class NotificationManagerService extends SystemService { mSnoozeHelper = snoozeHelper; mGroupHelper = groupHelper; mHistoryManager = historyManager; + if (Flags.allNotifsNeedTtl()) { + mTtlHelper = new TimeToLiveHelper(mNotificationManagerPrivate, getContext()); + } // This is a ManagedServices object that keeps track of the listeners. mListeners = notificationListeners; @@ -2551,10 +2606,12 @@ public class NotificationManagerService extends SystemService { getContext().registerReceiverAsUser(mPackageIntentReceiver, UserHandle.ALL, sdFilter, null, null); - IntentFilter timeoutFilter = new IntentFilter(ACTION_NOTIFICATION_TIMEOUT); - timeoutFilter.addDataScheme(SCHEME_TIMEOUT); - getContext().registerReceiver(mNotificationTimeoutReceiver, timeoutFilter, - Context.RECEIVER_EXPORTED_UNAUDITED); + if (!Flags.allNotifsNeedTtl()) { + IntentFilter timeoutFilter = new IntentFilter(ACTION_NOTIFICATION_TIMEOUT); + timeoutFilter.addDataScheme(SCHEME_TIMEOUT); + getContext().registerReceiver(mNotificationTimeoutReceiver, timeoutFilter, + Context.RECEIVER_EXPORTED_UNAUDITED); + } IntentFilter settingsRestoredFilter = new IntentFilter(Intent.ACTION_SETTING_RESTORED); getContext().registerReceiver(mRestoreReceiver, settingsRestoredFilter); @@ -2567,15 +2624,16 @@ public class NotificationManagerService extends SystemService { ReviewNotificationPermissionsReceiver.getFilter(), Context.RECEIVER_NOT_EXPORTED); - mAppOps.startWatchingMode(AppOpsManager.OP_POST_NOTIFICATION, null, - new AppOpsManager.OnOpChangedInternalListener() { - @Override - public void onOpChanged(@NonNull String op, @NonNull String packageName, - int userId) { - mHandler.post( - () -> handleNotificationPermissionChange(packageName, userId)); - } - }); + mAppOpsListener = new AppOpsManager.OnOpChangedInternalListener() { + @Override + public void onOpChanged(@NonNull String op, @NonNull String packageName, + int userId) { + mHandler.post( + () -> handleNotificationPermissionChange(packageName, userId)); + } + }; + + mAppOps.startWatchingMode(AppOpsManager.OP_POST_NOTIFICATION, null, mAppOpsListener); } /** @@ -2584,10 +2642,26 @@ public class NotificationManagerService extends SystemService { public void onDestroy() { getContext().unregisterReceiver(mIntentReceiver); getContext().unregisterReceiver(mPackageIntentReceiver); - getContext().unregisterReceiver(mNotificationTimeoutReceiver); + if (Flags.allNotifsNeedTtl()) { + mTtlHelper.destroy(); + } else { + getContext().unregisterReceiver(mNotificationTimeoutReceiver); + } getContext().unregisterReceiver(mRestoreReceiver); getContext().unregisterReceiver(mLocaleChangeReceiver); + mSettingsObserver.destroy(); + mRoleObserver.destroy(); + if (mShortcutHelper != null) { + mShortcutHelper.destroy(); + } + mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_PREFERENCES); + mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES); + mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES); + mStatsManager.clearPullAtomCallback(DND_MODE_RULE); + mAppOps.stopWatchingMode(mAppOpsListener); + mAlarmManager.cancelAll(); + if (mDeviceConfigChangedListener != null) { DeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigChangedListener); } @@ -2842,7 +2916,10 @@ public class NotificationManagerService extends SystemService { bubbsExtractor.setShortcutHelper(mShortcutHelper); } registerNotificationPreferencesPullers(); - new LockPatternUtils(getContext()).registerStrongAuthTracker(mStrongAuthTracker); + if (mLockUtils == null) { + mLockUtils = new LockPatternUtils(getContext()); + } + mLockUtils.registerStrongAuthTracker(mStrongAuthTracker); mAttentionHelper.onSystemReady(); } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) { // This observer will force an update when observe is called, causing us to @@ -2952,7 +3029,7 @@ public class NotificationManagerService extends SystemService { void updateNotificationChannelInt(String pkg, int uid, NotificationChannel channel, boolean fromListener) { - if (channel.getImportance() == NotificationManager.IMPORTANCE_NONE) { + if (channel.getImportance() == IMPORTANCE_NONE) { // cancel cancelAllNotificationsInt(MY_UID, MY_PID, pkg, channel.getId(), 0, 0, UserHandle.getUserId(uid), REASON_CHANNEL_BANNED @@ -3217,14 +3294,14 @@ public class NotificationManagerService extends SystemService { | SUPPRESSED_EFFECT_SCREEN_OFF); // set the deprecated effects according to the new more specific effects - if ((newSuppressedVisualEffects & Policy.SUPPRESSED_EFFECT_PEEK) != 0) { + if ((newSuppressedVisualEffects & SUPPRESSED_EFFECT_PEEK) != 0) { newSuppressedVisualEffects |= SUPPRESSED_EFFECT_SCREEN_ON; } - if ((newSuppressedVisualEffects & Policy.SUPPRESSED_EFFECT_LIGHTS) != 0 + if ((newSuppressedVisualEffects & SUPPRESSED_EFFECT_LIGHTS) != 0 && (newSuppressedVisualEffects - & Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT) != 0 + & SUPPRESSED_EFFECT_FULL_SCREEN_INTENT) != 0 && (newSuppressedVisualEffects - & Policy.SUPPRESSED_EFFECT_AMBIENT) != 0) { + & SUPPRESSED_EFFECT_AMBIENT) != 0) { newSuppressedVisualEffects |= SUPPRESSED_EFFECT_SCREEN_OFF; } } else { @@ -3292,7 +3369,7 @@ public class NotificationManagerService extends SystemService { if (n.extras != null) { title = n.extras.getCharSequence(Notification.EXTRA_TITLE); if (title == null) { - title = n.extras.getCharSequence(Notification.EXTRA_TITLE_BIG); + title = n.extras.getCharSequence(EXTRA_TITLE_BIG); } } return title == null ? getContext().getResources().getString( @@ -3311,9 +3388,9 @@ public class NotificationManagerService extends SystemService { if (nb.getStyle() instanceof Notification.BigTextStyle) { text = ((Notification.BigTextStyle) nb.getStyle()).getBigText(); - } else if (nb.getStyle() instanceof Notification.MessagingStyle) { - Notification.MessagingStyle ms = (Notification.MessagingStyle) nb.getStyle(); - final List<Notification.MessagingStyle.Message> messages = ms.getMessages(); + } else if (nb.getStyle() instanceof MessagingStyle) { + MessagingStyle ms = (MessagingStyle) nb.getStyle(); + final List<MessagingStyle.Message> messages = ms.getMessages(); if (messages != null && messages.size() > 0) { text = messages.get(messages.size() - 1).getText(); } @@ -3364,7 +3441,7 @@ public class NotificationManagerService extends SystemService { } private int getRealUserId(int userId) { - return userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId; + return userId == USER_ALL ? USER_SYSTEM : userId; } private ToastRecord getToastRecord(int uid, int pid, String packageName, boolean isSystemToast, @@ -4096,8 +4173,8 @@ public class NotificationManagerService extends SystemService { public void createConversationNotificationChannelForPackage(String pkg, int uid, NotificationChannel parentChannel, String conversationId) { enforceSystemOrSystemUI("only system can call this"); - Preconditions.checkNotNull(parentChannel); - Preconditions.checkNotNull(conversationId); + checkNotNull(parentChannel); + checkNotNull(conversationId); String parentId = parentChannel.getId(); NotificationChannel conversationChannel = parentChannel; conversationChannel.setId(String.format( @@ -4542,7 +4619,7 @@ public class NotificationManagerService extends SystemService { int uid = Binder.getCallingUid(); ArrayList<Integer> currentUsers = new ArrayList<>(); - currentUsers.add(UserHandle.USER_ALL); + currentUsers.add(USER_ALL); Binder.withCleanCallingIdentity(() -> { for (int user : mUm.getProfileIds(ActivityManager.getCurrentUser(), false)) { currentUsers.add(user); @@ -4798,7 +4875,7 @@ public class NotificationManagerService extends SystemService { * Register a listener binder directly with the notification manager. * * Only works with system callers. Apps should extend - * {@link android.service.notification.NotificationListenerService}. + * {@link NotificationListenerService}. */ @Override public void registerListener(final INotificationListener listener, @@ -4855,7 +4932,7 @@ public class NotificationManagerService extends SystemService { NotificationRecord r = mNotificationsByKey.get(keys[i]); if (r == null) continue; final int userId = r.getSbn().getUserId(); - if (userId != info.userid && userId != UserHandle.USER_ALL && + if (userId != info.userid && userId != USER_ALL && !mUserProfiles.isCurrentProfile(userId)) { continue; } @@ -4972,7 +5049,7 @@ public class NotificationManagerService extends SystemService { NotificationRecord r = mNotificationsByKey.get(keys[i]); if (r == null) continue; final int userId = r.getSbn().getUserId(); - if (userId != info.userid && userId != UserHandle.USER_ALL + if (userId != info.userid && userId != USER_ALL && !mUserProfiles.isCurrentProfile(userId)) { continue; } @@ -5639,7 +5716,7 @@ public class NotificationManagerService extends SystemService { } private void enforcePolicyAccess(int uid, String method) { - if (PackageManager.PERMISSION_GRANTED == getContext().checkCallingPermission( + if (PERMISSION_GRANTED == getContext().checkCallingPermission( android.Manifest.permission.MANAGE_NOTIFICATIONS)) { return; } @@ -5670,7 +5747,7 @@ public class NotificationManagerService extends SystemService { } private void enforcePolicyAccess(String pkg, String method) { - if (PackageManager.PERMISSION_GRANTED == getContext().checkCallingPermission( + if (PERMISSION_GRANTED == getContext().checkCallingPermission( android.Manifest.permission.MANAGE_NOTIFICATIONS)) { return; } @@ -5691,7 +5768,7 @@ public class NotificationManagerService extends SystemService { try { uid = getContext().getPackageManager().getPackageUidAsUser(pkg, UserHandle.getCallingUserId()); - if (PackageManager.PERMISSION_GRANTED == checkComponentPermission( + if (PERMISSION_GRANTED == checkComponentPermission( android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true)) { return true; @@ -5886,7 +5963,7 @@ public class NotificationManagerService extends SystemService { /** * Sets the notification policy. Apps that target API levels below - * {@link android.os.Build.VERSION_CODES#P} cannot change user-designated values to + * {@link Build.VERSION_CODES#P} cannot change user-designated values to * allow or disallow {@link Policy#PRIORITY_CATEGORY_ALARMS}, * {@link Policy#PRIORITY_CATEGORY_SYSTEM} and * {@link Policy#PRIORITY_CATEGORY_MEDIA} from bypassing dnd @@ -6254,7 +6331,7 @@ public class NotificationManagerService extends SystemService { @Override public void setPrivateNotificationsAllowed(boolean allow) { - if (PackageManager.PERMISSION_GRANTED + if (PERMISSION_GRANTED != getContext().checkCallingPermission(CONTROL_KEYGUARD_SECURE_NOTIFICATIONS)) { throw new SecurityException( "Requires CONTROL_KEYGUARD_SECURE_NOTIFICATIONS permission"); @@ -6275,7 +6352,7 @@ public class NotificationManagerService extends SystemService { @Override public boolean getPrivateNotificationsAllowed() { - if (PackageManager.PERMISSION_GRANTED + if (PERMISSION_GRANTED != getContext().checkCallingPermission(CONTROL_KEYGUARD_SECURE_NOTIFICATIONS)) { throw new SecurityException( "Requires CONTROL_KEYGUARD_SECURE_NOTIFICATIONS permission"); @@ -6563,9 +6640,9 @@ public class NotificationManagerService extends SystemService { // Add summary final ApplicationInfo appInfo = adjustedSbn.getNotification().extras.getParcelable( - Notification.EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo.class); + EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo.class); final Bundle extras = new Bundle(); - extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, appInfo); + extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, appInfo); final String channelId = notificationRecord.getChannel().getId(); final Notification summaryNotification = @@ -6859,6 +6936,11 @@ public class NotificationManagerService extends SystemService { if (!zenOnly) { pw.println("\n Usage Stats:"); mUsageStats.dump(pw, " ", filter); + + if (Flags.allNotifsNeedTtl()) { + pw.println("\n TimeToLive alarms:"); + mTtlHelper.dump(pw, " "); + } } } } @@ -7455,7 +7537,7 @@ public class NotificationManagerService extends SystemService { throws NameNotFoundException, RemoteException { final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser( pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, - (userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId); + (userId == USER_ALL) ? USER_SYSTEM : userId); Notification.addFieldsFromContext(ai, notification); if (notification.isForegroundService() && fgsPolicy == NOT_FOREGROUND_SERVICE) { @@ -7570,7 +7652,7 @@ public class NotificationManagerService extends SystemService { // Enforce NO_CLEAR flag on MediaStyle notification for apps with targetSdk >= V. if (CompatChanges.isChangeEnabled(ENFORCE_NO_CLEAR_FLAG_ON_MEDIA_NOTIFICATION, notificationUid)) { - notification.flags |= Notification.FLAG_NO_CLEAR; + notification.flags |= FLAG_NO_CLEAR; } } @@ -7622,7 +7704,7 @@ public class NotificationManagerService extends SystemService { // Check if an app has been given system exemption return mSystemExemptFromDismissal && mAppOps.checkOpNoThrow( AppOpsManager.OP_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS, ai.uid, - ai.packageName) == AppOpsManager.MODE_ALLOWED; + ai.packageName) == MODE_ALLOWED; } private boolean checkUseFullScreenIntentPermission(@NonNull AttributionSource attributionSource, @@ -7727,7 +7809,7 @@ public class NotificationManagerService extends SystemService { // Enqueue will trigger resort & flag is updated that way. r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE; mHandler.post( - new NotificationManagerService.EnqueueNotificationRunnable( + new EnqueueNotificationRunnable( r.getUser().getIdentifier(), r, isAppForeground, mPostNotificationTrackerFactory.newTracker(null))); } @@ -7749,7 +7831,7 @@ public class NotificationManagerService extends SystemService { @VisibleForTesting int resolveNotificationUid(String callingPkg, String targetPkg, int callingUid, int userId) { - if (userId == UserHandle.USER_ALL) { + if (userId == USER_ALL) { userId = USER_SYSTEM; } // posted from app A on behalf of app A @@ -8008,7 +8090,7 @@ public class NotificationManagerService extends SystemService { final String pkg = r.getSbn().getPackageName(); final int callingUid = r.getSbn().getUid(); return mPreferencesHelper.isGroupBlocked(pkg, callingUid, r.getChannel().getGroup()) - || r.getImportance() == NotificationManager.IMPORTANCE_NONE; + || r.getImportance() == IMPORTANCE_NONE; } protected class SnoozeNotificationRunnable implements Runnable { @@ -8347,7 +8429,11 @@ public class NotificationManagerService extends SystemService { } mEnqueuedNotifications.add(r); - scheduleTimeoutLocked(r); + if (Flags.allNotifsNeedTtl()) { + mTtlHelper.scheduleTimeoutLocked(r, SystemClock.elapsedRealtime()); + } else { + scheduleTimeoutLocked(r); + } final StatusBarNotification n = r.getSbn(); if (DBG) Slog.d(TAG, "EnqueueNotificationRunnable.run for: " + n.getKey()); @@ -8567,7 +8653,7 @@ public class NotificationManagerService extends SystemService { Slog.e(TAG, "Not posting notification without small icon: " + notification); if (old != null && !old.isCanceled) { mListeners.notifyRemovedLocked(r, - NotificationListenerService.REASON_ERROR, r.getStats()); + REASON_ERROR, r.getStats()); mHandler.post(new Runnable() { @Override public void run() { @@ -8935,7 +9021,7 @@ public class NotificationManagerService extends SystemService { try { isExemptFromRateLimiting = mPackageManager.checkPermission( android.Manifest.permission.UNLIMITED_TOASTS, pkg, userId) - == PackageManager.PERMISSION_GRANTED; + == PERMISSION_GRANTED; } catch (RemoteException e) { Slog.e(TAG, "Failed to connect with package manager"); } @@ -9460,7 +9546,11 @@ public class NotificationManagerService extends SystemService { int rank, int count, boolean wasPosted, String listenerName, @ElapsedRealtimeLong long cancellationElapsedTimeMs) { final String canceledKey = r.getKey(); - cancelScheduledTimeoutLocked(r); + if (Flags.allNotifsNeedTtl()) { + mTtlHelper.cancelScheduledTimeoutLocked(r); + } else { + cancelScheduledTimeoutLocked(r); + } // Record caller. recordCallerLocked(r); @@ -9637,7 +9727,7 @@ public class NotificationManagerService extends SystemService { // Uri, not when removing an individual listener. revokeUriPermission(permissionOwner, uri, UserHandle.getUserId(oldRecord.getUid()), - null, UserHandle.USER_ALL); + null, USER_ALL); } } } @@ -9735,9 +9825,9 @@ public class NotificationManagerService extends SystemService { } else { return // looking for USER_ALL notifications? match everything - userId == UserHandle.USER_ALL + userId == USER_ALL // a notification sent to USER_ALL matches any query - || r.getUserId() == UserHandle.USER_ALL + || r.getUserId() == USER_ALL // an exact user match || r.getUserId() == userId; } @@ -9816,7 +9906,7 @@ public class NotificationManagerService extends SystemService { continue; } // Don't remove notifications to all, if there's no package name specified - if (nullPkgIndicatesUserSwitch && pkg == null && r.getUserId() == UserHandle.USER_ALL) { + if (nullPkgIndicatesUserSwitch && pkg == null && r.getUserId() == USER_ALL) { continue; } if (!flagChecker.apply(r.getFlags())) { @@ -10314,7 +10404,7 @@ public class NotificationManagerService extends SystemService { return false; } - if (userId == UserHandle.USER_ALL) { + if (userId == USER_ALL) { userId = USER_SYSTEM; } @@ -10537,7 +10627,7 @@ public class NotificationManagerService extends SystemService { if (requiredPermission != null) { try { if (mPackageManager.checkPermission(requiredPermission, pkg, userId) - != PackageManager.PERMISSION_GRANTED) { + != PERMISSION_GRANTED) { canUseManagedServices = false; } } catch (RemoteException e) { @@ -10663,7 +10753,7 @@ public class NotificationManagerService extends SystemService { c.caption = "notification assistant"; c.serviceInterface = NotificationAssistantService.SERVICE_INTERFACE; c.xmlTag = TAG_ENABLED_NOTIFICATION_ASSISTANTS; - c.secureSettingName = Settings.Secure.ENABLED_NOTIFICATION_ASSISTANT; + c.secureSettingName = Secure.ENABLED_NOTIFICATION_ASSISTANT; c.bindPermission = Manifest.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE; c.settingsAction = Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS; c.clientLabel = R.string.notification_ranker_binding_label; @@ -11259,7 +11349,7 @@ public class NotificationManagerService extends SystemService { c.caption = "notification listener"; c.serviceInterface = NotificationListenerService.SERVICE_INTERFACE; c.xmlTag = TAG_ENABLED_NOTIFICATION_LISTENERS; - c.secureSettingName = Settings.Secure.ENABLED_NOTIFICATION_LISTENERS; + c.secureSettingName = Secure.ENABLED_NOTIFICATION_LISTENERS; c.bindPermission = android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE; c.settingsAction = Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS; c.clientLabel = R.string.notification_listener_binding_label; @@ -11667,7 +11757,7 @@ public class NotificationManagerService extends SystemService { // Managed Services. if (info.isSystemUi() && old != null && old.getNotification() != null && (old.getNotification().flags - & Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) { + & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) { final NotificationRankingUpdate update = makeRankingUpdateLocked(info); listenerCalls.add(() -> notifyPosted(info, oldSbn, update)); break; @@ -11696,8 +11786,8 @@ public class NotificationManagerService extends SystemService { continue; } // Grant access before listener is notified - final int targetUserId = (info.userid == UserHandle.USER_ALL) - ? UserHandle.USER_SYSTEM : info.userid; + final int targetUserId = (info.userid == USER_ALL) + ? USER_SYSTEM : info.userid; updateUriPermissions(r, old, info.component.getPackageName(), targetUserId); mPackageManagerInternal.grantImplicitAccess( @@ -11846,8 +11936,8 @@ public class NotificationManagerService extends SystemService { continue; } // Grant or revoke access synchronously - final int targetUserId = (info.userid == UserHandle.USER_ALL) - ? UserHandle.USER_SYSTEM : info.userid; + final int targetUserId = (info.userid == USER_ALL) + ? USER_SYSTEM : info.userid; if (grant) { // Grant permissions by passing arguments as if the notification is new. updateUriPermissions(/* newRecord */ r, /* oldRecord */ null, @@ -11919,7 +12009,7 @@ public class NotificationManagerService extends SystemService { } // Revoke access after all listeners have been updated - mHandler.post(() -> updateUriPermissions(null, r, null, UserHandle.USER_SYSTEM)); + mHandler.post(() -> updateUriPermissions(null, r, null, USER_SYSTEM)); } /** @@ -12079,7 +12169,7 @@ public class NotificationManagerService extends SystemService { StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn); try { listener.onNotificationPosted(sbnHolder, rankingUpdate); - } catch (android.os.DeadObjectException ex) { + } catch (DeadObjectException ex) { Slog.wtf(TAG, "unable to notify listener (posted): " + info, ex); } catch (RemoteException ex) { Slog.e(TAG, "unable to notify listener (posted): " + info, ex); @@ -12102,7 +12192,7 @@ public class NotificationManagerService extends SystemService { reason = REASON_LISTENER_CANCEL; } listener.onNotificationRemoved(sbnHolder, rankingUpdate, stats, reason); - } catch (android.os.DeadObjectException ex) { + } catch (DeadObjectException ex) { Slog.wtf(TAG, "unable to notify listener (removed): " + info, ex); } catch (RemoteException ex) { Slog.e(TAG, "unable to notify listener (removed): " + info, ex); @@ -12114,7 +12204,7 @@ public class NotificationManagerService extends SystemService { final INotificationListener listener = (INotificationListener) info.service; try { listener.onNotificationRankingUpdate(rankingUpdate); - } catch (android.os.DeadObjectException ex) { + } catch (DeadObjectException ex) { Slog.wtf(TAG, "unable to notify listener (ranking update): " + info, ex); } catch (RemoteException ex) { Slog.e(TAG, "unable to notify listener (ranking update): " + info, ex); @@ -12333,6 +12423,10 @@ public class NotificationManagerService extends SystemService { mRm.addOnRoleHoldersChangedListenerAsUser(mExecutor, this, UserHandle.ALL); } + void destroy() { + mRm.removeOnRoleHoldersChangedListenerAsUser(this, UserHandle.ALL); + } + @VisibleForTesting public boolean isApprovedPackageForRoleForUser(String role, String pkg, int userId) { return mNonBlockableDefaultApps.get(role).get(userId).contains(pkg); @@ -12621,7 +12715,7 @@ public class NotificationManagerService extends SystemService { .setContentIntent(PendingIntent.getActivity(getContext(), 0, tapIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .setStyle(new Notification.BigTextStyle()) - .setFlag(Notification.FLAG_NO_CLEAR, true) + .setFlag(FLAG_NO_CLEAR, true) .setAutoCancel(true) .addAction(remindMe) .addAction(dismiss) diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 97d26208571c..c69bead309f1 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -79,6 +79,7 @@ import dalvik.annotation.optimization.NeverCompile; import java.io.PrintWriter; import java.lang.reflect.Array; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -600,8 +601,7 @@ public final class NotificationRecord { pw.println(prefix + "headsUpContentView=" + formatRemoteViews(notification.headsUpContentView)); pw.println(prefix + String.format("color=0x%08x", notification.color)); - pw.println(prefix + "timeout=" - + TimeUtils.formatForLogging(notification.getTimeoutAfter())); + pw.println(prefix + "timeout=" + Duration.ofMillis(notification.getTimeoutAfter())); if (notification.actions != null && notification.actions.length > 0) { pw.println(prefix + "actions={"); final int N = notification.actions.length; diff --git a/services/core/java/com/android/server/notification/ShortcutHelper.java b/services/core/java/com/android/server/notification/ShortcutHelper.java index fc106b8ec4ac..86dcecf9290a 100644 --- a/services/core/java/com/android/server/notification/ShortcutHelper.java +++ b/services/core/java/com/android/server/notification/ShortcutHelper.java @@ -287,4 +287,11 @@ public class ShortcutHelper { } } } + + void destroy() { + if (mLauncherAppsCallbackRegistered) { + mLauncherAppsService.unregisterCallback(mLauncherAppsCallback); + mLauncherAppsCallbackRegistered = false; + } + } } diff --git a/services/core/java/com/android/server/notification/TimeToLiveHelper.java b/services/core/java/com/android/server/notification/TimeToLiveHelper.java new file mode 100644 index 000000000000..2facab74e28e --- /dev/null +++ b/services/core/java/com/android/server/notification/TimeToLiveHelper.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.notification; + +import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.SystemClock; +import android.util.Pair; +import android.util.Slog; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.pm.PackageManagerService; + +import java.io.PrintWriter; +import java.util.TreeSet; + +/** + * Handles canceling notifications when their time to live expires + */ +@FlaggedApi(Flags.FLAG_ALL_NOTIFS_NEED_TTL) +public class TimeToLiveHelper { + private static final String TAG = TimeToLiveHelper.class.getSimpleName(); + private static final String ACTION = "com.android.server.notification.TimeToLiveHelper"; + + private static final int REQUEST_CODE_TIMEOUT = 1; + private static final String SCHEME_TIMEOUT = "timeout"; + static final String EXTRA_KEY = "key"; + private final Context mContext; + private final NotificationManagerPrivate mNm; + private final AlarmManager mAm; + + @VisibleForTesting + final TreeSet<Pair<Long, String>> mKeys; + + public TimeToLiveHelper(NotificationManagerPrivate nm, Context context) { + mContext = context; + mNm = nm; + mAm = context.getSystemService(AlarmManager.class); + mKeys = new TreeSet<>((left, right) -> Long.compare(left.first, right.first)); + + IntentFilter timeoutFilter = new IntentFilter(ACTION); + timeoutFilter.addDataScheme(SCHEME_TIMEOUT); + mContext.registerReceiver(mNotificationTimeoutReceiver, timeoutFilter, + Context.RECEIVER_NOT_EXPORTED); + } + + void destroy() { + mContext.unregisterReceiver(mNotificationTimeoutReceiver); + } + + void dump(PrintWriter pw, String indent) { + pw.println(indent + "mKeys " + mKeys); + } + + private @NonNull PendingIntent getAlarmPendingIntent(String nextKey, int flags) { + flags |= PendingIntent.FLAG_IMMUTABLE; + return PendingIntent.getBroadcast(mContext, + REQUEST_CODE_TIMEOUT, + new Intent(ACTION) + .setPackage(PackageManagerService.PLATFORM_PACKAGE_NAME) + .setData(new Uri.Builder() + .scheme(SCHEME_TIMEOUT) + .appendPath(nextKey) + .build()) + .putExtra(EXTRA_KEY, nextKey) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), + flags); + } + + @VisibleForTesting + void scheduleTimeoutLocked(NotificationRecord record, long currentTime) { + removeMatchingEntry(record.getKey()); + + final long timeoutAfter = currentTime + record.getNotification().getTimeoutAfter(); + if (record.getNotification().getTimeoutAfter() > 0) { + final Long currentEarliestTime = mKeys.isEmpty() ? null : mKeys.first().first; + + // Maybe replace alarm with an earlier one + if (currentEarliestTime == null || timeoutAfter < currentEarliestTime) { + if (currentEarliestTime != null) { + cancelFirstAlarm(); + } + mKeys.add(Pair.create(timeoutAfter, record.getKey())); + maybeScheduleFirstAlarm(); + } else { + mKeys.add(Pair.create(timeoutAfter, record.getKey())); + } + } + } + + @VisibleForTesting + void cancelScheduledTimeoutLocked(NotificationRecord record) { + removeMatchingEntry(record.getKey()); + } + + private void removeMatchingEntry(String key) { + if (!mKeys.isEmpty() && key.equals(mKeys.first().second)) { + // cancel the first alarm, remove the first entry, maybe schedule the alarm for the new + // first entry + cancelFirstAlarm(); + mKeys.remove(mKeys.first()); + maybeScheduleFirstAlarm(); + } else { + // just remove the entry + Pair<Long, String> trackedPair = null; + for (Pair<Long, String> entry : mKeys) { + if (key.equals(entry.second)) { + trackedPair = entry; + break; + } + } + if (trackedPair != null) { + mKeys.remove(trackedPair); + } + } + } + + private void cancelFirstAlarm() { + final PendingIntent pi = getAlarmPendingIntent(mKeys.first().second, FLAG_CANCEL_CURRENT); + mAm.cancel(pi); + } + + private void maybeScheduleFirstAlarm() { + if (!mKeys.isEmpty()) { + final PendingIntent piNewFirst = getAlarmPendingIntent(mKeys.first().second, + FLAG_UPDATE_CURRENT); + mAm.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, + mKeys.first().first, piNewFirst); + } + } + + @VisibleForTesting + final BroadcastReceiver mNotificationTimeoutReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) { + return; + } + if (ACTION.equals(action)) { + Pair<Long, String> earliest = mKeys.first(); + String key = intent.getStringExtra(EXTRA_KEY); + if (!earliest.second.equals(key)) { + Slog.wtf(TAG, "Alarm triggered but wasn't the earliest we were tracking"); + } + removeMatchingEntry(key); + mNm.timeoutNotification(earliest.second); + } + } + }; +} diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index f7f76aaaee16..57ea233c0a2b 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -807,7 +807,7 @@ final class DefaultPermissionGrantPolicy { getDefaultSystemHandlerActivityPackage(pm, SearchManager.INTENT_ACTION_GLOBAL_SEARCH, userId), userId, MICROPHONE_PERMISSIONS, ALWAYS_LOCATION_PERMISSIONS, - NOTIFICATION_PERMISSIONS, PHONE_PERMISSIONS); + NOTIFICATION_PERMISSIONS, PHONE_PERMISSIONS, CALENDAR_PERMISSIONS); } // Voice recognition diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 2b43326f4c51..e280bdc7780b 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -466,20 +466,17 @@ final class AccessibilityController { } } - void drawMagnifiedRegionBorderIfNeeded(int displayId) { - if (Flags.alwaysDrawMagnificationFullscreenBorder()) { - return; - } - + void recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded(int displayId) { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace( - TAG + ".drawMagnifiedRegionBorderIfNeeded", + TAG + ".recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded", FLAGS_MAGNIFICATION_CALLBACK, "displayId=" + displayId); } + final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId); if (displayMagnifier != null) { - displayMagnifier.drawMagnifiedRegionBorderIfNeeded(); + displayMagnifier.recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded(); } // Not relevant for the window observer. } @@ -936,11 +933,13 @@ final class AccessibilityController { } } - void drawMagnifiedRegionBorderIfNeeded() { + void recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded() { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { - mAccessibilityTracing.logTrace(LOG_TAG + ".drawMagnifiedRegionBorderIfNeeded", + mAccessibilityTracing.logTrace(LOG_TAG + + ".recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded", FLAGS_MAGNIFICATION_CALLBACK); } + recomputeBounds(); if (!Flags.alwaysDrawMagnificationFullscreenBorder()) { mMagnifiedViewport.drawWindowIfNeeded(); @@ -1245,7 +1244,6 @@ final class AccessibilityController { } void drawWindowIfNeeded() { - recomputeBounds(); mWindow.postDrawIfNeeded(); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 83745edd8132..42373aa85053 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -4260,7 +4260,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A PendingIntentRecord rec = apr.get(); if (rec != null) { mAtmService.mPendingIntentController.cancelIntentSender(rec, - false /* cleanActivity */); + false /* cleanActivity */, + PendingIntentRecord.CANCEL_REASON_HOSTING_ACTIVITY_DESTROYED); } } pendingResults = null; diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index a5687e6fa4ab..a0f615b1ea58 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3779,17 +3779,25 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } EventLogTags.writeWmEnterPip(r.mUserId, System.identityHashCode(r), r.shortComponentName, Boolean.toString(isAutoEnter)); - r.setPictureInPictureParams(params); - r.mAutoEnteringPip = isAutoEnter; - mRootWindowContainer.moveActivityToPinnedRootTask(r, - null /* launchIntoPipHostActivity */, "enterPictureInPictureMode", - transition); - // Continue the pausing process after entering pip. - if (r.isState(PAUSING) && r.mPauseSchedulePendingForPip) { - r.getTask().schedulePauseActivity(r, false /* userLeaving */, - false /* pauseImmediately */, true /* autoEnteringPip */, "auto-pip"); - } - r.mAutoEnteringPip = false; + + // Ensure the ClientTransactionItems are bundled for this operation. + deferWindowLayout(); + try { + r.setPictureInPictureParams(params); + r.mAutoEnteringPip = isAutoEnter; + mRootWindowContainer.moveActivityToPinnedRootTask(r, + null /* launchIntoPipHostActivity */, "enterPictureInPictureMode", + transition); + // Continue the pausing process after entering pip. + if (r.isState(PAUSING) && r.mPauseSchedulePendingForPip) { + r.getTask().schedulePauseActivity(r, false /* userLeaving */, + false /* pauseImmediately */, true /* autoEnteringPip */, + "auto-pip"); + } + r.mAutoEnteringPip = false; + } finally { + continueWindowLayout(); + } } }; diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index b9979adbafd0..d709fa5726f1 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -1762,18 +1762,39 @@ class BackNavigationController { } private void onBackNavigationDone(Bundle result, int backType) { - boolean triggerBack = result != null && result.getBoolean( - BackNavigationInfo.KEY_TRIGGER_BACK); - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onBackNavigationDone backType=%s, " - + "triggerBack=%b", backType, triggerBack); - - synchronized (mWindowManagerService.mGlobalLock) { - mNavigationMonitor.stopMonitorForRemote(); - mBackAnimationInProgress = false; - mShowWallpaper = false; - // All animation should be done, clear any un-send animation. - mPendingAnimation = null; - mPendingAnimationBuilder = null; + if (result == null) { + return; + } + if (result.containsKey(BackNavigationInfo.KEY_NAVIGATION_FINISHED)) { + final boolean triggerBack = result.getBoolean( + BackNavigationInfo.KEY_NAVIGATION_FINISHED); + ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onBackNavigationDone backType=%s, " + + "triggerBack=%b", backType, triggerBack); + + synchronized (mWindowManagerService.mGlobalLock) { + mNavigationMonitor.stopMonitorForRemote(); + mBackAnimationInProgress = false; + mShowWallpaper = false; + // All animation should be done, clear any un-send animation. + mPendingAnimation = null; + mPendingAnimationBuilder = null; + } + } + if (result.getBoolean(BackNavigationInfo.KEY_GESTURE_FINISHED)) { + synchronized (mWindowManagerService.mGlobalLock) { + final AnimationHandler ah = mAnimationHandler; + if (!ah.mComposed || ah.mWaitTransition || ah.mOpenActivities == null + || (ah.mSwitchType != AnimationHandler.TASK_SWITCH + && ah.mSwitchType != AnimationHandler.ACTIVITY_SWITCH)) { + return; + } + for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) { + final ActivityRecord preDrawActivity = mAnimationHandler.mOpenActivities[i]; + if (!preDrawActivity.mLaunchTaskBehind) { + setLaunchBehind(preDrawActivity); + } + } + } } } diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 45cf10bd3f5e..5aa0ed7ce76c 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -327,14 +327,14 @@ final class LetterboxConfiguration { R.dimen.config_letterboxBackgroundWallpaperBlurRadius); mLetterboxBackgroundWallpaperDarkScrimAlpha = mContext.getResources().getFloat( R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha); - mLetterboxHorizontalPositionMultiplier = mContext.getResources().getFloat( - R.dimen.config_letterboxHorizontalPositionMultiplier); - mLetterboxVerticalPositionMultiplier = mContext.getResources().getFloat( - R.dimen.config_letterboxVerticalPositionMultiplier); - mLetterboxBookModePositionMultiplier = mContext.getResources().getFloat( - R.dimen.config_letterboxBookModePositionMultiplier); - mLetterboxTabletopModePositionMultiplier = mContext.getResources().getFloat( - R.dimen.config_letterboxTabletopModePositionMultiplier); + setLetterboxHorizontalPositionMultiplier(mContext.getResources().getFloat( + R.dimen.config_letterboxHorizontalPositionMultiplier)); + setLetterboxVerticalPositionMultiplier(mContext.getResources().getFloat( + R.dimen.config_letterboxVerticalPositionMultiplier)); + setLetterboxBookModePositionMultiplier(mContext.getResources().getFloat( + R.dimen.config_letterboxBookModePositionMultiplier)); + setLetterboxTabletopModePositionMultiplier(mContext.getResources() + .getFloat(R.dimen.config_letterboxTabletopModePositionMultiplier)); mIsHorizontalReachabilityEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsHorizontalReachabilityEnabled); mIsVerticalReachabilityEnabled = mContext.getResources().getBoolean( @@ -657,29 +657,8 @@ final class LetterboxConfiguration { * right side. */ float getLetterboxHorizontalPositionMultiplier(boolean isInBookMode) { - if (isInBookMode) { - if (mLetterboxBookModePositionMultiplier < 0.0f - || mLetterboxBookModePositionMultiplier > 1.0f) { - Slog.w(TAG, - "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=true): " - + mLetterboxBookModePositionMultiplier); - // Default to left position if invalid value is provided. - return 0.0f; - } else { - return mLetterboxBookModePositionMultiplier; - } - } else { - if (mLetterboxHorizontalPositionMultiplier < 0.0f - || mLetterboxHorizontalPositionMultiplier > 1.0f) { - Slog.w(TAG, - "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=false):" - + mLetterboxBookModePositionMultiplier); - // Default to central position if invalid value is provided. - return 0.5f; - } else { - return mLetterboxHorizontalPositionMultiplier; - } - } + return isInBookMode ? mLetterboxBookModePositionMultiplier + : mLetterboxHorizontalPositionMultiplier; } /* @@ -689,37 +668,28 @@ final class LetterboxConfiguration { * bottom side. */ float getLetterboxVerticalPositionMultiplier(boolean isInTabletopMode) { - if (isInTabletopMode) { - return (mLetterboxTabletopModePositionMultiplier < 0.0f - || mLetterboxTabletopModePositionMultiplier > 1.0f) - // Default to top position if invalid value is provided. - ? 0.0f : mLetterboxTabletopModePositionMultiplier; - } else { - return (mLetterboxVerticalPositionMultiplier < 0.0f - || mLetterboxVerticalPositionMultiplier > 1.0f) - // Default to central position if invalid value is provided. - ? 0.5f : mLetterboxVerticalPositionMultiplier; - } + return isInTabletopMode ? mLetterboxTabletopModePositionMultiplier + : mLetterboxVerticalPositionMultiplier; } /** - * Overrides horizontal position of a center of the letterboxed app window. If given value < 0 - * or > 1, then it and a value of {@link - * com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier} are ignored and - * central position (0.5) is used. + * Overrides horizontal position of a center of the letterboxed app window. + * + * @throws IllegalArgumentException If given value < 0 or > 1. */ void setLetterboxHorizontalPositionMultiplier(float multiplier) { - mLetterboxHorizontalPositionMultiplier = multiplier; + mLetterboxHorizontalPositionMultiplier = assertValidMultiplier(multiplier, + "mLetterboxHorizontalPositionMultiplier"); } /** - * Overrides vertical position of a center of the letterboxed app window. If given value < 0 - * or > 1, then it and a value of {@link - * com.android.internal.R.dimen.config_letterboxVerticalPositionMultiplier} are ignored and - * central position (0.5) is used. + * Overrides vertical position of a center of the letterboxed app window. + * + * @throws IllegalArgumentException If given value < 0 or > 1. */ void setLetterboxVerticalPositionMultiplier(float multiplier) { - mLetterboxVerticalPositionMultiplier = multiplier; + mLetterboxVerticalPositionMultiplier = assertValidMultiplier(multiplier, + "mLetterboxVerticalPositionMultiplier"); } /** @@ -740,6 +710,28 @@ final class LetterboxConfiguration { com.android.internal.R.dimen.config_letterboxVerticalPositionMultiplier); } + /** + * Sets tabletop mode position multiplier. + * + * @throws IllegalArgumentException If given value < 0 or > 1. + */ + @VisibleForTesting + void setLetterboxTabletopModePositionMultiplier(float multiplier) { + mLetterboxTabletopModePositionMultiplier = assertValidMultiplier(multiplier, + "mLetterboxTabletopModePositionMultiplier"); + } + + /** + * Sets tabletop mode position multiplier. + * + * @throws IllegalArgumentException If given value < 0 or > 1. + */ + @VisibleForTesting + void setLetterboxBookModePositionMultiplier(float multiplier) { + mLetterboxBookModePositionMultiplier = assertValidMultiplier(multiplier, + "mLetterboxBookModePositionMultiplier"); + } + /* * Whether horizontal reachability repositioning is allowed for letterboxed fullscreen apps in * landscape device orientation. @@ -1356,4 +1348,21 @@ final class LetterboxConfiguration { void resetUserAppAspectRatioFullscreenEnabled() { setUserAppAspectRatioFullscreenOverrideEnabled(false); } + + /** + * Checks whether the multiplier is between [0,1]. + * + * @param multiplierName sent in the exception if multiplier is invalid, for easier debugging. + * + * @return multiplier, if valid + * @throws IllegalArgumentException if outside bounds. + */ + private float assertValidMultiplier(float multiplier, String multiplierName) + throws IllegalArgumentException { + if (multiplier < 0.0f || multiplier > 1.0f) { + throw new IllegalArgumentException("Trying to set " + multiplierName + + " out of bounds: " + multiplier); + } + return multiplier; + } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 3eea6ac81b04..e9a877e6013d 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2378,6 +2378,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent> return false; } + return resumeFocusedTasksTopActivitiesUnchecked(targetRootTask, target, targetOptions, + deferPause); + } + + @VisibleForTesting + boolean resumeFocusedTasksTopActivitiesUnchecked( + Task targetRootTask, ActivityRecord target, ActivityOptions targetOptions, + boolean deferPause) { boolean result = false; if (targetRootTask != null && (targetRootTask.isTopRootTaskInDisplayArea() || getTopDisplayFocusedRootTask() == targetRootTask)) { diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java index b43a4540bbde..8afcf0e1e05a 100644 --- a/services/core/java/com/android/server/wm/WindowAnimator.java +++ b/services/core/java/com/android/server/wm/WindowAnimator.java @@ -150,7 +150,9 @@ public class WindowAnimator { dc.checkAppWindowsReadyToShow(); } if (accessibilityController.hasCallbacks()) { - accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId); + accessibilityController + .recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded( + dc.mDisplayId); } if (dc.isAnimating(animationFlags, ANIMATION_TYPE_ALL)) { diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 731184fbc39c..d340272ee2c7 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -848,7 +848,12 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier); + try { + mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier); + } catch (IllegalArgumentException e) { + getErrPrintWriter().println("Error: invalid multiplier value " + e); + return -1; + } } return 0; } @@ -867,7 +872,12 @@ public class WindowManagerShellCommand extends ShellCommand { return -1; } synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(multiplier); + try { + mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(multiplier); + } catch (IllegalArgumentException e) { + getErrPrintWriter().println("Error: invalid multiplier value " + e); + return -1; + } } return 0; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index e1ad1be2ef68..8c9317a32483 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -69,6 +69,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANI import static com.android.server.wm.ActivityRecord.State.PAUSING; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; +import static com.android.server.wm.ActivityTaskManagerService.isPip2ExperimentEnabled; import static com.android.server.wm.ActivityTaskSupervisor.REMOVE_FROM_RECENTS; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; @@ -829,18 +830,20 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } private int applyTaskChanges(Task tr, WindowContainerTransaction.Change c) { + final boolean wasPrevFocusableAndVisible = tr.isFocusableAndVisible(); + int effects = applyChanges(tr, c); final SurfaceControl.Transaction t = c.getBoundsChangeTransaction(); if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_HIDDEN) != 0) { if (tr.setForceHidden(FLAG_FORCE_HIDDEN_FOR_TASK_ORG, c.getHidden())) { - effects = TRANSACT_EFFECTS_LIFECYCLE; + effects |= TRANSACT_EFFECTS_LIFECYCLE; } } if ((c.getChangeMask() & CHANGE_FORCE_TRANSLUCENT) != 0) { tr.setForceTranslucent(c.getForceTranslucent()); - effects = TRANSACT_EFFECTS_LIFECYCLE; + effects |= TRANSACT_EFFECTS_LIFECYCLE; } if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING) != 0) { @@ -873,8 +876,17 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub boolean canEnterPip = activity.checkEnterPictureInPictureState( "applyTaskChanges", true /* beforeStopping */); if (canEnterPip) { - canEnterPip = mService.mActivityClientController - .requestPictureInPictureMode(activity); + mService.mTaskSupervisor.beginDeferResume(); + try { + canEnterPip = mService.mActivityClientController + .requestPictureInPictureMode(activity); + } finally { + mService.mTaskSupervisor.endDeferResume(); + if (canEnterPip && !isPip2ExperimentEnabled()) { + // Wait until the transaction is applied to only resume once. + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + } } if (!canEnterPip) { // Restore the flag to its previous state when the activity cannot enter PIP. @@ -883,6 +895,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } } + // Activity in this Task may resume/pause when enter/exit pip. + if (wasPrevFocusableAndVisible != tr.isFocusableAndVisible()) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + return effects; } @@ -948,7 +965,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } if ((c.getChangeMask() & CHANGE_FORCE_TRANSLUCENT) != 0) { taskFragment.setForceTranslucent(c.getForceTranslucent()); - effects = TRANSACT_EFFECTS_LIFECYCLE; + effects |= TRANSACT_EFFECTS_LIFECYCLE; } effects |= applyChanges(taskFragment, c); @@ -2293,6 +2310,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub TaskFragmentOrganizerToken organizerToken = creationParams.getOrganizer(); taskFragment.setTaskFragmentOrganizer(organizerToken, ownerActivity.getUid(), ownerActivity.info.processName); + if (mTaskFragmentOrganizerController.isSystemOrganizer(organizerToken.asBinder())) { + taskFragment.setOverrideOrientation(creationParams.getOverrideOrientation()); + } final int position; if (creationParams.getPairedPrimaryFragmentToken() != null) { // When there is a paired primary TaskFragment, we want to place the new TaskFragment diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index b0eee0881f63..1666fef13685 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -1708,7 +1708,6 @@ public class DisplayManagerServiceTest { * {@link VirtualDisplayConfig.Builder#setSurface(Surface)} */ @Test - @FlakyTest(bugId = 127687569) public void testCreateVirtualDisplay_setSurface() throws Exception { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); registerDefaultDisplays(displayManager); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java index 783971a1afe4..89b48bad2358 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java @@ -16,9 +16,18 @@ package com.android.server.am; +import static android.os.Process.INVALID_UID; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_NULL; +import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_ONE_SHOT_SENT; +import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_CANCELED; +import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_OWNER_FORCE_STOPPED; +import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_SUPERSEDED; +import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_USER_STOPPED; +import static com.android.server.am.PendingIntentRecord.cancelReasonToString; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.anyInt; @@ -34,6 +43,7 @@ import android.app.PendingIntent; import android.content.Intent; import android.content.pm.IPackageManager; import android.os.Looper; +import android.os.UserHandle; import androidx.test.runner.AndroidJUnit4; @@ -54,6 +64,7 @@ public class PendingIntentControllerTest { private static final String TEST_PACKAGE_NAME = "test-package-1"; private static final String TEST_FEATURE_ID = "test-feature-1"; private static final int TEST_CALLING_UID = android.os.Process.myUid(); + private static final int TEST_USER_ID = 0; private static final Intent[] TEST_INTENTS = new Intent[]{new Intent("com.test.intent")}; @Mock @@ -92,7 +103,7 @@ public class PendingIntentControllerTest { private PendingIntentRecord createPendingIntentRecord(int flags) { return mPendingIntentController.getIntentSender(ActivityManager.INTENT_SENDER_BROADCAST, - TEST_PACKAGE_NAME, TEST_FEATURE_ID, TEST_CALLING_UID, 0, null, null, 0, + TEST_PACKAGE_NAME, TEST_FEATURE_ID, TEST_CALLING_UID, TEST_USER_ID, null, null, 0, TEST_INTENTS, null, flags, null); } @@ -126,6 +137,54 @@ public class PendingIntentControllerTest { piCaptor.getValue().getTarget()); } + @Test + public void testCancellationReason() { + { + final PendingIntentRecord pir = createPendingIntentRecord(0); + assertCancelReason(CANCEL_REASON_NULL, pir.cancelReason); + } + + { + final PendingIntentRecord pir = createPendingIntentRecord(0); + mPendingIntentController.cancelIntentSender(pir); + assertCancelReason(CANCEL_REASON_OWNER_CANCELED, pir.cancelReason); + } + + { + final PendingIntentRecord pir = createPendingIntentRecord(0); + createPendingIntentRecord(PendingIntent.FLAG_CANCEL_CURRENT); + assertCancelReason(CANCEL_REASON_SUPERSEDED, pir.cancelReason); + } + + { + final PendingIntentRecord pir = createPendingIntentRecord(PendingIntent.FLAG_ONE_SHOT); + pir.send(0, null, null, null, null, null, null); + assertCancelReason(CANCEL_REASON_ONE_SHOT_SENT, pir.cancelReason); + } + + { + final PendingIntentRecord pir = createPendingIntentRecord(0); + mPendingIntentController.removePendingIntentsForPackage(TEST_PACKAGE_NAME, + TEST_USER_ID, UserHandle.getAppId(TEST_CALLING_UID), true, + CANCEL_REASON_OWNER_FORCE_STOPPED); + assertCancelReason(CANCEL_REASON_OWNER_FORCE_STOPPED, pir.cancelReason); + } + + { + final PendingIntentRecord pir = createPendingIntentRecord(0); + mPendingIntentController.removePendingIntentsForPackage(null, + TEST_USER_ID, INVALID_UID, true, + CANCEL_REASON_USER_STOPPED); + assertCancelReason(CANCEL_REASON_USER_STOPPED, pir.cancelReason); + } + } + + private void assertCancelReason(int expectedReason, int actualReason) { + final String errMsg = "Expected: " + cancelReasonToString(expectedReason) + + "; Actual: " + cancelReasonToString(actualReason); + assertEquals(errMsg, expectedReason, actualReason); + } + @After public void tearDown() { if (mMockingSession != null) { diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index 4e059b4b7432..8024915692aa 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -17,7 +17,6 @@ package com.android.server; import static com.android.server.GestureLauncherService.CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS; -import static com.android.server.GestureLauncherService.EMERGENCY_GESTURE_TAP_DETECTION_MIN_TIME_MS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -773,6 +772,9 @@ public class GestureLauncherServiceTest { @Test public void testInterceptPowerKeyDown_triggerEmergency_fiveFastTaps_gestureIgnored() { + when(mResources.getInteger( + com.android.internal.R.integer.config_defaultMinEmergencyGestureTapDurationMillis)) + .thenReturn(200); // Trigger emergency by tapping button 5 times long eventTime = triggerEmergencyGesture(/* tapIntervalMs= */ 1); @@ -1449,7 +1451,7 @@ public class GestureLauncherServiceTest { long emergencyGestureTapDetectionMinTimeMs = Settings.Global.getInt( mContext.getContentResolver(), Settings.Global.EMERGENCY_GESTURE_TAP_DETECTION_MIN_TIME_MS, - EMERGENCY_GESTURE_TAP_DETECTION_MIN_TIME_MS); + 200); assertTrue(intercepted); if (tapIntervalMs * 4 > emergencyGestureTapDetectionMinTimeMs) { assertTrue(outLaunched.value); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 1bf9a9d02431..5a1785175be7 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -69,10 +69,9 @@ import android.os.Handler; import android.os.IBinder; import android.os.LocaleList; import android.os.UserHandle; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; @@ -144,8 +143,7 @@ public class AccessibilityManagerServiceTest { ApplicationProvider.getApplicationContext()); @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final int ACTION_ID = 20; private static final String LABEL = "label"; @@ -624,7 +622,7 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) public void testOnClientChange_magnificationTwoFingerTripleTapEnabled_requestConnection() { when(mProxyManager.canRetrieveInteractiveWindowsLocked()).thenReturn(false); @@ -642,7 +640,7 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) public void testOnClientChange_magnificationTwoFingerTripleTapDisabled_requestDisconnection() { when(mProxyManager.canRetrieveInteractiveWindowsLocked()).thenReturn(false); @@ -704,7 +702,7 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) public void onClientChange_magnificationTwoFingerTripleTapDisabled_removeMagnificationButton() { final AccessibilityUserState userState = mA11yms.mUserStates.get( mA11yms.getCurrentUserIdLocked()); @@ -720,7 +718,7 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) public void onClientChange_magnificationTwoFingerTripleTapEnabled_keepMagnificationButton() { final AccessibilityUserState userState = mA11yms.mUserStates.get( mA11yms.getCurrentUserIdLocked()); @@ -772,7 +770,7 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test - @RequiresFlagsDisabled(com.android.systemui.Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) + @DisableFlags(com.android.systemui.Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) public void testPerformAccessibilityShortcut_hearingAids_startActivityWithExpectedComponent() { final AccessibilityUserState userState = mA11yms.mUserStates.get( mA11yms.getCurrentUserIdLocked()); @@ -790,7 +788,7 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test - @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) + @EnableFlags(com.android.systemui.Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) public void testPerformAccessibilityShortcut_hearingAids_sendExpectedBroadcast() { final AccessibilityUserState userState = mA11yms.mUserStates.get( mA11yms.getCurrentUserIdLocked()); @@ -949,7 +947,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES) + @EnableFlags(FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES) public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( @@ -1008,6 +1006,33 @@ public class AccessibilityManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_HARDWARE_SHORTCUT_DISABLES_WARNING) + public void enableHardwareShortcutsForTargets_shortcutDialogSetting_isShown() { + Settings.Secure.putInt( + mTestableContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, + AccessibilityShortcutController.DialogStatus.NOT_SHOWN + ); + + mockManageAccessibilityGranted(mTestableContext); + setupShortcutTargetServices(); + String target = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString(); + + mA11yms.enableShortcutsForTargets( + /* enable= */ true, + UserShortcutType.HARDWARE, + List.of(target), + mA11yms.getCurrentUserIdLocked()); + mTestableLooper.processAllMessages(); + + assertThat(Settings.Secure.getInt( + mTestableContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, + AccessibilityShortcutController.DialogStatus.NOT_SHOWN)) + .isEqualTo(AccessibilityShortcutController.DialogStatus.SHOWN); + } + + @Test public void enableShortcutsForTargets_disableSoftwareShortcut_shortcutTurnedOff() throws Exception { String target = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString(); @@ -1341,7 +1366,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_statusBarServiceNotGranted_throwsException() { mTestableContext.getTestablePermissions().setPermission( Manifest.permission.STATUS_BAR_SERVICE, PackageManager.PERMISSION_DENIED); @@ -1355,7 +1380,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_manageAccessibilityNotGranted_throwsException() { mockStatusBarServiceGranted(mTestableContext); mTestableContext.getTestablePermissions().setPermission( @@ -1369,7 +1394,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_qsTileChanges_updateA11yTilesInQsPanel() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); @@ -1389,7 +1414,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_sameQsTiles_noUpdateToA11yTilesInQsPanel() { notifyQuickSettingsTilesChanged_qsTileChanges_updateA11yTilesInQsPanel(); List<ComponentName> tiles = @@ -1406,7 +1431,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_serviceWarningRequired_qsShortcutRemainDisabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); @@ -1424,7 +1449,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_serviceWarningNotRequired_qsShortcutEnabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); @@ -1446,7 +1471,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_addFrameworkTile_qsShortcutEnabled() { mockStatusBarServiceGranted(mTestableContext); mockManageAccessibilityGranted(mTestableContext); @@ -1469,7 +1494,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void notifyQuickSettingsTilesChanged_removeFrameworkTile_qsShortcutDisabled() { notifyQuickSettingsTilesChanged_addFrameworkTile_qsShortcutEnabled(); Set<ComponentName> qsTiles = mA11yms.getCurrentUserState().getA11yQsTilesInQsPanel(); @@ -1487,7 +1512,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void restoreAccessibilityQsTargets_a11yQsTargetsRestored() { String daltonizerTile = AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(); @@ -1510,7 +1535,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsDisabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + @DisableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) public void restoreAccessibilityQsTargets_a11yQsTargetsNotRestored() { String daltonizerTile = AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(); diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 515898a883e8..e6cf0c38a10c 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -50,6 +50,7 @@ android_test { "SettingsLib", "libprotobuf-java-lite", "platformprotoslite", + "platform-parametric-runner-lib", ], libs: [ 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 4a61d32d00b2..805bc172035e 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -106,22 +106,23 @@ import static android.service.notification.NotificationListenerService.Ranking.U import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; - import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; +import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION; import static com.android.server.notification.NotificationManagerService.DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; import static com.android.server.notification.NotificationManagerService.NOTIFICATION_TTL; +import static com.android.server.notification.NotificationManagerService.TAG; import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_ADJUSTED; import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED; import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_UPDATED; - import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; - +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -130,7 +131,6 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertSame; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; - import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.isNull; @@ -138,10 +138,24 @@ import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import android.Manifest; import android.annotation.Nullable; @@ -168,6 +182,8 @@ import android.app.RemoteInput; import android.app.RemoteInputHistoryItem; import android.app.StatsManager; import android.app.admin.DevicePolicyManagerInternal; +import android.app.job.JobScheduler; +import android.app.role.RoleManager; import android.app.usage.UsageStatsManagerInternal; import android.companion.AssociationInfo; import android.companion.AssociationRequest; @@ -187,6 +203,7 @@ import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; +import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutServiceInternal; import android.content.pm.UserInfo; @@ -217,6 +234,7 @@ import android.os.WorkSource; import android.permission.PermissionManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.FlagsParameterization; import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.rule.LimitDevicesRule; import android.provider.DeviceConfig; @@ -235,7 +253,8 @@ import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import android.telecom.TelecomManager; -import android.testing.AndroidTestingRunner; +import android.testing.TestWithLooperRule; +import android.testing.TestableContentResolver; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import android.testing.TestablePermissions; @@ -245,13 +264,13 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; +import android.util.Log; import android.util.Pair; import android.util.Xml; +import android.view.accessibility.AccessibilityManager; import android.widget.RemoteViews; - import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; - import com.android.internal.R; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.config.sysui.TestableFlagResolver; @@ -259,6 +278,7 @@ import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.InstanceIdSequenceFake; import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.statusbar.NotificationVisibility; +import com.android.internal.widget.LockPatternUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.DeviceIdleInternal; @@ -282,13 +302,10 @@ import com.android.server.uri.UriGrantsManagerInternal; import com.android.server.utils.quota.MultiRateLimiter; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; - import com.google.android.collect.Lists; import com.google.common.collect.ImmutableList; - import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; - import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -306,6 +323,8 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -322,9 +341,9 @@ import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; @SmallTest -@RunWith(AndroidTestingRunner.class) -@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. +@RunWith(ParameterizedAndroidJunit4.class) @RunWithLooper +@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. public class NotificationManagerServiceTest extends UiServiceTestCase { private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId"; private static final String TEST_PACKAGE = "The.name.is.Package.Test.Package"; @@ -369,6 +388,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Mock private PermissionHelper mPermissionHelper; private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake(); + @Rule(order = Integer.MAX_VALUE) + public TestWithLooperRule mlooperRule = new TestWithLooperRule(); private TestableLooper mTestableLooper; @Mock private RankingHelper mRankingHelper; @@ -415,8 +436,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private final TestPostNotificationTrackerFactory mPostNotificationTrackerFactory = new TestPostNotificationTrackerFactory(); - @Mock - IIntentSender pi1; + private PendingIntent mActivityIntent; + private PendingIntent mActivityIntentImmutable; private static final int MAX_POST_DELAY = 1000; @@ -465,6 +486,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { StatsManager mStatsManager; @Mock AlarmManager mAlarmManager; + @Mock JobScheduler mJobScheduler; @Mock MultiRateLimiter mToastRateLimiter; BroadcastReceiver mPackageIntentReceiver; @@ -508,6 +530,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } } + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return FlagsParameterization.allCombinationsOf( + FLAG_ALL_NOTIFS_NEED_TTL); + } + + public NotificationManagerServiceTest(FlagsParameterization flags) { + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() throws Exception { // Shell permisssions will override permissions of our app, so add all necessary permissions @@ -540,12 +572,22 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternal); LocalServices.removeServiceForTest(PermissionPolicyInternal.class); LocalServices.addService(PermissionPolicyInternal.class, mPermissionPolicyInternal); + LocalServices.removeServiceForTest(ShortcutServiceInternal.class); + LocalServices.addService(ShortcutServiceInternal.class, mShortcutServiceInternal); mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager); mContext.addMockSystemService(NotificationManager.class, mMockNm); + mContext.addMockSystemService(RoleManager.class, mock(RoleManager.class)); + mContext.addMockSystemService(Context.LAUNCHER_APPS_SERVICE, mLauncherApps); + mContext.addMockSystemService(Context.USER_SERVICE, mUm); + mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, + mock(AccessibilityManager.class)); doNothing().when(mContext).sendBroadcast(any(), anyString()); doNothing().when(mContext).sendBroadcastAsUser(any(), any()); doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any()); + TestableContentResolver cr = mock(TestableContentResolver.class); + when(mContext.getContentResolver()).thenReturn(cr); + doNothing().when(cr).registerContentObserver(any(), anyBoolean(), any(), anyInt()); setDpmAppOppsExemptFromDismissal(false); @@ -648,11 +690,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { }); // TODO (b/291907312): remove feature flag - // NOTE: Prefer using the @EnableFlag annotation where possible. Do not add any android.app + // NOTE: Prefer using the @EnableFlags annotation where possible. Do not add any android.app // flags here. mSetFlagsRule.disableFlags( Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE); + mActivityIntent = spy(PendingIntent.getActivity(mContext, 0, + new Intent().setPackage(mPkg), PendingIntent.FLAG_MUTABLE)); + mActivityIntentImmutable = spy(PendingIntent.getActivity(mContext, 0, + new Intent().setPackage(mPkg), FLAG_IMMUTABLE)); + initNMS(); } @@ -689,6 +736,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mPowerManager, mPostNotificationTrackerFactory); mService.setAttentionHelper(mAttentionHelper); + mService.setLockPatternUtils(mock(LockPatternUtils.class)); // Return first true for RoleObserver main-thread check when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false); @@ -749,7 +797,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } assertNotNull("package intent receiver should exist", mPackageIntentReceiver); assertNotNull("User-switch receiver should exist", mUserSwitchIntentReceiver); - assertNotNull("Notification timeout receiver should exist", mNotificationTimeoutReceiver); + if (!Flags.allNotifsNeedTtl()) { + assertNotNull("Notification timeout receiver should exist", + mNotificationTimeoutReceiver); + } // Pretend the shortcut exists List<ShortcutInfo> shortcutInfos = new ArrayList<>(); @@ -834,9 +885,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { if (mFile != null) mFile.delete(); clearDeviceConfig(); + if (mActivityIntent != null) { + mActivityIntent.cancel(); + } + + mService.clearNotifications(); + TestableLooper.get(this).processAllMessages(); + try { mService.onDestroy(); } catch (IllegalStateException | IllegalArgumentException e) { + Log.e(TAG, "failed to destroy", e); // can throw if a broadcast receiver was never registered } @@ -846,6 +905,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // could cause issues, for example, messages that remove/cancel shown toasts (this causes // problematic interactions with mocks when they're no longer working as expected). mWorkerHandler.removeCallbacksAndMessages(null); + + if (TestableLooper.get(this) != null) { + // Must remove static reference to this test object to prevent leak (b/261039202) + TestableLooper.remove(this); + } } private void simulatePackageSuspendBroadcast(boolean suspend, String pkg, @@ -1005,7 +1069,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon) - .addAction(new Notification.Action.Builder(null, "test", null).build()); + .addAction(new Notification.Action.Builder(null, "test", mActivityIntent).build()) + .addAction(new Notification.Action.Builder( + null, "test", mActivityIntentImmutable).build()); if (extender != null) { nb.extend(extender); } @@ -1045,18 +1111,20 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private NotificationRecord generateMessageBubbleNotifRecord(NotificationChannel channel, String tag) { - return generateMessageBubbleNotifRecord(true, channel, 1, tag, null, false); + return generateMessageBubbleNotifRecord(true, channel, 1, tag, null, false, true); } private NotificationRecord generateMessageBubbleNotifRecord(boolean addMetadata, - NotificationChannel channel, int id, String tag, String groupKey, boolean isSummary) { + NotificationChannel channel, int id, String tag, String groupKey, boolean isSummary, + boolean mutable) { if (channel == null) { channel = mTestNotificationChannel; } if (tag == null) { tag = "tag"; } - Notification.Builder nb = getMessageStyleNotifBuilder(addMetadata, groupKey, isSummary); + Notification.Builder nb = getMessageStyleNotifBuilder(addMetadata, groupKey, + isSummary, mutable); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, id, tag, mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0); @@ -1129,18 +1197,15 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } private Notification.Builder getMessageStyleNotifBuilder(boolean addBubbleMetadata, - String groupKey, boolean isSummary) { + String groupKey, boolean isSummary, boolean mutable) { // Give it a person Person person = new Person.Builder() .setName("bubblebot") .build(); RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build(); - PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, - new Intent().setPackage(mContext.getPackageName()), - PendingIntent.FLAG_MUTABLE); Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon); Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply", - inputIntent).addRemoteInput(remoteInput) + mutable ? mActivityIntent : mActivityIntentImmutable).addRemoteInput(remoteInput) .build(); // Make it messaging style Notification.Builder nb = new Notification.Builder(mContext, @@ -1167,17 +1232,14 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } private Notification.BubbleMetadata getBubbleMetadata() { - PendingIntent pendingIntent = mock(PendingIntent.class); - Intent intent = mock(Intent.class); - when(pendingIntent.getIntent()).thenReturn(intent); - when(pendingIntent.getTarget()).thenReturn(pi1); - ActivityInfo info = new ActivityInfo(); info.resizeMode = RESIZE_MODE_RESIZEABLE; - when(intent.resolveActivityInfo(any(), anyInt())).thenReturn(info); + ResolveInfo ri = new ResolveInfo(); + ri.activityInfo = info; + when(mPackageManagerClient.resolveActivity(any(), anyInt())).thenReturn(ri); return new Notification.BubbleMetadata.Builder( - pendingIntent, + mActivityIntent, Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon)) .build(); } @@ -1189,7 +1251,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Notification that has bubble metadata NotificationRecord nrBubble = generateMessageBubbleNotifRecord(true /* addMetadata */, - mTestNotificationChannel, 1 /* id */, "tag", groupKey, false /* isSummary */); + mTestNotificationChannel, 1 /* id */, "tag", groupKey, false /* isSummary */, + true); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, nrBubble.getSbn().getTag(), nrBubble.getSbn().getId(), nrBubble.getSbn().getNotification(), @@ -1203,7 +1266,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Notification without bubble metadata NotificationRecord nrPlain = generateMessageBubbleNotifRecord(false /* addMetadata */, - mTestNotificationChannel, 2 /* id */, "tag", groupKey, false /* isSummary */); + mTestNotificationChannel, 2 /* id */, "tag", groupKey, false /* isSummary */, + true); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, nrPlain.getSbn().getTag(), nrPlain.getSbn().getId(), nrPlain.getSbn().getNotification(), @@ -1215,7 +1279,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Summary notification for both of those NotificationRecord nrSummary = generateMessageBubbleNotifRecord(false /* addMetadata */, - mTestNotificationChannel, 3 /* id */, "tag", groupKey, true /* isSummary */); + mTestNotificationChannel, 3 /* id */, "tag", groupKey, true /* isSummary */, + true); if (summaryAutoCancel) { nrSummary.getNotification().flags |= FLAG_AUTO_CANCEL; @@ -1232,6 +1297,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_ALL_NOTIFS_NEED_TTL) public void testLimitTimeOutBroadcast() { NotificationChannel channel = new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_HIGH); @@ -2501,8 +2567,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testCancelWithTagDoesNotCancelLifetimeExtended() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); final NotificationRecord notif = generateNotificationRecord(null); notif.getSbn().getNotification().flags = Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; @@ -2529,19 +2595,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(captor.getValue().getNotification().flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); - - mSetFlagsRule.disableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); - mBinderService.cancelNotificationWithTag(mPkg, mPkg, sbn.getTag(), sbn.getId(), - sbn.getUserId()); - waitForIdle(); - - assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(0); - assertThat(mService.getNotificationRecordCount()).isEqualTo(0); } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testCancelAllDoesNotCancelLifetimeExtended() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); // Adds a lifetime extended notification. final NotificationRecord notif = generateNotificationRecord(mTestNotificationChannel, 1, null, false); @@ -2978,9 +3036,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testCancelNotificationsFromListener_clearAll_NoClearLifetimeExt() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); final NotificationRecord notif = generateNotificationRecord( mTestNotificationChannel, 1, null, false); notif.getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; @@ -3211,9 +3269,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testCancelNotificationsFromListener_byKey_NoClearLifetimeExt() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); final NotificationRecord notif = generateNotificationRecord( mTestNotificationChannel, 3, null, false); notif.getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; @@ -5695,12 +5753,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mZenModeHelper).updateZenRulesOnLocaleChange(); } - private void simulateNotificationTimeoutBroadcast(String notificationKey) { - final Bundle extras = new Bundle(); - extras.putString(EXTRA_KEY, notificationKey); - final Intent intent = new Intent(ACTION_NOTIFICATION_TIMEOUT); - intent.putExtras(extras); - mNotificationTimeoutReceiver.onReceive(getContext(), intent); + private void simulateNotificationTimeout(String notificationKey) { + if (Flags.allNotifsNeedTtl()) { + mService.mNotificationManagerPrivate.timeoutNotification(notificationKey); + } else { + final Bundle extras = new Bundle(); + extras.putString(EXTRA_KEY, notificationKey); + final Intent intent = new Intent(ACTION_NOTIFICATION_TIMEOUT); + intent.putExtras(extras); + mNotificationTimeoutReceiver.onReceive(getContext(), intent); + } } @Test @@ -5709,7 +5771,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mTestNotificationChannel, 1, null, false); mService.addNotification(notif); - simulateNotificationTimeoutBroadcast(notif.getKey()); + simulateNotificationTimeout(notif.getKey()); waitForIdle(); // Check that the notification was cancelled. @@ -5725,7 +5787,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { notif.getSbn().getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE; mService.addNotification(notif); - simulateNotificationTimeoutBroadcast(notif.getKey()); + simulateNotificationTimeout(notif.getKey()); waitForIdle(); // Check that the notification was not cancelled. @@ -5741,7 +5803,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { notif.getSbn().getNotification().flags = Notification.FLAG_USER_INITIATED_JOB; mService.addNotification(notif); - simulateNotificationTimeoutBroadcast(notif.getKey()); + simulateNotificationTimeout(notif.getKey()); waitForIdle(); // Check that the notification was not cancelled. @@ -5751,15 +5813,15 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testTimeout_NoCancelLifetimeExtensionNotification() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); // Create a notification with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY final NotificationRecord notif = generateNotificationRecord(null); notif.getSbn().getNotification().flags = Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; mService.addNotification(notif); - simulateNotificationTimeoutBroadcast(notif.getKey()); + simulateNotificationTimeout(notif.getKey()); waitForIdle(); // Check that the notification was not cancelled. @@ -5839,8 +5901,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testStats_DirectReplyLifetimeExtendedPostsUpdate() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); r.getSbn().getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; mService.addNotification(r); @@ -6662,6 +6724,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(visitor, times(1)).accept(eq(personIcon.getUri())); verify(visitor, times(1)).accept(eq(verificationIcon.getUri())); verify(visitor, times(1)).accept(eq(hangUpUri)); + hangUpIntent.cancel(); } @Test @@ -6691,6 +6754,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(visitor, times(1)).accept(eq(verificationIcon.getUri())); verify(visitor, times(1)).accept(eq(answerIntent.getIntent().getData())); verify(visitor, times(1)).accept(eq(declineUri)); + answerIntent.cancel(); + declineIntent.cancel(); } @Test @@ -6767,6 +6832,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(visitor, times(1)).accept(eq(actionIntentUri)); verify(visitor).accept(eq(wearActionIcon.getUri())); verify(visitor, times(1)).accept(eq(wearActionIntentUri)); + displayIntent.cancel(); + actionIntent.cancel(); + wearActionIntent.cancel(); } @Test @@ -6788,6 +6856,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(visitor, times(1)).accept(eq(contentIntentUri)); verify(visitor, times(1)).accept(eq(deleteIntentUri)); + contentIntent.cancel(); + deleteIntent.cancel(); } @Test @@ -6813,6 +6883,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(visitor, times(1)).accept(eq(readPendingIntentUri)); verify(visitor, times(1)).accept(eq(replyPendingIntentUri)); + readPendingIntent.cancel(); + replyPendingIntent.cancel(); } @Test @@ -8587,8 +8659,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testOnNotificationSmartReplySent() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); final int replyIndex = 2; final String reply = "Hello"; final boolean modifiedBeforeSending = true; @@ -8613,8 +8685,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testStats_SmartReplyAlreadyLifetimeExtendedPostsUpdate() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); final int replyIndex = 2; final String reply = "Hello"; final boolean modifiedBeforeSending = true; @@ -8647,8 +8719,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testOnNotificationActionClick() { final int actionIndex = 2; final Notification.Action action = - new Notification.Action.Builder(null, "text", PendingIntent.getActivity( - mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build(); + new Notification.Action.Builder(null, "text", mActivityIntent).build(); final boolean generatedByAssistant = false; NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); @@ -8669,9 +8740,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testActionClickLifetimeExtendedCancel() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); - final Notification.Action action = new Notification.Action.Builder(null, "text", PendingIntent.getActivity( mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build(); @@ -8797,8 +8867,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testOnAssistantNotificationActionClick() { final int actionIndex = 1; final Notification.Action action = - new Notification.Action.Builder(null, "text", PendingIntent.getActivity( - mContext, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)).build(); + new Notification.Action.Builder(null, "text", mActivityIntent).build(); final boolean generatedByAssistant = true; NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); @@ -9313,7 +9382,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { BUBBLE_PREFERENCE_ALL /* app */, true /* channel */); - Notification.Builder nb = getMessageStyleNotifBuilder(true, null, false); + Notification.Builder nb = getMessageStyleNotifBuilder(true, null, false, true); nb.setShortcutId(null); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, null, mUid, 0, @@ -9357,7 +9426,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Messaging notif WITHOUT bubble metadata Notification.Builder nb = getMessageStyleNotifBuilder(false /* addBubbleMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, "testFlagBubbleNotifs_noFlag_notBubble", mUid, 0, @@ -10615,7 +10684,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(VALID_CONVO_SHORTCUT_ID).build(); Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(VALID_CONVO_SHORTCUT_ID); nb.setBubbleMetadata(metadata); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, @@ -10675,7 +10744,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( shortcutId).build(); Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(shortcutId); nb.setBubbleMetadata(metadata); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, @@ -11068,7 +11137,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { //Create notification record Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(VALID_CONVO_SHORTCUT_ID); nb.setChannelId(originalChannel.getId()); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, @@ -11104,7 +11173,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { //Create notification record Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(VALID_CONVO_SHORTCUT_ID); nb.setChannelId(originalChannel.getId()); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, @@ -11148,7 +11217,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { //Create notification record Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(VALID_CONVO_SHORTCUT_ID); nb.setChannelId(originalChannel.getId()); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, @@ -11193,7 +11262,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { //Create notification record without a shortcutId Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(null); nb.setChannelId(originalChannel.getId()); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, @@ -11328,7 +11397,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testRecordMessages_invalidMsg() throws RemoteException { Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(null); StatusBarNotification sbn = new StatusBarNotification(PKG_P, PKG_P, 1, "testRecordMessages_invalidMsg", mUid, 0, nb.build(), @@ -11369,7 +11438,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testRecordMessages_validMsg() throws RemoteException { Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(null); StatusBarNotification sbn = new StatusBarNotification(PKG_P, PKG_P, 1, "testRecordMessages_validMsg", mUid, 0, nb.build(), @@ -11405,7 +11474,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, - null /* groupKey */, false /* isSummary */); + null /* groupKey */, false /* isSummary */, true); nb.setShortcutId(null); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 1, "testRecordMessages_invalidMsg_afterValidMsg_2", mUid, 0, nb.build(), @@ -11625,10 +11694,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testImmutableBubbleIntent() throws Exception { - when(mAmi.getPendingIntentFlags(pi1)) - .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); + when(mAmi.getPendingIntentFlags(any())).thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); NotificationRecord r = generateMessageBubbleNotifRecord(true, - mTestNotificationChannel, 7, "testImmutableBubbleIntent", null, false); + mTestNotificationChannel, 7, "testImmutableBubbleIntent", null, false, false); try { mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(), r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId()); @@ -11642,10 +11710,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMutableBubbleIntent() throws Exception { - when(mAmi.getPendingIntentFlags(pi1)) - .thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT); NotificationRecord r = generateMessageBubbleNotifRecord(true, - mTestNotificationChannel, 7, "testMutableBubbleIntent", null, false); + mTestNotificationChannel, 7, "testMutableBubbleIntent", null, false, true); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(), r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId()); @@ -11658,10 +11724,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testImmutableDirectReplyActionIntent() throws Exception { - when(mAmi.getPendingIntentFlags(any(IIntentSender.class))) - .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); + when(mAmi.getPendingIntentFlags(any())).thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); NotificationRecord r = generateMessageBubbleNotifRecord(false, - mTestNotificationChannel, 7, "testImmutableDirectReplyActionIntent", null, false); + mTestNotificationChannel, 7, "testImmutableDirectReplyActionIntent", null, false, + false); try { mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(), r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId()); @@ -11675,10 +11741,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMutableDirectReplyActionIntent() throws Exception { - when(mAmi.getPendingIntentFlags(any(IIntentSender.class))) - .thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT); NotificationRecord r = generateMessageBubbleNotifRecord(false, - mTestNotificationChannel, 7, "testMutableDirectReplyActionIntent", null, false); + mTestNotificationChannel, 7, "testMutableDirectReplyActionIntent", null, false, + true); mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(), r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId()); @@ -11690,18 +11755,15 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testImmutableDirectReplyContextualActionIntent() throws Exception { - when(mAmi.getPendingIntentFlags(any(IIntentSender.class))) - .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); + when(mAmi.getPendingIntentFlags(any())).thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); ArrayList<Notification.Action> extraAction = new ArrayList<>(); RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build(); - PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, new Intent(), - PendingIntent.FLAG_IMMUTABLE); Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon); Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply", - inputIntent).addRemoteInput(remoteInput) + mActivityIntentImmutable).addRemoteInput(remoteInput) .build(); extraAction.add(replyAction); Bundle signals = new Bundle(); @@ -11722,18 +11784,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMutableDirectReplyContextualActionIntent() throws Exception { - when(mAmi.getPendingIntentFlags(any(IIntentSender.class))) - .thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT); when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); ArrayList<Notification.Action> extraAction = new ArrayList<>(); RemoteInput remoteInput = new RemoteInput.Builder("reply_key").setLabel("reply").build(); - PendingIntent inputIntent = PendingIntent.getActivity(mContext, 0, - new Intent().setPackage(mContext.getPackageName()), - PendingIntent.FLAG_MUTABLE); Icon icon = Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon); Notification.Action replyAction = new Notification.Action.Builder(icon, "Reply", - inputIntent).addRemoteInput(remoteInput) + mActivityIntent).addRemoteInput(remoteInput) .build(); extraAction.add(replyAction); Bundle signals = new Bundle(); @@ -11749,10 +11806,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testImmutableActionIntent() throws Exception { - when(mAmi.getPendingIntentFlags(any(IIntentSender.class))) - .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); + when(mAmi.getPendingIntentFlags(any())).thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); - mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(), r.getSbn().getId(), r.getNotification(), r.getSbn().getUserId()); @@ -11764,12 +11819,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testImmutableContextualActionIntent() throws Exception { - when(mAmi.getPendingIntentFlags(any(IIntentSender.class))) - .thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); + when(mAmi.getPendingIntentFlags(any())).thenReturn(FLAG_IMMUTABLE | FLAG_ONE_SHOT); when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); ArrayList<Notification.Action> extraAction = new ArrayList<>(); - extraAction.add(new Notification.Action(0, "hello", null)); + extraAction.add(new Notification.Action(0, "hello", mActivityIntentImmutable)); Bundle signals = new Bundle(); signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, extraAction); Adjustment adjustment = new Adjustment(r.getSbn().getPackageName(), r.getKey(), signals, "", @@ -12121,8 +12175,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCallNotificationsBypassBlock() throws Exception { - when(mAmi.getPendingIntentFlags(any(IIntentSender.class))) - .thenReturn(FLAG_MUTABLE | FLAG_ONE_SHOT); when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); Notification.Builder nb = new Notification.Builder( @@ -12145,8 +12197,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setName("caller") .build(); nb.setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))); - nb.setFullScreenIntent(mock(PendingIntent.class), true); + person, mActivityIntent)); + nb.setFullScreenIntent(mActivityIntent, true); sbn = new StatusBarNotification(mPkg, mPkg, 8, "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0); r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); @@ -12214,8 +12266,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.clearNotifications(); reset(mUsageStats); Person person = new Person.Builder().setName("caller").build(); - nb.setStyle(Notification.CallStyle.forOngoingCall(person, mock(PendingIntent.class))); - nb.setFullScreenIntent(mock(PendingIntent.class), true); + nb.setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)); + nb.setFullScreenIntent(mActivityIntent, true); sbn = new StatusBarNotification(mPkg, mPkg, 8, "tag", mUid, 0, nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0); r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); @@ -12709,7 +12761,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, NotificationManagerService.REVIEW_NOTIF_STATE_SHOULD_SHOW); mService.maybeShowInitialReviewPermissionsNotification(); - verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + verify(mMockNm, times(1)).notify(eq(TAG), eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), any(Notification.class)); } @@ -12744,7 +12796,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, NotificationManagerService.REVIEW_NOTIF_STATE_RESHOWN); mService.maybeShowInitialReviewPermissionsNotification(); - verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + verify(mMockNm, times(1)).notify(eq(TAG), eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), any(Notification.class)); } @@ -12760,7 +12812,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mInternalService.sendReviewPermissionsNotification(); // Notification should be sent - verify(mMockNm, times(1)).notify(eq(NotificationManagerService.TAG), + verify(mMockNm, times(1)).notify(eq(TAG), eq(SystemMessageProto.SystemMessage.NOTE_REVIEW_NOTIFICATION_PERMISSIONS), any(Notification.class)); @@ -12792,7 +12844,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .thenReturn(permissionState); Notification n = new Notification.Builder(mContext, "test") - .setFullScreenIntent(mock(PendingIntent.class), true) + .setFullScreenIntent(mActivityIntent, true) .build(); mService.fixNotification(n, mPkg, "tag", 9, mUserId, mUid, NOT_FOREGROUND_SERVICE, true); @@ -12860,7 +12912,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Person person = new Person.Builder().setName("caller").build(); Notification n = new Notification.Builder(mContext, "test") .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) + person, mActivityIntent)) .build(); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, "tag", mUid, 0, n, UserHandle.getUserHandleForUid(mUid), null, 0); @@ -12881,7 +12933,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification n = new Notification.Builder(mContext, "test") .setFlag(FLAG_FOREGROUND_SERVICE, true) .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) + person, mActivityIntent)) .build(); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, "tag", mUid, 0, n, UserHandle.getUserHandleForUid(mUid), null, 0); @@ -13040,8 +13092,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification n = new Notification.Builder(mContext, "test") // Without FLAG_FOREGROUND_SERVICE. //.setFlag(FLAG_FOREGROUND_SERVICE, true) - .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) + .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)) .build(); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, "tag", mUid, 0, n, UserHandle.getUserHandleForUid(mUid), null, 0); @@ -13057,8 +13108,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Person person = new Person.Builder().setName("caller").build(); Notification n = new Notification.Builder(mContext, "test") .setFlag(FLAG_USER_INITIATED_JOB, true) - .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) + .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)) .build(); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, "tag", mUid, 0, n, UserHandle.getUserHandleForUid(mUid), null, 0); @@ -13072,9 +13122,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void checkCallStyleNotification_allowedForFsiAllowed() throws Exception { Person person = new Person.Builder().setName("caller").build(); Notification n = new Notification.Builder(mContext, "test") - .setFullScreenIntent(mock(PendingIntent.class), true) - .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) + .setFullScreenIntent(mActivityIntent, true) + .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)) .build(); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, "tag", mUid, 0, n, UserHandle.getUserHandleForUid(mUid), null, 0); @@ -13089,8 +13138,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Person person = new Person.Builder().setName("caller").build(); Notification n = new Notification.Builder(mContext, "test") .setFlag(Notification.FLAG_FSI_REQUESTED_BUT_DENIED, true) - .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) + .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)) .build(); StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, "tag", mUid, 0, n, UserHandle.getUserHandleForUid(mUid), null, 0); @@ -13178,8 +13226,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .build(); Notification n = new Notification.Builder(mContext, "test") .setOngoing(true) - .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) + .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)) .build(); // When: fix the notification with NotificationManagerService @@ -14338,6 +14385,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { eq(REASON_NOTIFICATION_SERVICE), any()); verify(mAmi, times(3)).setPendingIntentAllowBgActivityStarts(any(), any(), eq(FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER | FLAG_SERVICE_SENDER)); + contentIntent.cancel(); + actionIntent2.cancel(); + actionIntent1.cancel(); } @Test @@ -14366,6 +14416,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { eq(REASON_NOTIFICATION_SERVICE), any()); verify(mAmi, times(4)).setPendingIntentAllowBgActivityStarts(any(), any(), eq(FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER | FLAG_SERVICE_SENDER)); + contentIntent.cancel(); + publicContentIntent.cancel(); + actionIntent.cancel(); + publicActionIntent.cancel(); } @Test @@ -14891,8 +14945,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR) public void testFixNotification_clearsLifetimeExtendedFlag() throws Exception { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR); Notification n = new Notification.Builder(mContext, "test") .setFlag(FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true) .build(); @@ -15321,7 +15375,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_ALL_NOTIFS_NEED_TTL) + @EnableFlags(FLAG_ALL_NOTIFS_NEED_TTL) public void testFixNotification_missingTtl() throws Exception { Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -15333,7 +15387,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_ALL_NOTIFS_NEED_TTL) + @EnableFlags(FLAG_ALL_NOTIFS_NEED_TTL) public void testFixNotification_doesNotOverwriteTtl() throws Exception { Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -15351,8 +15405,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { Notification.Builder nb = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setFlag(FLAG_USER_INITIATED_JOB, true) - .setStyle(Notification.CallStyle.forOngoingCall( - person, mock(PendingIntent.class))) + .setStyle(Notification.CallStyle.forOngoingCall(person, mActivityIntent)) .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, 1, testName, mUid, 0, nb.build(), userHandle, null, 0); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TimeToLiveHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/TimeToLiveHelperTest.java new file mode 100644 index 000000000000..8b46c8c409d4 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/TimeToLiveHelperTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.notification; + +import static com.android.server.notification.TimeToLiveHelper.EXTRA_KEY; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.os.SystemClock; +import android.os.UserHandle; +import android.service.notification.StatusBarNotification; +import android.testing.TestableLooper; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; +import com.android.server.UiServiceTestCase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.quality.Strictness; + +@SmallTest +@RunWith(AndroidJUnit4.class) +@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. +public class TimeToLiveHelperTest extends UiServiceTestCase { + + TimeToLiveHelper mHelper; + @Mock + NotificationManagerPrivate mNm; + @Mock + AlarmManager mAm; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext.addMockSystemService(AlarmManager.class, mAm); + mHelper = new TimeToLiveHelper(mNm, mContext); + } + + @After + public void tearDown() { + mHelper.destroy(); + } + + private NotificationRecord getRecord(String tag, int timeoutAfter) { + NotificationChannel channel = new NotificationChannel("id", "name", + NotificationManager.IMPORTANCE_HIGH); + Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setTimeoutAfter(timeoutAfter); + + StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 8, tag, mUid, 0, + nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0); + return new NotificationRecord(mContext, sbn, channel); + } + + @Test + public void testTimeout() { + mHelper.scheduleTimeoutLocked(getRecord("testTimeout", 1), 1); + + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(2L), any()); + assertThat(mHelper.mKeys).hasSize(1); + } + + @Test + public void testTimeoutExpires() { + NotificationRecord r = getRecord("testTimeoutExpires", 1); + + mHelper.scheduleTimeoutLocked(r, 1); + ArgumentCaptor<PendingIntent> captor = ArgumentCaptor.forClass(PendingIntent.class); + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(2L), captor.capture()); + + mHelper.mNotificationTimeoutReceiver.onReceive(mContext, captor.getValue().getIntent()); + + assertThat(mHelper.mKeys).isEmpty(); + } + + @Test + public void testTimeoutExpires_twoEntries() { + NotificationRecord first = getRecord("testTimeoutFirst", 1); + NotificationRecord later = getRecord("testTimeoutSecond", 2); + + mHelper.scheduleTimeoutLocked(first, 1); + mHelper.scheduleTimeoutLocked(later, 1); + + ArgumentCaptor<PendingIntent> captorSet = ArgumentCaptor.forClass(PendingIntent.class); + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(2L), captorSet.capture()); + + ArgumentCaptor<PendingIntent> captorNewSet = ArgumentCaptor.forClass(PendingIntent.class); + mHelper.mNotificationTimeoutReceiver.onReceive(mContext, captorSet.getValue().getIntent()); + + assertThat(mHelper.mKeys).hasSize(1); + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(3L), captorNewSet.capture()); + assertThat(captorSet.getValue().getIntent().getStringExtra(EXTRA_KEY)) + .isEqualTo(first.getKey()); + } + + @Test + public void testTimeout_earlierEntryAddedSecond() { + NotificationRecord later = getRecord("testTimeoutSecond", 2); + mHelper.scheduleTimeoutLocked(later, 1); + + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(3L), any()); + assertThat(mHelper.mKeys).hasSize(1); + + NotificationRecord first = getRecord("testTimeoutFirst", 1); + ArgumentCaptor<PendingIntent> captorSet = ArgumentCaptor.forClass(PendingIntent.class); + ArgumentCaptor<PendingIntent> captorCancel = ArgumentCaptor.forClass(PendingIntent.class); + + mHelper.scheduleTimeoutLocked(first, 1); + + assertThat(mHelper.mKeys).hasSize(2); + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(2L), captorSet.capture()); + assertThat(captorSet.getValue().getIntent().getStringExtra(EXTRA_KEY)) + .isEqualTo(first.getKey()); + assertThat(mHelper.mKeys.first().second).isEqualTo(first.getKey()); + + verify(mAm).cancel(captorCancel.capture()); + assertThat(captorCancel.getValue().getIntent().getStringExtra(EXTRA_KEY)) + .isEqualTo(later.getKey()); + } + + @Test + public void testTimeout_earlierEntryAddedFirst() { + NotificationRecord first = getRecord("testTimeoutFirst", 1); + NotificationRecord later = getRecord("testTimeoutSecond", 2); + + mHelper.scheduleTimeoutLocked(first, 1); + mHelper.scheduleTimeoutLocked(later, 1); + + assertThat(mHelper.mKeys).hasSize(2); + assertThat(mHelper.mKeys.first().second).isEqualTo(first.getKey()); + verify(mAm, never()).cancel((PendingIntent) any()); + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(2L), any()); + } + + @Test + public void testTimeout_updateEarliestEntry() { + NotificationRecord first = getRecord("testTimeoutFirst", 1); + + mHelper.scheduleTimeoutLocked(first, 1); + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(2L), any()); + + NotificationRecord firstUpdated = getRecord("testTimeoutFirst", 3); + ArgumentCaptor<PendingIntent> captorSet = ArgumentCaptor.forClass(PendingIntent.class); + ArgumentCaptor<PendingIntent> captorCancel = ArgumentCaptor.forClass(PendingIntent.class); + + mHelper.scheduleTimeoutLocked(firstUpdated, 1); + + assertThat(mHelper.mKeys).hasSize(1); + + // cancel original alarm + verify(mAm).cancel(captorCancel.capture()); + assertThat(captorCancel.getValue().getIntent().getStringExtra(EXTRA_KEY)) + .isEqualTo(first.getKey()); + + // schedule later alarm + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(4L), captorSet.capture()); + assertThat(captorSet.getValue().getIntent().getStringExtra(EXTRA_KEY)) + .isEqualTo(first.getKey()); + } + + @Test + public void testTimeout_twoEntries_updateEarliestEntry() { + NotificationRecord first = getRecord("testTimeoutFirst", 1); + NotificationRecord later = getRecord("testTimeoutSecond", 2); + + mHelper.scheduleTimeoutLocked(first, 1); + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(2L), any()); + + mHelper.scheduleTimeoutLocked(later, 1); + + NotificationRecord firstUpdated = getRecord("testTimeoutFirst", 3); + ArgumentCaptor<PendingIntent> captorSet = ArgumentCaptor.forClass(PendingIntent.class); + ArgumentCaptor<PendingIntent> captorCancel = ArgumentCaptor.forClass(PendingIntent.class); + + mHelper.scheduleTimeoutLocked(firstUpdated, 1); + + assertThat(mHelper.mKeys).hasSize(2); + assertThat(mHelper.mKeys.first().second).isEqualTo(later.getKey()); + + // "first" was canceled because it's now later + verify(mAm).cancel(captorCancel.capture()); + assertThat(captorCancel.getValue().getIntent().getStringExtra(EXTRA_KEY)) + .isEqualTo(first.getKey()); + + // "later" is now the first entry, and needs the matching alarm + verify(mAm).setExactAndAllowWhileIdle(anyInt(), eq(3L), captorSet.capture()); + assertThat(captorSet.getValue().getIntent().getStringExtra(EXTRA_KEY)) + .isEqualTo(later.getKey()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java index 80e169d8d579..b90fa21cb2b1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java @@ -25,6 +25,8 @@ import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_RE import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; +import static com.android.server.wm.testing.Assert.assertThrows; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -37,6 +39,8 @@ import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; +import com.android.server.wm.testing.Assert; + import org.junit.Before; import org.junit.Test; @@ -288,4 +292,56 @@ public class LetterboxConfigurationTest { false /* forTabletopMode */, LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); } + + @Test + public void test_setLetterboxHorizontalPositionMultiplier_validValues() { + assertThrows(IllegalArgumentException.class, + () -> mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(-1)); + assertThrows(IllegalArgumentException.class, + () -> mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(2)); + + // Does not throw an exception for values [0,1]. + mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0); + mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f); + mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(1); + } + + @Test + public void test_setLetterboxVerticalPositionMultiplier_validValues() { + assertThrows(IllegalArgumentException.class, + () -> mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(-1)); + assertThrows(IllegalArgumentException.class, + () -> mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(2)); + + // Does not throw an exception for values [0,1]. + mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0); + mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f); + mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(1); + } + + @Test + public void test_setLetterboxBookModePositionMultiplier_validValues() { + assertThrows(IllegalArgumentException.class, + () -> mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(-1)); + assertThrows(IllegalArgumentException.class, + () -> mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(2)); + + // Does not throw an exception for values [0,1]. + mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(0); + mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(0.5f); + mLetterboxConfiguration.setLetterboxBookModePositionMultiplier(1); + } + + @Test + public void test_setLetterboxTabletopModePositionMultiplier_validValues() { + assertThrows(IllegalArgumentException.class, + () -> mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(-1)); + assertThrows(IllegalArgumentException.class, + () -> mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(2)); + + // Does not throw an exception for values [0,1]. + mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(0); + mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(0.5f); + mLetterboxConfiguration.setLetterboxTabletopModePositionMultiplier(1); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index fbf142632c78..2e80bc721c7f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4143,31 +4143,6 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - public void testUpdateResolvedBoundsHorizontalPosition_invalidMultiplier_defaultToCenter() { - // Display configured as (2800, 1400). - - // Below 0.0. - assertHorizontalPositionForDifferentDisplayConfigsForPortraitActivity( - /* letterboxHorizontalPositionMultiplier */ -1.0f, - // At launch. - /* fixedOrientationLetterbox */ new Rect(1050, 0, 1750, 1400), - // After 90 degree rotation. - /* sizeCompatUnscaled */ new Rect(350, 0, 1050, 1400), - // After the display is resized to (700, 1400). - /* sizeCompatScaled */ new Rect(525, 0, 875, 700)); - - // Above 1.0 - assertHorizontalPositionForDifferentDisplayConfigsForPortraitActivity( - /* letterboxHorizontalPositionMultiplier */ 2.0f, - // At launch. - /* fixedOrientationLetterbox */ new Rect(1050, 0, 1750, 1400), - // After 90 degree rotation. - /* sizeCompatUnscaled */ new Rect(350, 0, 1050, 1400), - // After the display is resized to (700, 1400). - /* sizeCompatScaled */ new Rect(525, 0, 875, 700)); - } - - @Test public void testUpdateResolvedBoundsHorizontalPosition_right() { // Display configured as (2800, 1400). assertHorizontalPositionForDifferentDisplayConfigsForPortraitActivity( @@ -4398,31 +4373,6 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - public void testUpdateResolvedBoundsVerticalPosition_invalidMultiplier_defaultToCenter() { - // Display configured as (1400, 2800). - - // Below 0.0. - assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity( - /* letterboxVerticalPositionMultiplier */ -1.0f, - // At launch. - /* fixedOrientationLetterbox */ new Rect(0, 1050, 1400, 1750), - // After 90 degree rotation. - /* sizeCompatUnscaled */ new Rect(700, 350, 2100, 1050), - // After the display is resized to (1400, 700). - /* sizeCompatScaled */ new Rect(0, 525, 700, 875)); - - // Above 1.0 - assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity( - /* letterboxVerticalPositionMultiplier */ 2.0f, - // At launch. - /* fixedOrientationLetterbox */ new Rect(0, 1050, 1400, 1750), - // After 90 degree rotation. - /* sizeCompatUnscaled */ new Rect(700, 350, 2100, 1050), - // After the display is resized to (1400, 700). - /* sizeCompatScaled */ new Rect(0, 525, 700, 875)); - } - - @Test public void testUpdateResolvedBoundsVerticalPosition_bottom() { // Display configured as (1400, 2800). assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity( diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 52485eec8505..002a3d5a0d53 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -19,6 +19,8 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; @@ -1024,6 +1026,58 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test + public void testApplyTransaction_createTaskFragment_overrideOrientation_systemOrganizer() { + mSetFlagsRule.enableFlags(Flags.FLAG_TASK_FRAGMENT_SYSTEM_ORGANIZER_FLAG); + mController.unregisterOrganizer(mIOrganizer); + registerTaskFragmentOrganizer(mIOrganizer, true /* isSystemOrganizer */); + + final Task task = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(task); + final int uid = Binder.getCallingUid(); + activity.info.applicationInfo.uid = uid; + activity.getTask().effectiveUid = uid; + final IBinder fragmentToken = new Binder(); + + // Create a TaskFragment with OverrideOrientation set. + final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( + mOrganizerToken, fragmentToken, activity.token) + .setOverrideOrientation(SCREEN_ORIENTATION_BEHIND) + .build(); + mTransaction.setTaskFragmentOrganizer(mIOrganizer); + mTransaction.createTaskFragment(params); + assertApplyTransactionAllowed(mTransaction); + + // TaskFragment override orientation should be set for a system organizer. + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(fragmentToken); + assertNotNull(taskFragment); + assertEquals(SCREEN_ORIENTATION_BEHIND, taskFragment.getOverrideOrientation()); + } + + @Test + public void testApplyTransaction_createTaskFragment_overrideOrientation_nonSystemOrganizer() { + final Task task = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(task); + final int uid = Binder.getCallingUid(); + activity.info.applicationInfo.uid = uid; + activity.getTask().effectiveUid = uid; + final IBinder fragmentToken = new Binder(); + + // Create a TaskFragment with OverrideOrientation set. + final TaskFragmentCreationParams params = new TaskFragmentCreationParams.Builder( + mOrganizerToken, fragmentToken, activity.token) + .setOverrideOrientation(SCREEN_ORIENTATION_BEHIND) + .build(); + mTransaction.setTaskFragmentOrganizer(mIOrganizer); + mTransaction.createTaskFragment(params); + assertApplyTransactionAllowed(mTransaction); + + // TaskFragment override orientation is ignored for a non-system organizer. + final TaskFragment taskFragment = mWindowOrganizerController.getTaskFragment(fragmentToken); + assertNotNull(taskFragment); + assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, taskFragment.getOverrideOrientation()); + } + + @Test public void testApplyTransaction_reparentActivityToTaskFragment_triggerLifecycleUpdate() { final Task task = createTask(mDisplayContent); final ActivityRecord activity = createActivityRecord(task); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 3c5b12c68e1c..4837fcbfc262 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; @@ -752,6 +753,21 @@ public class TaskFragmentTest extends WindowTestsBase { } @Test + public void testGetOrientation_reportOverrideOrientation() { + final Task task = createTask(mDisplayContent); + final TaskFragment tf = createTaskFragmentWithActivity(task); + final ActivityRecord activity = tf.getTopMostActivity(); + tf.setOverrideOrientation(SCREEN_ORIENTATION_BEHIND); + + // Should report the override orientation + assertEquals(SCREEN_ORIENTATION_BEHIND, tf.getOrientation(SCREEN_ORIENTATION_UNSET)); + + // Should report the override orientation even if the activity requests a different value + activity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); + assertEquals(SCREEN_ORIENTATION_BEHIND, tf.getOrientation(SCREEN_ORIENTATION_UNSET)); + } + + @Test public void testUpdateImeParentForActivityEmbedding() { // Setup two activities in ActivityEmbedding. final Task task = createTask(mDisplayContent); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index ec2c968a8a0a..50db99ea6dfc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -1162,7 +1162,8 @@ public class WindowManagerServiceTests extends WindowTestsBase { invocationOnMock.callRealMethod(); return null; }).when(surface).lockCanvas(any()); - mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId); + mWm.mAccessibilityController + .recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded(displayId); waitUntilHandlersIdle(); try { verify(surface).lockCanvas(any()); @@ -1170,7 +1171,8 @@ public class WindowManagerServiceTests extends WindowTestsBase { clearInvocations(surface); // Invalidate and redraw. mWm.mAccessibilityController.onDisplaySizeChanged(mDisplayContent); - mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId); + mWm.mAccessibilityController + .recomputeMagnifiedRegionAndDrawMagnifiedRegionBorderIfNeeded(displayId); // Turn off magnification to release surface. mWm.mAccessibilityController.setMagnificationCallbacks(displayId, null); waitUntilHandlersIdle(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 43b424fab907..69b5c37466da 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -1681,7 +1681,8 @@ public class WindowOrganizerTests extends WindowTestsBase { WindowContainerToken wct = rootTask.mRemoteToken.toWindowContainerToken(); t.setWindowingMode(wct, WINDOWING_MODE_PINNED); mWm.mAtmService.mWindowOrganizerController.applyTransaction(t); - verify(mWm.mAtmService.mRootWindowContainer).resumeFocusedTasksTopActivities(); + verify(mWm.mAtmService.mRootWindowContainer).resumeFocusedTasksTopActivitiesUnchecked(any(), + any(), any(), anyBoolean()); clearInvocations(mWm.mAtmService.mRootWindowContainer); // The token for the PIP root task may have changed when the task entered PIP mode, so do @@ -1690,7 +1691,8 @@ public class WindowOrganizerTests extends WindowTestsBase { record.getRootTask().mRemoteToken.toWindowContainerToken(); t.setWindowingMode(newToken, WINDOWING_MODE_FULLSCREEN); mWm.mAtmService.mWindowOrganizerController.applyTransaction(t); - verify(mWm.mAtmService.mRootWindowContainer).resumeFocusedTasksTopActivities(); + verify(mWm.mAtmService.mRootWindowContainer).resumeFocusedTasksTopActivitiesUnchecked(any(), + any(), any(), anyBoolean()); } @Test diff --git a/tests/OneMedia/Android.bp b/tests/OneMedia/Android.bp index 5c7317735bc7..a43cd39f0dcb 100644 --- a/tests/OneMedia/Android.bp +++ b/tests/OneMedia/Android.bp @@ -16,6 +16,7 @@ android_app { platform_apis: true, certificate: "platform", libs: ["org.apache.http.legacy"], + optional_uses_libs: ["org.apache.http.legacy"], optimize: { enabled: false, }, diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java index b98161dd26fb..191f38d3df80 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslConverter.java @@ -65,8 +65,10 @@ public class AslConverter { return new AndroidSafetyLabelFactory() .createFromHrElements(XmlUtils.listOf(appMetadataBundles)); case ON_DEVICE: - throw new IllegalArgumentException( - "Parsing from on-device format is not supported at this time."); + Element bundleEle = + XmlUtils.getSingleChildElement(document, XmlUtils.OD_TAG_BUNDLE, true); + return new AndroidSafetyLabelFactory() + .createFromOdElements(XmlUtils.listOf(bundleEle)); default: throw new IllegalStateException("Unrecognized input format."); } @@ -89,8 +91,10 @@ public class AslConverter { switch (format) { case HUMAN_READABLE: - throw new IllegalArgumentException( - "Outputting human-readable format is not supported at this time."); + for (var child : asl.toHrDomElements(document)) { + document.appendChild(child); + } + break; case ON_DEVICE: for (var child : asl.toOdDomElements(document)) { document.appendChild(child); diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java index ecfad91a378f..72140a17297c 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabel.java @@ -65,6 +65,17 @@ public class AndroidSafetyLabel implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + Element aslEle = doc.createElement(XmlUtils.HR_TAG_APP_METADATA_BUNDLES); + aslEle.setAttribute(XmlUtils.HR_ATTR_VERSION, String.valueOf(mVersion)); + if (mSafetyLabels != null) { + XmlUtils.appendChildren(aslEle, mSafetyLabels.toHrDomElements(doc)); + } + if (mSystemAppSafetyLabel != null) { + XmlUtils.appendChildren(aslEle, mSystemAppSafetyLabel.toHrDomElements(doc)); + } + if (mTransparencyInfo != null) { + XmlUtils.appendChildren(aslEle, mTransparencyInfo.toHrDomElements(doc)); + } + return XmlUtils.listOf(aslEle); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java index 41ce6e55e989..c53cbbf99a46 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AndroidSafetyLabelFactory.java @@ -56,10 +56,32 @@ public class AndroidSafetyLabelFactory implements AslMarshallableFactory<Android version, systemAppSafetyLabel, safetyLabels, transparencyInfo); } - /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ + /** Creates an {@link AndroidSafetyLabel} from on-device DOM elements */ @Override public AndroidSafetyLabel createFromOdElements(List<Element> elements) throws MalformedXmlException { - return null; + Element bundleEle = XmlUtils.getSingleElement(elements); + Long version = XmlUtils.getOdLongEle(bundleEle, XmlUtils.OD_NAME_VERSION, true); + + Element safetyLabelsEle = + XmlUtils.getOdPbundleWithName(bundleEle, XmlUtils.OD_NAME_SAFETY_LABELS, false); + SafetyLabels safetyLabels = + new SafetyLabelsFactory().createFromOdElements(XmlUtils.listOf(safetyLabelsEle)); + + Element systemAppSafetyLabelEle = + XmlUtils.getOdPbundleWithName( + bundleEle, XmlUtils.OD_NAME_SYSTEM_APP_SAFETY_LABEL, false); + SystemAppSafetyLabel systemAppSafetyLabel = + new SystemAppSafetyLabelFactory() + .createFromOdElements(XmlUtils.listOf(systemAppSafetyLabelEle)); + + Element transparencyInfoEle = + XmlUtils.getOdPbundleWithName(bundleEle, XmlUtils.OD_NAME_TRANSPARENCY_INFO, false); + TransparencyInfo transparencyInfo = + new TransparencyInfoFactory() + .createFromOdElements(XmlUtils.listOf(transparencyInfoEle)); + + return new AndroidSafetyLabel( + version, systemAppSafetyLabel, safetyLabels, transparencyInfo); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java index 21f328d8e2d0..129733ebc1b6 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfo.java @@ -146,6 +146,55 @@ public class AppInfo implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + Element appInfoEle = doc.createElement(XmlUtils.HR_TAG_APP_INFO); + if (this.mTitle != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_TITLE, this.mTitle); + } + if (this.mDescription != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_DESCRIPTION, this.mDescription); + } + if (this.mContainsAds != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_CONTAINS_ADS, String.valueOf(this.mContainsAds)); + } + if (this.mObeyAps != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_OBEY_APS, String.valueOf(this.mObeyAps)); + } + if (this.mAdsFingerprinting != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_ADS_FINGERPRINTING, String.valueOf(this.mAdsFingerprinting)); + } + if (this.mSecurityFingerprinting != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_SECURITY_FINGERPRINTING, + String.valueOf(this.mSecurityFingerprinting)); + } + if (this.mPrivacyPolicy != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_PRIVACY_POLICY, this.mPrivacyPolicy); + } + if (this.mSecurityEndpoints != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_SECURITY_ENDPOINTS, String.join("|", this.mSecurityEndpoints)); + } + if (this.mFirstPartyEndpoints != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_FIRST_PARTY_ENDPOINTS, + String.join("|", this.mFirstPartyEndpoints)); + } + if (this.mServiceProviderEndpoints != null) { + appInfoEle.setAttribute( + XmlUtils.HR_ATTR_SERVICE_PROVIDER_ENDPOINTS, + String.join("|", this.mServiceProviderEndpoints)); + } + if (this.mCategory != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_CATEGORY, this.mCategory); + } + if (this.mEmail != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_EMAIL, this.mEmail); + } + if (this.mWebsite != null) { + appInfoEle.setAttribute(XmlUtils.HR_ATTR_WEBSITE, this.mWebsite); + } + return XmlUtils.listOf(appInfoEle); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java index 6fcf637fe069..c5069619e069 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/AppInfoFactory.java @@ -35,15 +35,16 @@ public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { return null; } - String title = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_TITLE); - String description = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_DESCRIPTION); + String title = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_TITLE, true); + String description = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_DESCRIPTION, true); Boolean containsAds = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_CONTAINS_ADS, true); Boolean obeyAps = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_OBEY_APS, true); Boolean adsFingerprinting = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_ADS_FINGERPRINTING, true); Boolean securityFingerprinting = XmlUtils.getBoolAttr(appInfoEle, XmlUtils.HR_ATTR_SECURITY_FINGERPRINTING, true); - String privacyPolicy = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_PRIVACY_POLICY); + String privacyPolicy = + XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_PRIVACY_POLICY, true); List<String> securityEndpoints = XmlUtils.getPipelineSplitAttr( appInfoEle, XmlUtils.HR_ATTR_SECURITY_ENDPOINTS, true); @@ -53,8 +54,8 @@ public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { List<String> serviceProviderEndpoints = XmlUtils.getPipelineSplitAttr( appInfoEle, XmlUtils.HR_ATTR_SERVICE_PROVIDER_ENDPOINTS, true); - String category = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_CATEGORY); - String email = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_EMAIL); + String category = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_CATEGORY, true); + String email = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_EMAIL, true); String website = XmlUtils.getStringAttr(appInfoEle, XmlUtils.HR_ATTR_WEBSITE, false); return new AppInfo( @@ -76,6 +77,48 @@ public class AppInfoFactory implements AslMarshallableFactory<AppInfo> { /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ @Override public AppInfo createFromOdElements(List<Element> elements) throws MalformedXmlException { - return null; + Element appInfoEle = XmlUtils.getSingleElement(elements); + if (appInfoEle == null) { + AslgenUtil.logI("No AppInfo found in od format."); + return null; + } + + String title = XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_TITLE, true); + String description = + XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_DESCRIPTION, true); + Boolean containsAds = + XmlUtils.getOdBoolEle(appInfoEle, XmlUtils.OD_NAME_CONTAINS_ADS, true); + Boolean obeyAps = XmlUtils.getOdBoolEle(appInfoEle, XmlUtils.OD_NAME_OBEY_APS, true); + Boolean adsFingerprinting = + XmlUtils.getOdBoolEle(appInfoEle, XmlUtils.OD_NAME_ADS_FINGERPRINTING, true); + Boolean securityFingerprinting = + XmlUtils.getOdBoolEle(appInfoEle, XmlUtils.OD_NAME_SECURITY_FINGERPRINTING, true); + String privacyPolicy = + XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_PRIVACY_POLICY, true); + List<String> securityEndpoints = + XmlUtils.getOdStringArray(appInfoEle, XmlUtils.OD_NAME_SECURITY_ENDPOINT, true); + List<String> firstPartyEndpoints = + XmlUtils.getOdStringArray(appInfoEle, XmlUtils.OD_NAME_FIRST_PARTY_ENDPOINT, true); + List<String> serviceProviderEndpoints = + XmlUtils.getOdStringArray( + appInfoEle, XmlUtils.OD_NAME_SERVICE_PROVIDER_ENDPOINT, true); + String category = XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_CATEGORY, true); + String email = XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_EMAIL, true); + String website = XmlUtils.getOdStringEle(appInfoEle, XmlUtils.OD_NAME_WEBSITE, false); + + return new AppInfo( + title, + description, + containsAds, + obeyAps, + adsFingerprinting, + securityFingerprinting, + privacyPolicy, + securityEndpoints, + firstPartyEndpoints, + serviceProviderEndpoints, + category, + email, + website); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java index eb975540ce70..d551953477d8 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataCategory.java @@ -63,6 +63,8 @@ public class DataCategory implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + throw new IllegalStateException( + "Turning DataCategory or DataType into human-readable DOM elements requires" + + " visibility into parent elements. The logic resides in DataLabels."); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java index 02b7189c09ba..97304cb36081 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java @@ -163,7 +163,9 @@ public class DataType implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + throw new IllegalStateException( + "Turning DataCategory or DataType into human-readable DOM elements requires" + + " visibility into parent elements. The logic resides in DataLabels."); } private static void maybeAddBoolToOdElement( diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java index efdc8d0a5f11..94fad9607880 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfo.java @@ -143,6 +143,31 @@ public class DeveloperInfo implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + Element developerInfoEle = doc.createElement(XmlUtils.HR_TAG_DEVELOPER_INFO); + if (mName != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_NAME, mName); + } + if (mEmail != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_EMAIL, mEmail); + } + if (mAddress != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_ADDRESS, mAddress); + } + if (mCountryRegion != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_COUNTRY_REGION, mCountryRegion); + } + if (mDeveloperRelationship != null) { + developerInfoEle.setAttribute( + XmlUtils.HR_ATTR_DEVELOPER_RELATIONSHIP, mDeveloperRelationship.toString()); + } + if (mWebsite != null) { + developerInfoEle.setAttribute(XmlUtils.HR_ATTR_WEBSITE, mWebsite); + } + if (mAppDeveloperRegistryId != null) { + developerInfoEle.setAttribute( + XmlUtils.HR_ATTR_APP_DEVELOPER_REGISTRY_ID, mAppDeveloperRegistryId); + } + + return XmlUtils.listOf(developerInfoEle); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java index c3e7ac35c545..0f3b41cd5d1a 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DeveloperInfoFactory.java @@ -34,15 +34,15 @@ public class DeveloperInfoFactory implements AslMarshallableFactory<DeveloperInf AslgenUtil.logI("No DeveloperInfo found in hr format."); return null; } - String name = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_NAME); - String email = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_EMAIL); - String address = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_ADDRESS); + String name = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_NAME, true); + String email = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_EMAIL, true); + String address = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_ADDRESS, true); String countryRegion = - XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_COUNTRY_REGION); + XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_COUNTRY_REGION, true); DeveloperInfo.DeveloperRelationship developerRelationship = DeveloperInfo.DeveloperRelationship.forString( XmlUtils.getStringAttr( - developerInfoEle, XmlUtils.HR_ATTR_DEVELOPER_RELATIONSHIP)); + developerInfoEle, XmlUtils.HR_ATTR_DEVELOPER_RELATIONSHIP, true)); String website = XmlUtils.getStringAttr(developerInfoEle, XmlUtils.HR_ATTR_WEBSITE, false); String appDeveloperRegistryId = XmlUtils.getStringAttr( @@ -61,6 +61,36 @@ public class DeveloperInfoFactory implements AslMarshallableFactory<DeveloperInf /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ @Override public DeveloperInfo createFromOdElements(List<Element> elements) throws MalformedXmlException { - return null; + Element developerInfoEle = XmlUtils.getSingleElement(elements); + if (developerInfoEle == null) { + AslgenUtil.logI("No DeveloperInfo found in od format."); + return null; + } + String name = XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_NAME, true); + String email = XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_EMAIL, true); + String address = XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_ADDRESS, true); + String countryRegion = + XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_COUNTRY_REGION, true); + DeveloperInfo.DeveloperRelationship developerRelationship = + DeveloperInfo.DeveloperRelationship.forValue( + (int) + (long) + XmlUtils.getOdLongEle( + developerInfoEle, + XmlUtils.OD_NAME_DEVELOPER_RELATIONSHIP, + true)); + String website = XmlUtils.getOdStringEle(developerInfoEle, XmlUtils.OD_NAME_WEBSITE, false); + String appDeveloperRegistryId = + XmlUtils.getOdStringEle( + developerInfoEle, XmlUtils.OD_NAME_APP_DEVELOPER_REGISTRY_ID, false); + + return new DeveloperInfo( + name, + email, + address, + countryRegion, + developerRelationship, + website, + appDeveloperRegistryId); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java index 576820dac6c6..6af80715f7c1 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabels.java @@ -74,6 +74,18 @@ public class SafetyLabels implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + Element safetyLabelsEle = doc.createElement(XmlUtils.HR_TAG_SAFETY_LABELS); + safetyLabelsEle.setAttribute(XmlUtils.HR_ATTR_VERSION, String.valueOf(mVersion)); + + if (mDataLabels != null) { + XmlUtils.appendChildren(safetyLabelsEle, mDataLabels.toHrDomElements(doc)); + } + if (mSecurityLabels != null) { + XmlUtils.appendChildren(safetyLabelsEle, mSecurityLabels.toHrDomElements(doc)); + } + if (mThirdPartyVerification != null) { + XmlUtils.appendChildren(safetyLabelsEle, mThirdPartyVerification.toHrDomElements(doc)); + } + return XmlUtils.listOf(safetyLabelsEle); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java index 7e1838f40680..2644b435311b 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SafetyLabelsFactory.java @@ -66,6 +66,37 @@ public class SafetyLabelsFactory implements AslMarshallableFactory<SafetyLabels> /** Creates an {@link AslMarshallableFactory} from on-device DOM elements */ @Override public SafetyLabels createFromOdElements(List<Element> elements) throws MalformedXmlException { - return null; + Element safetyLabelsEle = XmlUtils.getSingleElement(elements); + if (safetyLabelsEle == null) { + AslgenUtil.logI("No SafetyLabels found in od format."); + return null; + } + Long version = XmlUtils.getOdLongEle(safetyLabelsEle, XmlUtils.OD_NAME_VERSION, true); + + DataLabels dataLabels = + new DataLabelsFactory() + .createFromOdElements( + XmlUtils.listOf( + XmlUtils.getOdPbundleWithName( + safetyLabelsEle, + XmlUtils.OD_NAME_DATA_LABELS, + false))); + SecurityLabels securityLabels = + new SecurityLabelsFactory() + .createFromOdElements( + XmlUtils.listOf( + XmlUtils.getOdPbundleWithName( + safetyLabelsEle, + XmlUtils.OD_NAME_SECURITY_LABELS, + false))); + ThirdPartyVerification thirdPartyVerification = + new ThirdPartyVerificationFactory() + .createFromOdElements( + XmlUtils.listOf( + XmlUtils.getOdPbundleWithName( + safetyLabelsEle, + XmlUtils.OD_NAME_THIRD_PARTY_VERIFICATION, + false))); + return new SafetyLabels(version, dataLabels, securityLabels, thirdPartyVerification); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabels.java index 437343b14605..48643ba0e3ab 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabels.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabels.java @@ -54,6 +54,13 @@ public class SecurityLabels implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + Element ele = doc.createElement(XmlUtils.HR_TAG_SECURITY_LABELS); + if (mIsDataDeletable != null) { + ele.setAttribute(XmlUtils.HR_ATTR_IS_DATA_DELETABLE, String.valueOf(mIsDataDeletable)); + } + if (mIsDataEncrypted != null) { + ele.setAttribute(XmlUtils.HR_ATTR_IS_DATA_ENCRYPTED, String.valueOf(mIsDataEncrypted)); + } + return XmlUtils.listOf(ele); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabelsFactory.java index 9dc4712c33b0..525a80388261 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabelsFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SecurityLabelsFactory.java @@ -46,6 +46,15 @@ public class SecurityLabelsFactory implements AslMarshallableFactory<SecurityLab @Override public SecurityLabels createFromOdElements(List<Element> elements) throws MalformedXmlException { - return null; + Element ele = XmlUtils.getSingleElement(elements); + if (ele == null) { + AslgenUtil.logI("No SecurityLabels found in od format."); + return null; + } + Boolean isDataDeletable = + XmlUtils.getOdBoolEle(ele, XmlUtils.OD_NAME_IS_DATA_DELETABLE, false); + Boolean isDataEncrypted = + XmlUtils.getOdBoolEle(ele, XmlUtils.OD_NAME_IS_DATA_ENCRYPTED, false); + return new SecurityLabels(isDataDeletable, isDataEncrypted); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java index f0ecf93f2805..854c0d0ac3e1 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabel.java @@ -50,6 +50,9 @@ public class SystemAppSafetyLabel implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + Element systemAppSafetyLabelEle = + doc.createElement(XmlUtils.HR_TAG_SYSTEM_APP_SAFETY_LABEL); + systemAppSafetyLabelEle.setAttribute(XmlUtils.HR_ATTR_URL, mUrl); + return XmlUtils.listOf(systemAppSafetyLabelEle); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java index 5b7fe32f2735..c8e22b6c42cd 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/SystemAppSafetyLabelFactory.java @@ -36,7 +36,7 @@ public class SystemAppSafetyLabelFactory implements AslMarshallableFactory<Syste return null; } - String url = XmlUtils.getStringAttr(systemAppSafetyLabelEle, XmlUtils.HR_ATTR_URL); + String url = XmlUtils.getStringAttr(systemAppSafetyLabelEle, XmlUtils.HR_ATTR_URL, true); return new SystemAppSafetyLabel(url); } @@ -44,6 +44,12 @@ public class SystemAppSafetyLabelFactory implements AslMarshallableFactory<Syste @Override public SystemAppSafetyLabel createFromOdElements(List<Element> elements) throws MalformedXmlException { - return null; + Element systemAppSafetyLabelEle = XmlUtils.getSingleElement(elements); + if (systemAppSafetyLabelEle == null) { + AslgenUtil.logI("No SystemAppSafetyLabel found in od format."); + return null; + } + String url = XmlUtils.getOdStringEle(systemAppSafetyLabelEle, XmlUtils.OD_NAME_URL, true); + return new SystemAppSafetyLabel(url); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerification.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerification.java index 229b00243e0a..d74f3f062513 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerification.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerification.java @@ -44,6 +44,8 @@ public class ThirdPartyVerification implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + Element ele = doc.createElement(XmlUtils.HR_TAG_THIRD_PARTY_VERIFICATION); + ele.setAttribute(XmlUtils.HR_ATTR_URL, mUrl); + return XmlUtils.listOf(ele); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerificationFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerificationFactory.java index ac4d3836bcbd..197e7aa77743 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerificationFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/ThirdPartyVerificationFactory.java @@ -37,7 +37,7 @@ public class ThirdPartyVerificationFactory return null; } - String url = XmlUtils.getStringAttr(ele, XmlUtils.HR_ATTR_URL); + String url = XmlUtils.getStringAttr(ele, XmlUtils.HR_ATTR_URL, true); return new ThirdPartyVerification(url); } @@ -45,6 +45,13 @@ public class ThirdPartyVerificationFactory @Override public ThirdPartyVerification createFromOdElements(List<Element> elements) throws MalformedXmlException { - return null; + Element ele = XmlUtils.getSingleElement(elements); + if (ele == null) { + AslgenUtil.logI("No ThirdPartyVerification found in od format."); + return null; + } + + String url = XmlUtils.getOdStringEle(ele, XmlUtils.OD_NAME_URL, true); + return new ThirdPartyVerification(url); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java index ce7ef16ea54e..6a8700a10d3f 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfo.java @@ -61,6 +61,13 @@ public class TransparencyInfo implements AslMarshallable { /** Creates the human-readable DOM elements from the AslMarshallable Java Object. */ @Override public List<Element> toHrDomElements(Document doc) { - return List.of(); + Element transparencyInfoEle = doc.createElement(XmlUtils.HR_TAG_TRANSPARENCY_INFO); + if (mDeveloperInfo != null) { + XmlUtils.appendChildren(transparencyInfoEle, mDeveloperInfo.toHrDomElements(doc)); + } + if (mAppInfo != null) { + XmlUtils.appendChildren(transparencyInfoEle, mAppInfo.toHrDomElements(doc)); + } + return XmlUtils.listOf(transparencyInfoEle); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java index 123de01e57ba..94c564087918 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/TransparencyInfoFactory.java @@ -54,6 +54,23 @@ public class TransparencyInfoFactory implements AslMarshallableFactory<Transpare @Override public TransparencyInfo createFromOdElements(List<Element> elements) throws MalformedXmlException { - return null; + Element transparencyInfoEle = XmlUtils.getSingleElement(elements); + if (transparencyInfoEle == null) { + AslgenUtil.logI("No TransparencyInfo found in od format."); + return null; + } + + Element developerInfoEle = + XmlUtils.getOdPbundleWithName( + transparencyInfoEle, XmlUtils.OD_NAME_DEVELOPER_INFO, false); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromOdElements(XmlUtils.listOf(developerInfoEle)); + + Element appInfoEle = + XmlUtils.getOdPbundleWithName( + transparencyInfoEle, XmlUtils.OD_NAME_APP_INFO, false); + AppInfo appInfo = new AppInfoFactory().createFromOdElements(XmlUtils.listOf(appInfoEle)); + + return new TransparencyInfo(developerInfo, appInfo); } } diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java index 4f21b0c0ffad..1d54ead0a28d 100644 --- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java @@ -320,6 +320,63 @@ public class XmlUtils { return b; } + /** Gets an on-device Long attribute. */ + public static Long getOdLongEle(Element ele, String nameName, boolean required) + throws MalformedXmlException { + List<Element> longEles = + XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_LONG).stream() + .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName)) + .toList(); + if (longEles.size() > 1) { + throw new MalformedXmlException( + String.format("Found more than one %s in %s.", nameName, ele.getTagName())); + } + if (longEles.isEmpty()) { + if (required) { + throw new MalformedXmlException( + String.format("Found no %s in %s.", nameName, ele.getTagName())); + } + return null; + } + Element longEle = longEles.get(0); + Long l = null; + try { + l = Long.parseLong(longEle.getAttribute(XmlUtils.OD_ATTR_VALUE)); + } catch (NumberFormatException e) { + throw new MalformedXmlException( + String.format( + "%s in %s was not formatted as long", nameName, ele.getTagName())); + } + return l; + } + + /** Gets an on-device String attribute. */ + public static String getOdStringEle(Element ele, String nameName, boolean required) + throws MalformedXmlException { + List<Element> eles = + XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING).stream() + .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName)) + .toList(); + if (eles.size() > 1) { + throw new MalformedXmlException( + String.format("Found more than one %s in %s.", nameName, ele.getTagName())); + } + if (eles.isEmpty()) { + if (required) { + throw new MalformedXmlException( + String.format("Found no %s in %s.", nameName, ele.getTagName())); + } + return null; + } + String str = eles.get(0).getAttribute(XmlUtils.OD_ATTR_VALUE); + if (XmlUtils.isNullOrEmpty(str) && required) { + throw new MalformedXmlException( + String.format( + "%s in %s was empty or missing value", nameName, ele.getTagName())); + } + return str; + } + /** Gets a OD Pbundle Element attribute with the specified name. */ public static Element getOdPbundleWithName(Element ele, String nameName, boolean required) throws MalformedXmlException { @@ -379,7 +436,7 @@ public class XmlUtils { throw new MalformedXmlException( String.format("Found no %s in %s.", nameName, ele.getTagName())); } - return List.of(); + return null; } Element intArrayEle = intArrayEles.get(0); List<Element> itemEles = XmlUtils.getChildrenByTagName(intArrayEle, XmlUtils.OD_TAG_ITEM); @@ -390,6 +447,33 @@ public class XmlUtils { return ints; } + /** Gets on-device style String array. */ + public static List<String> getOdStringArray(Element ele, String nameName, boolean required) + throws MalformedXmlException { + List<Element> arrayEles = + XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING_ARRAY).stream() + .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName)) + .toList(); + if (arrayEles.size() > 1) { + throw new MalformedXmlException( + String.format("Found more than one %s in %s.", nameName, ele.getTagName())); + } + if (arrayEles.isEmpty()) { + if (required) { + throw new MalformedXmlException( + String.format("Found no %s in %s.", nameName, ele.getTagName())); + } + return null; + } + Element arrayEle = arrayEles.get(0); + List<Element> itemEles = XmlUtils.getChildrenByTagName(arrayEle, XmlUtils.OD_TAG_ITEM); + List<String> strs = new ArrayList<String>(); + for (Element itemEle : itemEles) { + strs.add(XmlUtils.getStringAttr(itemEle, XmlUtils.OD_ATTR_VALUE, true)); + } + return strs; + } + /** * Utility method for making a List from one element, to support easier refactoring if needed. * For example, List.of() doesn't support null elements. diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java index e2588d7bb3e7..d2e0fc338243 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/AslgenTests.java @@ -56,18 +56,35 @@ public class AslgenTests { InputStream hrStream = getClass().getClassLoader().getResourceAsStream(hrPath.toString()); - String hrContents = new String(hrStream.readAllBytes(), StandardCharsets.UTF_8); + String hrContents = + TestUtils.getFormattedXml( + new String(hrStream.readAllBytes(), StandardCharsets.UTF_8), false); InputStream odStream = getClass().getClassLoader().getResourceAsStream(odPath.toString()); - String odContents = new String(odStream.readAllBytes(), StandardCharsets.UTF_8); - AndroidSafetyLabel asl = + String odContents = + TestUtils.getFormattedXml( + new String(odStream.readAllBytes(), StandardCharsets.UTF_8), false); + AndroidSafetyLabel aslFromHr = AslConverter.readFromString(hrContents, AslConverter.Format.HUMAN_READABLE); - String out = AslConverter.getXmlAsString(asl, AslConverter.Format.ON_DEVICE); - System.out.println("out: " + out); + String aslToOdStr = + TestUtils.getFormattedXml( + AslConverter.getXmlAsString(aslFromHr, AslConverter.Format.ON_DEVICE), + false); + AndroidSafetyLabel aslFromOd = + AslConverter.readFromString(odContents, AslConverter.Format.ON_DEVICE); + String aslToHrStr = + TestUtils.getFormattedXml( + AslConverter.getXmlAsString( + aslFromOd, AslConverter.Format.HUMAN_READABLE), + false); - assertEquals( - TestUtils.getFormattedXml(out, false), - TestUtils.getFormattedXml(odContents, false)); + System.out.println("od expected: " + odContents); + System.out.println("asl to od: " + aslToOdStr); + assertEquals(odContents, aslToOdStr); + + System.out.println("hr expected: " + hrContents); + System.out.println("asl to hr: " + aslToHrStr); + assertEquals(hrContents, aslToHrStr); } } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java index 013700728e50..61a78232801c 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AndroidSafetyLabelTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; @RunWith(JUnit4.class) public class AndroidSafetyLabelTest { @@ -38,12 +37,9 @@ public class AndroidSafetyLabelTest { "with-system-app-safety-label.xml"; private static final String WITH_TRANSPARENCY_INFO_FILE_NAME = "with-transparency-info.xml"; - private Document mDoc = null; - @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for android safety label missing version. */ @@ -51,6 +47,7 @@ public class AndroidSafetyLabelTest { public void testAndroidSafetyLabelMissingVersion() throws Exception { System.out.println("starting testAndroidSafetyLabelMissingVersion."); hrToOdExpectException(MISSING_VERSION_FILE_NAME); + odToHrExpectException(MISSING_VERSION_FILE_NAME); } /** Test for android safety label valid empty. */ @@ -58,6 +55,7 @@ public class AndroidSafetyLabelTest { public void testAndroidSafetyLabelValidEmptyFile() throws Exception { System.out.println("starting testAndroidSafetyLabelValidEmptyFile."); testHrToOdAndroidSafetyLabel(VALID_EMPTY_FILE_NAME); + testOdToHrAndroidSafetyLabel(VALID_EMPTY_FILE_NAME); } /** Test for android safety label with safety labels. */ @@ -65,6 +63,7 @@ public class AndroidSafetyLabelTest { public void testAndroidSafetyLabelWithSafetyLabels() throws Exception { System.out.println("starting testAndroidSafetyLabelWithSafetyLabels."); testHrToOdAndroidSafetyLabel(WITH_SAFETY_LABELS_FILE_NAME); + testOdToHrAndroidSafetyLabel(WITH_SAFETY_LABELS_FILE_NAME); } /** Test for android safety label with system app safety label. */ @@ -72,6 +71,7 @@ public class AndroidSafetyLabelTest { public void testAndroidSafetyLabelWithSystemAppSafetyLabel() throws Exception { System.out.println("starting testAndroidSafetyLabelWithSystemAppSafetyLabel."); testHrToOdAndroidSafetyLabel(WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME); + testOdToHrAndroidSafetyLabel(WITH_SYSTEM_APP_SAFETY_LABEL_FILE_NAME); } /** Test for android safety label with transparency info. */ @@ -79,6 +79,7 @@ public class AndroidSafetyLabelTest { public void testAndroidSafetyLabelWithTransparencyInfo() throws Exception { System.out.println("starting testAndroidSafetyLabelWithTransparencyInfo."); testHrToOdAndroidSafetyLabel(WITH_TRANSPARENCY_INFO_FILE_NAME); + testOdToHrAndroidSafetyLabel(WITH_TRANSPARENCY_INFO_FILE_NAME); } private void hrToOdExpectException(String fileName) { @@ -86,12 +87,26 @@ public class AndroidSafetyLabelTest { new AndroidSafetyLabelFactory(), ANDROID_SAFETY_LABEL_HR_PATH, fileName); } + private void odToHrExpectException(String fileName) { + TestUtils.odToHrExpectException( + new AndroidSafetyLabelFactory(), ANDROID_SAFETY_LABEL_OD_PATH, fileName); + } + private void testHrToOdAndroidSafetyLabel(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, + TestUtils.document(), new AndroidSafetyLabelFactory(), ANDROID_SAFETY_LABEL_HR_PATH, ANDROID_SAFETY_LABEL_OD_PATH, fileName); } + + private void testOdToHrAndroidSafetyLabel(String fileName) throws Exception { + TestUtils.testOdToHr( + TestUtils.document(), + new AndroidSafetyLabelFactory(), + ANDROID_SAFETY_LABEL_OD_PATH, + ANDROID_SAFETY_LABEL_HR_PATH, + fileName); + } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java index a015e2eacac5..9e91c6f22641 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/AppInfoTest.java @@ -25,7 +25,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; import java.nio.file.Paths; import java.util.List; @@ -48,19 +47,31 @@ public class AppInfoTest { "serviceProviderEndpoints", "category", "email"); + public static final List<String> REQUIRED_FIELD_NAMES_OD = + List.of( + "title", + "description", + "contains_ads", + "obey_aps", + "ads_fingerprinting", + "security_fingerprinting", + "privacy_policy", + "security_endpoint", + "first_party_endpoint", + "service_provider_endpoint", + "category", + "email"); public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website"); + public static final List<String> OPTIONAL_FIELD_NAMES_OD = List.of("website"); private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; - private Document mDoc = null; - /** Logic for setting up tests (empty if not yet needed). */ public static void main(String[] params) throws Exception {} @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for all fields valid. */ @@ -68,6 +79,7 @@ public class AppInfoTest { public void testAllFieldsValid() throws Exception { System.out.println("starting testAllFieldsValid."); testHrToOdAppInfo(ALL_FIELDS_VALID_FILE_NAME); + testOdToHrAppInfo(ALL_FIELDS_VALID_FILE_NAME); } /** Tests missing required fields fails. */ @@ -75,7 +87,7 @@ public class AppInfoTest { public void testMissingRequiredFields() throws Exception { System.out.println("Starting testMissingRequiredFields"); for (String reqField : REQUIRED_FIELD_NAMES) { - System.out.println("testing missing required field: " + reqField); + System.out.println("testing missing required field hr: " + reqField); var appInfoEle = TestUtils.getElementsFromResource( Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); @@ -85,6 +97,17 @@ public class AppInfoTest { MalformedXmlException.class, () -> new AppInfoFactory().createFromHrElements(appInfoEle)); } + + for (String reqField : REQUIRED_FIELD_NAMES_OD) { + System.out.println("testing missing required field od: " + reqField); + var appInfoEle = + TestUtils.getElementsFromResource( + Paths.get(APP_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(appInfoEle.get(0), reqField); + assertThrows( + MalformedXmlException.class, + () -> new AppInfoFactory().createFromOdElements(appInfoEle)); + } } /** Tests missing optional fields passes. */ @@ -96,12 +119,34 @@ public class AppInfoTest { Paths.get(APP_INFO_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); ele.get(0).removeAttribute(optField); AppInfo appInfo = new AppInfoFactory().createFromHrElements(ele); - appInfo.toOdDomElements(mDoc); + appInfo.toOdDomElements(TestUtils.document()); + } + + for (String optField : OPTIONAL_FIELD_NAMES_OD) { + var ele = + TestUtils.getElementsFromResource( + Paths.get(APP_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(ele.get(0), optField); + AppInfo appInfo = new AppInfoFactory().createFromOdElements(ele); + appInfo.toHrDomElements(TestUtils.document()); } } private void testHrToOdAppInfo(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, new AppInfoFactory(), APP_INFO_HR_PATH, APP_INFO_OD_PATH, fileName); + TestUtils.document(), + new AppInfoFactory(), + APP_INFO_HR_PATH, + APP_INFO_OD_PATH, + fileName); + } + + private void testOdToHrAppInfo(String fileName) throws Exception { + TestUtils.testOdToHr( + TestUtils.document(), + new AppInfoFactory(), + APP_INFO_OD_PATH, + APP_INFO_HR_PATH, + fileName); } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java index 822f1753f662..ebb31865843f 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataCategoryTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; @RunWith(JUnit4.class) public class DataCategoryTest { @@ -57,15 +56,12 @@ public class DataCategoryTest { "data-category-personal-unrecognized-type.xml"; private static final String UNRECOGNIZED_CATEGORY_FILE_NAME = "data-category-unrecognized.xml"; - private Document mDoc = null; - /** Logic for setting up tests (empty if not yet needed). */ public static void main(String[] params) throws Exception {} @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for data category personal. */ @@ -207,7 +203,7 @@ public class DataCategoryTest { private void testHrToOdDataCategory(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, + TestUtils.document(), new DataCategoryFactory(), DATA_CATEGORY_HR_PATH, DATA_CATEGORY_OD_PATH, diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java index 6f6f2545a5d2..26617264b2e9 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DataLabelsTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; @RunWith(JUnit4.class) public class DataLabelsTest { @@ -65,12 +64,9 @@ public class DataLabelsTest { private static final String UNRECOGNIZED_FILE_NAME = "data-category-unrecognized.xml"; private static final String UNRECOGNIZED_TYPE_FILE_NAME = "data-category-unrecognized-type.xml"; - private Document mDoc = null; - @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for data labels accessed valid bool. */ diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java index ff8346a526ad..72e8d654b542 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/DeveloperInfoTest.java @@ -25,7 +25,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; import java.nio.file.Paths; import java.util.List; @@ -36,19 +35,20 @@ public class DeveloperInfoTest { private static final String DEVELOPER_INFO_OD_PATH = "com/android/asllib/developerinfo/od"; public static final List<String> REQUIRED_FIELD_NAMES = List.of("address", "countryRegion", "email", "name", "relationship"); + public static final List<String> REQUIRED_FIELD_NAMES_OD = + List.of("address", "country_region", "email", "name", "relationship"); public static final List<String> OPTIONAL_FIELD_NAMES = List.of("website", "registryId"); + public static final List<String> OPTIONAL_FIELD_NAMES_OD = + List.of("website", "app_developer_registry_id"); private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; - private Document mDoc = null; - /** Logic for setting up tests (empty if not yet needed). */ public static void main(String[] params) throws Exception {} @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for all fields valid. */ @@ -56,6 +56,7 @@ public class DeveloperInfoTest { public void testAllFieldsValid() throws Exception { System.out.println("starting testAllFieldsValid."); testHrToOdDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME); + testOdToHrDeveloperInfo(ALL_FIELDS_VALID_FILE_NAME); } /** Tests missing required fields fails. */ @@ -73,6 +74,18 @@ public class DeveloperInfoTest { MalformedXmlException.class, () -> new DeveloperInfoFactory().createFromHrElements(developerInfoEle)); } + + for (String reqField : REQUIRED_FIELD_NAMES_OD) { + System.out.println("testing missing required field od: " + reqField); + var developerInfoEle = + TestUtils.getElementsFromResource( + Paths.get(DEVELOPER_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(developerInfoEle.get(0), reqField); + + assertThrows( + MalformedXmlException.class, + () -> new DeveloperInfoFactory().createFromOdElements(developerInfoEle)); + } } /** Tests missing optional fields passes. */ @@ -85,16 +98,35 @@ public class DeveloperInfoTest { developerInfoEle.get(0).removeAttribute(optField); DeveloperInfo developerInfo = new DeveloperInfoFactory().createFromHrElements(developerInfoEle); - developerInfo.toOdDomElements(mDoc); + developerInfo.toOdDomElements(TestUtils.document()); + } + + for (String optField : OPTIONAL_FIELD_NAMES_OD) { + var developerInfoEle = + TestUtils.getElementsFromResource( + Paths.get(DEVELOPER_INFO_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(developerInfoEle.get(0), optField); + DeveloperInfo developerInfo = + new DeveloperInfoFactory().createFromOdElements(developerInfoEle); + developerInfo.toHrDomElements(TestUtils.document()); } } private void testHrToOdDeveloperInfo(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, + TestUtils.document(), new DeveloperInfoFactory(), DEVELOPER_INFO_HR_PATH, DEVELOPER_INFO_OD_PATH, fileName); } + + private void testOdToHrDeveloperInfo(String fileName) throws Exception { + TestUtils.testOdToHr( + TestUtils.document(), + new DeveloperInfoFactory(), + DEVELOPER_INFO_OD_PATH, + DEVELOPER_INFO_HR_PATH, + fileName); + } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java index c52d6c873646..bba6b548beaf 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SafetyLabelsTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; @RunWith(JUnit4.class) public class SafetyLabelsTest { @@ -36,12 +35,9 @@ public class SafetyLabelsTest { private static final String WITH_THIRD_PARTY_VERIFICATION_FILE_NAME = "with-third-party-verification.xml"; - private Document mDoc = null; - @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for safety labels missing version. */ @@ -49,6 +45,7 @@ public class SafetyLabelsTest { public void testSafetyLabelsMissingVersion() throws Exception { System.out.println("starting testSafetyLabelsMissingVersion."); hrToOdExpectException(MISSING_VERSION_FILE_NAME); + odToHrExpectException(MISSING_VERSION_FILE_NAME); } /** Test for safety labels valid empty. */ @@ -56,6 +53,7 @@ public class SafetyLabelsTest { public void testSafetyLabelsValidEmptyFile() throws Exception { System.out.println("starting testSafetyLabelsValidEmptyFile."); testHrToOdSafetyLabels(VALID_EMPTY_FILE_NAME); + testOdToHrSafetyLabels(VALID_EMPTY_FILE_NAME); } /** Test for safety labels with data labels. */ @@ -63,6 +61,7 @@ public class SafetyLabelsTest { public void testSafetyLabelsWithDataLabels() throws Exception { System.out.println("starting testSafetyLabelsWithDataLabels."); testHrToOdSafetyLabels(WITH_DATA_LABELS_FILE_NAME); + testOdToHrSafetyLabels(WITH_DATA_LABELS_FILE_NAME); } /** Test for safety labels with security labels. */ @@ -70,6 +69,7 @@ public class SafetyLabelsTest { public void testSafetyLabelsWithSecurityLabels() throws Exception { System.out.println("starting testSafetyLabelsWithSecurityLabels."); testHrToOdSafetyLabels(WITH_SECURITY_LABELS_FILE_NAME); + testOdToHrSafetyLabels(WITH_SECURITY_LABELS_FILE_NAME); } /** Test for safety labels with third party verification. */ @@ -77,18 +77,32 @@ public class SafetyLabelsTest { public void testSafetyLabelsWithThirdPartyVerification() throws Exception { System.out.println("starting testSafetyLabelsWithThirdPartyVerification."); testHrToOdSafetyLabels(WITH_THIRD_PARTY_VERIFICATION_FILE_NAME); + testOdToHrSafetyLabels(WITH_THIRD_PARTY_VERIFICATION_FILE_NAME); } private void hrToOdExpectException(String fileName) { TestUtils.hrToOdExpectException(new SafetyLabelsFactory(), SAFETY_LABELS_HR_PATH, fileName); } + private void odToHrExpectException(String fileName) { + TestUtils.odToHrExpectException(new SafetyLabelsFactory(), SAFETY_LABELS_OD_PATH, fileName); + } + private void testHrToOdSafetyLabels(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, + TestUtils.document(), new SafetyLabelsFactory(), SAFETY_LABELS_HR_PATH, SAFETY_LABELS_OD_PATH, fileName); } + + private void testOdToHrSafetyLabels(String fileName) throws Exception { + TestUtils.testOdToHr( + TestUtils.document(), + new SafetyLabelsFactory(), + SAFETY_LABELS_OD_PATH, + SAFETY_LABELS_HR_PATH, + fileName); + } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java index c0d0d728f762..a940bc63c685 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SecurityLabelsTest.java @@ -23,7 +23,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; import java.nio.file.Paths; import java.util.List; @@ -35,18 +34,17 @@ public class SecurityLabelsTest { public static final List<String> OPTIONAL_FIELD_NAMES = List.of("isDataDeletable", "isDataEncrypted"); + public static final List<String> OPTIONAL_FIELD_NAMES_OD = + List.of("is_data_deletable", "is_data_encrypted"); private static final String ALL_FIELDS_VALID_FILE_NAME = "all-fields-valid.xml"; - private Document mDoc = null; - /** Logic for setting up tests (empty if not yet needed). */ public static void main(String[] params) throws Exception {} @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for all fields valid. */ @@ -54,6 +52,7 @@ public class SecurityLabelsTest { public void testAllFieldsValid() throws Exception { System.out.println("starting testAllFieldsValid."); testHrToOdSecurityLabels(ALL_FIELDS_VALID_FILE_NAME); + testOdToHrSecurityLabels(ALL_FIELDS_VALID_FILE_NAME); } /** Tests missing optional fields passes. */ @@ -65,16 +64,33 @@ public class SecurityLabelsTest { Paths.get(SECURITY_LABELS_HR_PATH, ALL_FIELDS_VALID_FILE_NAME)); ele.get(0).removeAttribute(optField); SecurityLabels securityLabels = new SecurityLabelsFactory().createFromHrElements(ele); - securityLabels.toOdDomElements(mDoc); + securityLabels.toOdDomElements(TestUtils.document()); + } + for (String optField : OPTIONAL_FIELD_NAMES_OD) { + var ele = + TestUtils.getElementsFromResource( + Paths.get(SECURITY_LABELS_OD_PATH, ALL_FIELDS_VALID_FILE_NAME)); + TestUtils.removeOdChildEleWithName(ele.get(0), optField); + SecurityLabels securityLabels = new SecurityLabelsFactory().createFromOdElements(ele); + securityLabels.toHrDomElements(TestUtils.document()); } } private void testHrToOdSecurityLabels(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, + TestUtils.document(), new SecurityLabelsFactory(), SECURITY_LABELS_HR_PATH, SECURITY_LABELS_OD_PATH, fileName); } + + private void testOdToHrSecurityLabels(String fileName) throws Exception { + TestUtils.testOdToHr( + TestUtils.document(), + new SecurityLabelsFactory(), + SECURITY_LABELS_OD_PATH, + SECURITY_LABELS_HR_PATH, + fileName); + } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java index 191091a9e187..33c276487c64 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/SystemAppSafetyLabelTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; @RunWith(JUnit4.class) public class SystemAppSafetyLabelTest { @@ -34,15 +33,12 @@ public class SystemAppSafetyLabelTest { private static final String VALID_FILE_NAME = "valid.xml"; private static final String MISSING_URL_FILE_NAME = "missing-url.xml"; - private Document mDoc = null; - /** Logic for setting up tests (empty if not yet needed). */ public static void main(String[] params) throws Exception {} @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for valid. */ @@ -50,6 +46,7 @@ public class SystemAppSafetyLabelTest { public void testValid() throws Exception { System.out.println("starting testValid."); testHrToOdSystemAppSafetyLabel(VALID_FILE_NAME); + testOdToHrSystemAppSafetyLabel(VALID_FILE_NAME); } /** Tests missing url. */ @@ -57,6 +54,7 @@ public class SystemAppSafetyLabelTest { public void testMissingUrl() throws Exception { System.out.println("starting testMissingUrl."); hrToOdExpectException(MISSING_URL_FILE_NAME); + odToHrExpectException(MISSING_URL_FILE_NAME); } private void hrToOdExpectException(String fileName) { @@ -64,12 +62,26 @@ public class SystemAppSafetyLabelTest { new SystemAppSafetyLabelFactory(), SYSTEM_APP_SAFETY_LABEL_HR_PATH, fileName); } + private void odToHrExpectException(String fileName) { + TestUtils.odToHrExpectException( + new SystemAppSafetyLabelFactory(), SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName); + } + private void testHrToOdSystemAppSafetyLabel(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, + TestUtils.document(), new SystemAppSafetyLabelFactory(), SYSTEM_APP_SAFETY_LABEL_HR_PATH, SYSTEM_APP_SAFETY_LABEL_OD_PATH, fileName); } + + private void testOdToHrSystemAppSafetyLabel(String fileName) throws Exception { + TestUtils.testOdToHr( + TestUtils.document(), + new SystemAppSafetyLabelFactory(), + SYSTEM_APP_SAFETY_LABEL_OD_PATH, + SYSTEM_APP_SAFETY_LABEL_HR_PATH, + fileName); + } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java index ab8e85cd022b..ec86d0f863af 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/ThirdPartyVerificationTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; @RunWith(JUnit4.class) public class ThirdPartyVerificationTest { @@ -34,15 +33,12 @@ public class ThirdPartyVerificationTest { private static final String VALID_FILE_NAME = "valid.xml"; private static final String MISSING_URL_FILE_NAME = "missing-url.xml"; - private Document mDoc = null; - /** Logic for setting up tests (empty if not yet needed). */ public static void main(String[] params) throws Exception {} @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for valid. */ @@ -50,6 +46,7 @@ public class ThirdPartyVerificationTest { public void testValid() throws Exception { System.out.println("starting testValid."); testHrToOdThirdPartyVerification(VALID_FILE_NAME); + testOdToHrThirdPartyVerification(VALID_FILE_NAME); } /** Tests missing url. */ @@ -57,6 +54,7 @@ public class ThirdPartyVerificationTest { public void testMissingUrl() throws Exception { System.out.println("starting testMissingUrl."); hrToOdExpectException(MISSING_URL_FILE_NAME); + odToHrExpectException(MISSING_URL_FILE_NAME); } private void hrToOdExpectException(String fileName) { @@ -64,12 +62,26 @@ public class ThirdPartyVerificationTest { new ThirdPartyVerificationFactory(), THIRD_PARTY_VERIFICATION_HR_PATH, fileName); } + private void odToHrExpectException(String fileName) { + TestUtils.odToHrExpectException( + new ThirdPartyVerificationFactory(), THIRD_PARTY_VERIFICATION_OD_PATH, fileName); + } + private void testHrToOdThirdPartyVerification(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, + TestUtils.document(), new ThirdPartyVerificationFactory(), THIRD_PARTY_VERIFICATION_HR_PATH, THIRD_PARTY_VERIFICATION_OD_PATH, fileName); } + + private void testOdToHrThirdPartyVerification(String fileName) throws Exception { + TestUtils.testOdToHr( + TestUtils.document(), + new ThirdPartyVerificationFactory(), + THIRD_PARTY_VERIFICATION_OD_PATH, + THIRD_PARTY_VERIFICATION_HR_PATH, + fileName); + } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java index 56503f7d6c6b..f49424061427 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/marshallable/TransparencyInfoTest.java @@ -22,7 +22,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import org.w3c.dom.Document; @RunWith(JUnit4.class) public class TransparencyInfoTest { @@ -35,12 +34,9 @@ public class TransparencyInfoTest { private static final String WITH_DEVELOPER_INFO_FILE_NAME = "with-developer-info.xml"; private static final String WITH_APP_INFO_FILE_NAME = "with-app-info.xml"; - private Document mDoc = null; - @Before public void setUp() throws Exception { System.out.println("set up."); - mDoc = TestUtils.document(); } /** Test for transparency info valid empty. */ @@ -48,6 +44,7 @@ public class TransparencyInfoTest { public void testTransparencyInfoValidEmptyFile() throws Exception { System.out.println("starting testTransparencyInfoValidEmptyFile."); testHrToOdTransparencyInfo(VALID_EMPTY_FILE_NAME); + testOdToHrTransparencyInfo(VALID_EMPTY_FILE_NAME); } /** Test for transparency info with developer info. */ @@ -55,6 +52,7 @@ public class TransparencyInfoTest { public void testTransparencyInfoWithDeveloperInfo() throws Exception { System.out.println("starting testTransparencyInfoWithDeveloperInfo."); testHrToOdTransparencyInfo(WITH_DEVELOPER_INFO_FILE_NAME); + testOdToHrTransparencyInfo(WITH_DEVELOPER_INFO_FILE_NAME); } /** Test for transparency info with app info. */ @@ -62,14 +60,24 @@ public class TransparencyInfoTest { public void testTransparencyInfoWithAppInfo() throws Exception { System.out.println("starting testTransparencyInfoWithAppInfo."); testHrToOdTransparencyInfo(WITH_APP_INFO_FILE_NAME); + testOdToHrTransparencyInfo(WITH_APP_INFO_FILE_NAME); } private void testHrToOdTransparencyInfo(String fileName) throws Exception { TestUtils.testHrToOd( - mDoc, + TestUtils.document(), new TransparencyInfoFactory(), TRANSPARENCY_INFO_HR_PATH, TRANSPARENCY_INFO_OD_PATH, fileName); } + + private void testOdToHrTransparencyInfo(String fileName) throws Exception { + TestUtils.testOdToHr( + TestUtils.document(), + new TransparencyInfoFactory(), + TRANSPARENCY_INFO_OD_PATH, + TRANSPARENCY_INFO_HR_PATH, + fileName); + } } diff --git a/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java b/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java index 6a29b869be43..ea90993e0785 100644 --- a/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java +++ b/tools/app_metadata_bundles/src/test/java/com/android/asllib/testutils/TestUtils.java @@ -38,6 +38,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.Optional; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -98,6 +99,19 @@ public class TestUtils { return outStream.toString(StandardCharsets.UTF_8); } + /** Removes on-device style child with the corresponding name */ + public static void removeOdChildEleWithName(Element ele, String childNameName) { + Optional<Element> childEle = + XmlUtils.asElementList(ele.getChildNodes()).stream() + .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(childNameName)) + .findFirst(); + if (childEle.isEmpty()) { + throw new IllegalStateException( + String.format("%s was not found in %s", childNameName, ele.getTagName())); + } + ele.removeChild(childEle.get()); + } + /** * Gets formatted XML for slightly more robust comparison checking than naive string comparison. */ diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/missing-version.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/missing-version.xml new file mode 100644 index 000000000000..1aa3aa94ca6d --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/androidsafetylabel/od/missing-version.xml @@ -0,0 +1,2 @@ +<bundle> +</bundle>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/missing-version.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/missing-version.xml new file mode 100644 index 000000000000..3fbe3599cd82 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/safetylabels/od/missing-version.xml @@ -0,0 +1,2 @@ +<pbundle_as_map name="safety_labels"> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/missing-url.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/missing-url.xml new file mode 100644 index 000000000000..33b796552463 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/systemappsafetylabel/od/missing-url.xml @@ -0,0 +1,2 @@ +<pbundle_as_map name="system_app_safety_label"> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/thirdpartyverification/od/missing-url.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/thirdpartyverification/od/missing-url.xml new file mode 100644 index 000000000000..0b5a46f904e4 --- /dev/null +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/thirdpartyverification/od/missing-url.xml @@ -0,0 +1,2 @@ +<pbundle_as_map name="third_party_verification"> +</pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml index 862bda465b25..d16caaea320f 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/hr/with-developer-info.xml @@ -7,5 +7,5 @@ countryRegion="US" relationship="aosp" website="example.com" - appDeveloperRegistryId="registry_id" /> + registryId="registry_id" /> </transparency-info>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml index 101c98bd8e60..d7a4e1a959b7 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/transparencyinfo/od/with-developer-info.xml @@ -7,5 +7,6 @@ <string name="country_region" value="US"/> <long name="relationship" value="5"/> <string name="website" value="example.com"/> + <string name="app_developer_registry_id" value="registry_id"/> </pbundle_as_map> </pbundle_as_map>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/hr.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/hr.xml index 36beb93319cd..8f854ad1107e 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/hr.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/hr.xml @@ -1,6 +1,4 @@ <app-metadata-bundles version="123"> - <system-app-safety-label url="www.example.com"> - </system-app-safety-label> <safety-labels version="12345"> <data-labels> <data-shared dataCategory="location" @@ -21,6 +19,8 @@ <third-party-verification url="www.example.com"> </third-party-verification> </safety-labels> + <system-app-safety-label url="www.example.com"> + </system-app-safety-label> <transparency-info> <developer-info name="max" @@ -29,7 +29,7 @@ countryRegion="US" relationship="aosp" website="example.com" - appDeveloperRegistryId="registry_id" /> + registryId="registry_id" /> <app-info title="beervision" description="a beer app" containsAds="true" obeyAps="false" adsFingerprinting="false" securityFingerprinting="false" privacyPolicy="www.example.com" securityEndpoints="url1|url2|url3" firstPartyEndpoints="url1" serviceProviderEndpoints="url55|url56" category="Food and drink" email="max@maxloh.com" /> </transparency-info> </app-metadata-bundles>
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/od.xml b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/od.xml index db21280ad61b..8f1dc6475b78 100644 --- a/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/od.xml +++ b/tools/app_metadata_bundles/src/test/resources/com/android/asllib/validmappings/general/od.xml @@ -42,6 +42,7 @@ <string name="country_region" value="US"/> <long name="relationship" value="5"/> <string name="website" value="example.com"/> + <string name="app_developer_registry_id" value="registry_id"/> </pbundle_as_map> <pbundle_as_map name="app_info"> <string name="title" value="beervision"/> |