aboutsummaryrefslogtreecommitdiff
path: root/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java
diff options
context:
space:
mode:
Diffstat (limited to 'tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java')
-rw-r--r--tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java504
1 files changed, 504 insertions, 0 deletions
diff --git a/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java b/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java
new file mode 100644
index 00000000..26a78143
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2022 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 com.android.providers.contacts;
+
+import static com.android.providers.contacts.ContactsActor.MockUserManager.CLONE_PROFILE_USER;
+import static com.android.providers.contacts.ContactsActor.MockUserManager.PRIMARY_USER;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.spy;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.util.SparseArray;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Assert;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Set;
+
+@MediumTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+public class CloneContactsProvider2Test extends BaseContactsProvider2Test {
+
+ private ContactsActor mCloneContactsActor;
+ private SynchronousContactsProvider2 mCloneContactsProvider;
+
+ private SynchronousContactsProvider2 getCloneContactsProvider() {
+ return (SynchronousContactsProvider2) mCloneContactsActor.provider;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mCloneContactsActor = new ContactsActor(
+ new ContactsActor.AlteringUserContext(getContext(), CLONE_PROFILE_USER.id),
+ getContextPackageName(), SynchronousContactsProvider2.class, getAuthority());
+ mActor.mockUserManager.setUsers(ContactsActor.MockUserManager.PRIMARY_USER,
+ CLONE_PROFILE_USER);
+ mCloneContactsActor.mockUserManager.setUsers(ContactsActor.MockUserManager.PRIMARY_USER,
+ CLONE_PROFILE_USER);
+ mCloneContactsActor.mockUserManager.myUser = CLONE_PROFILE_USER.id;
+ mCloneContactsProvider = spy(getCloneContactsProvider());
+ mCloneContactsProvider.wipeData();
+ }
+
+ private ContentValues getSampleContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(ContactsContract.RawContacts.ACCOUNT_NAME, "test@test.com");
+ values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, "test.com");
+ values.put(ContactsContract.RawContacts.CUSTOM_RINGTONE, "custom");
+ values.put(ContactsContract.RawContacts.STARRED, "1");
+ return values;
+ }
+
+ private void getCloneContactsProviderWithMockedCallToParent(Uri uri) {
+ Cursor primaryProfileCursor = mActor.provider.query(uri,
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(primaryProfileCursor);
+ doReturn(primaryProfileCursor).when(mCloneContactsProvider)
+ .queryContactsProviderForUser(eq(uri), any(), any(), any(), any(),
+ any(), eq(PRIMARY_USER));
+ }
+
+ private void getCloneContactsProviderWithMockedOpenAssetFileCall(Uri uri)
+ throws FileNotFoundException {
+ AssetFileDescriptor fileDescriptor = mActor.provider.openAssetFile(uri, "r");
+ doReturn(fileDescriptor).when(mCloneContactsProvider)
+ .openAssetFileThroughParentProvider(eq(uri), eq("r"));
+ }
+
+ private String getCursorValue(Cursor c, String columnName) {
+ return c.getString(c.getColumnIndex(columnName));
+ }
+
+ private void assertEqualContentValues(ContentValues contentValues, Cursor cursor) {
+ for (String key: contentValues.getValues().keySet()) {
+ assertEquals(contentValues.get(key), getCursorValue(cursor, key));
+ }
+ }
+
+ private void assertRawContactsCursorEquals(Cursor expectedCursor, Cursor actualCursor,
+ Set<String> columnNames) {
+ assertNotNull(actualCursor);
+ assertEquals(expectedCursor.getCount(), actualCursor.getCount());
+ while (actualCursor.moveToNext()) {
+ expectedCursor.moveToNext();
+ for (String key: columnNames) {
+ assertEquals(getCursorValue(expectedCursor, key),
+ getCursorValue(actualCursor, key));
+ }
+ }
+ }
+
+ private void assertRejectedApplyBatchResults(ContentProviderResult[] res,
+ ArrayList<ContentProviderOperation> ops) {
+ assertEquals(ops.size(), res.length);
+ for (int i = 0;i < ops.size();i++) {
+ Uri expectedUri = ops.get(i).getUri()
+ .buildUpon()
+ .appendPath("0")
+ .build();
+ assertUriEquals(expectedUri, res[i].uri);
+ }
+ }
+
+ /**
+ * Asserts that no contacts are returned when queried by the given contacts provider
+ */
+ private void assertContactsProviderEmpty(ContactsProvider2 contactsProvider2) {
+ Cursor cursor = contactsProvider2.query(ContactsContract.RawContacts.CONTENT_URI,
+ new String[]{ContactsContract.RawContactsEntity._ID},
+ null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(cursor);
+ assertEquals(cursor.getCount(), 0);
+ }
+
+ private long insertRawContactsThroughPrimaryProvider(ContentValues values) {
+ Uri resultUri = mActor.resolver.insert(ContactsContract.RawContacts.CONTENT_URI,
+ values);
+ assertNotNull(resultUri);
+ return ContentUris.parseId(resultUri);
+ }
+
+ public void testAreContactWritesEnabled() {
+ // Check that writes are disabled for clone CP2
+ ContactsProvider2 cloneContactsProvider =
+ (ContactsProvider2) mCloneContactsActor.provider;
+ assertFalse(cloneContactsProvider.areContactWritesEnabled());
+
+ // Check that writes are enabled for primary CP2
+ ContactsProvider2 primaryContactsProvider = (ContactsProvider2) getProvider();
+ assertTrue(primaryContactsProvider.areContactWritesEnabled());
+ }
+
+ public void testCloneContactsProviderInsert() {
+ Uri resultUri =
+ mCloneContactsActor.resolver.insert(ContactsContract.RawContacts.CONTENT_URI,
+ getSampleContentValues());
+
+ // Here we expect a fakeUri returned to fail silently
+ Uri expectedUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon()
+ .appendPath("0")
+ .build();
+ assertUriEquals(expectedUri, resultUri);
+ // No contacts should be present in both clone and primary providers
+ assertContactsProviderEmpty(getContactsProvider());
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+ assertContactsProviderEmpty(mCloneContactsProvider);
+
+ }
+
+ public void testPrimaryContactsProviderInsert() {
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+ Cursor cursor = mActor.resolver.query(ContentUris.withAppendedId(
+ ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals(rawContactId,
+ cursor.getLong(cursor.getColumnIndex(ContactsContract.RawContacts._ID)));
+ assertEqualContentValues(inputContentValues, cursor);
+ }
+
+ public void testCloneContactsProviderUpdate() {
+ // Insert contact through the primary clone provider
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+
+ // Update display name in the input content values
+ ContentValues updatedContentValues = getSampleContentValues();
+ updatedContentValues.put(ContactsContract.RawContacts.STARRED,
+ "0");
+ updatedContentValues.put(ContactsContract.RawContacts.CUSTOM_RINGTONE,
+ "beethoven5");
+
+ // Call clone contacts provider update method to update the raw contact inserted earlier
+ int updateResult = mCloneContactsActor.resolver.update(
+ ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ updatedContentValues, null /* extras */);
+
+ // Check results, no rows should have been affected
+ assertEquals(0, updateResult);
+
+ // Check values associated with rawContactId by querying the database
+ Cursor cursor = mActor.resolver.query(ContentUris.withAppendedId(
+ ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEqualContentValues(inputContentValues, cursor);
+ }
+
+ public void testCloneContactsProviderDelete() {
+ // Insert contact through the primary clone provider
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+
+ // Delete the inserted row through clone provider
+ int deleteResult = mCloneContactsActor.resolver.delete(
+ ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ null);
+
+ // Check results, no rows should have been affected
+ assertEquals(0, deleteResult);
+
+ // Check that contact is present in the primary CP2 database
+ Cursor cursor = mActor.resolver.query(ContentUris.withAppendedId(
+ ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEqualContentValues(inputContentValues, cursor);
+ }
+
+ public void testCloneContactsProviderBulkInsert() {
+ int bulkInsertResult =
+ mCloneContactsActor.resolver.bulkInsert(ContactsContract.RawContacts.CONTENT_URI,
+ new ContentValues[]{ getSampleContentValues() });
+
+ // Check results, no rows should have been affected
+ assertEquals(0, bulkInsertResult);
+ // No contacts should be present in both clone and primary providers
+ assertContactsProviderEmpty(getContactsProvider());
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+ assertContactsProviderEmpty(mCloneContactsProvider);
+ }
+
+ public void testCloneContactsApplyBatch()
+ throws RemoteException, OperationApplicationException {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null /* value */)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null /* value */).build());
+
+ // Phone Number
+ ops.add(ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, "7XXXXXXXXXX")
+ .withValue(ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, "1").build());
+
+ // Display name/Contact name
+ ops.add(ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Name")
+ .build());
+
+ // Check results, fake uris should be returned for each of the insert operation
+ ContentProviderResult[] res = mCloneContactsActor.resolver.applyBatch(
+ ContactsContract.AUTHORITY, ops);
+ assertRejectedApplyBatchResults(res, ops);
+
+ // No contacts should be present in both clone and primary providers
+ assertContactsProviderEmpty(getContactsProvider());
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+ assertContactsProviderEmpty(mCloneContactsProvider);
+ }
+
+ public void testCloneContactsCallOperation() {
+ // Query Account Operation
+ Bundle response = mCloneContactsActor.resolver.call(ContactsContract.AUTHORITY_URI,
+ ContactsContract.Settings.QUERY_DEFAULT_ACCOUNT_METHOD, null /* arg */,
+ null /* extras */);
+ assertNotNull(response);
+ assertEquals(Bundle.EMPTY, response);
+
+ // Set account operation
+ Bundle bundle = new Bundle();
+ bundle.putString(ContactsContract.Settings.ACCOUNT_NAME, "test@test.com");
+ bundle.putString(ContactsContract.Settings.ACCOUNT_TYPE, "test.com");
+ Bundle setAccountResponse =
+ mCloneContactsActor.resolver.call(ContactsContract.AUTHORITY_URI,
+ ContactsContract.Settings.SET_DEFAULT_ACCOUNT_METHOD, null /* arg */, bundle);
+ assertNotNull(setAccountResponse);
+ assertEquals(Bundle.EMPTY, response);
+
+ // Authorization URI
+ Uri testUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 1);
+ final Bundle uriBundle = new Bundle();
+ uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, testUri);
+ final Bundle authResponse = mCloneContactsActor.resolver.call(
+ ContactsContract.AUTHORITY_URI,
+ ContactsContract.Authorization.AUTHORIZATION_METHOD,
+ null /* arg */,
+ uriBundle);
+ assertNotNull(authResponse);
+ assertEquals(Bundle.EMPTY, authResponse);
+ }
+
+ public void testCloneContactsProviderReads_callerNotInAllowlist() {
+ // Insert raw contact through the primary clone provider
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId);
+
+ // Mock call to parent profile contacts provider to return the correct result containing all
+ // contacts in the parent profile.
+ getCloneContactsProviderWithMockedCallToParent(uri);
+
+ // Mock call to ensure the caller package is not in the app-cloning allowlist
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+
+ // Test clone contacts provider read with the uri of the contact added above
+ mCloneContactsProvider.query(uri,
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+
+ // Check that the call passed through to the local query instead of redirecting to the
+ // parent provider
+ verify(mCloneContactsProvider, times(1))
+ .queryDirectoryIfNecessary(any(), any(), any(), any(), any(), any());
+ }
+
+ public void testContactsProviderReads_callerInAllowlist() {
+ // Insert raw contact through the primary clone provider
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId);
+
+ // Mock call to parent profile contacts provider to return the correct result containing all
+ // contacts in the parent profile.
+ getCloneContactsProviderWithMockedCallToParent(uri);
+
+ // Mock call to ensure the caller package is in the app-cloning allowlist
+ doReturn(true)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+
+ // Test clone contacts provider read with the uri of the contact added above
+ Cursor cursor = mCloneContactsProvider.query(uri,
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+
+ // Check that the call did not pass through to the local query and instead redirected to the
+ // parent provider
+ verify(mCloneContactsProvider, times(0))
+ .queryDirectoryIfNecessary(any(), any(), any(), any(), any(), any());
+ assertNotNull(cursor);
+ Cursor primaryProfileCursor = mActor.provider.query(uri,
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(primaryProfileCursor);
+ assertRawContactsCursorEquals(primaryProfileCursor, cursor,
+ inputContentValues.getValues().keySet());
+ }
+
+ public void testQueryPrimaryProfileProvider_callingFromParentUser() {
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId);
+
+ // Fetch primary contacts provider and call method to redirect to parent provider
+ final ContactsProvider2 primaryCP2 = (ContactsProvider2) getProvider();
+ Cursor cursor = primaryCP2.queryParentProfileContactsProvider(uri,
+ null /* projection */, null /* selection */, null /* selectionArgs */,
+ null /* sortOrder */, null /* cancellationSignal */);
+
+ // Assert that empty cursor is returned
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ }
+
+ public void testQueryPrimaryProfileProvider_incorrectAuthority() {
+ ContentValues inputContentValues = getSampleContentValues();
+ insertRawContactsThroughPrimaryProvider(inputContentValues);
+
+ Assert.assertThrows(IllegalArgumentException.class, () ->
+ mCloneContactsProvider.queryParentProfileContactsProvider(CallLog.CONTENT_URI,
+ null /* projection */, null /* selection */, null /* selectionArgs */,
+ null /* sortOrder */, null /* cancellationSignal */));
+ }
+
+ public void testOpenAssetFileMultiVCard() throws IOException {
+ final VCardTestUriCreator contacts = createVCardTestContacts();
+
+ // Mock call to parent profile contacts provider to return the correct asset file
+ getCloneContactsProviderWithMockedOpenAssetFileCall(contacts.getCombinedUri());
+
+ // Mock call to ensure the caller package is in the app-cloning allowlist
+ doReturn(true)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+
+ final AssetFileDescriptor descriptor =
+ mCloneContactsProvider.openAssetFile(contacts.getCombinedUri(), "r");
+ final FileInputStream inputStream = descriptor.createInputStream();
+ String data = readToEnd(inputStream);
+ inputStream.close();
+ descriptor.close();
+
+ // Ensure that the resulting VCard has both contacts
+ assertTrue(data.contains("N:Doe;John;;;"));
+ assertTrue(data.contains("N:Doh;Jane;;;"));
+ }
+
+ public void testOpenAssetFileMultiVCard_callerNotInAllowlist() throws IOException {
+ final VCardTestUriCreator contacts = createVCardTestContacts();
+
+ // Mock call to parent profile contacts provider to return the correct asset file
+ getCloneContactsProviderWithMockedOpenAssetFileCall(contacts.getCombinedUri());
+
+ // Mock call to ensure the caller package is not in the app-cloning allowlist
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+
+ final AssetFileDescriptor descriptor =
+ mCloneContactsProvider.openAssetFile(contacts.getCombinedUri(), "r");
+
+ // Check that the call passed through to the local call instead of redirecting to the
+ // parent provider
+ verify(mCloneContactsProvider, times(1))
+ .openAssetFile(eq(contacts.getCombinedUri()), any());
+ }
+
+ public void testIsAppAllowedToUseParentUsersContacts_AppInAllowlistCacheEmpty()
+ throws InterruptedException {
+ String testPackageName = mCloneContactsActor.packageName;
+ int processUid = Binder.getCallingUid();
+ doReturn(true)
+ .when(mCloneContactsProvider)
+ .doesPackageHaveALauncherActivity(eq(testPackageName), any());
+
+ SparseArray<ContactsProvider2.LaunchableCloneAppsCacheEntry> launchableCloneAppsCache =
+ mCloneContactsProvider.getLaunchableCloneAppsCacheForTesting();
+ launchableCloneAppsCache.clear();
+ boolean appAllowedToUseParentUsersContacts =
+ mCloneContactsProvider.isAppAllowedToUseParentUsersContacts(testPackageName);
+ assertTrue(appAllowedToUseParentUsersContacts);
+
+ // Check that the cache has been updated with an entry corresponding to current app uid
+ ContactsProvider2.LaunchableCloneAppsCacheEntry cacheEntry =
+ launchableCloneAppsCache.get(processUid);
+ assertNotNull(cacheEntry);
+ assertEquals(1, launchableCloneAppsCache.size());
+ assertTrue(cacheEntry.doesAppHaveLaunchableActivity);
+ }
+
+ public void testIsAppAllowedToUseParentUsersContacts_AppNotInAllowlistCacheEmtpy() {
+ String testPackageName = mCloneContactsActor.packageName;
+ int processUid = Binder.getCallingUid();
+
+ SparseArray<ContactsProvider2.LaunchableCloneAppsCacheEntry> launchableCloneAppsCache =
+ mCloneContactsProvider.getLaunchableCloneAppsCacheForTesting();
+ launchableCloneAppsCache.clear();
+ assertFalse(mCloneContactsProvider.isAppAllowedToUseParentUsersContacts(testPackageName));
+
+ // Check that the cache has been updated with an entry corresponding to current app uid
+ ContactsProvider2.LaunchableCloneAppsCacheEntry cacheEntry =
+ launchableCloneAppsCache.get(processUid);
+ assertNotNull(cacheEntry);
+ assertEquals(1, launchableCloneAppsCache.size());
+ assertFalse(cacheEntry.doesAppHaveLaunchableActivity);
+ }
+}