diff options
12 files changed, 314 insertions, 6 deletions
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 3ffd7c70b40d..5f3bad6ad1a8 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -303,8 +303,13 @@ interface IActivityManager { 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 c271e3c66adc..e1942da8ac7f 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -49,6 +49,7 @@ import android.os.IBinder; 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 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall } @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 45ace4010961..0f1442d864ba 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -33,6 +33,7 @@ import android.os.ICancellationSignal; 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 @@ abstract public class ContentProviderNative extends Binder implements IContentPr 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 @@ final class ContentProviderProxy implements IContentProvider } @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 6cd1cd3354c2..f32a4ab43357 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -55,6 +55,7 @@ import android.os.IBinder; 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.Log; 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 @@ public abstract class ContentResolver implements ContentInterface { 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 @@ public abstract class ContentResolver implements ContentInterface { 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 @@ public abstract class ContentResolver implements ContentInterface { } 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 6f477ff17c67..4658ba109d5f 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -27,6 +27,7 @@ import android.os.IBinder; 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 @@ public interface IContentProvider extends IInterface { @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 @@ public interface IContentProvider extends IInterface { 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 b85a332e9000..23f9f90248df 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 e140ad2a9a8f..9dcce1e51e0b 100644 --- a/core/tests/coretests/src/android/content/ContentResolverTest.java +++ b/core/tests/coretests/src/android/content/ContentResolverTest.java @@ -191,4 +191,22 @@ public class ContentResolverTest { 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 000000000000..a8c2f4059d50 --- /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 000000000000..7b9bdbcd7a17 --- /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 8b547c6bda42..4c5f705433d6 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 @@ public class ActivityManagerService extends IActivityManager.Stub * * 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 @@ public class ActivityManagerService extends IActivityManager.Stub 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 85e5916a63df..f7ec11c79476 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.annotation.Nullable; 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.pm.ProviderInfo; 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 @@ public class MockContentProvider extends ContentProvider { } @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 @@ public class MockContentProvider extends ContentProvider { 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 464abfb1a514..1831bcdf9df7 100644 --- a/test-mock/src/android/test/mock/MockIContentProvider.java +++ b/test-mock/src/android/test/mock/MockIContentProvider.java @@ -19,16 +19,19 @@ package android.test.mock; 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 @@ public class MockIContentProvider implements IContentProvider { } @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 { |