diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-06 22:45:30 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-12-06 22:45:30 +0000 |
commit | 01cd624ac1c185acef0ce51d4e5d65e7095c3692 (patch) | |
tree | e770e62be5d51bc0439022587c86ecb2fc094e21 | |
parent | 51b50236fc4dbb941b85e74b9441c69e61025282 (diff) | |
parent | 042b577709945f32a0f32635d606dcf558b794ab (diff) | |
download | base-01cd624ac1c185acef0ce51d4e5d65e7095c3692.tar.gz |
Merge cherrypicks of ['googleplex-android-review.googlesource.com/25102047', 'googleplex-android-review.googlesource.com/25043220', 'googleplex-android-review.googlesource.com/25379523', 'googleplex-android-review.googlesource.com/25116113', 'googleplex-android-review.googlesource.com/25381953', 'googleplex-android-review.googlesource.com/25410669', 'googleplex-android-review.googlesource.com/24709104', 'googleplex-android-review.googlesource.com/25188138', 'googleplex-android-review.googlesource.com/25188139'] into security-aosp-udc-release.android-security-14.0.0_r5
Change-Id: I3033da956d2bc85949434d103f800308a3014cdf
24 files changed, 1313 insertions, 600 deletions
diff --git a/core/java/android/app/UiAutomationConnection.java b/core/java/android/app/UiAutomationConnection.java index 34f0964cf823..9831885f291d 100644 --- a/core/java/android/app/UiAutomationConnection.java +++ b/core/java/android/app/UiAutomationConnection.java @@ -183,6 +183,11 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { @Override public void injectInputEventToInputFilter(InputEvent event) throws RemoteException { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } mAccessibilityManager.injectInputEventToInputFilter(event); } 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/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java index 58376a77c705..0801dd8c0bd8 100644 --- a/core/java/com/android/internal/content/FileSystemProvider.java +++ b/core/java/com/android/internal/content/FileSystemProvider.java @@ -62,16 +62,14 @@ import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; + import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; -import java.util.Deque; -import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Queue; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Predicate; -import java.util.regex.Pattern; /** * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local @@ -89,6 +87,8 @@ public abstract class FileSystemProvider extends DocumentsProvider { DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, DocumentsContract.QUERY_ARG_MIME_TYPES); + private static final int MAX_RESULTS_NUMBER = 23; + private static String joinNewline(String... args) { return TextUtils.join("\n", args); } @@ -375,62 +375,53 @@ public abstract class FileSystemProvider extends DocumentsProvider { } /** - * This method is similar to - * {@link DocumentsProvider#queryChildDocuments(String, String[], String)}. This method returns - * all children documents including hidden directories/files. - * - * <p> - * In a scoped storage world, access to "Android/data" style directories are hidden for privacy - * reasons. This method may show privacy sensitive data, so its usage should only be in - * restricted modes. - * - * @param parentDocumentId the directory to return children for. - * @param projection list of {@link Document} columns to put into the - * cursor. If {@code null} all supported columns should be - * included. - * @param sortOrder how to order the rows, formatted as an SQL - * {@code ORDER BY} clause (excluding the ORDER BY itself). - * Passing {@code null} will use the default sort order, which - * may be unordered. This ordering is a hint that can be used to - * prioritize how data is fetched from the network, but UI may - * always enforce a specific ordering - * @throws FileNotFoundException when parent document doesn't exist or query fails + * WARNING: this method should really be {@code final}, but for the backward compatibility it's + * not; new classes that extend {@link FileSystemProvider} should override + * {@link #queryChildDocuments(String, String[], String, boolean)}, not this method. */ - protected Cursor queryChildDocumentsShowAll( - String parentDocumentId, String[] projection, String sortOrder) + @Override + public Cursor queryChildDocuments(String documentId, String[] projection, String sortOrder) throws FileNotFoundException { - return queryChildDocuments(parentDocumentId, projection, sortOrder, File -> true); + return queryChildDocuments(documentId, projection, sortOrder, /* includeHidden */ false); } + /** + * This method is similar to {@link #queryChildDocuments(String, String[], String)}, however, it + * could return <b>all</b> content of the directory, <b>including restricted (hidden) + * directories and files</b>. + * <p> + * In the scoped storage world, some directories and files (e.g. {@code Android/data/} and + * {@code Android/obb/} on the external storage) are hidden for privacy reasons. + * Hence, this method may reveal privacy-sensitive data, thus should be used with extra care. + */ @Override - public Cursor queryChildDocuments( - String parentDocumentId, String[] projection, String sortOrder) - throws FileNotFoundException { - // Access to some directories is hidden for privacy reasons. - return queryChildDocuments(parentDocumentId, projection, sortOrder, this::shouldShow); + public final Cursor queryChildDocumentsForManage(String documentId, String[] projection, + String sortOrder) throws FileNotFoundException { + return queryChildDocuments(documentId, projection, sortOrder, /* includeHidden */ true); } - private Cursor queryChildDocuments( - String parentDocumentId, String[] projection, String sortOrder, - @NonNull Predicate<File> filter) throws FileNotFoundException { - final File parent = getFileForDocId(parentDocumentId); + protected Cursor queryChildDocuments(String documentId, String[] projection, String sortOrder, + boolean includeHidden) throws FileNotFoundException { + final File parent = getFileForDocId(documentId); final MatrixCursor result = new DirectoryCursor( - resolveProjection(projection), parentDocumentId, parent); + resolveProjection(projection), documentId, parent); + + if (!parent.isDirectory()) { + Log.w(TAG, '"' + documentId + "\" is not a directory"); + return result; + } - if (!filter.test(parent)) { - Log.w(TAG, "No permission to access parentDocumentId: " + parentDocumentId); + if (!includeHidden && shouldHideDocument(documentId)) { + Log.w(TAG, "Queried directory \"" + documentId + "\" is hidden"); return result; } - if (parent.isDirectory()) { - for (File file : FileUtils.listFilesOrEmpty(parent)) { - if (filter.test(file)) { - includeFile(result, null, file); - } - } - } else { - Log.w(TAG, "parentDocumentId '" + parentDocumentId + "' is not Directory"); + for (File file : FileUtils.listFilesOrEmpty(parent)) { + if (!includeHidden && shouldHideDocument(file)) continue; + + includeFile(result, null, file); } + return result; } @@ -452,23 +443,29 @@ public abstract class FileSystemProvider extends DocumentsProvider { * * @see ContentResolver#EXTRA_HONORED_ARGS */ - protected final Cursor querySearchDocuments( - File folder, String[] projection, Set<String> exclusion, Bundle queryArgs) - throws FileNotFoundException { + protected final Cursor querySearchDocuments(File folder, String[] projection, + Set<String> exclusion, Bundle queryArgs) throws FileNotFoundException { final MatrixCursor result = new MatrixCursor(resolveProjection(projection)); - final List<File> pending = new ArrayList<>(); - pending.add(folder); - while (!pending.isEmpty() && result.getCount() < 24) { - final File file = pending.remove(0); - if (shouldHide(file)) continue; + + // We'll be a running a BFS here. + final Queue<File> pending = new ArrayDeque<>(); + pending.offer(folder); + + while (!pending.isEmpty() && result.getCount() < MAX_RESULTS_NUMBER) { + final File file = pending.poll(); + + // Skip hidden documents (both files and directories) + if (shouldHideDocument(file)) continue; if (file.isDirectory()) { for (File child : FileUtils.listFilesOrEmpty(file)) { - pending.add(child); + pending.offer(child); } } - if (!exclusion.contains(file.getAbsolutePath()) && matchSearchQueryArguments(file, - queryArgs)) { + + if (exclusion.contains(file.getAbsolutePath())) continue; + + if (matchSearchQueryArguments(file, queryArgs)) { includeFile(result, null, file); } } @@ -612,26 +609,23 @@ public abstract class FileSystemProvider extends DocumentsProvider { final int flagIndex = ArrayUtils.indexOf(columns, Document.COLUMN_FLAGS); if (flagIndex != -1) { + final boolean isDir = mimeType.equals(Document.MIME_TYPE_DIR); int flags = 0; if (file.canWrite()) { - if (mimeType.equals(Document.MIME_TYPE_DIR)) { + flags |= Document.FLAG_SUPPORTS_DELETE; + flags |= Document.FLAG_SUPPORTS_RENAME; + flags |= Document.FLAG_SUPPORTS_MOVE; + if (isDir) { flags |= Document.FLAG_DIR_SUPPORTS_CREATE; - flags |= Document.FLAG_SUPPORTS_DELETE; - flags |= Document.FLAG_SUPPORTS_RENAME; - flags |= Document.FLAG_SUPPORTS_MOVE; - - if (shouldBlockFromTree(docId)) { - flags |= Document.FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE; - } - } else { flags |= Document.FLAG_SUPPORTS_WRITE; - flags |= Document.FLAG_SUPPORTS_DELETE; - flags |= Document.FLAG_SUPPORTS_RENAME; - flags |= Document.FLAG_SUPPORTS_MOVE; } } + if (isDir && shouldBlockDirectoryFromTree(docId)) { + flags |= Document.FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE; + } + if (mimeType.startsWith("image/")) { flags |= Document.FLAG_SUPPORTS_THUMBNAIL; } @@ -664,22 +658,36 @@ public abstract class FileSystemProvider extends DocumentsProvider { return row; } - private static final Pattern PATTERN_HIDDEN_PATH = Pattern.compile( - "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)$"); - /** - * In a scoped storage world, access to "Android/data" style directories are - * hidden for privacy reasons. + * Some providers may want to restrict access to certain directories and files, + * e.g. <i>"Android/data"</i> and <i>"Android/obb"</i> on the shared storage for + * privacy reasons. + * Such providers should override this method. */ - protected boolean shouldHide(@NonNull File file) { - return (PATTERN_HIDDEN_PATH.matcher(file.getAbsolutePath()).matches()); + protected boolean shouldHideDocument(@NonNull String documentId) + throws FileNotFoundException { + return false; } - private boolean shouldShow(@NonNull File file) { - return !shouldHide(file); + /** + * A variant of the {@link #shouldHideDocument(String)} that takes a {@link File} instead of + * a {@link String} {@code documentId}. + * + * @see #shouldHideDocument(String) + */ + protected final boolean shouldHideDocument(@NonNull File document) + throws FileNotFoundException { + return shouldHideDocument(getDocIdForFile(document)); } - protected boolean shouldBlockFromTree(@NonNull String docId) { + /** + * @return if the directory that should be blocked from being selected when the user launches + * an {@link Intent#ACTION_OPEN_DOCUMENT_TREE} intent. + * + * @see Document#FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE + */ + protected boolean shouldBlockDirectoryFromTree(@NonNull String documentId) + throws FileNotFoundException { return false; } 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/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java index 4c313b22f71e..3409c29d3c2c 100644 --- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java +++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java @@ -16,6 +16,8 @@ package com.android.externalstorage; +import static java.util.regex.Pattern.CASE_INSENSITIVE; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.usage.StorageStatsManager; @@ -64,7 +66,19 @@ import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.UUID; - +import java.util.regex.Pattern; + +/** + * Presents content of the shared (a.k.a. "external") storage. + * <p> + * Starting with Android 11 (R), restricts access to the certain sections of the shared storage: + * {@code Android/data/}, {@code Android/obb/} and {@code Android/sandbox/}, that will be hidden in + * the DocumentsUI by default. + * See <a href="https://developer.android.com/about/versions/11/privacy/storage"> + * Storage updates in Android 11</a>. + * <p> + * Documents ID format: {@code root:path/to/file}. + */ public class ExternalStorageProvider extends FileSystemProvider { private static final String TAG = "ExternalStorage"; @@ -75,7 +89,12 @@ public class ExternalStorageProvider extends FileSystemProvider { private static final Uri BASE_URI = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build(); - // docId format: root:path/to/file + /** + * Regex for detecting {@code /Android/data/}, {@code /Android/obb/} and + * {@code /Android/sandbox/} along with all their subdirectories and content. + */ + private static final Pattern PATTERN_RESTRICTED_ANDROID_SUBTREES = + Pattern.compile("^Android/(?:data|obb|sandbox)(?:/.+)?", CASE_INSENSITIVE); private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, @@ -278,76 +297,91 @@ public class ExternalStorageProvider extends FileSystemProvider { return projection != null ? projection : DEFAULT_ROOT_PROJECTION; } + /** + * Mark {@code Android/data/}, {@code Android/obb/} and {@code Android/sandbox/} on the + * integrated shared ("external") storage along with all their content and subdirectories as + * hidden. + */ @Override - public Cursor queryChildDocumentsForManage( - String parentDocId, String[] projection, String sortOrder) - throws FileNotFoundException { - return queryChildDocumentsShowAll(parentDocId, projection, sortOrder); + protected boolean shouldHideDocument(@NonNull String documentId) { + // Don't need to hide anything on USB drives. + if (isOnRemovableUsbStorage(documentId)) { + return false; + } + + final String path = getPathFromDocId(documentId); + return PATTERN_RESTRICTED_ANDROID_SUBTREES.matcher(path).matches(); } /** * Check that the directory is the root of storage or blocked file from tree. + * <p> + * Note, that this is different from hidden documents: blocked documents <b>WILL</b> appear + * the UI, but the user <b>WILL NOT</b> be able to select them. * - * @param docId the docId of the directory to be checked + * @param documentId the docId of the directory to be checked * @return true, should be blocked from tree. Otherwise, false. + * + * @see Document#FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE */ @Override - protected boolean shouldBlockFromTree(@NonNull String docId) { - try { - final File dir = getFileForDocId(docId, false /* visible */); - - // the file is null or it is not a directory - if (dir == null || !dir.isDirectory()) { - return false; - } + protected boolean shouldBlockDirectoryFromTree(@NonNull String documentId) + throws FileNotFoundException { + final File dir = getFileForDocId(documentId, false); + // The file is null or it is not a directory + if (dir == null || !dir.isDirectory()) { + return false; + } - // Allow all directories on USB, including the root. - try { - RootInfo rootInfo = getRootFromDocId(docId); - if ((rootInfo.flags & Root.FLAG_REMOVABLE_USB) == Root.FLAG_REMOVABLE_USB) { - return false; - } - } catch (FileNotFoundException e) { - Log.e(TAG, "Failed to determine rootInfo for docId"); - } + // Allow all directories on USB, including the root. + if (isOnRemovableUsbStorage(documentId)) { + return false; + } - final String path = getPathFromDocId(docId); + // Get canonical(!) path. Note that this path will have neither leading nor training "/". + // This the root's path will be just an empty string. + final String path = getPathFromDocId(documentId); - // Block the root of the storage - if (path.isEmpty()) { - return true; - } + // Block the root of the storage + if (path.isEmpty()) { + return true; + } - // Block Download folder from tree - if (TextUtils.equals(Environment.DIRECTORY_DOWNLOADS.toLowerCase(Locale.ROOT), - path.toLowerCase(Locale.ROOT))) { - return true; - } + // Block /Download/ and /Android/ folders from the tree. + if (equalIgnoringCase(path, Environment.DIRECTORY_DOWNLOADS) || + equalIgnoringCase(path, Environment.DIRECTORY_ANDROID)) { + return true; + } - // Block /Android - if (TextUtils.equals(Environment.DIRECTORY_ANDROID.toLowerCase(Locale.ROOT), - path.toLowerCase(Locale.ROOT))) { - return true; - } + // This shouldn't really make a difference, but just in case - let's block hidden + // directories as well. + if (shouldHideDocument(documentId)) { + return true; + } - // Block /Android/data, /Android/obb, /Android/sandbox and sub dirs - if (shouldHide(dir)) { - return true; - } + return false; + } + private boolean isOnRemovableUsbStorage(@NonNull String documentId) { + final RootInfo rootInfo; + try { + rootInfo = getRootFromDocId(documentId); + } catch (FileNotFoundException e) { + Log.e(TAG, "Failed to determine rootInfo for docId\"" + documentId + '"'); return false; - } catch (IOException e) { - throw new IllegalArgumentException( - "Failed to determine if " + docId + " should block from tree " + ": " + e); } + + return (rootInfo.flags & Root.FLAG_REMOVABLE_USB) != 0; } + @NonNull @Override - protected String getDocIdForFile(File file) throws FileNotFoundException { + protected String getDocIdForFile(@NonNull File file) throws FileNotFoundException { return getDocIdForFileMaybeCreate(file, false); } - private String getDocIdForFileMaybeCreate(File file, boolean createNewDir) + @NonNull + private String getDocIdForFileMaybeCreate(@NonNull File file, boolean createNewDir) throws FileNotFoundException { String path = file.getAbsolutePath(); @@ -417,31 +451,33 @@ public class ExternalStorageProvider extends FileSystemProvider { private File getFileForDocId(String docId, boolean visible, boolean mustExist) throws FileNotFoundException { RootInfo root = getRootFromDocId(docId); - return buildFile(root, docId, visible, mustExist); + return buildFile(root, docId, mustExist); } - private Pair<RootInfo, File> resolveDocId(String docId, boolean visible) - throws FileNotFoundException { + private Pair<RootInfo, File> resolveDocId(String docId) throws FileNotFoundException { RootInfo root = getRootFromDocId(docId); - return Pair.create(root, buildFile(root, docId, visible, true)); + return Pair.create(root, buildFile(root, docId, /* mustExist */ true)); } @VisibleForTesting - static String getPathFromDocId(String docId) throws IOException { + static String getPathFromDocId(String docId) { final int splitIndex = docId.indexOf(':', 1); final String docIdPath = docId.substring(splitIndex + 1); - // Get CanonicalPath and remove the first "/" - final String canonicalPath = new File(docIdPath).getCanonicalPath().substring(1); - if (canonicalPath.isEmpty()) { - return canonicalPath; + // Canonicalize path and strip the leading "/" + final String path; + try { + path = new File(docIdPath).getCanonicalPath().substring(1); + } catch (IOException e) { + Log.w(TAG, "Could not canonicalize \"" + docIdPath + '"'); + return ""; } - // remove trailing "/" - if (canonicalPath.charAt(canonicalPath.length() - 1) == '/') { - return canonicalPath.substring(0, canonicalPath.length() - 1); + // Remove the trailing "/" as well. + if (!path.isEmpty() && path.charAt(path.length() - 1) == '/') { + return path.substring(0, path.length() - 1); } else { - return canonicalPath; + return path; } } @@ -460,7 +496,7 @@ public class ExternalStorageProvider extends FileSystemProvider { return root; } - private File buildFile(RootInfo root, String docId, boolean visible, boolean mustExist) + private File buildFile(RootInfo root, String docId, boolean mustExist) throws FileNotFoundException { final int splitIndex = docId.indexOf(':', 1); final String path = docId.substring(splitIndex + 1); @@ -544,7 +580,7 @@ public class ExternalStorageProvider extends FileSystemProvider { @Override public Path findDocumentPath(@Nullable String parentDocId, String childDocId) throws FileNotFoundException { - final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId, false); + final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId); final RootInfo root = resolvedDocId.first; File child = resolvedDocId.second; @@ -648,6 +684,13 @@ public class ExternalStorageProvider extends FileSystemProvider { } } + /** + * Print the state into the given stream. + * Gets invoked when you run: + * <pre> + * adb shell dumpsys activity provider com.android.externalstorage/.ExternalStorageProvider + * </pre> + */ @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160); @@ -731,4 +774,8 @@ public class ExternalStorageProvider extends FileSystemProvider { } return bundle; } + + private static boolean equalIgnoringCase(@NonNull String a, @NonNull String b) { + return TextUtils.equals(a.toLowerCase(Locale.ROOT), b.toLowerCase(Locale.ROOT)); + } } 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/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java index 2469a98140e3..3750c44a4923 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -281,6 +281,11 @@ public class TileLifecycleManager extends BroadcastReceiver implements } @Override + public void onNullBinding(ComponentName name) { + executeSetBindService(false); + } + + @Override public void onServiceDisconnected(ComponentName name) { if (DEBUG) Log.d(TAG, "onServiceDisconnected " + name); handleDeath(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt index 68321f433c81..28b00b738a66 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt @@ -182,19 +182,19 @@ constructor( // Set hasPersistentDot to false. If the animationState is anything before ANIMATING_OUT, // the disappear animation will not animate into a dot but remove the chip entirely hasPersistentDot = false - // if we are currently showing a persistent dot, hide it - if (animationState.value == SHOWING_PERSISTENT_DOT) notifyHidePersistentDot() - // if we are currently animating into a dot, wait for the animation to finish and then hide - // the dot - if (animationState.value == ANIMATING_OUT) { - coroutineScope.launch { - withTimeout(DISAPPEAR_ANIMATION_DURATION) { - animationState.first { - it == SHOWING_PERSISTENT_DOT || it == IDLE || it == ANIMATION_QUEUED - } - notifyHidePersistentDot() - } + + if (animationState.value == SHOWING_PERSISTENT_DOT) { + // if we are currently showing a persistent dot, hide it and update the animationState + notifyHidePersistentDot() + if (scheduledEvent.value != null) { + animationState.value = ANIMATION_QUEUED + } else { + animationState.value = IDLE } + } else if (animationState.value == ANIMATING_OUT) { + // if we are currently animating out, hide the dot. The animationState will be updated + // once the animation has ended in the onAnimationEnd callback + notifyHidePersistentDot() } } @@ -376,14 +376,6 @@ constructor( Assert.isMainThread() val anims: List<Animator> = listeners.mapNotNull { it.onHidePersistentDot() } - if (animationState.value == SHOWING_PERSISTENT_DOT) { - if (scheduledEvent.value != null) { - animationState.value = ANIMATION_QUEUED - } else { - animationState.value = IDLE - } - } - if (anims.isNotEmpty()) { val aSet = AnimatorSet() aSet.playTogether(anims) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java index 67587e3a8914..37df93e4c809 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java @@ -373,6 +373,30 @@ public class TileLifecycleManagerTest extends SysuiTestCase { verify(falseContext).bindServiceAsUser(any(), any(), eq(flags), any()); } + @Test + public void testNullBindingCallsUnbind() { + Context mockContext = mock(Context.class); + // Binding has to succeed + when(mockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true); + TileLifecycleManager manager = new TileLifecycleManager(mHandler, mockContext, + mock(IQSService.class), + mMockPackageManagerAdapter, + mMockBroadcastDispatcher, + mTileServiceIntent, + mUser, + mExecutor); + + manager.executeSetBindService(true); + mExecutor.runAllReady(); + + ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class); + verify(mockContext).bindServiceAsUser(any(), captor.capture(), anyInt(), any()); + + captor.getValue().onNullBinding(mTileServiceComponentName); + mExecutor.runAllReady(); + verify(mockContext).unbindService(captor.getValue()); + } + private void mockChangeEnabled(long changeId, boolean enabled) { doReturn(enabled).when(() -> CompatChanges.isChangeEnabled(eq(changeId), anyString(), any(UserHandle.class))); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt index 8a6dfe518cbf..1079a7df8842 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt @@ -410,15 +410,16 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { // remove persistent dot systemStatusAnimationScheduler.removePersistentDot() - testScheduler.runCurrent() + + // verify that the onHidePersistentDot callback is invoked + verify(listener, times(1)).onHidePersistentDot() // skip disappear animation animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) testScheduler.runCurrent() - // verify that animationState changes to IDLE and onHidePersistentDot callback is invoked + // verify that animationState changes to IDLE assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState()) - verify(listener, times(1)).onHidePersistentDot() } @Test @@ -483,7 +484,6 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { // request removal of persistent dot systemStatusAnimationScheduler.removePersistentDot() - testScheduler.runCurrent() // schedule another high priority event while the event is animating out createAndScheduleFakePrivacyEvent() @@ -499,6 +499,42 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { verify(listener, times(1)).onHidePersistentDot() } + @Test + fun testDotIsRemoved_evenIfAnimatorCallbackIsDelayed() = runTest { + // Instantiate class under test with TestScope from runTest + initializeSystemStatusAnimationScheduler(testScope = this) + + // create and schedule high priority event + createAndScheduleFakePrivacyEvent() + + // skip chip animation lifecycle and fast forward to ANIMATING_OUT state + fastForwardAnimationToState(ANIMATING_OUT) + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any()) + + // request removal of persistent dot + systemStatusAnimationScheduler.removePersistentDot() + + // verify that the state is still ANIMATING_OUT + assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState()) + + // skip disappear animation duration + testScheduler.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION + 1) + // In an old implementation this would trigger a coroutine timeout causing the + // onHidePersistentDot callback to be missed. + testScheduler.runCurrent() + + // advance animator time to invoke onAnimationEnd callback + animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION) + testScheduler.runCurrent() + + // verify that onHidePersistentDot is invoked despite the animator callback being delayed + // (it's invoked more than DISAPPEAR_ANIMATION_DURATION after the dot removal was requested) + verify(listener, times(1)).onHidePersistentDot() + // verify that animationState is IDLE + assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState()) + } + private fun TestScope.fastForwardAnimationToState(@SystemAnimationState animationState: Int) { // this function should only be called directly after posting a status event assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState()) diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index fdb28ba9103e..531227947ba0 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -5232,6 +5232,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @Override public void injectInputEventToInputFilter(InputEvent event) { + mSecurityPolicy.enforceCallingPermission(Manifest.permission.INJECT_EVENTS, + "injectInputEventToInputFilter"); synchronized (mLock) { final long endMillis = SystemClock.uptimeMillis() + WAIT_INPUT_FILTER_INSTALL_TIMEOUT_MS; diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java index 7557071d0d4b..b93ee9b96e57 100644 --- a/services/autofill/java/com/android/server/autofill/Helper.java +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -25,8 +25,11 @@ import android.app.ActivityManager; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.ViewNode; import android.app.assist.AssistStructure.WindowNode; +import android.app.slice.Slice; +import android.app.slice.SliceItem; import android.content.ComponentName; import android.content.Context; +import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; import android.metrics.LogMaker; import android.os.UserManager; @@ -55,7 +58,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; - public final class Helper { private static final String TAG = "AutofillHelper"; @@ -93,7 +95,7 @@ public final class Helper { final AtomicBoolean permissionsOk = new AtomicBoolean(true); rView.visitUris(uri -> { - int uriOwnerId = android.content.ContentProvider.getUserIdFromUri(uri); + int uriOwnerId = android.content.ContentProvider.getUserIdFromUri(uri, userId); boolean allowed = uriOwnerId == userId; permissionsOk.set(allowed && permissionsOk.get()); }); @@ -125,6 +127,48 @@ public final class Helper { return (ok ? rView : null); } + /** + * Checks the URI permissions of the icon in the slice, to see if the current userId is able to + * access it. + * + * <p>Returns null if slice contains user inaccessible icons + * + * <p>TODO: instead of returning a null Slice when the current userId cannot access an icon, + * return a reconstructed Slice without the icons. This is currently non-trivial since there are + * no public methods to generically add SliceItems to Slices + */ + public static @Nullable Slice sanitizeSlice(Slice slice) { + if (slice == null) { + return null; + } + + int userId = ActivityManager.getCurrentUser(); + + // Recontruct the Slice, filtering out bad icons + for (SliceItem sliceItem : slice.getItems()) { + if (!sliceItem.getFormat().equals(SliceItem.FORMAT_IMAGE)) { + // Not an image slice + continue; + } + + Icon icon = sliceItem.getIcon(); + if (icon.getType() != Icon.TYPE_URI + && icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP) { + // No URIs to sanitize + continue; + } + + int iconUriId = android.content.ContentProvider.getUserIdFromUri(icon.getUri(), userId); + + if (iconUriId != userId) { + Slog.w(TAG, "sanitizeSlice() user: " + userId + " cannot access icons in Slice"); + return null; + } + } + + return slice; + } + @Nullable static AutofillId[] toArray(@Nullable ArraySet<AutofillId> set) { diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java index 70443f9153d1..024de4e825aa 100644 --- a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java +++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java @@ -27,6 +27,7 @@ import android.service.autofill.InlinePresentation; import android.util.Slog; import com.android.server.LocalServices; +import com.android.server.autofill.Helper; import com.android.server.autofill.RemoteInlineSuggestionRenderService; import com.android.server.inputmethod.InputMethodManagerInternal; @@ -39,24 +40,17 @@ import java.util.function.Consumer; final class RemoteInlineSuggestionViewConnector { private static final String TAG = RemoteInlineSuggestionViewConnector.class.getSimpleName(); - @Nullable - private final RemoteInlineSuggestionRenderService mRemoteRenderService; - @NonNull - private final InlinePresentation mInlinePresentation; - @Nullable - private final IBinder mHostInputToken; + @Nullable private final RemoteInlineSuggestionRenderService mRemoteRenderService; + @NonNull private final InlinePresentation mInlinePresentation; + @Nullable private final IBinder mHostInputToken; private final int mDisplayId; private final int mUserId; private final int mSessionId; - @NonNull - private final Runnable mOnAutofillCallback; - @NonNull - private final Runnable mOnErrorCallback; - @NonNull - private final Runnable mOnInflateCallback; - @NonNull - private final Consumer<IntentSender> mStartIntentSenderFromClientApp; + @NonNull private final Runnable mOnAutofillCallback; + @NonNull private final Runnable mOnErrorCallback; + @NonNull private final Runnable mOnInflateCallback; + @NonNull private final Consumer<IntentSender> mStartIntentSenderFromClientApp; RemoteInlineSuggestionViewConnector( @NonNull InlineFillUi.InlineFillUiInfo inlineFillUiInfo, @@ -81,8 +75,12 @@ final class RemoteInlineSuggestionViewConnector { * * @return true if the call is made to the remote renderer service, false otherwise. */ - public boolean renderSuggestion(int width, int height, - @NonNull IInlineSuggestionUiCallback callback) { + public boolean renderSuggestion( + int width, int height, @NonNull IInlineSuggestionUiCallback callback) { + if (Helper.sanitizeSlice(mInlinePresentation.getSlice()) == null) { + if (sDebug) Slog.d(TAG, "Skipped rendering inline suggestion."); + return false; + } if (mRemoteRenderService != null) { if (sDebug) Slog.d(TAG, "Request to recreate the UI"); mRemoteRenderService.renderSuggestion(callback, mInlinePresentation, width, height, diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 70382f1d5274..4488d86c60b5 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -443,7 +443,8 @@ final class SaveUi { } final BatchUpdates batchUpdates = pair.second; // First apply the updates... - final RemoteViews templateUpdates = batchUpdates.getUpdates(); + final RemoteViews templateUpdates = + Helper.sanitizeRemoteView(batchUpdates.getUpdates()); if (templateUpdates != null) { if (sDebug) Slog.d(TAG, "Applying template updates for batch update #" + i); templateUpdates.reapply(context, customSubtitleView); 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 29a19417b8fd..34f5b7f9dbb8 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: @@ -2344,4 +2358,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/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java index 017698943fc9..e8f78f31729c 100644 --- a/services/core/java/com/android/server/notification/SnoozeHelper.java +++ b/services/core/java/com/android/server/notification/SnoozeHelper.java @@ -118,7 +118,10 @@ public final class SnoozeHelper { protected boolean canSnooze(int numberToSnooze) { synchronized (mLock) { - if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) { + if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT + || (mPersistedSnoozedNotifications.size() + + mPersistedSnoozedNotificationsWithContext.size() + numberToSnooze) + > CONCURRENT_SNOOZE_LIMIT) { return false; } } @@ -343,6 +346,9 @@ public final class SnoozeHelper { if (groupSummaryKey != null) { NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey); + String trimmedKey = getTrimmedString(groupSummaryKey); + mPersistedSnoozedNotificationsWithContext.remove(trimmedKey); + mPersistedSnoozedNotifications.remove(trimmedKey); if (record != null && !record.isCanceled) { Runnable runnable = () -> { diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 6b65922c198e..dd3a3c5e6bbf 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1412,7 +1412,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { final long origId = Binder.clearCallingIdentity(); // TODO(b/64750076): Check if calling pid should really be -1. - final int res = getActivityStartController() + try { + if (options == null) { + options = new SafeActivityOptions(ActivityOptions.makeBasic()); + } + options.getOptions(r).setAvoidMoveToFront(); + final int res = getActivityStartController() .obtainStarter(intent, "startNextMatchingActivity") .setCaller(r.app.getThread()) .setResolvedType(r.resolvedType) @@ -1428,13 +1433,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { .setRealCallingUid(r.launchedFromUid) .setActivityOptions(options) .execute(); - Binder.restoreCallingIdentity(origId); - - r.finishing = wasFinishing; - if (res != ActivityManager.START_SUCCESS) { - return false; + r.finishing = wasFinishing; + return res == ActivityManager.START_SUCCESS; + } finally { + Binder.restoreCallingIdentity(origId); } - return true; } } 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/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java index 51b9c176a245..22c7f9c88867 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java @@ -18,6 +18,8 @@ package com.android.server.notification; import static com.android.server.notification.SnoozeHelper.CONCURRENT_SNOOZE_LIMIT; import static com.android.server.notification.SnoozeHelper.EXTRA_KEY; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -72,6 +74,14 @@ import java.io.IOException; public class SnoozeHelperTest extends UiServiceTestCase { private static final String TEST_CHANNEL_ID = "test_channel_id"; + private static final String XML_TAG_NAME = "snoozed-notifications"; + private static final String XML_SNOOZED_NOTIFICATION = "notification"; + private static final String XML_SNOOZED_NOTIFICATION_CONTEXT = "context"; + private static final String XML_SNOOZED_NOTIFICATION_KEY = "key"; + private static final String XML_SNOOZED_NOTIFICATION_TIME = "time"; + private static final String XML_SNOOZED_NOTIFICATION_CONTEXT_ID = "id"; + private static final String XML_SNOOZED_NOTIFICATION_VERSION_LABEL = "version"; + @Mock SnoozeHelper.Callback mCallback; @Mock AlarmManager mAm; @Mock ManagedServices.UserProfiles mUserProfiles; @@ -315,6 +325,53 @@ public class SnoozeHelperTest extends UiServiceTestCase { } @Test + public void testSnoozeLimit_maximumPersisted() throws XmlPullParserException, IOException { + final long snoozeTimeout = 1234; + final String snoozeContext = "ctx"; + // Serialize & deserialize notifications so that only persisted lists are used + TypedXmlSerializer serializer = Xml.newFastSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + serializer.startTag(null, XML_TAG_NAME); + // Serialize maximum number of timed + context snoozed notifications, half of each + for (int i = 0; i < CONCURRENT_SNOOZE_LIMIT; i++) { + final boolean timedNotification = i % 2 == 0; + if (timedNotification) { + serializer.startTag(null, XML_SNOOZED_NOTIFICATION); + } else { + serializer.startTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT); + } + serializer.attributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, 1); + serializer.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, "key" + i); + if (timedNotification) { + serializer.attributeLong(null, XML_SNOOZED_NOTIFICATION_TIME, snoozeTimeout); + serializer.endTag(null, XML_SNOOZED_NOTIFICATION); + } else { + serializer.attribute(null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID, snoozeContext); + serializer.endTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT); + } + } + serializer.endTag(null, XML_TAG_NAME); + serializer.endDocument(); + serializer.flush(); + + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), "utf-8"); + mSnoozeHelper.readXml(parser, 1); + // Verify that we can't snooze any more notifications + // and that the limit is caused by persisted notifications + assertThat(mSnoozeHelper.canSnooze(1)).isFalse(); + assertThat(mSnoozeHelper.isSnoozed(UserHandle.USER_SYSTEM, "pkg", "key0")).isFalse(); + assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, + "pkg", "key0")).isEqualTo(snoozeTimeout); + assertThat( + mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg", + "key1")).isEqualTo(snoozeContext); + } + + @Test public void testCancelByApp() throws Exception { NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM); NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM); @@ -587,6 +644,7 @@ public class SnoozeHelperTest extends UiServiceTestCase { @Test public void repostGroupSummary_repostsSummary() throws Exception { + final int snoozeDuration = 1000; IntArray profileIds = new IntArray(); profileIds.add(UserHandle.USER_SYSTEM); when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds); @@ -594,10 +652,44 @@ public class SnoozeHelperTest extends UiServiceTestCase { "pkg", 1, "one", UserHandle.SYSTEM, "group1", true); NotificationRecord r2 = getNotificationRecord( "pkg", 2, "two", UserHandle.SYSTEM, "group1", false); - mSnoozeHelper.snooze(r, 1000); - mSnoozeHelper.snooze(r2, 1000); + final long snoozeTime = System.currentTimeMillis() + snoozeDuration; + mSnoozeHelper.snooze(r, snoozeDuration); + mSnoozeHelper.snooze(r2, snoozeDuration); + assertEquals(2, mSnoozeHelper.getSnoozed().size()); + assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size()); + // Verify that summary notification was added to the persisted list + assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg", + r.getKey())).isAtLeast(snoozeTime); + + mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey()); + + verify(mCallback, times(1)).repost(UserHandle.USER_SYSTEM, r, false); + verify(mCallback, never()).repost(UserHandle.USER_SYSTEM, r2, false); + + assertEquals(1, mSnoozeHelper.getSnoozed().size()); + assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size()); + // Verify that summary notification was removed from the persisted list + assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg", + r.getKey())).isEqualTo(0); + } + + @Test + public void snoozeWithContext_repostGroupSummary_removesPersisted() throws Exception { + final String snoozeContext = "zzzzz"; + IntArray profileIds = new IntArray(); + profileIds.add(UserHandle.USER_SYSTEM); + when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds); + NotificationRecord r = getNotificationRecord( + "pkg", 1, "one", UserHandle.SYSTEM, "group1", true); + NotificationRecord r2 = getNotificationRecord( + "pkg", 2, "two", UserHandle.SYSTEM, "group1", false); + mSnoozeHelper.snooze(r, snoozeContext); + mSnoozeHelper.snooze(r2, snoozeContext); assertEquals(2, mSnoozeHelper.getSnoozed().size()); assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size()); + // Verify that summary notification was added to the persisted list + assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, + "pkg", r.getKey())).isEqualTo(snoozeContext); mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey()); @@ -606,6 +698,9 @@ public class SnoozeHelperTest extends UiServiceTestCase { assertEquals(1, mSnoozeHelper.getSnoozed().size()); assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size()); + // Verify that summary notification was removed from the persisted list + assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, + "pkg", r.getKey())).isNull(); } @Test |