diff options
6 files changed, 468 insertions, 89 deletions
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index 2b90f8bc..e7c00f86 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -27,6 +27,7 @@ import static com.android.providers.contacts.util.PhoneAccountHandleMigrationUti import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.WorkerThread; import android.app.AppOpsManager; @@ -48,6 +49,7 @@ import android.content.UriMatcher; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ProviderInfo; +import android.content.pm.UserInfo; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; @@ -5620,10 +5622,17 @@ public class ContactsProvider2 extends AbstractContactsProvider // If caller does not come from same profile, Check if it's privileged or allowed by // enterprise policy - if (!queryAllowedByEnterprisePolicy(uri)) { + if (!isCrossUserQueryAllowed(uri)) { return null; } + // TODO(b/253449368) - The call below should be gated by the app-cloning feature flag. + if (UserUtils.shouldUseParentsContacts(getContext()) && + isAppAllowedToUseParentUsersContacts(getCallingPackage())) { + return queryParentProfileContactsProvider(uri, projection, selection, selectionArgs, + sortOrder, cancellationSignal); + } + // Query the profile DB if appropriate. if (mapsToProfileDb(uri)) { switchToProfileMode(); @@ -5643,7 +5652,22 @@ public class ContactsProvider2 extends AbstractContactsProvider } } - private boolean queryAllowedByEnterprisePolicy(Uri uri) { + @VisibleForTesting + protected boolean isAppAllowedToUseParentUsersContacts(@Nullable String packageName) { + try { + for (String appName: getContext().getResources() + .getStringArray(com.android.internal.R.array.cloneable_apps)) { + if (packageName != null && packageName.equals(appName)) { + return true; + } + } + } catch (NotFoundException nfe) { + Log.w(TAG, "Resource corresponding to list of cloneable apps not found"); + } + return false; + } + + private boolean isCrossUserQueryAllowed(Uri uri) { if (isCallerFromSameUser()) { // Caller is on the same user; query allowed. return true; @@ -5651,12 +5675,21 @@ public class ContactsProvider2 extends AbstractContactsProvider if (!doesCallerHoldInteractAcrossUserPermission()) { // Cross-user and the caller has no INTERACT_ACROSS_USERS; don't allow query. // Technically, in a cross-profile sharing case, this would be a valid query. - // But for now we don't allow it. (We never allowe it and no one complained about it.) + // But for now we don't allow it. (We never allowed it and no one complained about it.) return false; } if (isCallerAnotherSelf()) { - // The caller is the other CP2 (which has INTERACT_ACROSS_USERS), meaning the reuest + // The caller is the other CP2 (which has INTERACT_ACROSS_USERS), meaning the request // is on behalf of a "real" client app. + + // TODO(b/253449368) - The condition below should only be checked behind the app-cloning + // feature flag + if (doesCallingProviderUseCurrentUsersContacts()) { + // The caller is the other CP2 (which has INTERACT_ACROSS_USERS), from the child + // user (of the current user) profile with the property of using parent's contacts + // set. + return true; + } // Consult the enterprise policy. return mEnterprisePolicyGuard.isCrossProfileAllowed(uri); } @@ -5668,6 +5701,22 @@ public class ContactsProvider2 extends AbstractContactsProvider } /** + * Returns true if calling contacts provider instance uses current users contacts. + * This can happen when the current user is the parent of the calling user and the calling user + * has the corresponding user property to use parent's contacts set. Please note that this + * cross-profile contact access will only be allowed if the call is redirected from the child + * user's CP2. + */ + private boolean doesCallingProviderUseCurrentUsersContacts() { + UserHandle callingUserHandle = UserHandle.getUserHandleForUid(Binder.getCallingUid()); + UserHandle currentUserHandle = android.os.Process.myUserHandle(); + boolean isCallerFromSameUser = callingUserHandle.equals(currentUserHandle); + return isCallerFromSameUser || + (UserUtils.shouldUseParentsContacts(getContext(), callingUserHandle) && + UserUtils.isParentUser(getContext(), currentUserHandle, callingUserHandle)); + } + + /** * Returns true if called by a different user's CP2. */ private boolean isCallerAnotherSelf() { @@ -5697,7 +5746,8 @@ public class ContactsProvider2 extends AbstractContactsProvider return !UserUtils.shouldUseParentsContacts(getContext()); } - private Cursor queryDirectoryIfNecessary(Uri uri, String[] projection, String selection, + @VisibleForTesting + protected Cursor queryDirectoryIfNecessary(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY); final long directoryId = @@ -5862,11 +5912,7 @@ public class ContactsProvider2 extends AbstractContactsProvider return createEmptyCursor(localUri, projection); } // Make sure authority is CP2 not other providers - if (!ContactsContract.AUTHORITY.equals(localUri.getAuthority())) { - Log.w(TAG, "Invalid authority: " + localUri.getAuthority()); - throw new IllegalArgumentException( - "Authority " + localUri.getAuthority() + " is not a valid CP2 authority."); - } + validateAuthority(localUri.getAuthority()); // Add the "user-id @" to the URI, and also pass the caller package name. final Uri remoteUri = maybeAddUserId(localUri, corpUserId).buildUpon() .appendQueryParameter(Directory.CALLER_PACKAGE_PARAM_KEY, getCallingPackage()) @@ -5879,6 +5925,69 @@ public class ContactsProvider2 extends AbstractContactsProvider return cursor; } + private Uri getParentProviderUri(Uri uri, @NonNull UserInfo parentUserInfo) { + // Add the "user-id @" of the parent to the URI + final Builder remoteUriBuilder = + maybeAddUserId(uri, parentUserInfo.getUserHandle().getIdentifier()) + .buildUpon(); + // Pass the caller package name query param and build the uri + return remoteUriBuilder + .appendQueryParameter(Directory.CALLER_PACKAGE_PARAM_KEY, + getRealCallerPackageName(uri)) + .build(); + } + + protected AssetFileDescriptor openAssetFileThroughParentProvider(Uri uri, String mode) + throws FileNotFoundException { + final UserInfo parentUserInfo = UserUtils.getProfileParentUser(getContext()); + if (parentUserInfo == null) { + return null; + } + validateAuthority(uri.getAuthority()); + final Uri remoteUri = getParentProviderUri(uri, parentUserInfo); + return getContext().getContentResolver().openAssetFile(remoteUri, mode, null); + } + + /** + * A helper function to query parent CP2, should only be called from users that are allowed to + * use parents contacts + */ + @VisibleForTesting + protected Cursor queryParentProfileContactsProvider(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder, + CancellationSignal cancellationSignal) { + final UserInfo parentUserInfo = UserUtils.getProfileParentUser(getContext()); + if (parentUserInfo == null) { + return createEmptyCursor(uri, projection); + } + // Make sure authority is CP2 not other providers + validateAuthority(uri.getAuthority()); + Cursor cursor = queryContactsProviderForUser(uri, projection, selection, selectionArgs, + sortOrder, cancellationSignal, parentUserInfo); + if (cursor == null) { + Log.w(TAG, "null cursor returned from primary CP2"); + return createEmptyCursor(uri, projection); + } + return cursor; + } + + @VisibleForTesting + protected Cursor queryContactsProviderForUser(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal, + UserInfo parentUserInfo) { + final Uri remoteUri = getParentProviderUri(uri, parentUserInfo); + return getContext().getContentResolver().query(remoteUri, projection, selection, + selectionArgs, sortOrder, cancellationSignal); + } + + private void validateAuthority(String authority) { + if (!ContactsContract.AUTHORITY.equals(authority)) { + Log.w(TAG, "Invalid authority: " + authority); + throw new IllegalArgumentException( + "Authority " + authority + " is not a valid CP2 authority."); + } + } + private Cursor addSnippetExtrasToCursor(Uri uri, Cursor cursor) { // If the cursor doesn't contain a snippet column, don't bother wrapping it. @@ -8771,10 +8880,20 @@ public class ContactsProvider2 extends AbstractContactsProvider if (!isDirectoryParamValid(uri)){ return null; } - if (!queryAllowedByEnterprisePolicy(uri)) { + if (!isCrossUserQueryAllowed(uri)) { return null; } waitForAccess(mode.equals("r") ? mReadAccessLatch : mWriteAccessLatch); + + // Redirect reads to parent provider if the corresponding user property is set and app + // is allow-listed to access parent's contacts + // TODO(b/253449368) - The call below should be gated by the app-cloning feature flag + if (mode.equals("r") && + UserUtils.shouldUseParentsContacts(getContext()) && + isAppAllowedToUseParentUsersContacts(getCallingPackage())) { + return openAssetFileThroughParentProvider(uri, mode); + } + final AssetFileDescriptor ret; if (mapsToProfileDb(uri)) { switchToProfileMode(); diff --git a/src/com/android/providers/contacts/util/UserUtils.java b/src/com/android/providers/contacts/util/UserUtils.java index 54fc8de3..effe8abe 100644 --- a/src/com/android/providers/contacts/util/UserUtils.java +++ b/src/com/android/providers/contacts/util/UserUtils.java @@ -22,6 +22,7 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.pm.UserInfo; import android.content.pm.UserProperties; +import android.os.UserHandle; import android.os.UserManager; import android.util.Log; @@ -83,8 +84,47 @@ public final class UserUtils { @SuppressLint("AndroidFrameworkRequiresPermission") public static boolean shouldUseParentsContacts(Context context) { + try { + final UserManager userManager = getUserManager(context); + final UserProperties userProperties = userManager.getUserProperties(context.getUser()); + return userProperties.getUseParentsContacts(); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Trying to fetch user properties for non-existing/partial user " + + context.getUser()); + return false; + } + } + + @SuppressLint("AndroidFrameworkRequiresPermission") + public static boolean shouldUseParentsContacts(Context context, UserHandle userHandle) { + try { + final UserManager userManager = getUserManager(context); + final UserProperties userProperties = userManager.getUserProperties(userHandle); + return userProperties.getUseParentsContacts(); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Trying to fetch user properties for non-existing/partial user " + + userHandle); + return false; + } + } + + /** + * Checks if the input profile user is the parent of the other user + * @return True if user1 is the parent profile of user2, false otherwise + */ + @SuppressLint("AndroidFrameworkRequiresPermission") + public static boolean isParentUser(Context context, UserHandle user1, UserHandle user2) { + if (user1 == null || user2 == null) return false; + final UserManager userManager = getUserManager(context); + UserInfo parentUserInfo = userManager.getProfileParent(user2.getIdentifier()); + return parentUserInfo != null + && parentUserInfo.getUserHandle() != null + && parentUserInfo.getUserHandle().equals(user1); + } + + @SuppressLint("AndroidFrameworkRequiresPermission") + public static UserInfo getProfileParentUser(Context context) { final UserManager userManager = getUserManager(context); - final UserProperties userProperties = userManager.getUserProperties(context.getUser()); - return userProperties.getUseParentsContacts(); + return userManager.getProfileParent(context.getUserId()); } } diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java index 20d6a15e..e8920530 100644 --- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java +++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java @@ -70,6 +70,8 @@ import com.android.providers.contacts.util.Hex; import com.android.providers.contacts.util.MockClock; import com.google.android.collect.Sets; +import java.io.FileInputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; @@ -1419,6 +1421,71 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase { getContactsProvider().getProfileProviderForTest().getDatabaseHelper(), values); } + protected class VCardTestUriCreator { + private String mLookup1; + private String mLookup2; + + public VCardTestUriCreator(String lookup1, String lookup2) { + super(); + mLookup1 = lookup1; + mLookup2 = lookup2; + } + + public Uri getUri1() { + return Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, mLookup1); + } + + public Uri getUri2() { + return Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, mLookup2); + } + + public Uri getCombinedUri() { + return Uri.withAppendedPath(Contacts.CONTENT_MULTI_VCARD_URI, + Uri.encode(mLookup1 + ":" + mLookup2)); + } + } + + protected VCardTestUriCreator createVCardTestContacts() { + final long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount, + RawContacts.SOURCE_ID, "4:12"); + DataUtil.insertStructuredName(mResolver, rawContactId1, "John", "Doe"); + + final long rawContactId2 = RawContactUtil.createRawContact(mResolver, mAccount, + RawContacts.SOURCE_ID, "3:4%121"); + DataUtil.insertStructuredName(mResolver, rawContactId2, "Jane", "Doh"); + + final long contactId1 = queryContactId(rawContactId1); + final long contactId2 = queryContactId(rawContactId2); + final Uri contact1Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1); + final Uri contact2Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId2); + final String lookup1 = + Uri.encode(Contacts.getLookupUri(mResolver, contact1Uri).getPathSegments().get(2)); + final String lookup2 = + Uri.encode(Contacts.getLookupUri(mResolver, contact2Uri).getPathSegments().get(2)); + return new VCardTestUriCreator(lookup1, lookup2); + } + + protected String readToEnd(FileInputStream inputStream) { + try { + System.out.println("DECLARED INPUT STREAM LENGTH: " + inputStream.available()); + int ch; + StringBuilder stringBuilder = new StringBuilder(); + int index = 0; + while (true) { + ch = inputStream.read(); + System.out.println("READ CHARACTER: " + index + " " + ch); + if (ch == -1) { + break; + } + stringBuilder.append((char)ch); + index++; + } + return stringBuilder.toString(); + } catch (IOException e) { + return null; + } + } + /** * A contact in the database, and the attributes used to create it. Construct using * {@link GoldenContactBuilder#build()}. diff --git a/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java b/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java index 18c6cb76..48a00da2 100644 --- a/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java +++ b/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java @@ -17,28 +17,50 @@ 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.Build; import android.os.Bundle; import android.os.RemoteException; +import android.provider.CallLog; import android.provider.ContactsContract; +import android.util.Log; + import androidx.test.filters.MediumTest; import androidx.test.filters.SdkSuppress; +import com.android.providers.contacts.testutil.DataUtil; +import com.android.providers.contacts.testutil.RawContactUtil; + +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; @@ -55,7 +77,8 @@ public class CloneContactsProvider2Test extends BaseContactsProvider2Test { mCloneContactsActor.mockUserManager.setUsers(ContactsActor.MockUserManager.PRIMARY_USER, CLONE_PROFILE_USER); mCloneContactsActor.mockUserManager.myUser = CLONE_PROFILE_USER.id; - getCloneContactsProvider().wipeData(); + mCloneContactsProvider = spy(getCloneContactsProvider()); + mCloneContactsProvider.wipeData(); } private ContentValues getSampleContentValues() { @@ -67,17 +90,43 @@ public class CloneContactsProvider2Test extends BaseContactsProvider2Test { 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) { - assertEquals(contentValues.get(ContactsContract.RawContacts.ACCOUNT_NAME), - cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME))); - assertEquals(contentValues.get(ContactsContract.RawContacts.ACCOUNT_TYPE), - cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_TYPE))); - assertEquals(contentValues.get(ContactsContract.RawContacts.CUSTOM_RINGTONE), - cursor.getString(cursor.getColumnIndex( - ContactsContract.RawContacts.CUSTOM_RINGTONE))); - assertEquals(contentValues.get(ContactsContract.RawContacts.STARRED), - cursor.getString(cursor.getColumnIndex( - ContactsContract.RawContacts.STARRED))); + 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, @@ -279,4 +328,128 @@ public class CloneContactsProvider2Test extends BaseContactsProvider2Test { 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()); + } } diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java index b625ac4a..69ae0fb2 100644 --- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java +++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java @@ -8163,50 +8163,6 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { assertEquals("default", helper.getProperty("existent1", "default")); } - private class VCardTestUriCreator { - private String mLookup1; - private String mLookup2; - - public VCardTestUriCreator(String lookup1, String lookup2) { - super(); - mLookup1 = lookup1; - mLookup2 = lookup2; - } - - public Uri getUri1() { - return Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, mLookup1); - } - - public Uri getUri2() { - return Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, mLookup2); - } - - public Uri getCombinedUri() { - return Uri.withAppendedPath(Contacts.CONTENT_MULTI_VCARD_URI, - Uri.encode(mLookup1 + ":" + mLookup2)); - } - } - - private VCardTestUriCreator createVCardTestContacts() { - final long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount, - RawContacts.SOURCE_ID, "4:12"); - DataUtil.insertStructuredName(mResolver, rawContactId1, "John", "Doe"); - - final long rawContactId2 = RawContactUtil.createRawContact(mResolver, mAccount, - RawContacts.SOURCE_ID, "3:4%121"); - DataUtil.insertStructuredName(mResolver, rawContactId2, "Jane", "Doh"); - - final long contactId1 = queryContactId(rawContactId1); - final long contactId2 = queryContactId(rawContactId2); - final Uri contact1Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1); - final Uri contact2Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId2); - final String lookup1 = - Uri.encode(Contacts.getLookupUri(mResolver, contact1Uri).getPathSegments().get(2)); - final String lookup2 = - Uri.encode(Contacts.getLookupUri(mResolver, contact2Uri).getPathSegments().get(2)); - return new VCardTestUriCreator(lookup1, lookup2); - } - public void testQueryMultiVCard() { // No need to create any contacts here, because the query for multiple vcards // does not go into the database at all @@ -9688,27 +9644,6 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { return c; } - private String readToEnd(FileInputStream inputStream) { - try { - System.out.println("DECLARED INPUT STREAM LENGTH: " + inputStream.available()); - int ch; - StringBuilder stringBuilder = new StringBuilder(); - int index = 0; - while (true) { - ch = inputStream.read(); - System.out.println("READ CHARACTER: " + index + " " + ch); - if (ch == -1) { - break; - } - stringBuilder.append((char)ch); - index++; - } - return stringBuilder.toString(); - } catch (IOException e) { - return null; - } - } - private void assertQueryParameter(String uriString, String parameter, String expectedValue) { assertEquals(expectedValue, ContactsProvider2.getQueryParameter( Uri.parse(uriString), parameter)); diff --git a/tests/src/com/android/providers/contacts/util/UserUtilsTest.java b/tests/src/com/android/providers/contacts/util/UserUtilsTest.java index 072df377..c672697a 100644 --- a/tests/src/com/android/providers/contacts/util/UserUtilsTest.java +++ b/tests/src/com/android/providers/contacts/util/UserUtilsTest.java @@ -18,6 +18,7 @@ package com.android.providers.contacts.util; import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY; import android.content.Context; +import android.os.UserHandle; import android.provider.ContactsContract; import android.test.suitebuilder.annotation.SmallTest; @@ -101,14 +102,58 @@ public class UserUtilsTest extends FixedAndroidTestCase { um.myUser = MockUserManager.PRIMARY_USER.id; assertFalse(UserUtils.shouldUseParentsContacts(c)); + assertFalse(UserUtils.shouldUseParentsContacts(c, + MockUserManager.PRIMARY_USER.getUserHandle())); um.myUser = MockUserManager.SECONDARY_USER.id; assertFalse(UserUtils.shouldUseParentsContacts(c)); + assertFalse(UserUtils.shouldUseParentsContacts(c, + MockUserManager.SECONDARY_USER.getUserHandle())); um.myUser = MockUserManager.CORP_USER.id; assertFalse(UserUtils.shouldUseParentsContacts(c)); + assertFalse(UserUtils.shouldUseParentsContacts(c, + MockUserManager.CORP_USER.getUserHandle())); um.myUser = MockUserManager.CLONE_PROFILE_USER.id; assertTrue(UserUtils.shouldUseParentsContacts(c)); + assertTrue(UserUtils.shouldUseParentsContacts(c, + MockUserManager.CLONE_PROFILE_USER.getUserHandle())); + + } + + public void testIsParentUser() { + final Context c = mActor.getProviderContext(); + final MockUserManager um = mActor.mockUserManager; + um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.SECONDARY_USER, + MockUserManager.CLONE_PROFILE_USER, MockUserManager.CORP_USER); + + UserHandle primaryProfileUserHandle = MockUserManager.PRIMARY_USER.getUserHandle(); + UserHandle cloneUserHandle = MockUserManager.CLONE_PROFILE_USER.getUserHandle(); + UserHandle corpUserHandle = MockUserManager.CORP_USER.getUserHandle(); + + assertTrue(UserUtils.isParentUser(c, primaryProfileUserHandle, cloneUserHandle)); + assertTrue(UserUtils.isParentUser(c, primaryProfileUserHandle, corpUserHandle)); + assertFalse(UserUtils.isParentUser(c, primaryProfileUserHandle, primaryProfileUserHandle)); + assertFalse(UserUtils.isParentUser(c, cloneUserHandle, cloneUserHandle)); + assertFalse(UserUtils.isParentUser(c, cloneUserHandle, primaryProfileUserHandle)); + assertFalse(UserUtils.isParentUser(c, corpUserHandle, primaryProfileUserHandle)); + } + + public void testGetProfileParent() { + final Context c = mActor.getProviderContext(); + final MockUserManager um = mActor.mockUserManager; + + um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.SECONDARY_USER, + MockUserManager.CLONE_PROFILE_USER, MockUserManager.CORP_USER); + + um.myUser = MockUserManager.PRIMARY_USER.id; + assertNull(UserUtils.getProfileParentUser(c)); + + um.myUser = MockUserManager.CLONE_PROFILE_USER.id; + assertEquals(MockUserManager.PRIMARY_USER, UserUtils.getProfileParentUser(c)); + + um.myUser = MockUserManager.CORP_USER.id; + assertEquals(MockUserManager.PRIMARY_USER, UserUtils.getProfileParentUser(c)); } } |