Allow sending of bulk Uri change notifications.

MediaProvider makes heavy use of Uri change notifications, which
currently need to be delivered one at a time through the Binder
interface.  To optimize this, allow callers to provide a collection
of multiple Uris to notify with a single Binder call.

Bug: 134170767
Test: atest cts/tests/tests/content/src/android/content/cts/ContentResolverTest.java
Change-Id: Ifef778e88bb772b5580f70929c6f2e9c166d1c0e
diff --git a/api/current.txt b/api/current.txt
index d5ad60c..98c77fc 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9650,8 +9650,9 @@
     method public static boolean isSyncPending(android.accounts.Account, String);
     method @NonNull public android.graphics.Bitmap loadThumbnail(@NonNull android.net.Uri, @NonNull android.util.Size, @Nullable android.os.CancellationSignal) throws java.io.IOException;
     method public void notifyChange(@NonNull android.net.Uri, @Nullable android.database.ContentObserver);
-    method public void notifyChange(@NonNull android.net.Uri, @Nullable android.database.ContentObserver, boolean);
+    method @Deprecated public void notifyChange(@NonNull android.net.Uri, @Nullable android.database.ContentObserver, boolean);
     method public void notifyChange(@NonNull android.net.Uri, @Nullable android.database.ContentObserver, int);
+    method public void notifyChange(@NonNull Iterable<android.net.Uri>, @Nullable android.database.ContentObserver, int);
     method @Nullable public final android.content.res.AssetFileDescriptor openAssetFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
     method @Nullable public final android.content.res.AssetFileDescriptor openAssetFileDescriptor(@NonNull android.net.Uri, @NonNull String) throws java.io.FileNotFoundException;
     method @Nullable public final android.content.res.AssetFileDescriptor openAssetFileDescriptor(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 2657cc5..61c8db5d 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -65,6 +65,7 @@
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Size;
+import android.util.SparseArray;
 
 import com.android.internal.util.MimeIconUtils;
 import com.android.internal.util.Preconditions;
@@ -2381,15 +2382,15 @@
      *            true.
      * @param syncToNetwork If true, same as {@link #NOTIFY_SYNC_TO_NETWORK}.
      * @see #requestSync(android.accounts.Account, String, android.os.Bundle)
+     * @deprecated callers should consider migrating to
+     *             {@link #notifyChange(Uri, ContentObserver, int)}, as it
+     *             offers support for many more options than just
+     *             {@link #NOTIFY_SYNC_TO_NETWORK}.
      */
+    @Deprecated
     public void notifyChange(@NonNull Uri uri, @Nullable ContentObserver observer,
             boolean syncToNetwork) {
-        Preconditions.checkNotNull(uri, "uri");
-        notifyChange(
-                ContentProvider.getUriWithoutUserId(uri),
-                observer,
-                syncToNetwork,
-                ContentProvider.getUserIdFromUri(uri, mContext.getUserId()));
+        notifyChange(uri, observer, syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0);
     }
 
     /**
@@ -2398,10 +2399,10 @@
      * To observe events sent through this call, use
      * {@link #registerContentObserver(Uri, boolean, ContentObserver)}.
      * <p>
-     * If syncToNetwork is true, this will attempt to schedule a local sync
-     * using the sync adapter that's registered for the authority of the
-     * provided uri. No account will be passed to the sync adapter, so all
-     * matching accounts will be synchronized.
+     * If {@link #NOTIFY_SYNC_TO_NETWORK} is set, this will attempt to schedule
+     * a local sync using the sync adapter that's registered for the authority
+     * of the provided uri. No account will be passed to the sync adapter, so
+     * all matching accounts will be synchronized.
      * <p>
      * Starting in {@link android.os.Build.VERSION_CODES#O}, all content
      * notifications must be backed by a valid {@link ContentProvider}.
@@ -2427,21 +2428,71 @@
     }
 
     /**
+     * Notify registered observers that several rows have been updated.
+     * <p>
+     * To observe events sent through this call, use
+     * {@link #registerContentObserver(Uri, boolean, ContentObserver)}.
+     * <p>
+     * If {@link #NOTIFY_SYNC_TO_NETWORK} is set, this will attempt to schedule
+     * a local sync using the sync adapter that's registered for the authority
+     * of the provided uri. No account will be passed to the sync adapter, so
+     * all matching accounts will be synchronized.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#O}, all content
+     * notifications must be backed by a valid {@link ContentProvider}.
+     *
+     * @param uris The uris of the content that was changed.
+     * @param observer The observer that originated the change, may be
+     *            <code>null</null>. The observer that originated the change
+     *            will only receive the notification if it has requested to
+     *            receive self-change notifications by implementing
+     *            {@link ContentObserver#deliverSelfNotifications()} to return
+     *            true.
+     * @param flags Flags such as {@link #NOTIFY_SYNC_TO_NETWORK} or
+     *            {@link #NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS}.
+     */
+    public void notifyChange(@NonNull Iterable<Uri> uris, @Nullable ContentObserver observer,
+            @NotifyFlags int flags) {
+        Preconditions.checkNotNull(uris, "uris");
+
+        // Cluster based on user ID
+        final SparseArray<ArrayList<Uri>> clusteredByUser = new SparseArray<>();
+        for (Uri uri : uris) {
+            final int userId = ContentProvider.getUserIdFromUri(uri, mContext.getUserId());
+            ArrayList<Uri> list = clusteredByUser.get(userId);
+            if (list == null) {
+                list = new ArrayList<>();
+                clusteredByUser.put(userId, list);
+            }
+            list.add(ContentProvider.getUriWithoutUserId(uri));
+        }
+
+        for (int i = 0; i < clusteredByUser.size(); i++) {
+            final int userId = clusteredByUser.keyAt(i);
+            final ArrayList<Uri> list = clusteredByUser.valueAt(i);
+            notifyChange(list.toArray(new Uri[list.size()]), observer, flags, userId);
+        }
+    }
+
+    /**
      * Notify registered observers within the designated user(s) that a row was updated.
      *
+     * @deprecated callers should consider migrating to
+     *             {@link #notifyChange(Uri, ContentObserver, int)}, as it
+     *             offers support for many more options than just
+     *             {@link #NOTIFY_SYNC_TO_NETWORK}.
      * @hide
      */
+    @Deprecated
     public void notifyChange(@NonNull Uri uri, ContentObserver observer, boolean syncToNetwork,
             @UserIdInt int userHandle) {
-        try {
-            getContentService().notifyChange(
-                    uri, observer == null ? null : observer.getContentObserver(),
-                    observer != null && observer.deliverSelfNotifications(),
-                    syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0,
-                    userHandle, mTargetSdkVersion, mContext.getPackageName());
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        notifyChange(uri, observer, syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0, userHandle);
+    }
+
+    /** {@hide} */
+    public void notifyChange(@NonNull Uri uri, ContentObserver observer, @NotifyFlags int flags,
+            @UserIdInt int userHandle) {
+        notifyChange(new Uri[] { uri }, observer, flags, userHandle);
     }
 
     /**
@@ -2449,11 +2500,11 @@
      *
      * @hide
      */
-    public void notifyChange(@NonNull Uri uri, ContentObserver observer, @NotifyFlags int flags,
+    public void notifyChange(@NonNull Uri[] uris, ContentObserver observer, @NotifyFlags int flags,
             @UserIdInt int userHandle) {
         try {
             getContentService().notifyChange(
-                    uri, observer == null ? null : observer.getContentObserver(),
+                    uris, observer == null ? null : observer.getContentObserver(),
                     observer != null && observer.deliverSelfNotifications(), flags,
                     userHandle, mTargetSdkVersion, mContext.getPackageName());
         } catch (RemoteException e) {
diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl
index a34a995..03c99e1 100644
--- a/core/java/android/content/IContentService.aidl
+++ b/core/java/android/content/IContentService.aidl
@@ -51,7 +51,7 @@
      *     hold the INTERACT_ACROSS_USERS_FULL permission.  Pseudousers USER_ALL
      *     USER_CURRENT are properly interpreted.
      */
-    void notifyChange(in Uri uri, IContentObserver observer,
+    void notifyChange(in Uri[] uris, IContentObserver observer,
             boolean observerWantsSelfNotifications, int flags,
             int userHandle, int targetSdkVersion, String callingPackage);
 
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 4a62bc5..bc7307b 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -394,6 +394,15 @@
      *     allowed.
      */
     @Override
+    public void notifyChange(Uri[] uris, IContentObserver observer,
+            boolean observerWantsSelfNotifications, int flags, int userHandle,
+            int targetSdkVersion, String callingPackage) {
+        for (Uri uri : uris) {
+            notifyChange(uri, observer, observerWantsSelfNotifications, flags, userHandle,
+                    targetSdkVersion, callingPackage);
+        }
+    }
+
     public void notifyChange(Uri uri, IContentObserver observer,
             boolean observerWantsSelfNotifications, int flags, int userHandle,
             int targetSdkVersion, String callingPackage) {
diff --git a/test-mock/src/android/test/mock/MockContentResolver.java b/test-mock/src/android/test/mock/MockContentResolver.java
index a70152c..8283019 100644
--- a/test-mock/src/android/test/mock/MockContentResolver.java
+++ b/test-mock/src/android/test/mock/MockContentResolver.java
@@ -16,6 +16,8 @@
 
 package android.test.mock;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -130,17 +132,47 @@
     }
 
     /**
-     * Overrides {@link android.content.ContentResolver#notifyChange(Uri, ContentObserver, boolean)
-     * ContentResolver.notifChange(Uri, ContentObserver, boolean)}. All parameters are ignored.
-     * The method hides providers linked to MockContentResolver from other observers in the system.
-     *
-     * @param uri (Ignored) The uri of the content provider.
-     * @param observer (Ignored) The observer that originated the change.
-     * @param syncToNetwork (Ignored) If true, attempt to sync the change to the network.
+     * Overrides the behavior from the parent class to completely ignore any
+     * content notifications sent to this object. This effectively hides clients
+     * from observers elsewhere in the system.
      */
     @Override
-    public void notifyChange(Uri uri,
-            ContentObserver observer,
+    public void notifyChange(@NonNull Uri uri, @Nullable ContentObserver observer) {
+    }
+
+    /**
+     * Overrides the behavior from the parent class to completely ignore any
+     * content notifications sent to this object. This effectively hides clients
+     * from observers elsewhere in the system.
+     *
+     * @deprecated callers should consider migrating to
+     *             {@link #notifyChange(Uri, ContentObserver, int)}, as it
+     *             offers support for many more options than just
+     *             {@link #NOTIFY_SYNC_TO_NETWORK}.
+     */
+    @Override
+    @Deprecated
+    public void notifyChange(@NonNull Uri uri, @Nullable ContentObserver observer,
             boolean syncToNetwork) {
     }
+
+    /**
+     * Overrides the behavior from the parent class to completely ignore any
+     * content notifications sent to this object. This effectively hides clients
+     * from observers elsewhere in the system.
+     */
+    @Override
+    public void notifyChange(@NonNull Uri uri, @Nullable ContentObserver observer,
+            @NotifyFlags int flags) {
+    }
+
+    /**
+     * Overrides the behavior from the parent class to completely ignore any
+     * content notifications sent to this object. This effectively hides clients
+     * from observers elsewhere in the system.
+     */
+    @Override
+    public void notifyChange(@NonNull Iterable<Uri> uris, @Nullable ContentObserver observer,
+            @NotifyFlags int flags) {
+    }
 }