Add async version of "canonicalize"

Fixes: b/147699082
Test: atest FrameworksCoreTests:android.content.ContentResolverTest
Change-Id: I2e851839a454ad5eabc981c76774d03b57a1aa09
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index e1942da..bd3298c7 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -580,6 +580,15 @@
         }
 
         @Override
+        public void canonicalizeAsync(String callingPkg, @Nullable String featureId, Uri uri,
+                RemoteCallback callback) {
+            final Bundle result = new Bundle();
+            result.putParcelable(ContentResolver.REMOTE_CALLBACK_RESULT,
+                    canonicalize(callingPkg, featureId, uri));
+            callback.sendResult(result);
+        }
+
+        @Override
         public Uri uncanonicalize(String callingPkg, String featureId,  Uri uri) {
             uri = validateIncomingUri(uri);
             int userId = getUserIdFromUri(uri);
diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java
index 0f1442d..7bc5901 100644
--- a/core/java/android/content/ContentProviderNative.java
+++ b/core/java/android/content/ContentProviderNative.java
@@ -359,6 +359,16 @@
                     return true;
                 }
 
+                case CANONICALIZE_ASYNC_TRANSACTION: {
+                    data.enforceInterface(IContentProvider.descriptor);
+                    String callingPkg = data.readString();
+                    String featureId = data.readString();
+                    Uri uri = Uri.CREATOR.createFromParcel(data);
+                    RemoteCallback callback = RemoteCallback.CREATOR.createFromParcel(data);
+                    canonicalizeAsync(callingPkg, featureId, uri, callback);
+                    return true;
+                }
+
                 case UNCANONICALIZE_TRANSACTION:
                 {
                     data.enforceInterface(IContentProvider.descriptor);
@@ -823,6 +833,25 @@
     }
 
     @Override
+    /* oneway */ public void canonicalizeAsync(String callingPkg, @Nullable String featureId,
+            Uri uri, RemoteCallback callback) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IContentProvider.descriptor);
+
+            data.writeString(callingPkg);
+            data.writeString(featureId);
+            uri.writeToParcel(data, 0);
+            callback.writeToParcel(data, 0);
+
+            mRemote.transact(IContentProvider.CANONICALIZE_ASYNC_TRANSACTION, data, null,
+                    Binder.FLAG_ONEWAY);
+        } finally {
+            data.recycle();
+        }
+    }
+
+    @Override
     public Uri uncanonicalize(String callingPkg, @Nullable String featureId, Uri url)
             throws RemoteException {
         Parcel data = Parcel.obtain();
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 0e0161f..b748cfa 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -712,14 +712,17 @@
      * {@link #CONTENT_PROVIDER_PUBLISH_TIMEOUT_MILLIS}.
      * @hide
      */
-    public static final int CONTENT_PROVIDER_WAIT_TIMEOUT_MILLIS =
+    public static final int CONTENT_PROVIDER_READY_TIMEOUT_MILLIS =
             CONTENT_PROVIDER_PUBLISH_TIMEOUT_MILLIS + 10 * 1000;
 
+    // Timeout given a ContentProvider that has already been started and connected to.
+    private static final int CONTENT_PROVIDER_TIMEOUT_MILLIS = 3 * 1000;
+
     // Should be >= {@link #CONTENT_PROVIDER_WAIT_TIMEOUT_MILLIS}, because that's how
     // long ActivityManagerService is giving a content provider to get published if a new process
     // needs to be started for that.
-    private static final int GET_TYPE_TIMEOUT_MILLIS =
-            CONTENT_PROVIDER_WAIT_TIMEOUT_MILLIS + 5 * 1000;
+    private static final int REMOTE_CONTENT_PROVIDER_TIMEOUT_MILLIS =
+            CONTENT_PROVIDER_READY_TIMEOUT_MILLIS + CONTENT_PROVIDER_TIMEOUT_MILLIS;
 
     public ContentResolver(@Nullable Context context) {
         this(context, null);
@@ -833,10 +836,10 @@
         IContentProvider provider = acquireExistingProvider(url);
         if (provider != null) {
             try {
-                final GetTypeResultListener resultListener = new GetTypeResultListener();
+                final StringResultListener resultListener = new StringResultListener();
                 provider.getTypeAsync(url, new RemoteCallback(resultListener));
-                resultListener.waitForResult();
-                return resultListener.type;
+                resultListener.waitForResult(CONTENT_PROVIDER_TIMEOUT_MILLIS);
+                return resultListener.result;
             } catch (RemoteException e) {
                 // Arbitrary and not worth documenting, as Activity
                 // Manager will kill this process shortly anyway.
@@ -854,13 +857,13 @@
         }
 
         try {
-            GetTypeResultListener resultListener = new GetTypeResultListener();
+            final StringResultListener resultListener = new StringResultListener();
             ActivityManager.getService().getProviderMimeTypeAsync(
                     ContentProvider.getUriWithoutUserId(url),
                     resolveUserId(url),
                     new RemoteCallback(resultListener));
-            resultListener.waitForResult();
-            return resultListener.type;
+            resultListener.waitForResult(REMOTE_CONTENT_PROVIDER_TIMEOUT_MILLIS);
+            return resultListener.result;
         } catch (RemoteException e) {
             // We just failed to send a oneway request to the System Server. Nothing to do.
             return null;
@@ -870,27 +873,29 @@
         }
     }
 
-    private static class GetTypeResultListener implements RemoteCallback.OnResultListener {
+    private abstract static class ResultListener<T> implements RemoteCallback.OnResultListener {
         @GuardedBy("this")
         public boolean done;
 
         @GuardedBy("this")
-        public String type;
+        public T result;
 
         @Override
         public void onResult(Bundle result) {
             synchronized (this) {
-                type = result.getString(REMOTE_CALLBACK_RESULT);
+                this.result = getResultFromBundle(result);
                 done = true;
                 notifyAll();
             }
         }
 
-        public void waitForResult() {
+        protected abstract T getResultFromBundle(Bundle result);
+
+        public void waitForResult(long timeout) {
             synchronized (this) {
                 if (!done) {
                     try {
-                        wait(GET_TYPE_TIMEOUT_MILLIS);
+                        wait(timeout);
                     } catch (InterruptedException e) {
                         // Ignore
                     }
@@ -899,6 +904,20 @@
         }
     }
 
+    private static class StringResultListener extends ResultListener<String> {
+        @Override
+        protected String getResultFromBundle(Bundle result) {
+            return result.getString(REMOTE_CALLBACK_RESULT);
+        }
+    }
+
+    private static class UriResultListener extends ResultListener<Uri> {
+        @Override
+        protected Uri getResultFromBundle(Bundle result) {
+            return result.getParcelable(REMOTE_CALLBACK_RESULT);
+        }
+    }
+
     /**
      * Query for the possible MIME types for the representations the given
      * content URL can be returned when opened as as stream with
@@ -1192,7 +1211,11 @@
         }
 
         try {
-            return provider.canonicalize(mPackageName, mFeatureId, url);
+            final UriResultListener resultListener = new UriResultListener();
+            provider.canonicalizeAsync(mPackageName, mFeatureId, url,
+                    new RemoteCallback(resultListener));
+            resultListener.waitForResult(CONTENT_PROVIDER_TIMEOUT_MILLIS);
+            return resultListener.result;
         } catch (RemoteException e) {
             // Arbitrary and not worth documenting, as Activity
             // Manager will kill this process shortly anyway.
diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java
index 4658ba1..37643da 100644
--- a/core/java/android/content/IContentProvider.java
+++ b/core/java/android/content/IContentProvider.java
@@ -45,7 +45,7 @@
     public String getType(Uri url) throws RemoteException;
 
     /**
-     * An oneway version of getType. The functionality is exactly the same, except that the
+     * A oneway version of getType. The functionality is exactly the same, except that the
      * call returns immediately, and the resulting type is returned when available via
      * a binder callback.
      */
@@ -126,6 +126,14 @@
     public Uri canonicalize(String callingPkg, @Nullable String featureId, Uri uri)
             throws RemoteException;
 
+    /**
+     * A oneway version of canonicalize. The functionality is exactly the same, except that the
+     * call returns immediately, and the resulting type is returned when available via
+     * a binder callback.
+     */
+    void canonicalizeAsync(String callingPkg, @Nullable String featureId, Uri uri,
+            RemoteCallback callback) throws RemoteException;
+
     public Uri uncanonicalize(String callingPkg, @Nullable String featureId, Uri uri)
             throws RemoteException;
 
@@ -162,4 +170,5 @@
     static final int REFRESH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 26;
     static final int CHECK_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 27;
     int GET_TYPE_ASYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 28;
+    int CANONICALIZE_ASYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 29;
 }
diff --git a/core/tests/coretests/src/android/content/ContentResolverTest.java b/core/tests/coretests/src/android/content/ContentResolverTest.java
index 78c4420..1737bd0 100644
--- a/core/tests/coretests/src/android/content/ContentResolverTest.java
+++ b/core/tests/coretests/src/android/content/ContentResolverTest.java
@@ -234,4 +234,12 @@
         assertThat(type).isNull();
         assertThat(end).isLessThan(start + 5000);
     }
+
+    @Test
+    public void testCanonicalize() {
+        Uri canonical = mResolver.canonicalize(
+                Uri.parse("content://android.content.FakeProviderRemote/something"));
+        assertThat(canonical).isEqualTo(
+                Uri.parse("content://android.content.FakeProviderRemote/canonical"));
+    }
 }
diff --git a/core/tests/coretests/src/android/content/FakeProviderRemote.java b/core/tests/coretests/src/android/content/FakeProviderRemote.java
index 7b9bdbc..1d7ba5d 100644
--- a/core/tests/coretests/src/android/content/FakeProviderRemote.java
+++ b/core/tests/coretests/src/android/content/FakeProviderRemote.java
@@ -54,4 +54,10 @@
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         return 0;
     }
+
+    @Override
+    public Uri canonicalize(Uri uri) {
+        return new Uri.Builder().scheme(uri.getScheme()).authority(uri.getAuthority())
+                .appendPath("canonical").build();
+    }
 }