diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-06 20:36:48 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-06 20:36:48 +0000 |
commit | 03d28ecbad53fcec00ae4d3f99276cb70fc5cefe (patch) | |
tree | e18e31ed6e82a07f0811adf308b61950a835bad6 | |
parent | a256a6c77c58cfa5d18847e294d850a39b8e6e8b (diff) | |
parent | c2ca5068f2d78a20984424473f9d4e7ac883feac (diff) | |
download | base-android14-mainline-networking-release.tar.gz |
Snap for 11185025 from c2ca5068f2d78a20984424473f9d4e7ac883feac to mainline-networking-releaseaml_net_341411030android14-mainline-networking-release
Change-Id: I4ff6ba0769c35087d69dc5fb83212ad3d026febc
22 files changed, 1247 insertions, 424 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt index e0e5a7c8a76e..b75ef2eaa555 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4116,10 +4116,18 @@ package android.content.pm { public final class UserProperties implements android.os.Parcelable { method public int describeContents(); + method public int getShowInQuietMode(); + method public int getShowInSharingSurfaces(); method public boolean isCredentialShareableWithParent(); method public boolean isMediaSharedWithParent(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.UserProperties> CREATOR; + field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2 + field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1 + field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0 + field public static final int SHOW_IN_SHARING_SURFACES_NO = 2; // 0x2 + field public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = 1; // 0x1 + field public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = 0; // 0x0 } } @@ -10968,6 +10976,7 @@ package android.os { method @NonNull public java.util.List<android.os.UserHandle> getEnabledProfiles(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getMainUser(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getPreviousForegroundUser(); + method @NonNull public String getProfileLabel(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public android.os.UserHandle getProfileParent(@NonNull android.os.UserHandle); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public int getRemainingCreatableProfileCount(@NonNull String); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public int getRemainingCreatableUserCount(@NonNull String); @@ -10976,6 +10985,7 @@ package android.os { method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public android.os.PersistableBundle getSeedAccountOptions(); method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public String getSeedAccountType(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public long[] getSerialNumbersOfUsers(boolean); + method @NonNull public android.graphics.drawable.Drawable getUserBadge(); method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.os.UserHandle> getUserHandles(boolean); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public android.graphics.Bitmap getUserIcon(); method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public android.content.pm.UserProperties getUserProperties(@NonNull android.os.UserHandle); diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java index 2669040403ea..e85b289ca497 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -19,6 +19,7 @@ package android.content.pm; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.os.Parcel; @@ -49,6 +50,8 @@ public final class UserProperties implements Parcelable { private static final String ATTR_SHOW_IN_LAUNCHER = "showInLauncher"; private static final String ATTR_START_WITH_PARENT = "startWithParent"; private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings"; + private static final String ATTR_SHOW_IN_QUIET_MODE = "showInQuietMode"; + private static final String ATTR_SHOW_IN_SHARING_SURFACES = "showInSharingSurfaces"; private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy"; private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts"; private static final String ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA = @@ -76,6 +79,10 @@ public final class UserProperties implements Parcelable { INDEX_MEDIA_SHARED_WITH_PARENT, INDEX_CREDENTIAL_SHAREABLE_WITH_PARENT, INDEX_DELETE_APP_WITH_PARENT, + INDEX_ALWAYS_VISIBLE, + INDEX_SHOW_IN_QUIET_MODE, + INDEX_SHOW_IN_SHARING_SURFACES, + INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE, }) @Retention(RetentionPolicy.SOURCE) private @interface PropertyIndex { @@ -91,6 +98,10 @@ public final class UserProperties implements Parcelable { private static final int INDEX_MEDIA_SHARED_WITH_PARENT = 8; private static final int INDEX_CREDENTIAL_SHAREABLE_WITH_PARENT = 9; private static final int INDEX_DELETE_APP_WITH_PARENT = 10; + private static final int INDEX_ALWAYS_VISIBLE = 11; + private static final int INDEX_SHOW_IN_QUIET_MODE = 12; + private static final int INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE = 13; + private static final int INDEX_SHOW_IN_SHARING_SURFACES = 14; /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */ private long mPropertiesPresent = 0; @@ -276,6 +287,81 @@ public final class UserProperties implements Parcelable { */ public static final int CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING = 1; + /** + * Possible values for the profile visibility when in quiet mode. This affects the profile data + * and apps surfacing in Settings, sharing surfaces, and file picker surfaces. It signifies + * whether the profile data and apps will be shown or not. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "SHOW_IN_QUIET_MODE_", + value = { + SHOW_IN_QUIET_MODE_PAUSED, + SHOW_IN_QUIET_MODE_HIDDEN, + SHOW_IN_QUIET_MODE_DEFAULT, + } + ) + public @interface ShowInQuietMode { + } + + /** + * Indicates that the profile should still be visible in quiet mode but should be shown as + * paused (e.g. by greying out its icons). + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; + /** + * Indicates that the profile should not be visible when the profile is in quiet mode. + * For example, the profile should not be shown in tabbed views in Settings, files sharing + * surfaces etc when in quiet mode. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; + /** + * Indicates that quiet mode should not have any effect on the profile visibility. If the + * profile is meant to be visible, it will remain visible and vice versa. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; + + /** + * Possible values for the profile apps visibility in sharing surfaces. This indicates the + * profile data and apps should be shown in separate tabs or mixed with its parent user's data + * and apps in sharing surfaces and file picker surfaces. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "SHOW_IN_SHARING_SURFACES_", + value = { + SHOW_IN_SHARING_SURFACES_SEPARATE, + SHOW_IN_SHARING_SURFACES_WITH_PARENT, + SHOW_IN_SHARING_SURFACES_NO, + } + ) + public @interface ShowInSharingSurfaces { + } + + /** + * Indicates that the profile data and apps should be shown in sharing surfaces intermixed with + * parent user's data and apps. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = SHOW_IN_LAUNCHER_WITH_PARENT; + + /** + * Indicates that the profile data and apps should be shown in sharing surfaces separate from + * parent user's data and apps. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = SHOW_IN_LAUNCHER_SEPARATE; + + /** + * Indicates that the profile data and apps should not be shown in sharing surfaces at all. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_SHARING_SURFACES_NO = SHOW_IN_LAUNCHER_NO; /** * Creates a UserProperties (intended for the SystemServer) that stores a reference to the given @@ -329,6 +415,8 @@ public final class UserProperties implements Parcelable { setShowInLauncher(orig.getShowInLauncher()); setMediaSharedWithParent(orig.isMediaSharedWithParent()); setCredentialShareableWithParent(orig.isCredentialShareableWithParent()); + setShowInQuietMode(orig.getShowInQuietMode()); + setShowInSharingSurfaces(orig.getShowInSharingSurfaces()); } /** @@ -405,6 +493,61 @@ public final class UserProperties implements Parcelable { private @ShowInSettings int mShowInSettings; /** + * Returns whether a user should be shown in the Settings and sharing surfaces depending on the + * {@link android.os.UserManager#requestQuietModeEnabled(boolean, android.os.UserHandle) + * quiet mode}. This is only applicable to profile users since the quiet mode concept is only + * applicable to profile users. + * + * <p> Please note that, in Settings, this property takes effect only if + * {@link #getShowInSettings()} does not return {@link #SHOW_IN_SETTINGS_NO}. + * Also note that in Sharing surfaces this property takes effect only if + * {@link #getShowInSharingSurfaces()} does not return {@link #SHOW_IN_SHARING_SURFACES_NO}. + * + * @return One of {@link #SHOW_IN_QUIET_MODE_HIDDEN}, + * {@link #SHOW_IN_QUIET_MODE_PAUSED}, or + * {@link #SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile should be + * shown in quiet mode or not. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public @ShowInQuietMode int getShowInQuietMode() { + // NOTE: Launcher currently does not make use of this property. + if (isPresent(INDEX_SHOW_IN_QUIET_MODE)) return mShowInQuietMode; + if (mDefaultProperties != null) return mDefaultProperties.mShowInQuietMode; + throw new SecurityException( + "You don't have permission to query ShowInQuietMode"); + } + /** @hide */ + public void setShowInQuietMode(@ShowInQuietMode int showInQuietMode) { + this.mShowInQuietMode = showInQuietMode; + setPresent(INDEX_SHOW_IN_QUIET_MODE); + } + private int mShowInQuietMode; + + /** + * Returns whether a user's data and apps should be shown in sharing surfaces in a separate tab + * or mixed with the parent user's data/apps. This is only applicable to profile users. + * + * @return One of {@link #SHOW_IN_SHARING_SURFACES_NO}, + * {@link #SHOW_IN_SHARING_SURFACES_SEPARATE}, or + * {@link #SHOW_IN_SHARING_SURFACES_WITH_PARENT} depending on whether the profile + * should be shown separate from its parent's data, mixed with the parent's data, or + * not shown at all. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public @ShowInSharingSurfaces int getShowInSharingSurfaces() { + if (isPresent(INDEX_SHOW_IN_SHARING_SURFACES)) return mShowInSharingSurfaces; + if (mDefaultProperties != null) return mDefaultProperties.mShowInSharingSurfaces; + throw new SecurityException( + "You don't have permission to query ShowInSharingSurfaces"); + } + /** @hide */ + public void setShowInSharingSurfaces(@ShowInSharingSurfaces int showInSharingSurfaces) { + this.mShowInSharingSurfaces = showInSharingSurfaces; + setPresent(INDEX_SHOW_IN_SHARING_SURFACES); + } + private int mShowInSharingSurfaces; + + /** * Returns whether a profile should be started when its parent starts (unless in quiet mode). * This only applies for users that have parents (i.e. for profiles). * @hide @@ -700,6 +843,12 @@ public final class UserProperties implements Parcelable { case ATTR_SHOW_IN_SETTINGS: setShowInSettings(parser.getAttributeInt(i)); break; + case ATTR_SHOW_IN_QUIET_MODE: + setShowInQuietMode(parser.getAttributeInt(i)); + break; + case ATTR_SHOW_IN_SHARING_SURFACES: + setShowInSharingSurfaces(parser.getAttributeInt(i)); + break; case ATTR_INHERIT_DEVICE_POLICY: setInheritDevicePolicy(parser.getAttributeInt(i)); break; @@ -750,6 +899,13 @@ public final class UserProperties implements Parcelable { if (isPresent(INDEX_SHOW_IN_SETTINGS)) { serializer.attributeInt(null, ATTR_SHOW_IN_SETTINGS, mShowInSettings); } + if (isPresent(INDEX_SHOW_IN_QUIET_MODE)) { + serializer.attributeInt(null, ATTR_SHOW_IN_QUIET_MODE, + mShowInQuietMode); + } + if (isPresent(INDEX_SHOW_IN_SHARING_SURFACES)) { + serializer.attributeInt(null, ATTR_SHOW_IN_SHARING_SURFACES, mShowInSharingSurfaces); + } if (isPresent(INDEX_INHERIT_DEVICE_POLICY)) { serializer.attributeInt(null, ATTR_INHERIT_DEVICE_POLICY, mInheritDevicePolicy); @@ -792,6 +948,8 @@ public final class UserProperties implements Parcelable { dest.writeInt(mShowInLauncher); dest.writeBoolean(mStartWithParent); dest.writeInt(mShowInSettings); + dest.writeInt(mShowInQuietMode); + dest.writeInt(mShowInSharingSurfaces); dest.writeInt(mInheritDevicePolicy); dest.writeBoolean(mUseParentsContacts); dest.writeBoolean(mUpdateCrossProfileIntentFiltersOnOTA); @@ -813,6 +971,8 @@ public final class UserProperties implements Parcelable { mShowInLauncher = source.readInt(); mStartWithParent = source.readBoolean(); mShowInSettings = source.readInt(); + mShowInQuietMode = source.readInt(); + mShowInSharingSurfaces = source.readInt(); mInheritDevicePolicy = source.readInt(); mUseParentsContacts = source.readBoolean(); mUpdateCrossProfileIntentFiltersOnOTA = source.readBoolean(); @@ -848,6 +1008,10 @@ public final class UserProperties implements Parcelable { private @ShowInLauncher int mShowInLauncher = SHOW_IN_LAUNCHER_WITH_PARENT; private boolean mStartWithParent = false; private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT; + private @ShowInQuietMode int mShowInQuietMode = + SHOW_IN_QUIET_MODE_PAUSED; + private @ShowInSharingSurfaces int mShowInSharingSurfaces = + SHOW_IN_SHARING_SURFACES_SEPARATE; private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO; private boolean mUseParentsContacts = false; private boolean mUpdateCrossProfileIntentFiltersOnOTA = false; @@ -876,6 +1040,19 @@ public final class UserProperties implements Parcelable { return this; } + /** Sets the value for {@link #mShowInQuietMode} */ + public Builder setShowInQuietMode(@ShowInQuietMode int showInQuietMode) { + mShowInQuietMode = showInQuietMode; + return this; + } + + /** Sets the value for {@link #mShowInSharingSurfaces}. */ + public Builder setShowInSharingSurfaces(@ShowInSharingSurfaces int showInSharingSurfaces) { + mShowInSharingSurfaces = showInSharingSurfaces; + return this; + } + + /** Sets the value for {@link #mInheritDevicePolicy}*/ public Builder setInheritDevicePolicy( @InheritDevicePolicy int inheritRestrictionsDevicePolicy) { @@ -932,6 +1109,8 @@ public final class UserProperties implements Parcelable { mShowInLauncher, mStartWithParent, mShowInSettings, + mShowInQuietMode, + mShowInSharingSurfaces, mInheritDevicePolicy, mUseParentsContacts, mUpdateCrossProfileIntentFiltersOnOTA, @@ -948,6 +1127,8 @@ public final class UserProperties implements Parcelable { @ShowInLauncher int showInLauncher, boolean startWithParent, @ShowInSettings int showInSettings, + @ShowInQuietMode int showInQuietMode, + @ShowInSharingSurfaces int showInSharingSurfaces, @InheritDevicePolicy int inheritDevicePolicy, boolean useParentsContacts, boolean updateCrossProfileIntentFiltersOnOTA, @CrossProfileIntentFilterAccessControlLevel int crossProfileIntentFilterAccessControl, @@ -959,6 +1140,8 @@ public final class UserProperties implements Parcelable { setShowInLauncher(showInLauncher); setStartWithParent(startWithParent); setShowInSettings(showInSettings); + setShowInQuietMode(showInQuietMode); + setShowInSharingSurfaces(showInSharingSurfaces); setInheritDevicePolicy(inheritDevicePolicy); setUseParentsContacts(useParentsContacts); setUpdateCrossProfileIntentFiltersOnOTA(updateCrossProfileIntentFiltersOnOTA); diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 8e1d2d6c97e6..4f4676950cf0 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -129,6 +129,7 @@ interface IUserManager { int getUserBadgeColorResId(int userId); int getUserBadgeDarkColorResId(int userId); boolean hasBadge(int userId); + int getProfileLabelResId(int userId); boolean isUserUnlocked(int userId); boolean isUserRunning(int userId); boolean isUserForeground(int userId); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 84f1880213b8..683bc3189c43 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -16,6 +16,7 @@ package android.os; +import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED; import static android.app.admin.DevicePolicyResources.Strings.Core.WORK_PROFILE_BADGED_LABEL; import static android.app.admin.DevicePolicyResources.UNDEFINED; @@ -5393,6 +5394,38 @@ public class UserManager { } /** + * Retrieves a user badge associated with the current context user. This is only + * applicable to profile users since non-profile users do not have badges. + * + * @return A {@link Drawable} user badge corresponding to the context user + * @throws android.content.res.Resources.NotFoundException if the user is not a profile or + * does not have a badge defined. + * @hide + */ + @SystemApi + @UserHandleAware( + requiresAnyOfPermissionsIfNotCallerProfileGroup = { + Manifest.permission.MANAGE_USERS, + Manifest.permission.INTERACT_ACROSS_USERS}) + @SuppressLint("UnflaggedApi") // b/306636213 + public @NonNull Drawable getUserBadge() { + if (!isProfile(mUserId)) { + throw new Resources.NotFoundException("No badge found for this user."); + } + if (isManagedProfile(mUserId)) { + DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); + return dpm.getResources().getDrawable( + android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON_BADGE, + SOLID_COLORED, () -> getDefaultUserBadge(mUserId)); + } + return getDefaultUserBadge(mUserId); + } + + private Drawable getDefaultUserBadge(@UserIdInt int userId) { + return mContext.getResources().getDrawable(getUserBadgeResId(userId), mContext.getTheme()); + } + + /** * If the target user is a profile of the calling user or the caller * is itself a profile, then this returns a copy of the label with * badging for accessibility services like talkback. E.g. passing in "Email" @@ -5434,6 +5467,44 @@ public class UserManager { } /** + * Returns the string/label that should be used to represent the context user. For example, + * this string can represent a profile in tabbed views. This is only applicable to + * {@link #isProfile() profile users}. This string is translated to the device default language. + * + * @return String representing the label for the context user. + * + * @throws android.content.res.Resources.NotFoundException if the user does not have a label + * defined. + * + * @hide + */ + @SystemApi + @SuppressLint("UnflaggedApi") // b/306636213 + @UserHandleAware( + requiresAnyOfPermissionsIfNotCallerProfileGroup = { + Manifest.permission.MANAGE_USERS, + Manifest.permission.QUERY_USERS, + Manifest.permission.INTERACT_ACROSS_USERS}) + public @NonNull String getProfileLabel() { + if (isManagedProfile(mUserId)) { + DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); + return dpm.getResources().getString( + android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB, + () -> getDefaultProfileLabel(mUserId)); + } + return getDefaultProfileLabel(mUserId); + } + + private String getDefaultProfileLabel(int userId) { + try { + final int resourceId = mService.getProfileLabelResId(userId); + return Resources.getSystem().getString(resourceId); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * If the user is a {@link UserManager#isProfile profile}, checks if the user * shares media with its parent user (the user that created this profile). * Returns false for any other type of user. diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d695c0cb3760..1374fa0e92c4 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9926,6 +9926,13 @@ public final class Settings { public static final String SPATIAL_AUDIO_ENABLED = "spatial_audio_enabled"; /** + * Internal collection of audio device inventory items + * The device item stored are {@link com.android.server.audio.AdiDeviceState} + * @hide + */ + public static final String AUDIO_DEVICE_INVENTORY = "audio_device_inventory"; + + /** * Indicates whether notification display on the lock screen is enabled. * <p> * Type: int (0 for false, 1 for true) diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index a5b2b853fddd..ae4cdfda8c38 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6295,4 +6295,20 @@ ul.</string> <string name="keyboard_layout_notification_multiple_selected_title">Physical keyboards configured</string> <!-- Notification message when multiple keyboards with selected layouts have been connected the first time simultaneously [CHAR LIMIT=NOTIF_BODY] --> <string name="keyboard_layout_notification_multiple_selected_message">Tap to view keyboards</string> + + <!-- Private profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> + <string name="profile_label_private">Private</string> + <!-- Clone profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> + <string name="profile_label_clone">Clone</string> + <!-- Work profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> + <string name="profile_label_work">Work</string> + <!-- 2nd Work profile label on a screen in case a device has more than one work profiles. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> + <string name="profile_label_work_2">Work 2</string> + <!-- 3rd Work profile label on a screen in case a device has more than two work profiles. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> + <string name="profile_label_work_3">Work 3</string> + <!-- Test profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> + <string name="profile_label_test">Test</string> + <!-- Communal profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] --> + <string name="profile_label_communal">Communal</string> + </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 2425d367e1db..a4ad258cca88 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1074,6 +1074,13 @@ <java-symbol type="string" name="managed_profile_label_badge_2" /> <java-symbol type="string" name="managed_profile_label_badge_3" /> <java-symbol type="string" name="clone_profile_label_badge" /> + <java-symbol type="string" name="profile_label_private" /> + <java-symbol type="string" name="profile_label_clone" /> + <java-symbol type="string" name="profile_label_work" /> + <java-symbol type="string" name="profile_label_work_2" /> + <java-symbol type="string" name="profile_label_work_3" /> + <java-symbol type="string" name="profile_label_test" /> + <java-symbol type="string" name="profile_label_communal" /> <java-symbol type="string" name="mediasize_unknown_portrait" /> <java-symbol type="string" name="mediasize_unknown_landscape" /> <java-symbol type="string" name="mediasize_iso_a0" /> diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java index af3c295b8d6c..5a274353f68e 100644 --- a/media/java/android/media/AudioDeviceAttributes.java +++ b/media/java/android/media/AudioDeviceAttributes.java @@ -68,7 +68,7 @@ public final class AudioDeviceAttributes implements Parcelable { /** * The unique address of the device. Some devices don't have addresses, only an empty string. */ - private final @NonNull String mAddress; + private @NonNull String mAddress; /** * The non-unique name of the device. Some devices don't have names, only an empty string. * Should not be used as a unique identifier for a device. @@ -188,6 +188,21 @@ public final class AudioDeviceAttributes implements Parcelable { /** * @hide + * Copy Constructor. + * @param ada the copied AudioDeviceAttributes + */ + public AudioDeviceAttributes(AudioDeviceAttributes ada) { + mRole = ada.getRole(); + mType = ada.getType(); + mAddress = ada.getAddress(); + mName = ada.getName(); + mNativeType = ada.getInternalType(); + mAudioProfiles = ada.getAudioProfiles(); + mAudioDescriptors = ada.getAudioDescriptors(); + } + + /** + * @hide * Returns the role of a device * @return the role */ @@ -218,6 +233,15 @@ public final class AudioDeviceAttributes implements Parcelable { /** * @hide + * Sets the device address. Only used by audio service. + */ + public void setAddress(@NonNull String address) { + Objects.requireNonNull(address); + mAddress = address; + } + + /** + * @hide * Returns the name of the audio device, or an empty string for devices without one * @return the device name */ diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 873b434aa4fd..b957bbddf9d6 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -700,6 +700,7 @@ public class SettingsBackupTest { Settings.Secure.ASSIST_SCREENSHOT_ENABLED, Settings.Secure.ASSIST_STRUCTURE_ENABLED, Settings.Secure.ATTENTIVE_TIMEOUT, + Settings.Secure.AUDIO_DEVICE_INVENTORY, // setting not controllable by user Settings.Secure.AUTOFILL_FEATURE_FIELD_CLASSIFICATION, Settings.Secure.AUTOFILL_USER_DATA_MAX_CATEGORY_COUNT, Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE, diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java new file mode 100644 index 000000000000..1c456a781f7a --- /dev/null +++ b/services/core/java/com/android/server/audio/AdiDeviceState.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.audio; + +import static android.media.AudioSystem.DEVICE_NONE; +import static android.media.AudioSystem.isBluetoothDevice; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import java.util.Objects; + +/** + * Class representing all devices that were previously or are currently connected. Data is + * persisted in {@link android.provider.Settings.Secure} + */ +/*package*/ final class AdiDeviceState { + private static final String TAG = "AS.AdiDeviceState"; + + private static final String SETTING_FIELD_SEPARATOR = ","; + + @AudioDeviceInfo.AudioDeviceType + private final int mDeviceType; + + private final int mInternalDeviceType; + @NonNull + private final String mDeviceAddress; + /** Unique device id from internal device type and address. */ + private final Pair<Integer, String> mDeviceId; + private boolean mSAEnabled; + private boolean mHasHeadTracker = false; + private boolean mHeadTrackerEnabled; + + /** + * Constructor + * + * @param deviceType external audio device type + * @param internalDeviceType if not set pass {@link DEVICE_NONE}, in this case the + * default conversion of the external type will be used + * @param address must be non-null for wireless devices + * @throws NullPointerException if a null address is passed for a wireless device + */ + AdiDeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, + int internalDeviceType, + @Nullable String address) { + mDeviceType = deviceType; + if (internalDeviceType != DEVICE_NONE) { + mInternalDeviceType = internalDeviceType; + } else { + mInternalDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalDevice(deviceType); + + } + mDeviceAddress = isBluetoothDevice(mInternalDeviceType) ? Objects.requireNonNull( + address) : ""; + mDeviceId = new Pair<>(mInternalDeviceType, mDeviceAddress); + } + + public Pair<Integer, String> getDeviceId() { + return mDeviceId; + } + + @AudioDeviceInfo.AudioDeviceType + public int getDeviceType() { + return mDeviceType; + } + + public int getInternalDeviceType() { + return mInternalDeviceType; + } + + @NonNull + public String getDeviceAddress() { + return mDeviceAddress; + } + + public void setSAEnabled(boolean sAEnabled) { + mSAEnabled = sAEnabled; + } + + public boolean isSAEnabled() { + return mSAEnabled; + } + + public void setHeadTrackerEnabled(boolean headTrackerEnabled) { + mHeadTrackerEnabled = headTrackerEnabled; + } + + public boolean isHeadTrackerEnabled() { + return mHeadTrackerEnabled; + } + + public void setHasHeadTracker(boolean hasHeadTracker) { + mHasHeadTracker = hasHeadTracker; + } + + + public boolean hasHeadTracker() { + return mHasHeadTracker; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + // type check and cast + if (getClass() != obj.getClass()) { + return false; + } + final AdiDeviceState sads = (AdiDeviceState) obj; + return mDeviceType == sads.mDeviceType + && mInternalDeviceType == sads.mInternalDeviceType + && mDeviceAddress.equals(sads.mDeviceAddress) // NonNull + && mSAEnabled == sads.mSAEnabled + && mHasHeadTracker == sads.mHasHeadTracker + && mHeadTrackerEnabled == sads.mHeadTrackerEnabled; + } + + @Override + public int hashCode() { + return Objects.hash(mDeviceType, mInternalDeviceType, mDeviceAddress, mSAEnabled, + mHasHeadTracker, mHeadTrackerEnabled); + } + + @Override + public String toString() { + return "type: " + mDeviceType + + " internal type: 0x" + Integer.toHexString(mInternalDeviceType) + + " addr: " + mDeviceAddress + " enabled: " + mSAEnabled + + " HT: " + mHasHeadTracker + " HTenabled: " + mHeadTrackerEnabled; + } + + public String toPersistableString() { + return (new StringBuilder().append(mDeviceType) + .append(SETTING_FIELD_SEPARATOR).append(mDeviceAddress) + .append(SETTING_FIELD_SEPARATOR).append(mSAEnabled ? "1" : "0") + .append(SETTING_FIELD_SEPARATOR).append(mHasHeadTracker ? "1" : "0") + .append(SETTING_FIELD_SEPARATOR).append(mHeadTrackerEnabled ? "1" : "0") + .append(SETTING_FIELD_SEPARATOR).append(mInternalDeviceType) + .toString()); + } + + /** + * Gets the max size (including separators) when persisting the elements with + * {@link AdiDeviceState#toPersistableString()}. + */ + public static int getPeristedMaxSize() { + return 36; /* (mDeviceType)2 + (mDeviceAddresss)17 + (mInternalDeviceType)9 + (mSAEnabled)1 + + (mHasHeadTracker)1 + (mHasHeadTrackerEnabled)1 + + (SETTINGS_FIELD_SEPARATOR)5 */ + } + + @Nullable + public static AdiDeviceState fromPersistedString(@Nullable String persistedString) { + if (persistedString == null) { + return null; + } + if (persistedString.isEmpty()) { + return null; + } + String[] fields = TextUtils.split(persistedString, SETTING_FIELD_SEPARATOR); + // we may have 5 fields for the legacy AdiDeviceState and 6 containing the internal + // device type + if (fields.length != 5 && fields.length != 6) { + // expecting all fields, fewer may mean corruption, ignore those settings + return null; + } + try { + final int deviceType = Integer.parseInt(fields[0]); + int internalDeviceType = -1; + if (fields.length == 6) { + internalDeviceType = Integer.parseInt(fields[5]); + } + final AdiDeviceState deviceState = new AdiDeviceState(deviceType, + internalDeviceType, fields[1]); + deviceState.setHasHeadTracker(Integer.parseInt(fields[2]) == 1); + deviceState.setHasHeadTracker(Integer.parseInt(fields[3]) == 1); + deviceState.setHeadTrackerEnabled(Integer.parseInt(fields[4]) == 1); + return deviceState; + } catch (NumberFormatException e) { + Log.e(TAG, "unable to parse setting for AdiDeviceState: " + persistedString, e); + return null; + } + } + + public AudioDeviceAttributes getAudioDeviceAttributes() { + return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, + mDeviceType, mDeviceAddress); + } + +} diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 32c033230a16..054a26f9c0fb 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -50,6 +50,7 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.util.PrintWriterPrinter; @@ -69,8 +70,11 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; -/** @hide */ -/*package*/ final class AudioDeviceBroker { +/** + * @hide + * (non final for mocking/spying) + */ +public class AudioDeviceBroker { private static final String TAG = "AS.AudioDeviceBroker"; @@ -1149,8 +1153,8 @@ import java.util.concurrent.atomic.AtomicBoolean; } /*package*/ void registerStrategyPreferredDevicesDispatcher( - @NonNull IStrategyPreferredDevicesDispatcher dispatcher) { - mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher); + @NonNull IStrategyPreferredDevicesDispatcher dispatcher, boolean isPrivileged) { + mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher, isPrivileged); } /*package*/ void unregisterStrategyPreferredDevicesDispatcher( @@ -1159,8 +1163,8 @@ import java.util.concurrent.atomic.AtomicBoolean; } /*package*/ void registerStrategyNonDefaultDevicesDispatcher( - @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) { - mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher); + @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher, boolean isPrivileged) { + mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher, isPrivileged); } /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher( @@ -1178,8 +1182,8 @@ import java.util.concurrent.atomic.AtomicBoolean; } /*package*/ void registerCapturePresetDevicesRoleDispatcher( - @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) { - mDeviceInventory.registerCapturePresetDevicesRoleDispatcher(dispatcher); + @NonNull ICapturePresetDevicesRoleDispatcher dispatcher, boolean isPrivileged) { + mDeviceInventory.registerCapturePresetDevicesRoleDispatcher(dispatcher, isPrivileged); } /*package*/ void unregisterCapturePresetDevicesRoleDispatcher( @@ -1187,6 +1191,11 @@ import java.util.concurrent.atomic.AtomicBoolean; mDeviceInventory.unregisterCapturePresetDevicesRoleDispatcher(dispatcher); } + /* package */ List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesListUnchecked( + List<AudioDeviceAttributes> devices) { + return mAudioService.anonymizeAudioDeviceAttributesListUnchecked(devices); + } + /*package*/ void registerCommunicationDeviceDispatcher( @NonNull ICommunicationDeviceDispatcher dispatcher) { mCommDevDispatchers.register(dispatcher); @@ -1850,6 +1859,9 @@ import java.util.concurrent.atomic.AtomicBoolean; final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; BtHelper.onNotifyPreferredAudioProfileApplied(btDevice); } break; + case MSG_PERSIST_AUDIO_DEVICE_SETTINGS: + onPersistAudioDeviceSettings(); + break; default: Log.wtf(TAG, "Invalid message " + msg.what); } @@ -1927,6 +1939,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52; + private static final int MSG_PERSIST_AUDIO_DEVICE_SETTINGS = 54; + private static boolean isMessageHandledUnderWakelock(int msgId) { switch(msgId) { case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: @@ -2354,4 +2368,95 @@ import java.util.concurrent.atomic.AtomicBoolean; info.getId(), null /*mixerAttributes*/); } + + /** + * post a message to persist the audio device settings. + * Message is delayed by 1s on purpose in case of successive changes in quick succession (at + * init time for instance) + * Note this method is made public to work around a Mockito bug where it needs to be public + * in order to be mocked by a test a the same package + * (see https://code.google.com/archive/p/mockito/issues/127) + */ + public void persistAudioDeviceSettings() { + sendMsg(MSG_PERSIST_AUDIO_DEVICE_SETTINGS, SENDMSG_REPLACE, /*delay*/ 1000); + } + + void onPersistAudioDeviceSettings() { + final String deviceSettings = mDeviceInventory.getDeviceSettings(); + Log.v(TAG, "saving audio device settings: " + deviceSettings); + final SettingsAdapter settings = mAudioService.getSettings(); + boolean res = settings.putSecureStringForUser(mAudioService.getContentResolver(), + Settings.Secure.AUDIO_DEVICE_INVENTORY, + deviceSettings, UserHandle.USER_CURRENT); + if (!res) { + Log.e(TAG, "error saving audio device settings: " + deviceSettings); + } + } + + void onReadAudioDeviceSettings() { + final SettingsAdapter settingsAdapter = mAudioService.getSettings(); + final ContentResolver contentResolver = mAudioService.getContentResolver(); + String settings = settingsAdapter.getSecureStringForUser(contentResolver, + Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT); + if (settings == null) { + Log.i(TAG, "reading spatial audio device settings from legacy key" + + Settings.Secure.SPATIAL_AUDIO_ENABLED); + // legacy string format for key SPATIAL_AUDIO_ENABLED has the same order of fields like + // the strings for key AUDIO_DEVICE_INVENTORY. This will ensure to construct valid + // device settings when calling {@link #setDeviceSettings()} + settings = settingsAdapter.getSecureStringForUser(contentResolver, + Settings.Secure.SPATIAL_AUDIO_ENABLED, UserHandle.USER_CURRENT); + if (settings == null) { + Log.i(TAG, "no spatial audio device settings stored with legacy key"); + } else if (!settings.equals("")) { + // Delete old key value and update the new key + if (!settingsAdapter.putSecureStringForUser(contentResolver, + Settings.Secure.SPATIAL_AUDIO_ENABLED, + /*value=*/"", + UserHandle.USER_CURRENT)) { + Log.w(TAG, "cannot erase the legacy audio device settings with key " + + Settings.Secure.SPATIAL_AUDIO_ENABLED); + } + if (!settingsAdapter.putSecureStringForUser(contentResolver, + Settings.Secure.AUDIO_DEVICE_INVENTORY, + settings, + UserHandle.USER_CURRENT)) { + Log.e(TAG, "error updating the new audio device settings with key " + + Settings.Secure.AUDIO_DEVICE_INVENTORY); + } + } + } + + if (settings != null && !settings.equals("")) { + setDeviceSettings(settings); + } + } + + void setDeviceSettings(String settings) { + mDeviceInventory.setDeviceSettings(settings); + } + + /** Test only method. */ + String getDeviceSettings() { + return mDeviceInventory.getDeviceSettings(); + } + + List<AdiDeviceState> getImmutableDeviceInventory() { + return mDeviceInventory.getImmutableDeviceInventory(); + } + + void addDeviceStateToInventory(AdiDeviceState deviceState) { + mDeviceInventory.addDeviceStateToInventory(deviceState); + } + + AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada, + int canonicalType) { + return mDeviceInventory.findDeviceStateForAudioDeviceAttributes(ada, canonicalType); + } + + //------------------------------------------------ + // for testing purposes only + void clearDeviceInventory() { + mDeviceInventory.clearDeviceInventory(); + } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 0c7f11f98809..5332acae13ad 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -15,6 +15,8 @@ */ package com.android.server.audio; +import static android.media.AudioSystem.isBluetoothDevice; + import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; @@ -59,6 +61,7 @@ import com.google.android.collect.Sets; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -78,12 +81,82 @@ public class AudioDeviceInventory { private static final String TAG = "AS.AudioDeviceInventory"; + private static final String SETTING_DEVICE_SEPARATOR_CHAR = "|"; + private static final String SETTING_DEVICE_SEPARATOR = "\\|"; + // lock to synchronize all access to mConnectedDevices and mApmConnectedDevices private final Object mDevicesLock = new Object(); //Audio Analytics ids. private static final String mMetricsId = "audio.device."; + private final Object mDeviceInventoryLock = new Object(); + @GuardedBy("mDeviceInventoryLock") + private final HashMap<Pair<Integer, String>, AdiDeviceState> mDeviceInventory = new HashMap<>(); + + List<AdiDeviceState> getImmutableDeviceInventory() { + synchronized (mDeviceInventoryLock) { + return new ArrayList<AdiDeviceState>(mDeviceInventory.values()); + } + } + + void addDeviceStateToInventory(AdiDeviceState deviceState) { + synchronized (mDeviceInventoryLock) { + mDeviceInventory.put(deviceState.getDeviceId(), deviceState); + } + } + + /** + * Adds a new entry in mDeviceInventory if the AudioDeviceAttributes passed is an sink + * Bluetooth device and no corresponding entry already exists. + * @param ada the device to add if needed + */ + void addAudioDeviceInInventoryIfNeeded(AudioDeviceAttributes ada) { + if (!AudioSystem.isBluetoothOutDevice(ada.getInternalType())) { + return; + } + synchronized (mDeviceInventoryLock) { + if (findDeviceStateForAudioDeviceAttributes(ada, ada.getType()) != null) { + return; + } + AdiDeviceState ads = new AdiDeviceState( + ada.getType(), ada.getInternalType(), ada.getAddress()); + mDeviceInventory.put(ads.getDeviceId(), ads); + } + mDeviceBroker.persistAudioDeviceSettings(); + } + + /** + * Finds the device state that matches the passed {@link AudioDeviceAttributes} and device + * type. Note: currently this method only returns a valid device for A2DP and BLE devices. + * + * @param ada attributes of device to match + * @param canonicalDeviceType external device type to match + * @return the found {@link AdiDeviceState} matching a cached A2DP or BLE device or + * {@code null} otherwise. + */ + @Nullable + AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada, + int canonicalDeviceType) { + final boolean isWireless = isBluetoothDevice(ada.getInternalType()); + synchronized (mDeviceInventoryLock) { + for (AdiDeviceState deviceState : mDeviceInventory.values()) { + if (deviceState.getDeviceType() == canonicalDeviceType + && (!isWireless || ada.getAddress().equals( + deviceState.getDeviceAddress()))) { + return deviceState; + } + } + } + return null; + } + + void clearDeviceInventory() { + synchronized (mDeviceInventoryLock) { + mDeviceInventory.clear(); + } + } + // List of connected devices // Key for map created from DeviceInfo.makeDeviceListKey() @GuardedBy("mDevicesLock") @@ -341,6 +414,12 @@ public class AudioDeviceInventory { mAppliedPresetRolesInt.forEach((key, devices) -> { pw.println(" " + prefix + "preset: " + key.first + " role:" + key.second + " devices:" + devices); }); + pw.println("\ndevices:\n"); + synchronized (mDeviceInventoryLock) { + for (AdiDeviceState device : mDeviceInventory.values()) { + pw.println("\t" + device + "\n"); + } + } } //------------------------------------------------------------ @@ -884,8 +963,8 @@ public class AudioDeviceInventory { /*package*/ void registerStrategyPreferredDevicesDispatcher( - @NonNull IStrategyPreferredDevicesDispatcher dispatcher) { - mPrefDevDispatchers.register(dispatcher); + @NonNull IStrategyPreferredDevicesDispatcher dispatcher, boolean isPrivileged) { + mPrefDevDispatchers.register(dispatcher, isPrivileged); } /*package*/ void unregisterStrategyPreferredDevicesDispatcher( @@ -894,8 +973,8 @@ public class AudioDeviceInventory { } /*package*/ void registerStrategyNonDefaultDevicesDispatcher( - @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) { - mNonDefDevDispatchers.register(dispatcher); + @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher, boolean isPrivileged) { + mNonDefDevDispatchers.register(dispatcher, isPrivileged); } /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher( @@ -976,8 +1055,8 @@ public class AudioDeviceInventory { } /*package*/ void registerCapturePresetDevicesRoleDispatcher( - @NonNull ICapturePresetDevicesRoleDispatcher dispatcher) { - mDevRoleCapturePresetDispatchers.register(dispatcher); + @NonNull ICapturePresetDevicesRoleDispatcher dispatcher, boolean isPrivileged) { + mDevRoleCapturePresetDispatchers.register(dispatcher, isPrivileged); } /*package*/ void unregisterCapturePresetDevicesRoleDispatcher( @@ -1198,7 +1277,7 @@ public class AudioDeviceInventory { AudioDeviceInfo device = Stream.of(connectedDevices) .filter(d -> d.getInternalType() == ada.getInternalType()) - .filter(d -> (!AudioSystem.isBluetoothDevice(d.getInternalType()) + .filter(d -> (!isBluetoothDevice(d.getInternalType()) || (d.getAddress().equals(ada.getAddress())))) .findFirst() .orElse(null); @@ -1304,6 +1383,8 @@ public class AudioDeviceInventory { updateBluetoothPreferredModes_l(connect ? btDevice : null /*connectedDevice*/); if (!connect) { purgeDevicesRoles_l(); + } else { + addAudioDeviceInInventoryIfNeeded(attributes); } } mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record(); @@ -1570,6 +1651,7 @@ public class AudioDeviceInventory { setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/); updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/); + addAudioDeviceInInventoryIfNeeded(ada); } static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER, @@ -1619,7 +1701,7 @@ public class AudioDeviceInventory { } for (DeviceInfo di : mConnectedDevices.values()) { - if (!AudioSystem.isBluetoothDevice(di.mDeviceType)) { + if (!isBluetoothDevice(di.mDeviceType)) { continue; } AudioDeviceAttributes ada = @@ -1733,7 +1815,7 @@ public class AudioDeviceInventory { } HashSet<String> processedAddresses = new HashSet<>(0); for (DeviceInfo di : mConnectedDevices.values()) { - if (!AudioSystem.isBluetoothDevice(di.mDeviceType) + if (!isBluetoothDevice(di.mDeviceType) || processedAddresses.contains(di.mDeviceAddress)) { continue; } @@ -1743,7 +1825,7 @@ public class AudioDeviceInventory { + di.mDeviceAddress + ", preferredProfiles: " + preferredProfiles); } for (DeviceInfo di2 : mConnectedDevices.values()) { - if (!AudioSystem.isBluetoothDevice(di2.mDeviceType) + if (!isBluetoothDevice(di2.mDeviceType) || !di.mDeviceAddress.equals(di2.mDeviceAddress)) { continue; } @@ -1869,9 +1951,9 @@ public class AudioDeviceInventory { final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType, AudioSystem.DEVICE_OUT_HEARING_AID); mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType); - - mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( - AudioSystem.DEVICE_OUT_HEARING_AID, address, name), + AudioDeviceAttributes ada = new AudioDeviceAttributes( + AudioSystem.DEVICE_OUT_HEARING_AID, address, name); + mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( @@ -1881,6 +1963,7 @@ public class AudioDeviceInventory { mDeviceBroker.postApplyVolumeOnDevice(streamType, AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable"); setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/); + addAudioDeviceInInventoryIfNeeded(ada); new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable") .set(MediaMetrics.Property.ADDRESS, address != null ? address : "") .set(MediaMetrics.Property.DEVICE, @@ -1983,6 +2066,7 @@ public class AudioDeviceInventory { sensorUuid)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false); + addAudioDeviceInInventoryIfNeeded(ada); } if (streamType == AudioSystem.STREAM_DEFAULT) { @@ -2313,6 +2397,9 @@ public class AudioDeviceInventory { final int nbDispatchers = mPrefDevDispatchers.beginBroadcast(); for (int i = 0; i < nbDispatchers; i++) { try { + if (!((Boolean) mPrefDevDispatchers.getBroadcastCookie(i))) { + devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices); + } mPrefDevDispatchers.getBroadcastItem(i).dispatchPrefDevicesChanged( strategy, devices); } catch (RemoteException e) { @@ -2326,6 +2413,9 @@ public class AudioDeviceInventory { final int nbDispatchers = mNonDefDevDispatchers.beginBroadcast(); for (int i = 0; i < nbDispatchers; i++) { try { + if (!((Boolean) mNonDefDevDispatchers.getBroadcastCookie(i))) { + devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices); + } mNonDefDevDispatchers.getBroadcastItem(i).dispatchNonDefDevicesChanged( strategy, devices); } catch (RemoteException e) { @@ -2339,6 +2429,9 @@ public class AudioDeviceInventory { final int nbDispatchers = mDevRoleCapturePresetDispatchers.beginBroadcast(); for (int i = 0; i < nbDispatchers; ++i) { try { + if (!((Boolean) mDevRoleCapturePresetDispatchers.getBroadcastCookie(i))) { + devices = mDeviceBroker.anonymizeAudioDeviceAttributesListUnchecked(devices); + } mDevRoleCapturePresetDispatchers.getBroadcastItem(i).dispatchDevicesRoleChanged( capturePreset, role, devices); } catch (RemoteException e) { @@ -2359,6 +2452,41 @@ public class AudioDeviceInventory { } } + /*package*/ String getDeviceSettings() { + int deviceCatalogSize = 0; + synchronized (mDeviceInventoryLock) { + deviceCatalogSize = mDeviceInventory.size(); + + final StringBuilder settingsBuilder = new StringBuilder( + deviceCatalogSize * AdiDeviceState.getPeristedMaxSize()); + + Iterator<AdiDeviceState> iterator = mDeviceInventory.values().iterator(); + if (iterator.hasNext()) { + settingsBuilder.append(iterator.next().toPersistableString()); + } + while (iterator.hasNext()) { + settingsBuilder.append(SETTING_DEVICE_SEPARATOR_CHAR); + settingsBuilder.append(iterator.next().toPersistableString()); + } + return settingsBuilder.toString(); + } + } + + /*package*/ void setDeviceSettings(String settings) { + clearDeviceInventory(); + String[] devSettings = TextUtils.split(Objects.requireNonNull(settings), + SETTING_DEVICE_SEPARATOR); + // small list, not worth overhead of Arrays.stream(devSettings) + for (String setting : devSettings) { + AdiDeviceState devState = AdiDeviceState.fromPersistedString(setting); + // Note if the device is not compatible with spatialization mode or the device + // type is not canonical, it will be ignored in {@link SpatializerHelper}. + if (devState != null) { + addDeviceStateToInventory(devState); + } + } + } + //---------------------------------------------------------- // For tests only diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 53ed38edffe4..d82cef546315 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -381,7 +381,6 @@ public class AudioService extends IAudioService.Stub private static final int MSG_DISPATCH_AUDIO_MODE = 40; private static final int MSG_ROUTING_UPDATED = 41; private static final int MSG_INIT_HEADTRACKING_SENSORS = 42; - private static final int MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS = 43; private static final int MSG_ADD_ASSISTANT_SERVICE_UID = 44; private static final int MSG_REMOVE_ASSISTANT_SERVICE_UID = 45; private static final int MSG_UPDATE_ACTIVE_ASSISTANT_SERVICE_UID = 46; @@ -1021,6 +1020,8 @@ public class AudioService extends IAudioService.Stub mAudioPolicy = audioPolicy; mPlatformType = AudioSystem.getPlatformType(context); + mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem); + mIsSingleVolume = AudioSystem.isSingleVolume(context); mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -1033,13 +1034,14 @@ public class AudioService extends IAudioService.Stub mSfxHelper = new SoundEffectsHelper(mContext, playerBase -> ignorePlayerLogs(playerBase)); - final boolean binauralEnabledDefault = SystemProperties.getBoolean( + boolean binauralEnabledDefault = SystemProperties.getBoolean( "ro.audio.spatializer_binaural_enabled_default", true); - final boolean transauralEnabledDefault = SystemProperties.getBoolean( + boolean transauralEnabledDefault = SystemProperties.getBoolean( "ro.audio.spatializer_transaural_enabled_default", true); - final boolean headTrackingEnabledDefault = mContext.getResources().getBoolean( + boolean headTrackingEnabledDefault = mContext.getResources().getBoolean( com.android.internal.R.bool.config_spatial_audio_head_tracking_enabled_default); - mSpatializerHelper = new SpatializerHelper(this, mAudioSystem, + + mSpatializerHelper = new SpatializerHelper(this, mAudioSystem, mDeviceBroker, binauralEnabledDefault, transauralEnabledDefault, headTrackingEnabledDefault); mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); @@ -1207,8 +1209,6 @@ public class AudioService extends IAudioService.Stub mUseFixedVolume = mContext.getResources().getBoolean( com.android.internal.R.bool.config_useFixedVolume); - mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem); - mRecordMonitor = new RecordingActivityMonitor(mContext); mRecordMonitor.registerRecordingCallback(mVoiceRecordingActivityMonitor, true); @@ -2831,8 +2831,11 @@ public class AudioService extends IAudioService.Stub if (devices == null) { return AudioSystem.ERROR; } + + devices = retrieveBluetoothAddresses(devices); + final String logString = String.format( - "setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s", + "setPreferredDevicesForStrategy u/pid:%d/%d strat:%d dev:%s", Binder.getCallingUid(), Binder.getCallingPid(), strategy, devices.stream().map(e -> e.toString()).collect(Collectors.joining(","))); sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); @@ -2888,7 +2891,7 @@ public class AudioService extends IAudioService.Stub status, strategy)); return new ArrayList<AudioDeviceAttributes>(); } else { - return devices; + return anonymizeAudioDeviceAttributesList(devices); } } @@ -2903,6 +2906,9 @@ public class AudioService extends IAudioService.Stub @NonNull AudioDeviceAttributes device) { super.setDeviceAsNonDefaultForStrategy_enforcePermission(); Objects.requireNonNull(device); + + device = retrieveBluetoothAddress(device); + final String logString = String.format( "setDeviceAsNonDefaultForStrategy u/pid:%d/%d strat:%d dev:%s", Binder.getCallingUid(), Binder.getCallingPid(), strategy, device.toString()); @@ -2929,6 +2935,9 @@ public class AudioService extends IAudioService.Stub AudioDeviceAttributes device) { super.removeDeviceAsNonDefaultForStrategy_enforcePermission(); Objects.requireNonNull(device); + + device = retrieveBluetoothAddress(device); + final String logString = String.format( "removeDeviceAsNonDefaultForStrategy strat:%d dev:%s", strategy, device.toString()); sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); @@ -2963,7 +2972,7 @@ public class AudioService extends IAudioService.Stub status, strategy)); return new ArrayList<AudioDeviceAttributes>(); } else { - return devices; + return anonymizeAudioDeviceAttributesList(devices); } } @@ -2976,7 +2985,8 @@ public class AudioService extends IAudioService.Stub return; } enforceModifyAudioRoutingPermission(); - mDeviceBroker.registerStrategyPreferredDevicesDispatcher(dispatcher); + mDeviceBroker.registerStrategyPreferredDevicesDispatcher( + dispatcher, isBluetoothPrividged()); } /** @see AudioManager#removeOnPreferredDevicesForStrategyChangedListener( @@ -3000,7 +3010,8 @@ public class AudioService extends IAudioService.Stub return; } enforceModifyAudioRoutingPermission(); - mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher(dispatcher); + mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher( + dispatcher, isBluetoothPrividged()); } /** @see AudioManager#removeOnNonDefaultDevicesForStrategyChangedListener( @@ -3016,7 +3027,7 @@ public class AudioService extends IAudioService.Stub } /** - * @see AudioManager#setPreferredDeviceForCapturePreset(int, AudioDeviceAttributes) + * @see AudioManager#setPreferredDevicesForCapturePreset(int, AudioDeviceAttributes) */ public int setPreferredDevicesForCapturePreset( int capturePreset, List<AudioDeviceAttributes> devices) { @@ -3035,6 +3046,8 @@ public class AudioService extends IAudioService.Stub return AudioSystem.ERROR; } + devices = retrieveBluetoothAddresses(devices); + final int status = mDeviceBroker.setPreferredDevicesForCapturePresetSync( capturePreset, devices); if (status != AudioSystem.SUCCESS) { @@ -3081,7 +3094,7 @@ public class AudioService extends IAudioService.Stub status, capturePreset)); return new ArrayList<AudioDeviceAttributes>(); } else { - return devices; + return anonymizeAudioDeviceAttributesList(devices); } } @@ -3095,7 +3108,8 @@ public class AudioService extends IAudioService.Stub return; } enforceModifyAudioRoutingPermission(); - mDeviceBroker.registerCapturePresetDevicesRoleDispatcher(dispatcher); + mDeviceBroker.registerCapturePresetDevicesRoleDispatcher( + dispatcher, isBluetoothPrividged()); } /** @@ -3115,7 +3129,9 @@ public class AudioService extends IAudioService.Stub public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributes( @NonNull AudioAttributes attributes) { enforceQueryStateOrModifyRoutingPermission(); - return getDevicesForAttributesInt(attributes, false /* forVolume */); + + return new ArrayList<AudioDeviceAttributes>(anonymizeAudioDeviceAttributesList( + getDevicesForAttributesInt(attributes, false /* forVolume */))); } /** @see AudioManager#getAudioDevicesForAttributes(AudioAttributes) @@ -3125,7 +3141,8 @@ public class AudioService extends IAudioService.Stub */ public @NonNull ArrayList<AudioDeviceAttributes> getDevicesForAttributesUnprotected( @NonNull AudioAttributes attributes) { - return getDevicesForAttributesInt(attributes, false /* forVolume */); + return new ArrayList<AudioDeviceAttributes>(anonymizeAudioDeviceAttributesList( + getDevicesForAttributesInt(attributes, false /* forVolume */))); } /** @@ -6567,6 +6584,10 @@ public class AudioService extends IAudioService.Stub return mContentResolver; } + /*package*/ SettingsAdapter getSettings() { + return mSettings; + } + /////////////////////////////////////////////////////////////////////////// // Internal methods /////////////////////////////////////////////////////////////////////////// @@ -7314,6 +7335,8 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(device); AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior); + device = retrieveBluetoothAddress(device); + sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:" + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:" + device.getAddress() + " behavior:" @@ -7397,6 +7420,8 @@ public class AudioService extends IAudioService.Stub // verify parameters Objects.requireNonNull(device); + device = retrieveBluetoothAddress(device); + return getDeviceVolumeBehaviorInt(device); } @@ -7471,9 +7496,12 @@ public class AudioService extends IAudioService.Stub /** * see AudioManager.setWiredDeviceConnectionState() */ - public void setWiredDeviceConnectionState(AudioDeviceAttributes attributes, + public void setWiredDeviceConnectionState(@NonNull AudioDeviceAttributes attributes, @ConnectionState int state, String caller) { super.setWiredDeviceConnectionState_enforcePermission(); + Objects.requireNonNull(attributes); + + attributes = retrieveBluetoothAddress(attributes); if (state != CONNECTION_STATE_CONNECTED && state != CONNECTION_STATE_DISCONNECTED) { @@ -7514,6 +7542,9 @@ public class AudioService extends IAudioService.Stub boolean connected) { Objects.requireNonNull(device); enforceModifyAudioRoutingPermission(); + + device = retrieveBluetoothAddress(device); + mDeviceBroker.setTestDeviceConnectionState(device, connected ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED); // simulate a routing update from native @@ -9209,10 +9240,6 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.onInitSensors(); break; - case MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS: - onPersistSpatialAudioDeviceSettings(); - break; - case MSG_RESET_SPATIALIZER: mSpatializerHelper.reset(/* featureEnabled */ mHasSpatializerEffect); break; @@ -10269,39 +10296,103 @@ public class AudioService extends IAudioService.Stub } void onInitSpatializer() { - final String settings = mSettings.getSecureStringForUser(mContentResolver, - Settings.Secure.SPATIAL_AUDIO_ENABLED, UserHandle.USER_CURRENT); - if (settings == null) { - Log.e(TAG, "error reading spatial audio device settings"); - } - mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect, settings); + mDeviceBroker.onReadAudioDeviceSettings(); + mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect); mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect); } + private boolean isBluetoothPrividged() { + return PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( + android.Manifest.permission.BLUETOOTH_CONNECT) + || Binder.getCallingUid() == Process.SYSTEM_UID; + } + + List<AudioDeviceAttributes> retrieveBluetoothAddresses(List<AudioDeviceAttributes> devices) { + if (isBluetoothPrividged()) { + return devices; + } + + List<AudioDeviceAttributes> checkedDevices = new ArrayList<AudioDeviceAttributes>(); + for (AudioDeviceAttributes ada : devices) { + if (ada == null) { + continue; + } + checkedDevices.add(retrieveBluetoothAddressUncheked(ada)); + } + return checkedDevices; + } + + AudioDeviceAttributes retrieveBluetoothAddress(@NonNull AudioDeviceAttributes ada) { + if (isBluetoothPrividged()) { + return ada; + } + return retrieveBluetoothAddressUncheked(ada); + } + + AudioDeviceAttributes retrieveBluetoothAddressUncheked(@NonNull AudioDeviceAttributes ada) { + Objects.requireNonNull(ada); + if (AudioSystem.isBluetoothDevice(ada.getInternalType())) { + String anonymizedAddress = anonymizeBluetoothAddress(ada.getAddress()); + for (AdiDeviceState ads : mDeviceBroker.getImmutableDeviceInventory()) { + if (!(AudioSystem.isBluetoothDevice(ads.getInternalDeviceType()) + && (ada.getInternalType() == ads.getInternalDeviceType()) + && anonymizedAddress.equals(anonymizeBluetoothAddress( + ads.getDeviceAddress())))) { + continue; + } + ada.setAddress(ads.getDeviceAddress()); + break; + } + } + return ada; + } + /** - * post a message to persist the spatial audio device settings. - * Message is delayed by 1s on purpose in case of successive changes in quick succession (at - * init time for instance) - * Note this method is made public to work around a Mockito bug where it needs to be public - * in order to be mocked by a test a the same package - * (see https://code.google.com/archive/p/mockito/issues/127) - */ - public void persistSpatialAudioDeviceSettings() { - sendMsg(mAudioHandler, - MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS, - SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0, TAG, - /*delay*/ 1000); + * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app + * Must match the implementation of BluetoothUtils.toAnonymizedAddress() + * @param address Mac address to be anonymized + * @return anonymized mac address + */ + static String anonymizeBluetoothAddress(String address) { + if (address == null || address.length() != "AA:BB:CC:DD:EE:FF".length()) { + return null; + } + return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length()); } - void onPersistSpatialAudioDeviceSettings() { - final String settings = mSpatializerHelper.getSADeviceSettings(); - Log.v(TAG, "saving spatial audio device settings: " + settings); - boolean res = mSettings.putSecureStringForUser(mContentResolver, - Settings.Secure.SPATIAL_AUDIO_ENABLED, - settings, UserHandle.USER_CURRENT); - if (!res) { - Log.e(TAG, "error saving spatial audio device settings: " + settings); + private List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesList( + List<AudioDeviceAttributes> devices) { + if (isBluetoothPrividged()) { + return devices; } + return anonymizeAudioDeviceAttributesListUnchecked(devices); + } + + /* package */ List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesListUnchecked( + List<AudioDeviceAttributes> devices) { + List<AudioDeviceAttributes> anonymizedDevices = new ArrayList<AudioDeviceAttributes>(); + for (AudioDeviceAttributes ada : devices) { + anonymizedDevices.add(anonymizeAudioDeviceAttributesUnchecked(ada)); + } + return anonymizedDevices; + } + + private AudioDeviceAttributes anonymizeAudioDeviceAttributesUnchecked( + AudioDeviceAttributes ada) { + if (!AudioSystem.isBluetoothDevice(ada.getInternalType())) { + return ada; + } + AudioDeviceAttributes res = new AudioDeviceAttributes(ada); + res.setAddress(anonymizeBluetoothAddress(ada.getAddress())); + return res; + } + + private AudioDeviceAttributes anonymizeAudioDeviceAttributes(AudioDeviceAttributes ada) { + if (isBluetoothPrividged()) { + return ada; + } + + return anonymizeAudioDeviceAttributesUnchecked(ada); } //========================================================================================== @@ -10351,13 +10442,16 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(usages); Objects.requireNonNull(device); enforceModifyAudioRoutingPermission(); + + final AudioDeviceAttributes ada = retrieveBluetoothAddress(device); + if (timeOutMs <= 0 || usages.length == 0) { throw new IllegalArgumentException("Invalid timeOutMs/usagesToMute"); } Log.i(TAG, "muteAwaitConnection dev:" + device + " timeOutMs:" + timeOutMs + " usages:" + Arrays.toString(usages)); - if (mDeviceBroker.isDeviceConnected(device)) { + if (mDeviceBroker.isDeviceConnected(ada)) { // not throwing an exception as there could be a race between a connection (server-side, // notification of connection in flight) and a mute operation (client-side) Log.i(TAG, "muteAwaitConnection ignored, device (" + device + ") already connected"); @@ -10369,12 +10463,19 @@ public class AudioService extends IAudioService.Stub + mMutingExpectedDevice); throw new IllegalStateException("muteAwaitConnection already in progress"); } - mMutingExpectedDevice = device; + mMutingExpectedDevice = ada; mMutedUsagesAwaitingConnection = usages; - mPlaybackMonitor.muteAwaitConnection(usages, device, timeOutMs); + mPlaybackMonitor.muteAwaitConnection(usages, ada, timeOutMs); } - dispatchMuteAwaitConnection(cb -> { try { - cb.dispatchOnMutedUntilConnection(device, usages); } catch (RemoteException e) { } }); + dispatchMuteAwaitConnection((cb, isPrivileged) -> { + try { + AudioDeviceAttributes dev = ada; + if (!isPrivileged) { + dev = anonymizeAudioDeviceAttributesUnchecked(ada); + } + cb.dispatchOnMutedUntilConnection(dev, usages); + } catch (RemoteException e) { } + }); } @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) @@ -10383,7 +10484,7 @@ public class AudioService extends IAudioService.Stub super.getMutingExpectedDevice_enforcePermission(); synchronized (mMuteAwaitConnectionLock) { - return mMutingExpectedDevice; + return anonymizeAudioDeviceAttributes(mMutingExpectedDevice); } } @@ -10392,6 +10493,9 @@ public class AudioService extends IAudioService.Stub public void cancelMuteAwaitConnection(@NonNull AudioDeviceAttributes device) { Objects.requireNonNull(device); enforceModifyAudioRoutingPermission(); + + final AudioDeviceAttributes ada = retrieveBluetoothAddress(device); + Log.i(TAG, "cancelMuteAwaitConnection for device:" + device); final int[] mutedUsages; synchronized (mMuteAwaitConnectionLock) { @@ -10401,7 +10505,7 @@ public class AudioService extends IAudioService.Stub Log.i(TAG, "cancelMuteAwaitConnection ignored, no expected device"); return; } - if (!device.equalTypeAddress(mMutingExpectedDevice)) { + if (!ada.equalTypeAddress(mMutingExpectedDevice)) { Log.e(TAG, "cancelMuteAwaitConnection ignored, got " + device + "] but expected device is" + mMutingExpectedDevice); throw new IllegalStateException("cancelMuteAwaitConnection for wrong device"); @@ -10411,8 +10515,14 @@ public class AudioService extends IAudioService.Stub mMutedUsagesAwaitingConnection = null; mPlaybackMonitor.cancelMuteAwaitConnection("cancelMuteAwaitConnection dev:" + device); } - dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent( - AudioManager.MuteAwaitConnectionCallback.EVENT_CANCEL, device, mutedUsages); + dispatchMuteAwaitConnection((cb, isPrivileged) -> { + try { + AudioDeviceAttributes dev = ada; + if (!isPrivileged) { + dev = anonymizeAudioDeviceAttributesUnchecked(ada); + } + cb.dispatchOnUnmutedEvent( + AudioManager.MuteAwaitConnectionCallback.EVENT_CANCEL, dev, mutedUsages); } catch (RemoteException e) { } }); } @@ -10426,7 +10536,7 @@ public class AudioService extends IAudioService.Stub super.registerMuteAwaitConnectionDispatcher_enforcePermission(); if (register) { - mMuteAwaitConnectionDispatchers.register(cb); + mMuteAwaitConnectionDispatchers.register(cb, isBluetoothPrividged()); } else { mMuteAwaitConnectionDispatchers.unregister(cb); } @@ -10450,8 +10560,14 @@ public class AudioService extends IAudioService.Stub mPlaybackMonitor.cancelMuteAwaitConnection( "checkMuteAwaitConnection device " + device + " connected, unmuting"); } - dispatchMuteAwaitConnection(cb -> { try { cb.dispatchOnUnmutedEvent( - AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION, device, mutedUsages); + dispatchMuteAwaitConnection((cb, isPrivileged) -> { + try { + AudioDeviceAttributes ada = device; + if (!isPrivileged) { + ada = anonymizeAudioDeviceAttributesUnchecked(device); + } + cb.dispatchOnUnmutedEvent(AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION, + ada, mutedUsages); } catch (RemoteException e) { } }); } @@ -10471,7 +10587,8 @@ public class AudioService extends IAudioService.Stub mMutingExpectedDevice = null; mMutedUsagesAwaitingConnection = null; } - dispatchMuteAwaitConnection(cb -> { try { + dispatchMuteAwaitConnection((cb, isPrivileged) -> { + try { cb.dispatchOnUnmutedEvent( AudioManager.MuteAwaitConnectionCallback.EVENT_TIMEOUT, timedOutDevice, mutedUsages); @@ -10479,13 +10596,14 @@ public class AudioService extends IAudioService.Stub } private void dispatchMuteAwaitConnection( - java.util.function.Consumer<IMuteAwaitConnectionCallback> callback) { + java.util.function.BiConsumer<IMuteAwaitConnectionCallback, Boolean> callback) { final int nbDispatchers = mMuteAwaitConnectionDispatchers.beginBroadcast(); // lazy initialization as errors unlikely ArrayList<IMuteAwaitConnectionCallback> errorList = null; for (int i = 0; i < nbDispatchers; i++) { try { - callback.accept(mMuteAwaitConnectionDispatchers.getBroadcastItem(i)); + callback.accept(mMuteAwaitConnectionDispatchers.getBroadcastItem(i), + (Boolean) mMuteAwaitConnectionDispatchers.getBroadcastCookie(i)); } catch (Exception e) { if (errorList == null) { errorList = new ArrayList<>(1); @@ -12951,6 +13069,9 @@ public class AudioService extends IAudioService.Stub @NonNull AudioDeviceAttributes device, @IntRange(from = 0) long delayMillis) { Objects.requireNonNull(device, "device must not be null"); enforceModifyAudioRoutingPermission(); + + device = retrieveBluetoothAddress(device); + final String getterKey = "additional_output_device_delay=" + device.getInternalType() + "," + device.getAddress(); // "getter" key as an id. final String setterKey = getterKey + "," + delayMillis; // append the delay for setter @@ -12971,6 +13092,9 @@ public class AudioService extends IAudioService.Stub @IntRange(from = 0) public long getAdditionalOutputDeviceDelay(@NonNull AudioDeviceAttributes device) { Objects.requireNonNull(device, "device must not be null"); + + device = retrieveBluetoothAddress(device); + final String key = "additional_output_device_delay"; final String reply = AudioSystem.getParameters( key + "=" + device.getInternalType() + "," + device.getAddress()); @@ -12998,6 +13122,9 @@ public class AudioService extends IAudioService.Stub @IntRange(from = 0) public long getMaxAdditionalOutputDeviceDelay(@NonNull AudioDeviceAttributes device) { Objects.requireNonNull(device, "device must not be null"); + + device = retrieveBluetoothAddress(device); + final String key = "max_additional_output_device_delay"; final String reply = AudioSystem.getParameters( key + "=" + device.getInternalType() + "," + device.getAddress()); diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 462c9381b904..969dd60a8012 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -16,6 +16,8 @@ package com.android.server.audio; +import static android.media.AudioSystem.isBluetoothDevice; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -47,13 +49,13 @@ import android.util.Pair; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.utils.EventLogger; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.UUID; /** @@ -73,11 +75,12 @@ public class SpatializerHelper { private final @NonNull AudioSystemAdapter mASA; private final @NonNull AudioService mAudioService; + private final @NonNull AudioDeviceBroker mDeviceBroker; private @Nullable SensorManager mSensorManager; //------------------------------------------------------------ - private static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(14) { + /*package*/ static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(14) { { append(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, SpatializationMode.SPATIALIZER_TRANSAURAL); append(AudioDeviceInfo.TYPE_WIRED_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL); @@ -98,13 +101,6 @@ public class SpatializerHelper { } }; - private static final int[] WIRELESS_TYPES = { AudioDeviceInfo.TYPE_BLUETOOTH_SCO, - AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, - AudioDeviceInfo.TYPE_BLE_HEADSET, - AudioDeviceInfo.TYPE_BLE_SPEAKER, - AudioDeviceInfo.TYPE_BLE_BROADCAST - }; - // Spatializer state machine /*package*/ static final int STATE_UNINITIALIZED = 0; /*package*/ static final int STATE_NOT_SUPPORTED = 1; @@ -114,10 +110,15 @@ public class SpatializerHelper { /*package*/ static final int STATE_DISABLED_AVAILABLE = 6; private int mState = STATE_UNINITIALIZED; + @VisibleForTesting boolean mBinauralEnabledDefault; + @VisibleForTesting boolean mTransauralEnabledDefault; + @VisibleForTesting boolean mHeadTrackingEnabledDefault; + private boolean mFeatureEnabled = false; /** current level as reported by native Spatializer in callback */ private int mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; private int mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + private boolean mTransauralSupported = false; private boolean mBinauralSupported = false; private boolean mIsHeadTrackingSupported = false; @@ -160,31 +161,21 @@ public class SpatializerHelper { */ private final ArrayList<Integer> mSACapableDeviceTypes = new ArrayList<>(0); - /** - * List of devices where Spatial Audio is possible. Each device can be enabled or disabled - * (== user choice to use or not) - */ - @GuardedBy("this") - private final ArrayList<SADeviceState> mSADevices = new ArrayList<>(0); - //------------------------------------------------------ // initialization - @SuppressWarnings("StaticAssignmentInConstructor") SpatializerHelper(@NonNull AudioService mother, @NonNull AudioSystemAdapter asa, - boolean binauralEnabledDefault, - boolean transauralEnabledDefault, - boolean headTrackingEnabledDefault) { + @NonNull AudioDeviceBroker deviceBroker, boolean binauralEnabledDefault, + boolean transauralEnabledDefault, boolean headTrackingEnabledDefault) { mAudioService = mother; mASA = asa; - // "StaticAssignmentInConstructor" warning is suppressed as the SpatializerHelper being - // constructed here is the factory for SADeviceState, thus SADeviceState and its - // private static field sHeadTrackingEnabledDefault should never be accessed directly. - SADeviceState.sBinauralEnabledDefault = binauralEnabledDefault; - SADeviceState.sTransauralEnabledDefault = transauralEnabledDefault; - SADeviceState.sHeadTrackingEnabledDefault = headTrackingEnabledDefault; + mDeviceBroker = deviceBroker; + + mBinauralEnabledDefault = binauralEnabledDefault; + mTransauralEnabledDefault = transauralEnabledDefault; + mHeadTrackingEnabledDefault = headTrackingEnabledDefault; } - synchronized void init(boolean effectExpected, @Nullable String settings) { + synchronized void init(boolean effectExpected) { loglogi("init effectExpected=" + effectExpected); if (!effectExpected) { loglogi("init(): setting state to STATE_NOT_SUPPORTED due to effect not expected"); @@ -288,10 +279,11 @@ public class SpatializerHelper { } } - // When initialized from AudioService, the settings string will be non-null. - // Saved settings need to be applied after spatialization support is initialized above. - if (settings != null) { - setSADeviceSettings(settings); + // Log the saved device states that are compatible with SA + for (AdiDeviceState deviceState : mDeviceBroker.getImmutableDeviceInventory()) { + if (isSADevice(deviceState)) { + logDeviceState(deviceState, "setSADeviceSettings"); + } } // for both transaural / binaural, we are not forcing enablement as the init() method @@ -331,7 +323,7 @@ public class SpatializerHelper { mState = STATE_UNINITIALIZED; mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED; - init(true, null /* settings */); + init(/*effectExpected=*/true); setSpatializerEnabledInt(featureEnabled); } @@ -372,7 +364,7 @@ public class SpatializerHelper { final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0); // is media routed to a new device? - if (isWireless(currentDevice.getType())) { + if (isBluetoothDevice(currentDevice.getInternalType())) { addWirelessDeviceIfNew(currentDevice); } @@ -520,8 +512,8 @@ public class SpatializerHelper { synchronized @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() { // build unionOf(mCompatibleAudioDevices, mEnabledDevice) - mDisabledAudioDevices ArrayList<AudioDeviceAttributes> compatList = new ArrayList<>(); - for (SADeviceState deviceState : mSADevices) { - if (deviceState.mEnabled) { + for (AdiDeviceState deviceState : mDeviceBroker.getImmutableDeviceInventory()) { + if (deviceState.isSAEnabled() && isSADevice(deviceState)) { compatList.add(deviceState.getAudioDeviceAttributes()); } } @@ -548,29 +540,48 @@ public class SpatializerHelper { return; } loglogi("addCompatibleAudioDevice: dev=" + ada); - final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); - SADeviceState deviceUpdated = null; // non-null on update. + final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); + initSAState(deviceState); + AdiDeviceState updatedDevice = null; // non-null on update. if (deviceState != null) { - if (forceEnable && !deviceState.mEnabled) { - deviceUpdated = deviceState; - deviceUpdated.mEnabled = true; + if (forceEnable && !deviceState.isSAEnabled()) { + updatedDevice = deviceState; + updatedDevice.setSAEnabled(true); } } else { // When adding, force the device type to be a canonical one. - final int canonicalDeviceType = getCanonicalDeviceType(ada.getType()); + final int canonicalDeviceType = getCanonicalDeviceType(ada.getType(), + ada.getInternalType()); if (canonicalDeviceType == AudioDeviceInfo.TYPE_UNKNOWN) { Log.e(TAG, "addCompatibleAudioDevice with incompatible AudioDeviceAttributes " + ada); return; } - deviceUpdated = new SADeviceState(canonicalDeviceType, ada.getAddress()); - mSADevices.add(deviceUpdated); + updatedDevice = new AdiDeviceState(canonicalDeviceType, ada.getInternalType(), + ada.getAddress()); + initSAState(updatedDevice); + mDeviceBroker.addDeviceStateToInventory(updatedDevice); } - if (deviceUpdated != null) { + if (updatedDevice != null) { onRoutingUpdated(); - mAudioService.persistSpatialAudioDeviceSettings(); - logDeviceState(deviceUpdated, "addCompatibleAudioDevice"); + mDeviceBroker.persistAudioDeviceSettings(); + logDeviceState(updatedDevice, "addCompatibleAudioDevice"); + } + } + + private void initSAState(AdiDeviceState device) { + if (device == null) { + return; } + + int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(device.getDeviceType(), + Integer.MIN_VALUE); + device.setSAEnabled(spatMode == SpatializationMode.SPATIALIZER_BINAURAL + ? mBinauralEnabledDefault + : spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL + ? mTransauralEnabledDefault + : false); + device.setHeadTrackerEnabled(mHeadTrackingEnabledDefault); } private static final String METRICS_DEVICE_PREFIX = "audio.spatializer.device."; @@ -580,29 +591,30 @@ public class SpatializerHelper { // // There may be different devices with the same device type (aliasing). // We always send the full device state info on each change. - private void logDeviceState(SADeviceState deviceState, String event) { + static void logDeviceState(AdiDeviceState deviceState, String event) { final int deviceType = AudioDeviceInfo.convertDeviceTypeToInternalDevice( - deviceState.mDeviceType); + deviceState.getDeviceType()); final String deviceName = AudioSystem.getDeviceName(deviceType); new MediaMetrics.Item(METRICS_DEVICE_PREFIX + deviceName) - .set(MediaMetrics.Property.ADDRESS, deviceState.mDeviceAddress) - .set(MediaMetrics.Property.ENABLED, deviceState.mEnabled ? "true" : "false") - .set(MediaMetrics.Property.EVENT, TextUtils.emptyIfNull(event)) - .set(MediaMetrics.Property.HAS_HEAD_TRACKER, - deviceState.mHasHeadTracker ? "true" : "false") // this may be updated later. - .set(MediaMetrics.Property.HEAD_TRACKER_ENABLED, - deviceState.mHeadTrackerEnabled ? "true" : "false") - .record(); + .set(MediaMetrics.Property.ADDRESS, deviceState.getDeviceAddress()) + .set(MediaMetrics.Property.ENABLED, deviceState.isSAEnabled() ? "true" : "false") + .set(MediaMetrics.Property.EVENT, TextUtils.emptyIfNull(event)) + .set(MediaMetrics.Property.HAS_HEAD_TRACKER, + deviceState.hasHeadTracker() ? "true" + : "false") // this may be updated later. + .set(MediaMetrics.Property.HEAD_TRACKER_ENABLED, + deviceState.isHeadTrackerEnabled() ? "true" : "false") + .record(); } synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { loglogi("removeCompatibleAudioDevice: dev=" + ada); - final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); - if (deviceState != null && deviceState.mEnabled) { - deviceState.mEnabled = false; + final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); + if (deviceState != null && deviceState.isSAEnabled()) { + deviceState.setSAEnabled(false); onRoutingUpdated(); - mAudioService.persistSpatialAudioDeviceSettings(); + mDeviceBroker.persistAudioDeviceSettings(); logDeviceState(deviceState, "removeCompatibleAudioDevice"); } } @@ -611,8 +623,9 @@ public class SpatializerHelper { * Returns a possibly aliased device type which is used * for spatial audio settings (or TYPE_UNKNOWN if it doesn't exist). */ - private static @AudioDeviceInfo.AudioDeviceType int getCanonicalDeviceType(int deviceType) { - if (isWireless(deviceType)) return deviceType; + @AudioDeviceInfo.AudioDeviceType + private static int getCanonicalDeviceType(int deviceType, int internalDeviceType) { + if (isBluetoothDevice(internalDeviceType)) return deviceType; final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE); if (spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL) { @@ -629,18 +642,9 @@ public class SpatializerHelper { */ @GuardedBy("this") @Nullable - private SADeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada) { - final int deviceType = ada.getType(); - final boolean isWireless = isWireless(deviceType); - final int canonicalDeviceType = getCanonicalDeviceType(deviceType); - - for (SADeviceState deviceState : mSADevices) { - if (deviceState.mDeviceType == canonicalDeviceType - && (!isWireless || ada.getAddress().equals(deviceState.mDeviceAddress))) { - return deviceState; - } - } - return null; + private AdiDeviceState findDeviceStateForAudioDeviceAttributes(AudioDeviceAttributes ada) { + return mDeviceBroker.findDeviceStateForAudioDeviceAttributes(ada, + getCanonicalDeviceType(ada.getType(), ada.getInternalType())); } /** @@ -662,14 +666,14 @@ public class SpatializerHelper { Log.e(TAG, "no spatialization mode found for device type:" + deviceType); return new Pair<>(false, false); } - final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); + final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); if (deviceState == null) { // no matching device state? Log.i(TAG, "no spatialization device state found for Spatial Audio device:" + ada); return new Pair<>(false, false); } // found the matching device state. - return new Pair<>(deviceState.mEnabled, true /* available */); + return new Pair<>(deviceState.isSAEnabled(), true /* available */); } private synchronized void addWirelessDeviceIfNew(@NonNull AudioDeviceAttributes ada) { @@ -678,16 +682,19 @@ public class SpatializerHelper { } if (findDeviceStateForAudioDeviceAttributes(ada) == null) { // wireless device types should be canonical, but we translate to be sure. - final int canonicalDeviceType = getCanonicalDeviceType((ada.getType())); + final int canonicalDeviceType = getCanonicalDeviceType(ada.getType(), + ada.getInternalType()); if (canonicalDeviceType == AudioDeviceInfo.TYPE_UNKNOWN) { Log.e(TAG, "addWirelessDeviceIfNew with incompatible AudioDeviceAttributes " + ada); return; } - final SADeviceState deviceState = - new SADeviceState(canonicalDeviceType, ada.getAddress()); - mSADevices.add(deviceState); - mAudioService.persistSpatialAudioDeviceSettings(); + final AdiDeviceState deviceState = + new AdiDeviceState(canonicalDeviceType, ada.getInternalType(), + ada.getAddress()); + initSAState(deviceState); + mDeviceBroker.addDeviceStateToInventory(deviceState); + mDeviceBroker.persistAudioDeviceSettings(); logDeviceState(deviceState, "addWirelessDeviceIfNew"); // may be updated later. } } @@ -756,6 +763,12 @@ public class SpatializerHelper { return false; } + private boolean isSADevice(AdiDeviceState deviceState) { + return deviceState.getDeviceType() == getCanonicalDeviceType(deviceState.getDeviceType(), + deviceState.getInternalDeviceType()) && isDeviceCompatibleWithSpatializationModes( + deviceState.getAudioDeviceAttributes()); + } + synchronized void setFeatureEnabled(boolean enabled) { loglogi("setFeatureEnabled(" + enabled + ") was featureEnabled:" + mFeatureEnabled); if (mFeatureEnabled == enabled) { @@ -768,7 +781,7 @@ public class SpatializerHelper { return; } if (mState == STATE_UNINITIALIZED) { - init(true, null /* settings */); + init(true); } setSpatializerEnabledInt(true); } else { @@ -1137,16 +1150,16 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, ignoring setHeadTrackerEnabled to " + enabled + " for " + ada); } - final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); + final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); if (deviceState == null) return; - if (!deviceState.mHasHeadTracker) { + if (!deviceState.hasHeadTracker()) { Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled + " device:" + ada + " on a device without headtracker"); return; } Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada); - deviceState.mHeadTrackerEnabled = enabled; - mAudioService.persistSpatialAudioDeviceSettings(); + deviceState.setHeadTrackerEnabled(enabled); + mDeviceBroker.persistAudioDeviceSettings(); logDeviceState(deviceState, "setHeadTrackerEnabled"); // check current routing to see if it affects the headtracking mode @@ -1170,8 +1183,8 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, hasHeadTracker always false for " + ada); return false; } - final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); - return deviceState != null && deviceState.mHasHeadTracker; + final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); + return deviceState != null && deviceState.hasHeadTracker(); } /** @@ -1184,14 +1197,14 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, setHasHeadTracker always false for " + ada); return false; } - final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); + final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); if (deviceState != null) { - if (!deviceState.mHasHeadTracker) { - deviceState.mHasHeadTracker = true; - mAudioService.persistSpatialAudioDeviceSettings(); + if (!deviceState.hasHeadTracker()) { + deviceState.setHasHeadTracker(true); + mDeviceBroker.persistAudioDeviceSettings(); logDeviceState(deviceState, "setHasHeadTracker"); } - return deviceState.mHeadTrackerEnabled; + return deviceState.isHeadTrackerEnabled(); } Log.e(TAG, "setHasHeadTracker: device not found for:" + ada); return false; @@ -1202,9 +1215,9 @@ public class SpatializerHelper { Log.v(TAG, "no headtracking support, isHeadTrackerEnabled always false for " + ada); return false; } - final SADeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); + final AdiDeviceState deviceState = findDeviceStateForAudioDeviceAttributes(ada); return deviceState != null - && deviceState.mHasHeadTracker && deviceState.mHeadTrackerEnabled; + && deviceState.hasHeadTracker() && deviceState.isHeadTrackerEnabled(); } synchronized boolean isHeadTrackerAvailable() { @@ -1543,144 +1556,6 @@ public class SpatializerHelper { pw.println("\tsupports binaural:" + mBinauralSupported + " / transaural:" + mTransauralSupported); pw.println("\tmSpatOutput:" + mSpatOutput); - pw.println("\tdevices:"); - for (SADeviceState device : mSADevices) { - pw.println("\t\t" + device); - } - } - - /*package*/ static final class SADeviceState { - private static boolean sBinauralEnabledDefault = true; - private static boolean sTransauralEnabledDefault = true; - private static boolean sHeadTrackingEnabledDefault = false; - final @AudioDeviceInfo.AudioDeviceType int mDeviceType; - final @NonNull String mDeviceAddress; - boolean mEnabled; - boolean mHasHeadTracker = false; - boolean mHeadTrackerEnabled; - static final String SETTING_FIELD_SEPARATOR = ","; - static final String SETTING_DEVICE_SEPARATOR_CHAR = "|"; - static final String SETTING_DEVICE_SEPARATOR = "\\|"; - - /** - * Constructor - * @param deviceType - * @param address must be non-null for wireless devices - * @throws NullPointerException if a null address is passed for a wireless device - */ - SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @Nullable String address) { - mDeviceType = deviceType; - mDeviceAddress = isWireless(deviceType) ? Objects.requireNonNull(address) : ""; - final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE); - mEnabled = spatMode == SpatializationMode.SPATIALIZER_BINAURAL - ? sBinauralEnabledDefault - : spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL - ? sTransauralEnabledDefault - : false; - mHeadTrackerEnabled = sHeadTrackingEnabledDefault; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - // type check and cast - if (getClass() != obj.getClass()) { - return false; - } - final SADeviceState sads = (SADeviceState) obj; - return mDeviceType == sads.mDeviceType - && mDeviceAddress.equals(sads.mDeviceAddress) - && mEnabled == sads.mEnabled - && mHasHeadTracker == sads.mHasHeadTracker - && mHeadTrackerEnabled == sads.mHeadTrackerEnabled; - } - - @Override - public int hashCode() { - return Objects.hash(mDeviceType, mDeviceAddress, mEnabled, mHasHeadTracker, - mHeadTrackerEnabled); - } - - @Override - public String toString() { - return "type: " + mDeviceType + " addr: " + mDeviceAddress + " enabled: " + mEnabled - + " HT: " + mHasHeadTracker + " HTenabled: " + mHeadTrackerEnabled; - } - - String toPersistableString() { - return (new StringBuilder().append(mDeviceType) - .append(SETTING_FIELD_SEPARATOR).append(mDeviceAddress) - .append(SETTING_FIELD_SEPARATOR).append(mEnabled ? "1" : "0") - .append(SETTING_FIELD_SEPARATOR).append(mHasHeadTracker ? "1" : "0") - .append(SETTING_FIELD_SEPARATOR).append(mHeadTrackerEnabled ? "1" : "0") - .toString()); - } - - static @Nullable SADeviceState fromPersistedString(@Nullable String persistedString) { - if (persistedString == null) { - return null; - } - if (persistedString.isEmpty()) { - return null; - } - String[] fields = TextUtils.split(persistedString, SETTING_FIELD_SEPARATOR); - if (fields.length != 5) { - // expecting all fields, fewer may mean corruption, ignore those settings - return null; - } - try { - final int deviceType = Integer.parseInt(fields[0]); - final SADeviceState deviceState = new SADeviceState(deviceType, fields[1]); - deviceState.mEnabled = Integer.parseInt(fields[2]) == 1; - deviceState.mHasHeadTracker = Integer.parseInt(fields[3]) == 1; - deviceState.mHeadTrackerEnabled = Integer.parseInt(fields[4]) == 1; - return deviceState; - } catch (NumberFormatException e) { - Log.e(TAG, "unable to parse setting for SADeviceState: " + persistedString, e); - return null; - } - } - - public AudioDeviceAttributes getAudioDeviceAttributes() { - return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, - mDeviceType, mDeviceAddress == null ? "" : mDeviceAddress); - } - - } - - /*package*/ synchronized String getSADeviceSettings() { - // expected max size of each String for each SADeviceState is 25 (accounting for separator) - final StringBuilder settingsBuilder = new StringBuilder(mSADevices.size() * 25); - for (int i = 0; i < mSADevices.size(); i++) { - settingsBuilder.append(mSADevices.get(i).toPersistableString()); - if (i != mSADevices.size() - 1) { - settingsBuilder.append(SADeviceState.SETTING_DEVICE_SEPARATOR_CHAR); - } - } - return settingsBuilder.toString(); - } - - /*package*/ synchronized void setSADeviceSettings(@NonNull String persistedSettings) { - String[] devSettings = TextUtils.split(Objects.requireNonNull(persistedSettings), - SADeviceState.SETTING_DEVICE_SEPARATOR); - // small list, not worth overhead of Arrays.stream(devSettings) - for (String setting : devSettings) { - SADeviceState devState = SADeviceState.fromPersistedString(setting); - // Note if the device is not compatible with spatialization mode - // or the device type is not canonical, it is ignored. - if (devState != null - && devState.mDeviceType == getCanonicalDeviceType(devState.mDeviceType) - && isDeviceCompatibleWithSpatializationModes( - devState.getAudioDeviceAttributes())) { - mSADevices.add(devState); - logDeviceState(devState, "setSADeviceSettings"); - } - } } private static String spatStateString(int state) { @@ -1702,15 +1577,6 @@ public class SpatializerHelper { } } - private static boolean isWireless(@AudioDeviceInfo.AudioDeviceType int deviceType) { - for (int type : WIRELESS_TYPES) { - if (type == deviceType) { - return true; - } - } - return false; - } - private int getHeadSensorHandleUpdateTracker() { int headHandle = -1; if (sRoutingDevices.isEmpty()) { @@ -1780,11 +1646,6 @@ public class SpatializerHelper { //------------------------------------------------ // for testing purposes only - - /*package*/ void clearSADevices() { - mSADevices.clear(); - } - /*package*/ synchronized void forceStateForTest(int state) { mState = state; } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index b03fb31fbb1e..bea6a2f5c22c 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1877,6 +1877,19 @@ public class UserManagerService extends IUserManager.Stub { return userTypeDetails.getBadgeNoBackground(); } + @Override + public @StringRes int getProfileLabelResId(@UserIdInt int userId) { + checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, + "getProfileLabelResId"); + final UserInfo userInfo = getUserInfoNoChecks(userId); + final UserTypeDetails userTypeDetails = getUserTypeDetails(userInfo); + if (userInfo == null || userTypeDetails == null) { + return Resources.ID_NULL; + } + final int userIndex = userInfo.profileBadge; + return userTypeDetails.getLabel(userIndex); + } + public boolean isProfile(@UserIdInt int userId) { checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isProfile"); return isProfileUnchecked(userId); diff --git a/services/core/java/com/android/server/pm/UserTypeDetails.java b/services/core/java/com/android/server/pm/UserTypeDetails.java index 6065372e1ea0..f97daebd0bea 100644 --- a/services/core/java/com/android/server/pm/UserTypeDetails.java +++ b/services/core/java/com/android/server/pm/UserTypeDetails.java @@ -54,8 +54,15 @@ public final class UserTypeDetails { /** Whether users of this type can be created. */ private final boolean mEnabled; - // TODO(b/142482943): Currently unused and not set. Hook this up. - private final int mLabel; + /** + * Resource IDs ({@link StringRes}) of the user's labels. This might be used to label a + * user/profile in tabbed views, etc. + * The values are resource IDs referring to the strings not the strings themselves. + * + * <p>This is an array because, in general, there may be multiple users of the same user type. + * In this case, the user is indexed according to its {@link UserInfo#profileBadge}. + */ + private final @Nullable int[] mLabels; /** * Maximum number of this user type allowed on the device. @@ -157,8 +164,8 @@ public final class UserTypeDetails { private final @NonNull UserProperties mDefaultUserProperties; private UserTypeDetails(@NonNull String name, boolean enabled, int maxAllowed, - @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags, int label, - int maxAllowedPerParent, + @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags, + @Nullable int[] labels, int maxAllowedPerParent, int iconBadge, int badgePlain, int badgeNoBackground, @Nullable int[] badgeLabels, @Nullable int[] badgeColors, @Nullable int[] darkThemeBadgeColors, @@ -177,11 +184,10 @@ public final class UserTypeDetails { this.mDefaultSystemSettings = defaultSystemSettings; this.mDefaultSecureSettings = defaultSecureSettings; this.mDefaultCrossProfileIntentFilters = defaultCrossProfileIntentFilters; - this.mIconBadge = iconBadge; this.mBadgePlain = badgePlain; this.mBadgeNoBackground = badgeNoBackground; - this.mLabel = label; + this.mLabels = labels; this.mBadgeLabels = badgeLabels; this.mBadgeColors = badgeColors; this.mDarkThemeBadgeColors = darkThemeBadgeColors; @@ -229,9 +235,16 @@ public final class UserTypeDetails { return mDefaultUserInfoPropertyFlags | mBaseType; } - // TODO(b/142482943) Hook this up; it is currently unused. - public int getLabel() { - return mLabel; + /** + * Returns the resource ID corresponding to the badgeIndexth label name where the badgeIndex is + * expected to be the {@link UserInfo#profileBadge} of the user. If badgeIndex exceeds the + * number of labels, returns the label for the highest index. + */ + public @StringRes int getLabel(int badgeIndex) { + if (mLabels == null || mLabels.length == 0 || badgeIndex < 0) { + return Resources.ID_NULL; + } + return mLabels[Math.min(badgeIndex, mLabels.length - 1)]; } /** Returns whether users of this user type should be badged. */ @@ -348,7 +361,6 @@ public final class UserTypeDetails { pw.print(prefix); pw.print("mMaxAllowedPerParent: "); pw.println(mMaxAllowedPerParent); pw.print(prefix); pw.print("mDefaultUserInfoFlags: "); pw.println(UserInfo.flagsToString(mDefaultUserInfoPropertyFlags)); - pw.print(prefix); pw.print("mLabel: "); pw.println(mLabel); mDefaultUserProperties.println(pw, prefix); final String restrictionsPrefix = prefix + " "; @@ -381,6 +393,8 @@ public final class UserTypeDetails { pw.println(mBadgeColors != null ? mBadgeColors.length : "0(null)"); pw.print(prefix); pw.print("mDarkThemeBadgeColors.length: "); pw.println(mDarkThemeBadgeColors != null ? mDarkThemeBadgeColors.length : "0(null)"); + pw.print(prefix); pw.print("mLabels.length: "); + pw.println(mLabels != null ? mLabels.length : "0(null)"); } /** Builder for a {@link UserTypeDetails}; see that class for documentation. */ @@ -397,13 +411,14 @@ public final class UserTypeDetails { private @Nullable List<DefaultCrossProfileIntentFilter> mDefaultCrossProfileIntentFilters = null; private int mEnabled = 1; - private int mLabel = Resources.ID_NULL; + private @Nullable int[] mLabels = null; private @Nullable int[] mBadgeLabels = null; private @Nullable int[] mBadgeColors = null; private @Nullable int[] mDarkThemeBadgeColors = null; private @DrawableRes int mIconBadge = Resources.ID_NULL; private @DrawableRes int mBadgePlain = Resources.ID_NULL; private @DrawableRes int mBadgeNoBackground = Resources.ID_NULL; + private @DrawableRes int mStatusBarIcon = Resources.ID_NULL; // Default UserProperties cannot be null but for efficiency we don't initialize it now. // If it isn't set explicitly, {@link UserProperties.Builder#build()} will be used. private @Nullable UserProperties mDefaultUserProperties = null; @@ -471,8 +486,9 @@ public final class UserTypeDetails { return this; } - public Builder setLabel(int label) { - mLabel = label; + /** Returns labels */ + public Builder setLabels(@StringRes int ... labels) { + mLabels = labels; return this; } @@ -545,7 +561,7 @@ public final class UserTypeDetails { mMaxAllowed, mBaseType, mDefaultUserInfoPropertyFlags, - mLabel, + mLabels, mMaxAllowedPerParent, mIconBadge, mBadgePlain, diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java index a814ca46fa5e..aa1ae2fed0db 100644 --- a/services/core/java/com/android/server/pm/UserTypeFactory.java +++ b/services/core/java/com/android/server/pm/UserTypeFactory.java @@ -49,6 +49,7 @@ import android.os.UserManager; import android.util.ArrayMap; import android.util.Slog; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.XmlUtils; @@ -123,7 +124,7 @@ public final class UserTypeFactory { .setName(USER_TYPE_PROFILE_CLONE) .setBaseType(FLAG_PROFILE) .setMaxAllowedPerParent(1) - .setLabel(0) + .setLabels(R.string.profile_label_clone) .setIconBadge(com.android.internal.R.drawable.ic_clone_icon_badge) .setBadgePlain(com.android.internal.R.drawable.ic_clone_badge) // Clone doesn't use BadgeNoBackground, so just set to BadgePlain as a placeholder. @@ -148,6 +149,10 @@ public final class UserTypeFactory { UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM) .setCrossProfileIntentResolutionStrategy(UserProperties .CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING) + .setShowInQuietMode( + UserProperties.SHOW_IN_QUIET_MODE_DEFAULT) + .setShowInSharingSurfaces( + UserProperties.SHOW_IN_SHARING_SURFACES_WITH_PARENT) .setMediaSharedWithParent(true) .setCredentialShareableWithParent(true) .setDeleteAppWithParent(true)); @@ -163,7 +168,10 @@ public final class UserTypeFactory { .setBaseType(FLAG_PROFILE) .setDefaultUserInfoPropertyFlags(FLAG_MANAGED_PROFILE) .setMaxAllowedPerParent(1) - .setLabel(0) + .setLabels( + R.string.profile_label_work, + R.string.profile_label_work_2, + R.string.profile_label_work_3) .setIconBadge(com.android.internal.R.drawable.ic_corp_icon_badge_case) .setBadgePlain(com.android.internal.R.drawable.ic_corp_badge_case) .setBadgeNoBackground(com.android.internal.R.drawable.ic_corp_badge_no_background) @@ -186,6 +194,10 @@ public final class UserTypeFactory { .setStartWithParent(true) .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE) .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE) + .setShowInQuietMode( + UserProperties.SHOW_IN_QUIET_MODE_PAUSED) + .setShowInSharingSurfaces( + UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) .setCredentialShareableWithParent(true)); } @@ -201,7 +213,10 @@ public final class UserTypeFactory { .setName(USER_TYPE_PROFILE_TEST) .setBaseType(FLAG_PROFILE) .setMaxAllowedPerParent(2) - .setLabel(0) + .setLabels( + R.string.profile_label_test, + R.string.profile_label_test, + R.string.profile_label_test) .setIconBadge(com.android.internal.R.drawable.ic_test_icon_badge_experiment) .setBadgePlain(com.android.internal.R.drawable.ic_test_badge_experiment) .setBadgeNoBackground(com.android.internal.R.drawable.ic_test_badge_no_background) diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java index 31599eed539d..aba24fbd55b7 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java @@ -29,13 +29,14 @@ import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.Intent; import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.AudioSystem; import android.media.BluetoothProfileConnectionInfo; import android.util.Log; -import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.After; @@ -54,7 +55,6 @@ public class AudioDeviceBrokerTest { private static final String TAG = "AudioDeviceBrokerTest"; private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100; - private Context mContext; // the actual class under test private AudioDeviceBroker mAudioDeviceBroker; @@ -67,13 +67,13 @@ public class AudioDeviceBrokerTest { @Before public void setUp() throws Exception { - mContext = InstrumentationRegistry.getTargetContext(); + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); mMockAudioService = mock(AudioService.class); mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem)); mSpySystemServer = spy(new NoOpSystemServerAdapter()); - mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory, + mAudioDeviceBroker = new AudioDeviceBroker(context, mMockAudioService, mSpyDevInventory, mSpySystemServer, mSpyAudioSystem); mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker); @@ -197,6 +197,37 @@ public class AudioDeviceBrokerTest { any(Intent.class)); } + /** + * Test that constructing an AdiDeviceState instance requires a non-null address for a + * wireless type, but can take null for a non-wireless type; + * @throws Exception + */ + @Test + public void testAdiDeviceStateNullAddressCtor() throws Exception { + try { + new AdiDeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, + AudioManager.DEVICE_OUT_SPEAKER, null); + new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, null); + Assert.fail(); + } catch (NullPointerException e) { } + } + + @Test + public void testAdiDeviceStateStringSerialization() throws Exception { + Log.i(TAG, "starting testAdiDeviceStateStringSerialization"); + final AdiDeviceState devState = new AdiDeviceState( + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioManager.DEVICE_OUT_SPEAKER, "bla"); + devState.setHasHeadTracker(false); + devState.setHeadTrackerEnabled(false); + devState.setSAEnabled(true); + final String persistString = devState.toPersistableString(); + final AdiDeviceState result = AdiDeviceState.fromPersistedString(persistString); + Log.i(TAG, "original:" + devState); + Log.i(TAG, "result :" + result); + Assert.assertEquals(devState, result); + } + private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection, boolean mockMediaPlayback, boolean guaranteeSingleConnection) throws Exception { when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC)) diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java index 3ad24de4cdca..ad09ef0ccdc1 100644 --- a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java @@ -15,8 +15,6 @@ */ package com.android.server.audio; -import com.android.server.audio.SpatializerHelper.SADeviceState; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doNothing; @@ -26,12 +24,12 @@ import static org.mockito.Mockito.when; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; -import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioSystem; import android.util.Log; import androidx.test.filters.MediumTest; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.Assert; @@ -55,72 +53,25 @@ public class SpatializerHelperTest { @Mock private AudioService mMockAudioService; @Spy private AudioSystemAdapter mSpyAudioSystem; - @Mock private AudioSystemAdapter mMockAudioSystem; + @Spy private AudioDeviceBroker mSpyDeviceBroker; @Before public void setUp() throws Exception { mMockAudioService = mock(AudioService.class); - } - - /** - * Initializes mSpatHelper, the SpatizerHelper instance under test, to use the mock or spy - * AudioSystemAdapter - * @param useSpyAudioSystem true to use the spy adapter, mSpyAudioSystem, or false to use - * the mock adapter, mMockAudioSystem. - */ - private void setUpSpatHelper(boolean useSpyAudioSystem) { - final AudioSystemAdapter asAdapter; - if (useSpyAudioSystem) { - mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); - asAdapter = mSpyAudioSystem; - mMockAudioSystem = null; - } else { - mSpyAudioSystem = null; - mMockAudioSystem = mock(NoOpAudioSystemAdapter.class); - asAdapter = mMockAudioSystem; - } - mSpatHelper = new SpatializerHelper(mMockAudioService, asAdapter, - true /*binauralEnabledDefault*/, - true /*transauralEnabledDefault*/, - false /*headTrackingEnabledDefault*/); - - } - /** - * Test that constructing an SADeviceState instance requires a non-null address for a - * wireless type, but can take null for a non-wireless type; - * @throws Exception - */ - @Test - public void testSADeviceStateNullAddressCtor() throws Exception { - setUpSpatHelper(true /*useSpyAudioSystem*/); - try { - SADeviceState devState = new SADeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null); - devState = new SADeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, null); - Assert.fail(); - } catch (NullPointerException e) { } - } - - @Test - public void testSADeviceStateStringSerialization() throws Exception { - Log.i(TAG, "starting testSADeviceStateStringSerialization"); - setUpSpatHelper(true /*useSpyAudioSystem*/); - final SADeviceState devState = new SADeviceState( - AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "bla"); - devState.mHasHeadTracker = false; - devState.mHeadTrackerEnabled = false; - devState.mEnabled = true; - final String persistString = devState.toPersistableString(); - final SADeviceState result = SADeviceState.fromPersistedString(persistString); - Log.i(TAG, "original:" + devState); - Log.i(TAG, "result :" + result); - Assert.assertEquals(devState, result); + mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); + mSpyDeviceBroker = spy( + new AudioDeviceBroker( + InstrumentationRegistry.getInstrumentation().getTargetContext(), + mMockAudioService, mSpyAudioSystem)); + mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem, + mSpyDeviceBroker, /*binauralEnabledDefault=*/true, /*transauralEnabledDefault=*/ + true, /*headTrackingEnabledDefault*/false); } @Test - public void testSADeviceSettings() throws Exception { + public void testAdiDeviceStateSettings() throws Exception { Log.i(TAG, "starting testSADeviceSettings"); - setUpSpatHelper(true /*useSpyAudioSystem*/); final AudioDeviceAttributes dev1 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""); final AudioDeviceAttributes dev2 = @@ -128,7 +79,7 @@ public class SpatializerHelperTest { final AudioDeviceAttributes dev3 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop"); - doNothing().when(mMockAudioService).persistSpatialAudioDeviceSettings(); + doNothing().when(mSpyDeviceBroker).persistAudioDeviceSettings(); mSpatHelper.initForTest(true /*binaural*/, true /*transaural*/); // test with single device @@ -163,11 +114,11 @@ public class SpatializerHelperTest { * the original one. */ private void checkAddSettings() throws Exception { - String settings = mSpatHelper.getSADeviceSettings(); + String settings = mSpyDeviceBroker.getDeviceSettings(); Log.i(TAG, "device settings: " + settings); - mSpatHelper.clearSADevices(); - mSpatHelper.setSADeviceSettings(settings); - String settingsRestored = mSpatHelper.getSADeviceSettings(); + mSpyDeviceBroker.clearDeviceInventory(); + mSpyDeviceBroker.setDeviceSettings(settings); + String settingsRestored = mSpyDeviceBroker.getDeviceSettings(); Log.i(TAG, "device settingsRestored: " + settingsRestored); Assert.assertEquals(settings, settingsRestored); } @@ -179,7 +130,6 @@ public class SpatializerHelperTest { @Test public void testNoRoutingCanBeSpatialized() throws Exception { Log.i(TAG, "Starting testNoRoutingCanBeSpatialized"); - setUpSpatHelper(false /*useSpyAudioSystem*/); mSpatHelper.forceStateForTest(SpatializerHelper.STATE_ENABLED_AVAILABLE); final ArrayList<AudioDeviceAttributes> emptyList = new ArrayList<>(0); @@ -191,12 +141,12 @@ public class SpatializerHelperTest { .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1).build(); - when(mMockAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean())) + when(mSpyAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean())) .thenReturn(emptyList); Assert.assertFalse("can be spatialized on empty routing", mSpatHelper.canBeSpatialized(media, spatialFormat)); - when(mMockAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean())) + when(mSpyAudioSystem.getDevicesForAttributes(any(AudioAttributes.class), anyBoolean())) .thenReturn(listWithNull); Assert.assertFalse("can be spatialized on null routing", mSpatHelper.canBeSpatialized(media, spatialFormat)); diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java index 2675f05ed8fe..29ff7732da7e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java @@ -60,6 +60,8 @@ public class UserManagerServiceUserPropertiesTest { .setShowInLauncher(21) .setStartWithParent(false) .setShowInSettings(45) + .setShowInSharingSurfaces(78) + .setShowInQuietMode(12) .setInheritDevicePolicy(67) .setUseParentsContacts(false) .setCrossProfileIntentFilterAccessControl(10) @@ -71,6 +73,8 @@ public class UserManagerServiceUserPropertiesTest { final UserProperties actualProps = new UserProperties(defaultProps); actualProps.setShowInLauncher(14); actualProps.setShowInSettings(32); + actualProps.setShowInSharingSurfaces(46); + actualProps.setShowInQuietMode(27); actualProps.setInheritDevicePolicy(51); actualProps.setUseParentsContacts(true); actualProps.setCrossProfileIntentFilterAccessControl(20); @@ -223,6 +227,10 @@ public class UserManagerServiceUserPropertiesTest { assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher()); assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent()); assertThat(expected.getShowInSettings()).isEqualTo(actual.getShowInSettings()); + assertThat(expected.getShowInSharingSurfaces()).isEqualTo( + actual.getShowInSharingSurfaces()); + assertThat(expected.getShowInQuietMode()) + .isEqualTo(actual.getShowInQuietMode()); assertThat(expected.getInheritDevicePolicy()).isEqualTo(actual.getInheritDevicePolicy()); assertThat(expected.getUseParentsContacts()).isEqualTo(actual.getUseParentsContacts()); assertThat(expected.getCrossProfileIntentFilterAccessControl()) diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java index ff9a79e61fe0..fe2bf38f65ea 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java @@ -90,6 +90,8 @@ public class UserManagerServiceUserTypeTest { .setMediaSharedWithParent(true) .setCredentialShareableWithParent(false) .setShowInSettings(900) + .setShowInSharingSurfaces(20) + .setShowInQuietMode(30) .setInheritDevicePolicy(340) .setDeleteAppWithParent(true); @@ -104,8 +106,8 @@ public class UserManagerServiceUserTypeTest { .setIconBadge(28) .setBadgePlain(29) .setBadgeNoBackground(30) - .setLabel(31) .setMaxAllowedPerParent(32) + .setLabels(34, 35, 36) .setDefaultRestrictions(restrictions) .setDefaultSystemSettings(systemSettings) .setDefaultSecureSettings(secureSettings) @@ -120,8 +122,10 @@ public class UserManagerServiceUserTypeTest { assertEquals(28, type.getIconBadge()); assertEquals(29, type.getBadgePlain()); assertEquals(30, type.getBadgeNoBackground()); - assertEquals(31, type.getLabel()); assertEquals(32, type.getMaxAllowedPerParent()); + assertEquals(34, type.getLabel(0)); + assertEquals(35, type.getLabel(1)); + assertEquals(36, type.getLabel(2)); assertTrue(UserRestrictionsUtils.areEqual(restrictions, type.getDefaultRestrictions())); assertNotSame(restrictions, type.getDefaultRestrictions()); @@ -157,6 +161,9 @@ public class UserManagerServiceUserTypeTest { assertTrue(type.getDefaultUserPropertiesReference().isMediaSharedWithParent()); assertFalse(type.getDefaultUserPropertiesReference().isCredentialShareableWithParent()); assertEquals(900, type.getDefaultUserPropertiesReference().getShowInSettings()); + assertEquals(20, type.getDefaultUserPropertiesReference().getShowInSharingSurfaces()); + assertEquals(30, + type.getDefaultUserPropertiesReference().getShowInQuietMode()); assertEquals(340, type.getDefaultUserPropertiesReference() .getInheritDevicePolicy()); assertTrue(type.getDefaultUserPropertiesReference().getDeleteAppWithParent()); @@ -193,7 +200,7 @@ public class UserManagerServiceUserTypeTest { assertEquals(Resources.ID_NULL, type.getBadgeNoBackground()); assertEquals(Resources.ID_NULL, type.getBadgeLabel(0)); assertEquals(Resources.ID_NULL, type.getBadgeColor(0)); - assertEquals(Resources.ID_NULL, type.getLabel()); + assertEquals(Resources.ID_NULL, type.getLabel(0)); assertTrue(type.getDefaultRestrictions().isEmpty()); assertTrue(type.getDefaultSystemSettings().isEmpty()); assertTrue(type.getDefaultSecureSettings().isEmpty()); @@ -210,6 +217,10 @@ public class UserManagerServiceUserTypeTest { props.getCrossProfileIntentResolutionStrategy()); assertFalse(props.isMediaSharedWithParent()); assertFalse(props.isCredentialShareableWithParent()); + assertFalse(props.getDeleteAppWithParent()); + assertEquals(UserProperties.SHOW_IN_LAUNCHER_SEPARATE, props.getShowInSharingSurfaces()); + assertEquals(UserProperties.SHOW_IN_QUIET_MODE_PAUSED, + props.getShowInQuietMode()); assertFalse(type.hasBadge()); } @@ -298,8 +309,12 @@ public class UserManagerServiceUserTypeTest { .setCredentialShareableWithParent(true) .setShowInSettings(20) .setInheritDevicePolicy(21) + .setDeleteAppWithParent(true) + .setShowInSharingSurfaces(22) + .setShowInQuietMode(24) .setDeleteAppWithParent(true); + final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>(); builders.put(userTypeAosp1, new UserTypeDetails.Builder() .setName(userTypeAosp1) @@ -338,6 +353,9 @@ public class UserManagerServiceUserTypeTest { assertEquals(20, aospType.getDefaultUserPropertiesReference().getShowInSettings()); assertEquals(21, aospType.getDefaultUserPropertiesReference() .getInheritDevicePolicy()); + assertEquals(22, aospType.getDefaultUserPropertiesReference().getShowInSharingSurfaces()); + assertEquals(24, + aospType.getDefaultUserPropertiesReference().getShowInQuietMode()); assertTrue(aospType.getDefaultUserPropertiesReference().getDeleteAppWithParent()); // userTypeAosp2 should be modified. @@ -379,6 +397,10 @@ public class UserManagerServiceUserTypeTest { assertFalse(aospType.getDefaultUserPropertiesReference() .isCredentialShareableWithParent()); assertEquals(23, aospType.getDefaultUserPropertiesReference().getShowInSettings()); + assertEquals(22, + aospType.getDefaultUserPropertiesReference().getShowInSharingSurfaces()); + assertEquals(24, + aospType.getDefaultUserPropertiesReference().getShowInQuietMode()); assertEquals(450, aospType.getDefaultUserPropertiesReference() .getInheritDevicePolicy()); assertFalse(aospType.getDefaultUserPropertiesReference() diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index 6bcda3fbcf43..6997530d1a9d 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; import android.annotation.UserIdInt; @@ -30,6 +31,7 @@ import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.content.pm.UserProperties; import android.content.res.Resources; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; @@ -207,6 +209,9 @@ public final class UserManagerTest { .isEqualTo(cloneUserProperties.isCredentialShareableWithParent()); assertThrows(SecurityException.class, cloneUserProperties::getDeleteAppWithParent); + compareDrawables(mUserManager.getUserBadge(), + Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain())); + // Verify clone user parent assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id); @@ -804,6 +809,9 @@ public final class UserManagerTest { assertThat(mUserManager.getUserBadgeNoBackgroundResId(userId)) .isEqualTo(userTypeDetails.getBadgeNoBackground()); + compareDrawables(mUserManager.getUserBadge(), + Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain())); + final int badgeIndex = userInfo.profileBadge; assertThat(mUserManager.getUserBadgeColor(userId)).isEqualTo( Resources.getSystem().getColor(userTypeDetails.getBadgeColor(badgeIndex), null)); @@ -1554,4 +1562,10 @@ public final class UserManagerTest { .getBoolean(com.android.internal.R.bool.config_isMainUserPermanentAdmin); } + private void compareDrawables(Drawable actual, Drawable expected) { + assertEquals(actual.getIntrinsicWidth(), expected.getIntrinsicWidth()); + assertEquals(actual.getIntrinsicHeight(), expected.getIntrinsicHeight()); + assertEquals(actual.getLevel(), expected.getLevel()); + } + } |