diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index e1942da8ac7fefea60a724bf121ec43a734ff518..bd3298c79fffdb9c5d94026bf4059091a20bf5b4 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -579,6 +579,15 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall } } + @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); diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index 0f1442d864ba9604e531d4c0375ec1145e472121..7bc59013bcfe942ecb3f549fe40f2d071bb67f6a 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -359,6 +359,16 @@ abstract public class ContentProviderNative extends Binder implements IContentPr 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); @@ -822,6 +832,25 @@ final class ContentProviderProxy implements IContentProvider } } + @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 { diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 0e0161ff4e9f6fb8fee3c2e17a21100d12ed81f4..b748cfa775ed6c88a6c69fcce998184c1259babb 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -712,14 +712,17 @@ public abstract class ContentResolver implements ContentInterface { * {@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 @@ public abstract class ContentResolver implements ContentInterface { 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 @@ public abstract class ContentResolver implements ContentInterface { } 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 @@ public abstract class ContentResolver implements ContentInterface { } } - 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 @@ public abstract class ContentResolver implements ContentInterface { } } + 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 @@ public abstract class ContentResolver implements ContentInterface { } 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 4658ba109d5f1c02011d6ae400261772961d9082..37643da375df6cee3d890af8b5830e62ccf1dead 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -45,7 +45,7 @@ public interface IContentProvider extends IInterface { 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 interface IContentProvider extends IInterface { 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 @@ public interface IContentProvider extends IInterface { 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 78c4420b9496e6da2421533a2bfa824297ab390c..1737bd0fa20bd7719239d975c389852fb19181f9 100644 --- a/core/tests/coretests/src/android/content/ContentResolverTest.java +++ b/core/tests/coretests/src/android/content/ContentResolverTest.java @@ -234,4 +234,12 @@ public class ContentResolverTest { 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 7b9bdbcd7a17383df8849a61cb33816161729c78..1d7ba5d9be465e06b94e391ce3cbd352e6651d9e 100644 --- a/core/tests/coretests/src/android/content/FakeProviderRemote.java +++ b/core/tests/coretests/src/android/content/FakeProviderRemote.java @@ -54,4 +54,10 @@ public class FakeProviderRemote extends ContentProvider { 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(); + } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 9082807f6bbcacfdbaa1852b2f47ef74a285a67f..a6efce65c6ccc4b9b604bed45fd70916fd0e6465 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -7266,7 +7266,7 @@ public class ActivityManagerService extends IActivityManager.Stub // Wait for the provider to be published... final long timeout = - SystemClock.uptimeMillis() + ContentResolver.CONTENT_PROVIDER_WAIT_TIMEOUT_MILLIS; + SystemClock.uptimeMillis() + ContentResolver.CONTENT_PROVIDER_READY_TIMEOUT_MILLIS; boolean timedOut = false; synchronized (cpr) { while (cpr.provider == null) { diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 3f0cda3b8e5af46c1ea602a99c0d6b723d4072f3..b7199f7cdd5a0fc362661c3606918387ccd3ac20 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -29,6 +29,7 @@ android_test { libs: [ "android.test.runner", "android.test.base", + "android.test.mock", ], dxflags: ["--multi-dex"], diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 0adf15c8b6372fef05a92f0594df722cc3ed7014..6ff7b656bdce051e7568c322e63a7cab36a09f22 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -36,9 +36,10 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -67,6 +68,7 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.provider.Settings.Secure; import android.service.notification.ConversationChannelWrapper; +import android.test.mock.MockIContentProvider; import android.test.suitebuilder.annotation.SmallTest; import android.testing.TestableContentResolver; import android.util.ArrayMap; @@ -87,6 +89,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlSerializer; @@ -122,7 +125,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Mock NotificationUsageStats mUsageStats; @Mock RankingHandler mHandler; @Mock PackageManager mPm; - @Mock IContentProvider mTestIContentProvider; + @Spy IContentProvider mTestIContentProvider = new MockIContentProvider(); @Mock Context mContext; @Mock ZenModeHelper mMockZenModeHelper; @@ -167,12 +170,12 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(testContentProvider.getIContentProvider()).thenReturn(mTestIContentProvider); contentResolver.addProvider(TEST_AUTHORITY, testContentProvider); - when(mTestIContentProvider.canonicalize(any(), any(), eq(SOUND_URI))) - .thenReturn(CANONICAL_SOUND_URI); - when(mTestIContentProvider.canonicalize(any(), any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(CANONICAL_SOUND_URI); - when(mTestIContentProvider.uncanonicalize(any(), any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(SOUND_URI); + doReturn(CANONICAL_SOUND_URI) + .when(mTestIContentProvider).canonicalize(any(), any(), eq(SOUND_URI)); + doReturn(CANONICAL_SOUND_URI) + .when(mTestIContentProvider).canonicalize(any(), any(), eq(CANONICAL_SOUND_URI)); + doReturn(SOUND_URI) + .when(mTestIContentProvider).uncanonicalize(any(), any(), eq(CANONICAL_SOUND_URI)); mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); @@ -503,12 +506,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { .appendQueryParameter("title", "Test") .appendQueryParameter("canonical", "1") .build(); - when(mTestIContentProvider.canonicalize(any(), any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(canonicalBasedOnLocal); - when(mTestIContentProvider.uncanonicalize(any(), any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(localUri); - when(mTestIContentProvider.uncanonicalize(any(), any(), eq(canonicalBasedOnLocal))) - .thenReturn(localUri); + doReturn(canonicalBasedOnLocal) + .when(mTestIContentProvider).canonicalize(any(), any(), eq(CANONICAL_SOUND_URI)); + doReturn(localUri) + .when(mTestIContentProvider).uncanonicalize(any(), any(), eq(CANONICAL_SOUND_URI)); + doReturn(localUri) + .when(mTestIContentProvider).uncanonicalize(any(), any(), + eq(canonicalBasedOnLocal)); NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); @@ -527,10 +531,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testRestoreXml_withNonExistentCanonicalizedSoundUri() throws Exception { Thread.sleep(3000); - when(mTestIContentProvider.canonicalize(any(), any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(null); - when(mTestIContentProvider.uncanonicalize(any(), any(), eq(CANONICAL_SOUND_URI))) - .thenReturn(null); + doReturn(null) + .when(mTestIContentProvider).canonicalize(any(), any(), eq(CANONICAL_SOUND_URI)); + doReturn(null) + .when(mTestIContentProvider).uncanonicalize(any(), any(), eq(CANONICAL_SOUND_URI)); NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); @@ -554,7 +558,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testRestoreXml_withUncanonicalizedNonLocalSoundUri() throws Exception { // Not a local uncanonicalized uri, simulating that it fails to exist locally - when(mTestIContentProvider.canonicalize(any(), any(), eq(SOUND_URI))).thenReturn(null); + doReturn(null) + .when(mTestIContentProvider).canonicalize(any(), any(), eq(SOUND_URI)); String id = "id"; String backupWithUncanonicalizedSoundUri = "<ranking version=\"1\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" diff --git a/test-mock/src/android/test/mock/MockContentProvider.java b/test-mock/src/android/test/mock/MockContentProvider.java index f7ec11c794764175c69e772979768ff9654f5838..d1d64d39b688424ff4291ca03b95b11fafa9ffda 100644 --- a/test-mock/src/android/test/mock/MockContentProvider.java +++ b/test-mock/src/android/test/mock/MockContentProvider.java @@ -155,6 +155,12 @@ public class MockContentProvider extends ContentProvider { return MockContentProvider.this.canonicalize(uri); } + @Override + public void canonicalizeAsync(String callingPkg, String featureId, Uri uri, + RemoteCallback callback) { + MockContentProvider.this.canonicalizeAsync(uri, callback); + } + @Override public Uri uncanonicalize(String callingPkg, @Nullable String featureId, Uri uri) throws RemoteException { @@ -289,6 +295,18 @@ public class MockContentProvider extends ContentProvider { throw new UnsupportedOperationException("unimplemented mock method call"); } + /** + * @hide + */ + @SuppressWarnings("deprecation") + public void canonicalizeAsync(Uri uri, RemoteCallback callback) { + AsyncTask.SERIAL_EXECUTOR.execute(() -> { + final Bundle bundle = new Bundle(); + bundle.putParcelable(ContentResolver.REMOTE_CALLBACK_RESULT, canonicalize(uri)); + callback.sendResult(bundle); + }); + } + /** * @hide */ diff --git a/test-mock/src/android/test/mock/MockIContentProvider.java b/test-mock/src/android/test/mock/MockIContentProvider.java index 1831bcdf9df7a2b50c0353707fa7f6ce466e6177..223bcc59039d44321e4f2b8f11ab9a7f2e238ff9 100644 --- a/test-mock/src/android/test/mock/MockIContentProvider.java +++ b/test-mock/src/android/test/mock/MockIContentProvider.java @@ -145,11 +145,22 @@ public class MockIContentProvider implements IContentProvider { } @Override - public Uri canonicalize(String callingPkg, @Nullable String featureId, Uri uri) - throws RemoteException { + public Uri canonicalize(String callingPkg, @Nullable String featureId, Uri uri) { throw new UnsupportedOperationException("unimplemented mock method"); } + @Override + @SuppressWarnings("deprecation") + public void canonicalizeAsync(String callingPkg, String featureId, Uri uri, + RemoteCallback remoteCallback) { + AsyncTask.SERIAL_EXECUTOR.execute(() -> { + final Bundle bundle = new Bundle(); + bundle.putParcelable(ContentResolver.REMOTE_CALLBACK_RESULT, + canonicalize(callingPkg, featureId, uri)); + remoteCallback.sendResult(bundle); + }); + } + @Override public Uri uncanonicalize(String callingPkg, @Nullable String featureId, Uri uri) throws RemoteException {