summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-11 21:39:00 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-12-11 21:39:00 +0000
commit354f66dda739d6b4cd9528f74938e2c50ff08151 (patch)
tree90d8ff8c3b6ff20117eb5b335a6e654e6e1f8480
parent1d63f597970d300e2931bd262f3c5336601758bd (diff)
parent78dfa18b6a6d9176f68a6b12f67261d2be05e932 (diff)
downloadbase-android11-platform-release.tar.gz
Merge cherrypicks of ['googleplex-android-review.googlesource.com/24077613', 'googleplex-android-review.googlesource.com/25102101', 'googleplex-android-review.googlesource.com/25064728', 'googleplex-android-review.googlesource.com/23262449', 'googleplex-android-review.googlesource.com/24708693', 'googleplex-android-review.googlesource.com/25369997'] into rvc-platform-release.android-platform-11.0.0_r40android-platform-11.0.0_r39android11-platform-release
Change-Id: I142162ace2d0edb4a7fbc487cf9295f6a4a2886a
-rw-r--r--core/java/android/content/Context.java9
-rw-r--r--core/java/com/android/internal/content/FileSystemProvider.java168
-rw-r--r--core/proto/android/server/activitymanagerservice.proto1
-rw-r--r--packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java162
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java35
-rw-r--r--services/autofill/java/com/android/server/autofill/Helper.java48
-rw-r--r--services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java18
-rw-r--r--services/autofill/java/com/android/server/autofill/ui/SaveUi.java3
-rw-r--r--services/core/java/com/android/server/am/ConnectionRecord.java5
-rw-r--r--services/core/java/com/android/server/am/ProcessRecord.java20
-rw-r--r--services/core/java/com/android/server/am/ServiceRecord.java2
-rw-r--r--services/core/java/com/android/server/notification/SnoozeHelper.java23
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java3
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java53
-rw-r--r--services/core/java/com/android/server/wm/WindowProcessController.java35
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java105
17 files changed, 510 insertions, 185 deletions
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 8472144a92cf..8de5c8db6bc3 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -242,6 +242,7 @@ public abstract class Context {
BIND_IMPORTANT,
BIND_ADJUST_WITH_ACTIVITY,
BIND_NOT_PERCEPTIBLE,
+ BIND_DENY_ACTIVITY_STARTS,
BIND_INCLUDE_CAPABILITIES
})
@Retention(RetentionPolicy.SOURCE)
@@ -356,6 +357,14 @@ public abstract class Context {
/*********** Hidden flags below this line ***********/
/**
+ * Flag for {@link #bindService}: If binding from an app that is visible, the bound service is
+ * allowed to start an activity from background. Add a flag so that this behavior can be opted
+ * out.
+ * @hide
+ */
+ public static final int BIND_DENY_ACTIVITY_STARTS = 0X000004000;
+
+ /**
* Flag for {@link #bindService}: This flag is intended to be used only by the system to adjust
* the scheduling policy for IMEs (and any other out-of-process user-visible components that
* work closely with the top app) so that UI hosted in such services can have the same
diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java
index 3b5fecfc600a..c0ee49c892b7 100644
--- a/core/java/com/android/internal/content/FileSystemProvider.java
+++ b/core/java/com/android/internal/content/FileSystemProvider.java
@@ -63,13 +63,13 @@ 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.Arrays;
import java.util.LinkedList;
import java.util.List;
+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
@@ -87,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);
}
@@ -394,56 +396,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);
- 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");
+ resolveProjection(projection), documentId, parent);
+
+ if (!parent.isDirectory()) {
+ Log.w(TAG, '"' + documentId + "\" is not a directory");
+ return result;
+ }
+
+ if (!includeHidden && shouldHideDocument(documentId)) {
+ Log.w(TAG, "Queried directory \"" + documentId + "\" is hidden");
+ return result;
}
+
+ for (File file : FileUtils.listFilesOrEmpty(parent)) {
+ if (!includeHidden && shouldHideDocument(file)) continue;
+
+ includeFile(result, null, file);
+ }
+
return result;
}
@@ -465,23 +464,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 LinkedList<File> pending = new LinkedList<>();
- pending.add(folder);
- while (!pending.isEmpty() && result.getCount() < 24) {
- final File file = pending.removeFirst();
- 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);
}
}
@@ -600,26 +605,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;
}
@@ -652,22 +654,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/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index 2d2ead455a4d..a2a33c844de8 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -524,6 +524,7 @@ message ConnectionRecordProto {
DEAD = 15;
NOT_PERCEPTIBLE = 16;
INCLUDE_CAPABILITIES = 17;
+ DENY_ACTIVITY_STARTS = 18;
}
repeated Flag flags = 3;
optional string service_name = 4;
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 087275e73ee8..53e17e35953d 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;
@@ -61,9 +63,22 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collections;
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";
@@ -74,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,
@@ -276,70 +296,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 */);
+ 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;
+ }
- // 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.
+ if (isOnRemovableUsbStorage(documentId)) {
+ 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");
- }
+ // 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);
- final String path = getPathFromDocId(docId);
+ // Block the root of the storage
+ if (path.isEmpty()) {
+ return true;
+ }
- // Block the root of the storage
- if (path.isEmpty()) {
- return true;
- }
+ // Block /Download/ and /Android/ folders from the tree.
+ if (equalIgnoringCase(path, Environment.DIRECTORY_DOWNLOADS) ||
+ equalIgnoringCase(path, Environment.DIRECTORY_ANDROID)) {
+ return true;
+ }
- // Block Download folder from tree
- if (TextUtils.equals(Environment.DIRECTORY_DOWNLOADS.toLowerCase(),
- path.toLowerCase())) {
- 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;
+ }
- if (TextUtils.equals(Environment.DIRECTORY_ANDROID.toLowerCase(),
- path.toLowerCase())) {
- 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();
@@ -409,26 +450,30 @@ 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) {
final int splitIndex = docId.indexOf(':', 1);
- final String path = docId.substring(splitIndex + 1);
+ final String docIdPath = docId.substring(splitIndex + 1);
- if (path.isEmpty()) {
- return path;
+ // 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 (path.charAt(path.length() - 1) == '/') {
+ // Remove the trailing "/" as well.
+ if (!path.isEmpty() && path.charAt(path.length() - 1) == '/') {
return path.substring(0, path.length() - 1);
} else {
return path;
@@ -450,7 +495,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);
@@ -529,7 +574,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;
@@ -633,6 +678,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);
@@ -716,4 +768,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/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index aa51771864b2..68a83f872983 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -221,6 +221,11 @@ public class TileLifecycleManager extends BroadcastReceiver implements
}
@Override
+ public void onNullBinding(ComponentName name) {
+ setBindService(false);
+ }
+
+ @Override
public void onServiceDisconnected(ComponentName name) {
if (DEBUG) Log.d(TAG, "onServiceDisconnected " + name);
handleDeath();
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 42fd288d94ee..c03b2eeee577 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
@@ -22,13 +22,16 @@ import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.content.pm.PackageInfo;
import android.content.pm.ServiceInfo;
import android.net.Uri;
@@ -51,7 +54,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mockito;
+import org.mockito.ArgumentCaptor;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -59,10 +62,10 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
private static final int TEST_FAIL_TIMEOUT = 5000;
private final PackageManagerAdapter mMockPackageManagerAdapter =
- Mockito.mock(PackageManagerAdapter.class);
+ mock(PackageManagerAdapter.class);
private final BroadcastDispatcher mMockBroadcastDispatcher =
- Mockito.mock(BroadcastDispatcher.class);
- private final IQSTileService.Stub mMockTileService = Mockito.mock(IQSTileService.Stub.class);
+ mock(BroadcastDispatcher.class);
+ private final IQSTileService.Stub mMockTileService = mock(IQSTileService.Stub.class);
private ComponentName mTileServiceComponentName;
private Intent mTileServiceIntent;
private UserHandle mUser;
@@ -87,7 +90,7 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
mThread.start();
mHandler = Handler.createAsync(mThread.getLooper());
mStateManager = new TileLifecycleManager(mHandler, mContext,
- Mockito.mock(IQSService.class), new Tile(),
+ mock(IQSService.class), new Tile(),
mTileServiceIntent,
mUser,
mMockPackageManagerAdapter,
@@ -247,4 +250,26 @@ public class TileLifecycleManagerTest extends SysuiTestCase {
public void testToggleableTile() throws Exception {
assertTrue(mStateManager.isToggleableTile());
}
+
+ @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),
+ new Tile(),
+ mTileServiceIntent,
+ mUser,
+ mMockPackageManagerAdapter,
+ mMockBroadcastDispatcher);
+
+ manager.setBindService(true);
+
+ ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class);
+ verify(mockContext).bindServiceAsUser(any(), captor.capture(), anyInt(), any());
+
+ captor.getValue().onNullBinding(mTileServiceComponentName);
+ verify(mockContext).unbindService(captor.getValue());
+ }
}
diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java
index 8954a0c39091..86c1a50015aa 100644
--- a/services/autofill/java/com/android/server/autofill/Helper.java
+++ b/services/autofill/java/com/android/server/autofill/Helper.java
@@ -23,7 +23,10 @@ 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.graphics.drawable.Icon;
import android.metrics.LogMaker;
import android.service.autofill.Dataset;
import android.util.ArrayMap;
@@ -45,7 +48,6 @@ import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
-
public final class Helper {
private static final String TAG = "AutofillHelper";
@@ -83,7 +85,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());
});
@@ -115,6 +117,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 7257255d1ee4..40bcc0ac0f89 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,12 +40,9 @@ 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;
@@ -82,8 +80,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 7b45ef881e94..30f78c20f147 100644
--- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
@@ -418,7 +418,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/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java
index 6d9d3fbe41bd..cf9e3ce1eadb 100644
--- a/services/core/java/com/android/server/am/ConnectionRecord.java
+++ b/services/core/java/com/android/server/am/ConnectionRecord.java
@@ -67,6 +67,7 @@ final class ConnectionRecord {
Context.BIND_NOT_VISIBLE,
Context.BIND_NOT_PERCEPTIBLE,
Context.BIND_INCLUDE_CAPABILITIES,
+ Context.BIND_DENY_ACTIVITY_STARTS,
};
private static final int[] BIND_PROTO_ENUMS = new int[] {
ConnectionRecordProto.AUTO_CREATE,
@@ -86,6 +87,7 @@ final class ConnectionRecord {
ConnectionRecordProto.NOT_VISIBLE,
ConnectionRecordProto.NOT_PERCEPTIBLE,
ConnectionRecordProto.INCLUDE_CAPABILITIES,
+ ConnectionRecordProto.DENY_ACTIVITY_STARTS,
};
void dump(PrintWriter pw, String prefix) {
@@ -219,6 +221,9 @@ final class ConnectionRecord {
if ((flags & Context.BIND_NOT_PERCEPTIBLE) != 0) {
sb.append("!PRCP ");
}
+ if ((flags & Context.BIND_DENY_ACTIVITY_STARTS) != 0) {
+ sb.append("BALFD ");
+ }
if ((flags & Context.BIND_INCLUDE_CAPABILITIES) != 0) {
sb.append("CAPS ");
}
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 284903d390d4..c9737306854a 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -1368,14 +1368,14 @@ class ProcessRecord implements WindowProcessListener {
!mAllowBackgroundActivityStartsTokens.isEmpty());
}
- void addBoundClientUid(int clientUid) {
+ void addBoundClientUid(int clientUid, String clientPackageName, int bindFlags) {
mBoundClientUids.add(clientUid);
- mWindowProcessController.setBoundClientUids(mBoundClientUids);
+ mWindowProcessController.addBoundClientUid(clientUid, clientPackageName, bindFlags);
}
void updateBoundClientUids() {
+ clearBoundClientUids();
if (mServices.isEmpty()) {
- clearBoundClientUids();
return;
}
// grab a set of clientUids of all connections of all services
@@ -1388,12 +1388,14 @@ class ProcessRecord implements WindowProcessListener {
for (int conni = 0; conni < N; conni++) {
ArrayList<ConnectionRecord> c = conns.valueAt(conni);
for (int i = 0; i < c.size(); i++) {
- boundClientUids.add(c.get(i).clientUid);
+ ConnectionRecord cr = c.get(i);
+ boundClientUids.add(cr.clientUid);
+ mWindowProcessController
+ .addBoundClientUid(cr.clientUid, cr.clientPackageName, cr.flags);
}
}
}
mBoundClientUids = boundClientUids;
- mWindowProcessController.setBoundClientUids(mBoundClientUids);
}
void addBoundClientUidsOfNewService(ServiceRecord sr) {
@@ -1404,15 +1406,17 @@ class ProcessRecord implements WindowProcessListener {
for (int conni = conns.size() - 1; conni >= 0; conni--) {
ArrayList<ConnectionRecord> c = conns.valueAt(conni);
for (int i = 0; i < c.size(); i++) {
- mBoundClientUids.add(c.get(i).clientUid);
+ ConnectionRecord cr = c.get(i);
+ mBoundClientUids.add(cr.clientUid);
+ mWindowProcessController
+ .addBoundClientUid(cr.clientUid, cr.clientPackageName, cr.flags);
}
}
- mWindowProcessController.setBoundClientUids(mBoundClientUids);
}
void clearBoundClientUids() {
mBoundClientUids.clear();
- mWindowProcessController.setBoundClientUids(mBoundClientUids);
+ mWindowProcessController.clearBoundClientUids();
}
void setActiveInstrumentation(ActiveInstrumentation instr) {
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 0e628289a09f..2577bc8fe727 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -634,7 +634,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
// if we have a process attached, add bound client uid of this connection to it
if (app != null) {
- app.addBoundClientUid(c.clientUid);
+ app.addBoundClientUid(c.clientUid, c.clientPackageName, c.flags);
}
}
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index a6cae49c4fd6..4a3d51b20f7c 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -139,13 +139,29 @@ public class SnoozeHelper {
protected boolean canSnooze(int numberToSnooze) {
synchronized (mLock) {
- if ((mPackages.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) {
+ if ((mPackages.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT
+ || (countPersistedNotificationsLocked() + numberToSnooze)
+ > CONCURRENT_SNOOZE_LIMIT) {
return false;
}
}
return true;
}
+ private int countPersistedNotificationsLocked() {
+ int numNotifications = 0;
+ for (ArrayMap<String, String> persistedWithContext :
+ mPersistedSnoozedNotificationsWithContext.values()) {
+ numNotifications += persistedWithContext.size();
+ }
+ for (ArrayMap<String, Long> persistedWithDuration :
+ mPersistedSnoozedNotifications.values()) {
+ numNotifications += persistedWithDuration.size();
+ }
+ return numNotifications;
+ }
+
+
@NonNull
protected Long getSnoozeTimeForUnpostedNotification(int userId, String pkg, String key) {
Long time = null;
@@ -448,6 +464,11 @@ public class SnoozeHelper {
mPackages.remove(groupSummaryKey);
mUsers.remove(groupSummaryKey);
+ final String trimmedKey = getTrimmedString(groupSummaryKey);
+ removeRecordLocked(pkg, trimmedKey, userId, mPersistedSnoozedNotifications);
+ removeRecordLocked(pkg, trimmedKey, userId,
+ mPersistedSnoozedNotificationsWithContext);
+
if (record != null && !record.isCanceled) {
Runnable runnable = () -> {
MetricsLogger.action(record.getLogMaker()
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index aaf27d32452d..ca018dc970c0 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -2745,7 +2745,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
if (!mContext.bindServiceAsUser(intent, newConn,
Context.BIND_AUTO_CREATE | Context.BIND_SHOWING_UI
| Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
- | Context.BIND_INCLUDE_CAPABILITIES,
+ | Context.BIND_INCLUDE_CAPABILITIES
+ | Context.BIND_DENY_ACTIVITY_STARTS,
new UserHandle(serviceUserId))) {
String msg = "Unable to bind service: "
+ componentName;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 56d40d6fb769..5c4227489fa4 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1220,29 +1220,38 @@ 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()
- .obtainStarter(intent, "startNextMatchingActivity")
- .setCaller(r.app.getThread())
- .setResolvedType(r.resolvedType)
- .setActivityInfo(aInfo)
- .setResultTo(resultTo != null ? resultTo.appToken : null)
- .setResultWho(resultWho)
- .setRequestCode(requestCode)
- .setCallingPid(-1)
- .setCallingUid(r.launchedFromUid)
- .setCallingPackage(r.launchedFromPackage)
- .setCallingFeatureId(r.launchedFromFeatureId)
- .setRealCallingPid(-1)
- .setRealCallingUid(r.launchedFromUid)
- .setActivityOptions(options)
- .execute();
- Binder.restoreCallingIdentity(origId);
-
- r.finishing = wasFinishing;
- if (res != ActivityManager.START_SUCCESS) {
- return false;
+ try {
+ if (options == null) {
+ options = new SafeActivityOptions(ActivityOptions.makeBasic());
+ }
+ // Fixes b/230492947
+ // Prevents background activity launch through #startNextMatchingActivity
+ // An activity going into the background could still go back to the foreground
+ // if the intent used matches both:
+ // - the activity in the background
+ // - a second activity.
+ options.getOptions(r).setAvoidMoveToFront();
+ final int res = getActivityStartController()
+ .obtainStarter(intent, "startNextMatchingActivity")
+ .setCaller(r.app.getThread())
+ .setResolvedType(r.resolvedType)
+ .setActivityInfo(aInfo)
+ .setResultTo(resultTo != null ? resultTo.appToken : null)
+ .setResultWho(resultWho)
+ .setRequestCode(requestCode)
+ .setCallingPid(-1)
+ .setCallingUid(r.launchedFromUid)
+ .setCallingPackage(r.launchedFromPackage)
+ .setCallingFeatureId(r.launchedFromFeatureId)
+ .setRealCallingPid(-1)
+ .setRealCallingUid(r.launchedFromUid)
+ .setActivityOptions(options)
+ .execute();
+ r.finishing = wasFinishing;
+ return res == ActivityManager.START_SUCCESS;
+ } finally {
+ Binder.restoreCallingIdentity(origId);
}
- return true;
}
}
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index e6a35f1267b5..a1d7450d9136 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -530,16 +530,41 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio
}
private boolean isBoundByForegroundUid() {
- for (int i = mBoundClientUids.size() - 1; i >= 0; --i) {
- if (mAtm.isUidForeground(mBoundClientUids.valueAt(i))) {
- return true;
+ synchronized (this) {
+ if (mBoundClientUids != null) {
+ for (int i = mBoundClientUids.size() - 1; i >= 0; --i) {
+ if (mAtm.isUidForeground(mBoundClientUids.valueAt(i))) {
+ return true;
+ }
+ }
}
}
return false;
}
- public void setBoundClientUids(ArraySet<Integer> boundClientUids) {
- mBoundClientUids = boundClientUids;
+ /**
+ * Clear all bound client Uids.
+ */
+ public void clearBoundClientUids() {
+ synchronized (this) {
+ if (mBoundClientUids == null) {
+ mBoundClientUids = new ArraySet<>();
+ } else {
+ mBoundClientUids.clear();
+ }
+ }
+ }
+
+ /**
+ * Add bound client Uid.
+ */
+ public void addBoundClientUid(int clientUid, String clientPackageName, int bindFlags) {
+ if ((bindFlags & Context.BIND_DENY_ACTIVITY_STARTS) == 0) {
+ if (mBoundClientUids == null) {
+ mBoundClientUids = new ArraySet<>();
+ }
+ mBoundClientUids.add(clientUid);
+ }
}
public void setInstrumenting(boolean instrumenting,
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 2b6fbea5130a..47ee4d5a026e 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.assertNull;
@@ -74,6 +76,16 @@ import java.util.Collections;
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";
+ private static final String XML_SNOOZED_NOTIFICATION_PKG = "pkg";
+ private static final String XML_SNOOZED_NOTIFICATION_USER_ID = "user-id";
+
@Mock SnoozeHelper.Callback mCallback;
@Mock AlarmManager mAm;
@Mock ManagedServices.UserProfiles mUserProfiles;
@@ -328,6 +340,57 @@ 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
+ XmlSerializer serializer = new FastXmlSerializer();
+ 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.attribute(null, XML_SNOOZED_NOTIFICATION_PKG, "pkg");
+ serializer.attribute(null, XML_SNOOZED_NOTIFICATION_USER_ID,
+ String.valueOf(UserHandle.USER_SYSTEM));
+ serializer.attribute(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, "1");
+ serializer.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, "key" + i);
+ if (timedNotification) {
+ serializer.attribute(null, XML_SNOOZED_NOTIFICATION_TIME,
+ String.valueOf(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();
+
+ XmlPullParser parser = Xml.newPullParser();
+ 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);
@@ -600,6 +663,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);
@@ -607,10 +671,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());
@@ -619,6 +717,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