Add async version of getProviderMimeType

Fixes: b/147646960
Test: atest FrameworksCoreTests:android.content.ContentResolverTest

Change-Id: I04c15ac008fe14b215f954af150226dc94f22232
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 3ffd7c7..5f3bad6 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -303,8 +303,13 @@
     boolean isTopActivityImmersive();
     void crashApplication(int uid, int initialPid, in String packageName, int userId,
             in String message, boolean force);
-    @UnsupportedAppUsage
+    /** @deprecated -- use getProviderMimeTypeAsync */
+    @UnsupportedAppUsage(maxTargetSdk = 29, publicAlternatives =
+            "Use {@link android.content.ContentResolver#getType} public API instead.")
     String getProviderMimeType(in Uri uri, int userId);
+
+    oneway void getProviderMimeTypeAsync(in Uri uri, int userId, in RemoteCallback resultCallback);
+
     // Cause the specified process to dump the specified heap.
     boolean dumpHeap(in String process, int userId, boolean managed, boolean mallocInfo,
             boolean runGc, in String path, in ParcelFileDescriptor fd,
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index c271e3c..e1942da 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -49,6 +49,7 @@
 import android.os.ICancellationSignal;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -300,6 +301,13 @@
         }
 
         @Override
+        public void getTypeAsync(Uri uri, RemoteCallback callback) {
+            final Bundle result = new Bundle();
+            result.putString(ContentResolver.REMOTE_CALLBACK_RESULT, getType(uri));
+            callback.sendResult(result);
+        }
+
+        @Override
         public Uri insert(String callingPkg, @Nullable String featureId, Uri uri,
                 ContentValues initialValues, Bundle extras) {
             uri = validateIncomingUri(uri);
diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java
index 45ace40..0f1442d 100644
--- a/core/java/android/content/ContentProviderNative.java
+++ b/core/java/android/content/ContentProviderNative.java
@@ -33,6 +33,7 @@
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
+import android.os.RemoteCallback;
 import android.os.RemoteException;
 
 import java.io.FileNotFoundException;
@@ -146,6 +147,14 @@
                     return true;
                 }
 
+                case GET_TYPE_ASYNC_TRANSACTION: {
+                    data.enforceInterface(IContentProvider.descriptor);
+                    Uri url = Uri.CREATOR.createFromParcel(data);
+                    RemoteCallback callback = RemoteCallback.CREATOR.createFromParcel(data);
+                    getTypeAsync(url, callback);
+                    return true;
+                }
+
                 case INSERT_TRANSACTION:
                 {
                     data.enforceInterface(IContentProvider.descriptor);
@@ -495,6 +504,22 @@
     }
 
     @Override
+    /* oneway */ public void getTypeAsync(Uri uri, RemoteCallback callback) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        try {
+            data.writeInterfaceToken(IContentProvider.descriptor);
+
+            uri.writeToParcel(data, 0);
+            callback.writeToParcel(data, 0);
+
+            mRemote.transact(IContentProvider.GET_TYPE_ASYNC_TRANSACTION, data, null,
+                    IBinder.FLAG_ONEWAY);
+        } finally {
+            data.recycle();
+        }
+    }
+
+    @Override
     public Uri insert(String callingPkg, @Nullable String featureId, Uri url,
             ContentValues values, Bundle extras) throws RemoteException
     {
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 6cd1cd3..f32a4ab 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -55,6 +55,7 @@
 import android.os.ICancellationSignal;
 import android.os.OperationCanceledException;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
@@ -67,6 +68,7 @@
 import android.util.Size;
 import android.util.SparseArray;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.MimeIconUtils;
 
 import dalvik.system.CloseGuard;
@@ -695,6 +697,9 @@
     private static final int SLOW_THRESHOLD_MILLIS = 500;
     private final Random mRandom = new Random();  // guarded by itself
 
+    /** @hide */
+    public static final String REMOTE_CALLBACK_RESULT = "result";
+
     public ContentResolver(@Nullable Context context) {
         this(context, null);
     }
@@ -807,7 +812,10 @@
         IContentProvider provider = acquireExistingProvider(url);
         if (provider != null) {
             try {
-                return provider.getType(url);
+                final GetTypeResultListener resultListener = new GetTypeResultListener();
+                provider.getTypeAsync(url, new RemoteCallback(resultListener));
+                resultListener.waitForResult();
+                return resultListener.type;
             } catch (RemoteException e) {
                 // Arbitrary and not worth documenting, as Activity
                 // Manager will kill this process shortly anyway.
@@ -825,17 +833,53 @@
         }
 
         try {
-            String type = ActivityManager.getService().getProviderMimeType(
-                    ContentProvider.getUriWithoutUserId(url), resolveUserId(url));
-            return type;
+            GetTypeResultListener resultListener = new GetTypeResultListener();
+            ActivityManager.getService().getProviderMimeTypeAsync(
+                    ContentProvider.getUriWithoutUserId(url),
+                    resolveUserId(url),
+                    new RemoteCallback(resultListener));
+            resultListener.waitForResult();
+            return resultListener.type;
         } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+            // We just failed to send a oneway request to the System Server. Nothing to do.
+            return null;
         } catch (java.lang.Exception e) {
             Log.w(TAG, "Failed to get type for: " + url + " (" + e.getMessage() + ")");
             return null;
         }
     }
 
+    private static final int GET_TYPE_TIMEOUT_MILLIS = 3000;
+
+    private static class GetTypeResultListener implements RemoteCallback.OnResultListener {
+        @GuardedBy("this")
+        public boolean done;
+
+        @GuardedBy("this")
+        public String type;
+
+        @Override
+        public void onResult(Bundle result) {
+            synchronized (this) {
+                type = result.getString(REMOTE_CALLBACK_RESULT);
+                done = true;
+                notifyAll();
+            }
+        }
+
+        public void waitForResult() {
+            synchronized (this) {
+                if (!done) {
+                    try {
+                        wait(GET_TYPE_TIMEOUT_MILLIS);
+                    } catch (InterruptedException e) {
+                        // Ignore
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Query for the possible MIME types for the representations the given
      * content URL can be returned when opened as as stream with
diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java
index 6f477ff..4658ba1 100644
--- a/core/java/android/content/IContentProvider.java
+++ b/core/java/android/content/IContentProvider.java
@@ -27,6 +27,7 @@
 import android.os.ICancellationSignal;
 import android.os.IInterface;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteCallback;
 import android.os.RemoteException;
 
 import java.io.FileNotFoundException;
@@ -42,6 +43,14 @@
             @Nullable Bundle queryArgs, @Nullable ICancellationSignal cancellationSignal)
             throws RemoteException;
     public String getType(Uri url) throws RemoteException;
+
+    /**
+     * An 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.
+     */
+    void getTypeAsync(Uri uri, RemoteCallback callback) throws RemoteException;
+
     @Deprecated
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "Use {@link "
             + "ContentProviderClient#insert(android.net.Uri, android.content.ContentValues)} "
@@ -152,4 +161,5 @@
     static final int UNCANONICALIZE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 25;
     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;
 }
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index b85a332..23f9f90 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1315,6 +1315,17 @@
                 android:process=":RedactingProvider">
         </provider>
 
+        <provider
+            android:name="android.content.FakeProviderLocal"
+            android:authorities="android.content.FakeProviderLocal">
+        </provider>
+
+        <provider
+            android:name="android.content.FakeProviderRemote"
+            android:authorities="android.content.FakeProviderRemote"
+            android:process=":FakeProvider">
+        </provider>
+
         <!-- Application components used for os tests -->
 
         <service android:name="android.os.MessengerService"
diff --git a/core/tests/coretests/src/android/content/ContentResolverTest.java b/core/tests/coretests/src/android/content/ContentResolverTest.java
index e140ad2..9dcce1e 100644
--- a/core/tests/coretests/src/android/content/ContentResolverTest.java
+++ b/core/tests/coretests/src/android/content/ContentResolverTest.java
@@ -191,4 +191,22 @@
         assertEquals(uri, ContentResolver
                 .translateDeprecatedDataPath(ContentResolver.translateDeprecatedDataPath(uri)));
     }
+
+    @Test
+    public void testGetType_localProvider() {
+        // This provider is running in the same process as the test and is already registered with
+        // the ContentResolver when the application starts, see
+        // ActivityThread#installContentProviders. This allows ContentResolver to follow a
+        // streamlined code path.
+        String type = mResolver.getType(Uri.parse("content://android.content.FakeProviderLocal"));
+        assertEquals("fake/local", type);
+    }
+
+    @Test
+    public void testGetType_remoteProvider() {
+        // This provider is running in a different process, which will need to be started
+        // in order to acquire the provider
+        String type = mResolver.getType(Uri.parse("content://android.content.FakeProviderRemote"));
+        assertEquals("fake/remote", type);
+    }
 }
diff --git a/core/tests/coretests/src/android/content/FakeProviderLocal.java b/core/tests/coretests/src/android/content/FakeProviderLocal.java
new file mode 100644
index 0000000..a8c2f40
--- /dev/null
+++ b/core/tests/coretests/src/android/content/FakeProviderLocal.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 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 android.content;
+
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * A dummy content provider for tests.  This provider runs in the same process as the test.
+ */
+public class FakeProviderLocal extends ContentProvider {
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return "fake/local";
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+}
diff --git a/core/tests/coretests/src/android/content/FakeProviderRemote.java b/core/tests/coretests/src/android/content/FakeProviderRemote.java
new file mode 100644
index 0000000..7b9bdbc
--- /dev/null
+++ b/core/tests/coretests/src/android/content/FakeProviderRemote.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 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 android.content;
+
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * A dummy content provider for tests.  This provider runs in a different process from the test.
+ */
+public class FakeProviderRemote extends ContentProvider {
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return "fake/remote";
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8b547c6..4c5f705 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -7786,7 +7786,10 @@
      *
      * Test cases are at cts/tests/appsecurity-tests/test-apps/UsePermissionDiffCert/
      *     src/com/android/cts/usespermissiondiffcertapp/AccessPermissionWithDiffSigTest.java
+     *
+     * @deprecated -- use getProviderMimeTypeAsync.
      */
+    @Deprecated
     public String getProviderMimeType(Uri uri, int userId) {
         enforceNotIsolatedCaller("getProviderMimeType");
         final String name = uri.getAuthority();
@@ -7847,6 +7850,43 @@
         return null;
     }
 
+    /**
+     * Allows apps to retrieve the MIME type of a URI.
+     * If an app is in the same user as the ContentProvider, or if it is allowed to interact across
+     * users, then it does not need permission to access the ContentProvider.
+     * Either way, it needs cross-user uri grants.
+     */
+    @Override
+    public void getProviderMimeTypeAsync(Uri uri, int userId, RemoteCallback resultCallback) {
+        enforceNotIsolatedCaller("getProviderMimeTypeAsync");
+        final String name = uri.getAuthority();
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        final int safeUserId = mUserController.unsafeConvertIncomingUser(userId);
+        final long ident = canClearIdentity(callingPid, callingUid, userId)
+                ? Binder.clearCallingIdentity() : 0;
+        try {
+            final ContentProviderHolder holder = getContentProviderExternalUnchecked(name, null,
+                    callingUid, "*getmimetype*", safeUserId);
+            if (holder != null) {
+                holder.provider.getTypeAsync(uri, new RemoteCallback(result -> {
+                    final long identity = Binder.clearCallingIdentity();
+                    try {
+                        removeContentProviderExternalUnchecked(name, null, safeUserId);
+                    } finally {
+                        Binder.restoreCallingIdentity(identity);
+                    }
+                    resultCallback.sendResult(result);
+                }));
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Content provider dead retrieving " + uri, e);
+            resultCallback.sendResult(Bundle.EMPTY);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     int checkContentProviderUriPermission(Uri uri, int userId, int callingUid, int modeFlags) {
         final String name = uri.getAuthority();
         final long ident = Binder.clearCallingIdentity();
diff --git a/test-mock/src/android/test/mock/MockContentProvider.java b/test-mock/src/android/test/mock/MockContentProvider.java
index 85e5916..f7ec11c 100644
--- a/test-mock/src/android/test/mock/MockContentProvider.java
+++ b/test-mock/src/android/test/mock/MockContentProvider.java
@@ -21,6 +21,7 @@
 import android.content.ContentProvider;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.IContentProvider;
@@ -31,10 +32,12 @@
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteCallback;
 import android.os.RemoteException;
 
 import java.io.FileNotFoundException;
@@ -81,6 +84,11 @@
         }
 
         @Override
+        public void getTypeAsync(Uri uri, RemoteCallback callback) throws RemoteException {
+            MockContentProvider.this.getTypeAsync(uri, callback);
+        }
+
+        @Override
         public Uri insert(String callingPackage, @Nullable String featureId, Uri url,
                 ContentValues initialValues, Bundle extras) throws RemoteException {
             return MockContentProvider.this.insert(url, initialValues, extras);
@@ -212,6 +220,18 @@
         throw new UnsupportedOperationException("unimplemented mock method");
     }
 
+    /**
+     * @hide
+     */
+    @SuppressWarnings("deprecation")
+    public void getTypeAsync(Uri uri, RemoteCallback remoteCallback) {
+        AsyncTask.SERIAL_EXECUTOR.execute(() -> {
+            final Bundle bundle = new Bundle();
+            bundle.putString(ContentResolver.REMOTE_CALLBACK_RESULT, getType(uri));
+            remoteCallback.sendResult(bundle);
+        });
+    }
+
     @Override
     public Uri insert(Uri uri, ContentValues values) {
         throw new UnsupportedOperationException("unimplemented mock method");
diff --git a/test-mock/src/android/test/mock/MockIContentProvider.java b/test-mock/src/android/test/mock/MockIContentProvider.java
index 464abfb..1831bcd 100644
--- a/test-mock/src/android/test/mock/MockIContentProvider.java
+++ b/test-mock/src/android/test/mock/MockIContentProvider.java
@@ -19,16 +19,19 @@
 import android.annotation.Nullable;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.EntityIterator;
 import android.content.IContentProvider;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteCallback;
 import android.os.RemoteException;
 
 import java.io.FileNotFoundException;
@@ -61,6 +64,16 @@
     }
 
     @Override
+    @SuppressWarnings("deprecation")
+    public void getTypeAsync(Uri uri, RemoteCallback remoteCallback) {
+        AsyncTask.SERIAL_EXECUTOR.execute(() -> {
+            final Bundle bundle = new Bundle();
+            bundle.putString(ContentResolver.REMOTE_CALLBACK_RESULT, getType(uri));
+            remoteCallback.sendResult(bundle);
+        });
+    }
+
+    @Override
     @SuppressWarnings("unused")
     public Uri insert(String callingPackage, @Nullable String featureId, Uri url,
             ContentValues initialValues, Bundle extras) throws RemoteException {