diff options
author | Xin Li <delphij@google.com> | 2023-08-22 11:37:02 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2023-08-22 11:37:02 -0700 |
commit | ea7903d4ca5a70810e4c98badaaf47d984ca5d64 (patch) | |
tree | 07b9d7c8d5cb7f54fd26900734f30c7c06b10a7a | |
parent | 889f532dfa50057888479820ebbe71f4cf600119 (diff) | |
parent | ecd0d356c988225ab7c5b5528b5bbceb791e1403 (diff) | |
download | ContactsProvider-ea7903d4ca5a70810e4c98badaaf47d984ca5d64.tar.gz |
Merge Android U (ab/10368041)
Bug: 291102124
Merged-In: I9065bd5c0923465efa9cb94fc33dafa8729b78d6
Change-Id: I5769646bd1186d087850fde9b41f4676ed0ebe8b
19 files changed, 1285 insertions, 157 deletions
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml index bcaedf7a..d5dbf950 100644 --- a/res/values-et/strings.xml +++ b/res/values-et/strings.xml @@ -19,7 +19,7 @@ <string name="sharedUserLabel" msgid="8024311725474286801">"Androidi tuumrakendused"</string> <string name="app_label" msgid="3389954322874982620">"Kontaktiruum"</string> <string name="provider_label" msgid="6012150850819899907">"Kontaktid"</string> - <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktisikute uuendamiseks on vaja rohkem mäluruumi."</string> + <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktisikute uuendamiseks on vaja rohkem mälu"</string> <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kontaktide salvestusruumi uuendamine"</string> <string name="upgrade_out_of_memory_notification_text" msgid="2581831842693151968">"Puudutage täiendamise lõpetamiseks."</string> <string name="default_directory" msgid="93961630309570294">"Kontaktid"</string> diff --git a/src/com/android/providers/contacts/CallLogDatabaseHelper.java b/src/com/android/providers/contacts/CallLogDatabaseHelper.java index 73480994..63ba17da 100644 --- a/src/com/android/providers/contacts/CallLogDatabaseHelper.java +++ b/src/com/android/providers/contacts/CallLogDatabaseHelper.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteCantOpenDatabaseException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.preference.PreferenceManager; @@ -539,9 +540,15 @@ public class CallLogDatabaseHelper { } @VisibleForTesting - @Nullable // We return null during tests when migration is not needed. + @Nullable // We return null during tests when migration is not needed or database + // is unavailable. SQLiteDatabase getContactsWritableDatabaseForMigration() { - return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase(); + try { + return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase(); + } catch (SQLiteCantOpenDatabaseException e) { + Log.i(TAG, "Exception caught during opening database for migration: " + e); + return null; + } } public PhoneAccountHandleMigrationUtils getPhoneAccountHandleMigrationUtils() { diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index 3f61a15a..56162916 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -22,13 +22,15 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils.TELEPHONY_COMPONENT_NAME; -import android.os.Looper; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.WorkerThread; import android.app.AppOpsManager; +import android.app.BroadcastOptions; import android.app.SearchManager; import android.content.BroadcastReceiver; import android.content.ContentProviderOperation; @@ -47,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; @@ -69,6 +72,7 @@ import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; +import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.os.RemoteException; @@ -129,10 +133,14 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import android.util.SparseArray; import com.android.common.content.ProjectionMap; import com.android.common.content.SyncStateContentProviderHelper; import com.android.common.io.MoreCloseables; +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.config.appcloning.AppCloningDeviceConfigHelper; import com.android.internal.util.ArrayUtils; import com.android.providers.contacts.ContactLookupKey.LookupKeySegment; import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; @@ -282,6 +290,14 @@ public class ContactsProvider2 extends AbstractContactsProvider /** Rate limit (in milliseconds) for dangling contacts cleanup. Do it at most once per day. */ private static final int DANGLING_CONTACTS_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000; + /** Time after which an entry in the launchable clone packages cache is invalidated and needs to + * be refreshed. + */ + private static final int LAUNCHABLE_CLONE_APPS_CACHE_ENTRY_REFRESH_INTERVAL = 10 * 60 * 1000; + + /** This value indicates the frequency of cleanup of the launchable clone apps cache */ + private static final int LAUNCHABLE_CLONE_APPS_CACHE_CLEANUP_LIMIT = 7 * 24 * 60 * 60 * 1000; + /** Maximum length of a phone number that can be inserted into the database */ private static final int PHONE_NUMBER_LENGTH_LIMIT = 1000; @@ -341,6 +357,7 @@ public class ContactsProvider2 extends AbstractContactsProvider public static final int CONTACTS_ID_PHOTO_CORP = 1027; public static final int CONTACTS_ID_DISPLAY_PHOTO_CORP = 1028; public static final int CONTACTS_FILTER_ENTERPRISE = 1029; + public static final int CONTACTS_ENTERPRISE = 1030; public static final int RAW_CONTACTS = 2002; public static final int RAW_CONTACTS_ID = 2003; @@ -1206,6 +1223,7 @@ public class ContactsProvider2 extends AbstractContactsProvider matcher.addURI(ContactsContract.AUTHORITY, "contacts/frequent", CONTACTS_FREQUENT); matcher.addURI(ContactsContract.AUTHORITY, "contacts/delete_usage", CONTACTS_DELETE_USAGE); + matcher.addURI(ContactsContract.AUTHORITY, "contacts/enterprise", CONTACTS_ENTERPRISE); matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter_enterprise", CONTACTS_FILTER_ENTERPRISE); matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter_enterprise/*", @@ -1355,6 +1373,7 @@ public class ContactsProvider2 extends AbstractContactsProvider String authority; String accountName; String accountType; + String packageName; } /** @@ -1368,6 +1387,18 @@ public class ContactsProvider2 extends AbstractContactsProvider long groupId; } + @VisibleForTesting + protected static class LaunchableCloneAppsCacheEntry { + boolean doesAppHaveLaunchableActivity; + long lastUpdatedAt; + + public LaunchableCloneAppsCacheEntry(boolean doesAppHaveLaunchableActivity, + long lastUpdatedAt) { + this.doesAppHaveLaunchableActivity = doesAppHaveLaunchableActivity; + this.lastUpdatedAt = lastUpdatedAt; + } + } + /** * The thread-local holder of the active transaction. Shared between this and the profile * provider, to keep transactions on both databases synchronized. @@ -1431,6 +1462,18 @@ public class ContactsProvider2 extends AbstractContactsProvider private ArrayMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = new ArrayMap<>(); /** + * Cache to store info on whether a cloned app has a launchable activity. This will be used to + * provide it access to query cross-profile contacts. + * The key to this map is the uid of the cloned app. The entries in the cache are refreshed + * after {@link LAUNCHABLE_CLONE_APPS_CACHE_ENTRY_REFRESH_INTERVAL} to ensure the uid values + * stored in the cache aren't stale. + */ + @VisibleForTesting + @GuardedBy("mLaunchableCloneAppsCache") + protected final SparseArray<LaunchableCloneAppsCacheEntry> mLaunchableCloneAppsCache = + new SparseArray<>(); + + /** * Sub-provider for handling profile requests against the profile database. */ private ProfileProvider mProfileProvider; @@ -1481,6 +1524,9 @@ public class ContactsProvider2 extends AbstractContactsProvider private long mLastDanglingContactsCleanup = 0; + @GuardedBy("mLaunchableCloneAppsCache") + private long mLastLaunchableCloneAppsCacheCleanup = 0; + private FastScrollingIndexCache mFastScrollingIndexCache; // Stats about FastScrollingIndex. @@ -1493,6 +1539,8 @@ public class ContactsProvider2 extends AbstractContactsProvider private Set<PhoneAccountHandle> mMigratedPhoneAccountHandles; + private AppCloningDeviceConfigHelper mAppCloningDeviceConfigHelper; + /** * Subscription change will trigger ACTION_PHONE_ACCOUNT_REGISTERED that broadcasts new * PhoneAccountHandle that is created based on the new subscription. This receiver is used @@ -1568,6 +1616,7 @@ public class ContactsProvider2 extends AbstractContactsProvider mFastScrollingIndexCache = FastScrollingIndexCache.getInstance(getContext()); mSubscriptionManager = getContext().getSystemService(SubscriptionManager.class); + mAppCloningDeviceConfigHelper = AppCloningDeviceConfigHelper.getInstance(getContext()); mContactsHelper = getDatabaseHelper(); mDbHelper.set(mContactsHelper); @@ -1579,7 +1628,7 @@ public class ContactsProvider2 extends AbstractContactsProvider if (mContactsHelper.getPhoneAccountHandleMigrationUtils() .isPhoneAccountMigrationPending()) { - IntentFilter filter = new IntentFilter(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); + IntentFilter filter = new IntentFilter(TelecomManager.ACTION_PHONE_ACCOUNT_REGISTERED); getContext().registerReceiver(mBroadcastReceiver, filter); } @@ -1720,6 +1769,13 @@ public class ContactsProvider2 extends AbstractContactsProvider return new PhotoPriorityResolver(context); } + @VisibleForTesting + protected SparseArray<LaunchableCloneAppsCacheEntry> getLaunchableCloneAppsCacheForTesting() { + synchronized (mLaunchableCloneAppsCache) { + return mLaunchableCloneAppsCache; + } + } + protected void scheduleBackgroundTask(int task) { scheduleBackgroundTask(task, null); } @@ -2138,6 +2194,20 @@ public class ContactsProvider2 extends AbstractContactsProvider } /** + * Returns whether contacts sharing is enabled allowing the clone contacts provider to use the + * parent contacts providers contacts data to serve its requests. The method returns true if + * the device supports clone profile contacts sharing and the feature flag for the same is + * turned on. + * + * @return true/false if contact sharing is enabled/disabled + */ + @VisibleForTesting + protected boolean isContactSharingEnabledForCloneProfile() { + return getContext().getResources().getBoolean(R.bool.config_enableAppCloningBuildingBlocks) + && mAppCloningDeviceConfigHelper.getEnableAppCloningBuildingBlocks(); + } + + /** * Maximum dimension (height or width) of photo thumbnails. */ public int getMaxThumbnailDim() { @@ -2297,7 +2367,8 @@ public class ContactsProvider2 extends AbstractContactsProvider .setUriType(sUriMatcher.match(uri)) .setCallerIsSyncAdapter(readBooleanQueryParameter( uri, ContactsContract.CALLER_IS_SYNCADAPTER, false)) - .setStartNanos(SystemClock.elapsedRealtimeNanos()); + .setStartNanos(SystemClock.elapsedRealtimeNanos()) + .setUid(Binder.getCallingUid()); Uri resultUri = null; try { @@ -2305,6 +2376,12 @@ public class ContactsProvider2 extends AbstractContactsProvider mContactsHelper.validateContentValues(getCallingPackage(), values); + if (!areContactWritesEnabled()) { + // Returning fake uri since the insert was rejected + Log.w(TAG, "Blocked insert with uri [" + uri + "]. Contact writes not enabled " + + "for the user"); + return rejectInsert(uri, values); + } if (mapsToProfileDbWithInsertedValues(uri, values)) { switchToProfileMode(); resultUri = mProfileProvider.insert(uri, values); @@ -2330,7 +2407,8 @@ public class ContactsProvider2 extends AbstractContactsProvider .setUriType(sUriMatcher.match(uri)) .setCallerIsSyncAdapter(readBooleanQueryParameter( uri, ContactsContract.CALLER_IS_SYNCADAPTER, false)) - .setStartNanos(SystemClock.elapsedRealtimeNanos()); + .setStartNanos(SystemClock.elapsedRealtimeNanos()) + .setUid(Binder.getCallingUid()); int updates = 0; try { @@ -2339,6 +2417,12 @@ public class ContactsProvider2 extends AbstractContactsProvider mContactsHelper.validateContentValues(getCallingPackage(), values); mContactsHelper.validateSql(getCallingPackage(), selection); + if (!areContactWritesEnabled()) { + // Returning 0, no rows were updated as writes are disabled + Log.w(TAG, "Blocked update with uri [" + uri + "]. Contact writes not enabled " + + "for the user"); + return 0; + } if (mapsToProfileDb(uri)) { switchToProfileMode(); updates = mProfileProvider.update(uri, values, selection, selectionArgs); @@ -2362,7 +2446,8 @@ public class ContactsProvider2 extends AbstractContactsProvider .setUriType(sUriMatcher.match(uri)) .setCallerIsSyncAdapter(readBooleanQueryParameter( uri, ContactsContract.CALLER_IS_SYNCADAPTER, false)) - .setStartNanos(SystemClock.elapsedRealtimeNanos()); + .setStartNanos(SystemClock.elapsedRealtimeNanos()) + .setUid(Binder.getCallingUid()); int deletes = 0; try { @@ -2370,6 +2455,12 @@ public class ContactsProvider2 extends AbstractContactsProvider mContactsHelper.validateSql(getCallingPackage(), selection); + if (!areContactWritesEnabled()) { + // Returning 0, no rows were deleted as writes are disabled + Log.w(TAG, "Blocked delete with uri [" + uri + "]. Contact writes not enabled " + + "for the user"); + return 0; + } if (mapsToProfileDb(uri)) { switchToProfileMode(); deletes = mProfileProvider.delete(uri, selection, selectionArgs); @@ -2386,9 +2477,25 @@ public class ContactsProvider2 extends AbstractContactsProvider } } + private void notifySimAccountsChanged() { + // This allows us to discard older broadcasts still waiting to be delivered. + final Bundle options = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE) + .toBundle(); + + getContext().sendBroadcast(new Intent(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED), null, + options); + } + @Override public Bundle call(String method, String arg, Bundle extras) { waitForAccess(mReadAccessLatch); + if (!areContactWritesEnabled()) { + // Returning EMPTY Bundle since the call was rejected and no rows were affected + Log.w(TAG, "Blocked call to method [" + method + "], not enabled for the user"); + return Bundle.EMPTY; + } switchToContactMode(); if (Authorization.AUTHORIZATION_METHOD.equals(method)) { Uri uri = extras.getParcelable(Authorization.KEY_URI_TO_AUTHORIZE); @@ -2438,7 +2545,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } finally { db.endTransaction(); } - getContext().sendBroadcast(new Intent(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED)); + notifySimAccountsChanged(); return response; } else if (SimContacts.REMOVE_SIM_ACCOUNT_METHOD.equals(method)) { ContactsPermissions.enforceCallingOrSelfPermission(getContext(), @@ -2458,7 +2565,7 @@ public class ContactsProvider2 extends AbstractContactsProvider } finally { db.endTransaction(); } - getContext().sendBroadcast(new Intent(SimContacts.ACTION_SIM_ACCOUNTS_CHANGED)); + notifySimAccountsChanged(); return response; } else if (SimContacts.QUERY_SIM_ACCOUNTS_METHOD.equals(method)) { ContactsPermissions.enforceCallingOrSelfPermission(getContext(), READ_PERMISSION); @@ -2607,6 +2714,12 @@ public class ContactsProvider2 extends AbstractContactsProvider @Override public int bulkInsert(Uri uri, ContentValues[] values) { waitForAccess(mWriteAccessLatch); + if (!areContactWritesEnabled()) { + // Returning 0, no rows were affected since writes are disabled + Log.w(TAG, "Blocked bulkInsert with uri [" + uri + "]. Contact writes not enabled " + + "for the user"); + return 0; + } return super.bulkInsert(uri, values); } @@ -5552,7 +5665,8 @@ public class ContactsProvider2 extends AbstractContactsProvider .setUriType(sUriMatcher.match(uri)) .setCallerIsSyncAdapter(readBooleanQueryParameter( uri, ContactsContract.CALLER_IS_SYNCADAPTER, false)) - .setStartNanos(SystemClock.elapsedRealtimeNanos()); + .setStartNanos(SystemClock.elapsedRealtimeNanos()) + .setUid(Binder.getCallingUid()); Cursor cursor = null; try { @@ -5590,10 +5704,15 @@ 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; } + if (shouldRedirectQueryToParentProvider()) { + return queryParentProfileContactsProvider(uri, projection, selection, selectionArgs, + sortOrder, cancellationSignal); + } + // Query the profile DB if appropriate. if (mapsToProfileDb(uri)) { switchToProfileMode(); @@ -5613,7 +5732,68 @@ public class ContactsProvider2 extends AbstractContactsProvider } } - private boolean queryAllowedByEnterprisePolicy(Uri uri) { + /** + * Check if the query should be redirected to the parent profile's contacts provider. + */ + private boolean shouldRedirectQueryToParentProvider() { + return isContactSharingEnabledForCloneProfile() && + UserUtils.shouldUseParentsContacts(getContext()) && + isAppAllowedToUseParentUsersContacts(getCallingPackage()); + } + + /** + * Check if the app with the given package name is allowed to use parent user's contacts to + * serve the contacts read queries. + */ + @VisibleForTesting + protected boolean isAppAllowedToUseParentUsersContacts(@Nullable String packageName) { + final int callingUid = Binder.getCallingUid(); + final UserHandle user = Binder.getCallingUserHandle(); + + synchronized (mLaunchableCloneAppsCache) { + maybeCleanupLaunchableCloneAppsCacheLocked(); + + final long now = System.currentTimeMillis(); + LaunchableCloneAppsCacheEntry cacheEntry = mLaunchableCloneAppsCache.get(callingUid); + if (cacheEntry == null || ((now - cacheEntry.lastUpdatedAt) + >= LAUNCHABLE_CLONE_APPS_CACHE_ENTRY_REFRESH_INTERVAL)) { + boolean result = doesPackageHaveALauncherActivity(packageName, user); + mLaunchableCloneAppsCache.put(callingUid, + new LaunchableCloneAppsCacheEntry(result, now)); + } + return mLaunchableCloneAppsCache.get(callingUid).doesAppHaveLaunchableActivity; + } + } + + /** + * Clean up the launchable clone apps cache to ensure it doesn't have any stale entries taking + * up additional space. The frequency of the cleanup is governed by {@link + * LAUNCHABLE_CLONE_APPS_CACHE_CLEANUP_LIMIT}. + */ + @GuardedBy("mLaunchableCloneAppsCache") + private void maybeCleanupLaunchableCloneAppsCacheLocked() { + long now = System.currentTimeMillis(); + if (now - mLastLaunchableCloneAppsCacheCleanup + >= LAUNCHABLE_CLONE_APPS_CACHE_CLEANUP_LIMIT) { + mLaunchableCloneAppsCache.clear(); + mLastLaunchableCloneAppsCacheCleanup = now; + } + } + + @VisibleForTesting + @SuppressLint("AndroidFrameworkRequiresPermission") + protected boolean doesPackageHaveALauncherActivity(String packageName, UserHandle user) { + Intent launcherCategoryIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setPackage(packageName); + final PackageManager pm = getContext().getPackageManager(); + return !pm.queryIntentActivitiesAsUser(launcherCategoryIntent, + PackageManager.ResolveInfoFlags.of(PackageManager.GET_ACTIVITIES), + user) + .isEmpty(); + } + + private boolean isCrossUserQueryAllowed(Uri uri) { if (isCallerFromSameUser()) { // Caller is on the same user; query allowed. return true; @@ -5621,14 +5801,22 @@ 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. + + if (isContactSharingEnabledForCloneProfile() && + 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); + return mEnterprisePolicyGuard.isCrossProfileAllowed(uri, getRealCallerPackageName(uri)); } return true; } @@ -5638,6 +5826,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() { @@ -5655,7 +5859,19 @@ public class ContactsProvider2 extends AbstractContactsProvider || context.checkCallingPermission(INTERACT_ACROSS_USERS) == PERMISSION_GRANTED; } - private Cursor queryDirectoryIfNecessary(Uri uri, String[] projection, String selection, + /** + * Returns true if the contact writes are enabled for the current instance of ContactsProvider. + * The ContactsProvider instance running in the clone profile should block inserts, updates + * and deletes and hence should return false. + */ + @VisibleForTesting + protected boolean areContactWritesEnabled() { + return !isContactSharingEnabledForCloneProfile() || + !UserUtils.shouldUseParentsContacts(getContext()); + } + + @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 = @@ -5703,14 +5919,14 @@ public class ContactsProvider2 extends AbstractContactsProvider final String passedPackage = queryUri.getQueryParameter( Directory.CALLER_PACKAGE_PARAM_KEY); if (TextUtils.isEmpty(passedPackage)) { - Log.wtfStack(TAG, - "Cross-profile query with no " + Directory.CALLER_PACKAGE_PARAM_KEY); - return "UNKNOWN"; + throw new IllegalArgumentException( + "Cross-profile query with no " + Directory.CALLER_PACKAGE_PARAM_KEY + + " param. Uri: " + queryUri); } return passedPackage; } else { // Otherwise, just return the real calling package name. - return getCallingPackage(); + return getCallingPackageUnchecked(); } } @@ -5749,8 +5965,20 @@ public class ContactsProvider2 extends AbstractContactsProvider if (projection == null) { projection = getDefaultProjection(uri); } + int galUid = -1; + try { + galUid = getContext().getPackageManager().getPackageUid(directoryInfo.packageName, + PackageManager.MATCH_ALL); + } catch (NameNotFoundException e) { + // Shouldn't happen, but just in case. + Log.w(TAG, "getPackageUid() failed", e); + } + final LogFields.Builder logBuilder = LogFields.Builder.aLogFields() + .setApiType(LogUtils.ApiType.GAL_CALL) + .setUriType(sUriMatcher.match(uri)) + .setUid(galUid); - Cursor cursor; + Cursor cursor = null; try { if (VERBOSE_LOGGING) { Log.v(TAG, "Making directory query: uri=" + directoryUri + @@ -5768,6 +5996,9 @@ public class ContactsProvider2 extends AbstractContactsProvider } catch (RuntimeException e) { Log.w(TAG, "Directory query failed", e); return null; + } finally { + LogUtils.log( + logBuilder.setResultCount(cursor == null ? 0 : cursor.getCount()).build()); } if (cursor.getCount() > 0) { @@ -5804,11 +6035,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()) @@ -5821,6 +6048,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. @@ -5862,7 +6152,8 @@ public class ContactsProvider2 extends AbstractContactsProvider Directory._ID, Directory.DIRECTORY_AUTHORITY, Directory.ACCOUNT_NAME, - Directory.ACCOUNT_TYPE + Directory.ACCOUNT_TYPE, + Directory.PACKAGE_NAME }; public static final int DIRECTORY_ID = 0; @@ -5888,6 +6179,8 @@ public class ContactsProvider2 extends AbstractContactsProvider info.authority = cursor.getString(DirectoryQuery.AUTHORITY); info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); + info.packageName = + cursor.getString(cursor.getColumnIndex(Directory.PACKAGE_NAME)); mDirectoryCache.put(id, info); } } finally { @@ -6341,8 +6634,6 @@ public class ContactsProvider2 extends AbstractContactsProvider new Object[] {getMaxDisplayPhotoDim(), getMaxThumbnailDim()}); } case PHONES_ENTERPRISE: { - ContactsPermissions.enforceCallingOrSelfPermission(getContext(), - INTERACT_ACROSS_USERS); return queryMergedDataPhones(uri, projection, selection, selectionArgs, sortOrder, cancellationSignal); } @@ -6510,6 +6801,9 @@ public class ContactsProvider2 extends AbstractContactsProvider } break; } + case CONTACTS_ENTERPRISE: + return queryMergedContacts(projection, selection, selectionArgs, sortOrder, + cancellationSignal); case PHONES_FILTER_ENTERPRISE: case CALLABLES_FILTER_ENTERPRISE: case EMAILS_FILTER_ENTERPRISE: @@ -7273,8 +7567,7 @@ public class ContactsProvider2 extends AbstractContactsProvider final Cursor[] cursorArray = new Cursor[] { primaryCursor, rewriteCorpDirectories(corpCursor) }; - final MergeCursor mergeCursor = new MergeCursor(cursorArray); - return mergeCursor; + return new MergeCursor(cursorArray); } catch (Throwable th) { if (primaryCursor != null) { primaryCursor.close(); @@ -7288,6 +7581,35 @@ public class ContactsProvider2 extends AbstractContactsProvider } /** + * Handles {@link Contacts#ENTERPRISE_CONTENT_URI}. + */ + private Cursor queryMergedContacts(String[] projection, String selection, + String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) { + final Uri localUri = Contacts.CONTENT_URI; + final Cursor primaryCursor = queryLocal(localUri, projection, selection, selectionArgs, + sortOrder, Directory.DEFAULT, cancellationSignal); + try { + final int managedUserId = UserUtils.getCorpUserId(getContext()); + if (managedUserId < 0) { + // No managed profile or policy not allowed + return primaryCursor; + } + final Cursor managedCursor = queryCorpContacts(localUri, projection, selection, + selectionArgs, sortOrder, new String[] {Contacts._ID}, + Directory.ENTERPRISE_DEFAULT, cancellationSignal); + final Cursor[] cursorArray = new Cursor[] { + primaryCursor, managedCursor + }; + return new MergeCursor(cursorArray); + } catch (Throwable th) { + if (primaryCursor != null) { + primaryCursor.close(); + } + throw th; + } + } + + /** * Handles {@link Phone#ENTERPRISE_CONTENT_URI}. */ private Cursor queryMergedDataPhones(Uri uri, String[] projection, String selection, @@ -7310,8 +7632,6 @@ public class ContactsProvider2 extends AbstractContactsProvider final Cursor primaryCursor = queryLocal(localUri, projection, selection, selectionArgs, sortOrder, directoryId, null); try { - // PHONES_ENTERPRISE should not be guarded by EnterprisePolicyGuard as Bluetooth app is - // responsible to guard it. final int corpUserId = UserUtils.getCorpUserId(getContext()); if (corpUserId < 0) { // No Corp user or policy not allowed @@ -7321,21 +7641,10 @@ public class ContactsProvider2 extends AbstractContactsProvider final Cursor managedCursor = queryCorpContacts(localUri, projection, selection, selectionArgs, sortOrder, new String[] {RawContacts.CONTACT_ID}, null, cancellationSignal); - if (managedCursor == null) { - // No corp results. Just return the local result. - return primaryCursor; - } final Cursor[] cursorArray = new Cursor[] { primaryCursor, managedCursor }; - // Sort order is not supported yet, will be fixed in M when we have - // merged provider - // MergeCursor will copy all the contacts from two cursors, which may - // cause OOM if there's a lot of contacts. But it's only used by - // Bluetooth, and Bluetooth will loop through the Cursor and put all - // content in ArrayList anyway, so we ignore OOM issue here for now - final MergeCursor mergeCursor = new MergeCursor(cursorArray); - return mergeCursor; + return new MergeCursor(cursorArray); } catch (Throwable th) { if (primaryCursor != null) { primaryCursor.close(); @@ -8702,13 +9011,25 @@ public class ContactsProvider2 extends AbstractContactsProvider public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { boolean success = false; try { + if (!mode.equals("r") && !areContactWritesEnabled()) { + Log.w(TAG, "Blocked openAssetFile with uri [" + uri + "]. Contact writes not " + + "enabled for the user"); + return null; + } 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 + if (mode.equals("r") && shouldRedirectQueryToParentProvider()) { + return openAssetFileThroughParentProvider(uri, mode); + } + final AssetFileDescriptor ret; if (mapsToProfileDb(uri)) { switchToProfileMode(); @@ -9022,6 +9343,8 @@ public class ContactsProvider2 extends AbstractContactsProvider builder.encodedPath(uri.getEncodedPath()); builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId - Directory.ENTERPRISE_DIRECTORY_ID_BASE)); + builder.appendQueryParameter(Directory.CALLER_PACKAGE_PARAM_KEY, + getRealCallerPackageName(uri)); addQueryParametersFromUri(builder, uri, MODIFIED_KEY_SET_FOR_ENTERPRISE_FILTER); // If work profile is not available, it will throw FileNotFoundException @@ -9075,12 +9398,14 @@ public class ContactsProvider2 extends AbstractContactsProvider throw new FileNotFoundException(uri.toString()); } // Convert the URI into: - // content://USER@com.android.contacts/contacts_corp/ID/{photo,display_photo} + // content://USER@com.android.contacts/contacts/ID/{photo,display_photo} // If work profile is not available, it will throw FileNotFoundException final Uri corpUri = maybeAddUserId( ContentUris.appendId(Contacts.CONTENT_URI.buildUpon(), contactId) .appendPath(displayPhoto ? Contacts.Photo.DISPLAY_PHOTO : Contacts.Photo.CONTENT_DIRECTORY) + .appendQueryParameter(Directory.CALLER_PACKAGE_PARAM_KEY, + getRealCallerPackageName(uri)) .build(), corpUserId); // TODO Make sure it doesn't leak any FDs. diff --git a/src/com/android/providers/contacts/DataRowHandlerForNickname.java b/src/com/android/providers/contacts/DataRowHandlerForNickname.java index 03b96a3a..2806cf34 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForNickname.java +++ b/src/com/android/providers/contacts/DataRowHandlerForNickname.java @@ -35,9 +35,14 @@ public class DataRowHandlerForNickname extends DataRowHandlerForCommonDataKind { Nickname.LABEL); } + private void applySimpleFieldMaxSize(ContentValues cv) { + applySimpleFieldMaxSize(cv, Nickname.NAME); + } + @Override public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values) { + applySimpleFieldMaxSize(values); String nickname = values.getAsString(Nickname.NAME); long dataId = super.insert(db, txContext, rawContactId, values); @@ -53,6 +58,7 @@ public class DataRowHandlerForNickname extends DataRowHandlerForCommonDataKind { @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { + applySimpleFieldMaxSize(values); long dataId = c.getLong(DataUpdateQuery._ID); long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); diff --git a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java index 66a3b1bd..74f22595 100644 --- a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java +++ b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java @@ -22,6 +22,7 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.Data; + import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.android.providers.contacts.SearchIndexManager.IndexBuilder; import com.android.providers.contacts.aggregation.AbstractContactAggregator; @@ -37,9 +38,15 @@ public class DataRowHandlerForOrganization extends DataRowHandlerForCommonDataKi Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL); } + private void applySimpleFieldMaxSize(ContentValues cv) { + applySimpleFieldMaxSize(cv, Organization.COMPANY); + applySimpleFieldMaxSize(cv, Organization.TITLE); + } + @Override public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values) { + applySimpleFieldMaxSize(values); String company = values.getAsString(Organization.COMPANY); String title = values.getAsString(Organization.TITLE); @@ -52,6 +59,7 @@ public class DataRowHandlerForOrganization extends DataRowHandlerForCommonDataKi @Override public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter) { + applySimpleFieldMaxSize(values); if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) { return false; } diff --git a/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuard.java b/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuard.java index 46841050..12a03053 100644 --- a/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuard.java +++ b/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuard.java @@ -19,8 +19,10 @@ package com.android.providers.contacts.enterprise; import android.annotation.NonNull; import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.UserHandle; +import android.os.UserManager; import android.provider.ContactsContract; import android.provider.ContactsContract.Directory; import android.provider.Settings; @@ -29,8 +31,11 @@ import android.util.Log; import com.android.providers.contacts.ContactsProvider2; import com.android.providers.contacts.ProfileAwareUriMatcher; import com.android.providers.contacts.util.UserUtils; + import com.google.common.annotations.VisibleForTesting; +import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.provider.Settings.Secure.MANAGED_PROFILE_CONTACT_REMOTE_SEARCH; /** @@ -44,22 +49,25 @@ public class EnterprisePolicyGuard { final private Context mContext; final private DevicePolicyManager mDpm; + final private PackageManager mPm; public EnterprisePolicyGuard(Context context) { mContext = context; mDpm = context.getSystemService(DevicePolicyManager.class); + mPm = context.getPackageManager(); } /** * Check if cross profile query is allowed for the given uri * * @param uri Uri that we want to check. + * @param callingPackage Name of the client package that called CP2 in the other profile * @return True if cross profile query is allowed for this uri */ - public boolean isCrossProfileAllowed(@NonNull Uri uri) { + public boolean isCrossProfileAllowed(@NonNull Uri uri, @NonNull String callingPackage) { final int uriCode = sUriMatcher.match(uri); final UserHandle currentHandle = new UserHandle(UserUtils.getCurrentUserHandle(mContext)); - if (uriCode == -1 || currentHandle == null) { + if (uriCode == -1) { return false; } @@ -67,11 +75,14 @@ public class EnterprisePolicyGuard { return true; } - final boolean isCallerIdEnabled = !mDpm.getCrossProfileCallerIdDisabled(currentHandle); + final boolean isCallerIdEnabled = + mDpm.hasManagedProfileCallerIdAccess(currentHandle, callingPackage); final boolean isContactsSearchPolicyEnabled = - !mDpm.getCrossProfileContactsSearchDisabled(currentHandle); + mDpm.hasManagedProfileContactsAccess(currentHandle, callingPackage); final boolean isBluetoothContactSharingEnabled = !mDpm.getBluetoothContactSharingDisabled(currentHandle); + final boolean isManagedProfileEnabled = !UserUtils.getUserManager(mContext) + .isQuietModeEnabled(new UserHandle(UserUtils.getCorpUserId(mContext))); final boolean isContactRemoteSearchUserEnabled = isContactRemoteSearchUserSettingEnabled(); final String directory = uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); @@ -81,24 +92,34 @@ public class EnterprisePolicyGuard { Log.v(TAG, "isContactsSearchPolicyEnabled: " + isContactsSearchPolicyEnabled); Log.v(TAG, "isBluetoothContactSharingEnabled: " + isBluetoothContactSharingEnabled); Log.v(TAG, "isContactRemoteSearchUserEnabled: " + isContactRemoteSearchUserEnabled); + Log.v(TAG, "isManagedProfileEnabled: " + isManagedProfileEnabled); } // If it is a remote directory, it is allowed only when // (i) The uri supports directory // (ii) User enables it in settings + // (iii) The managed profile is enabled if (directory != null) { final long directoryId = Long.parseLong(directory); if (Directory.isRemoteDirectoryId(directoryId) && !(isCrossProfileDirectorySupported(uri) - && isContactRemoteSearchUserEnabled)) { + && isContactRemoteSearchUserEnabled + && isManagedProfileEnabled)) { return false; } } + final boolean isAllowedByCallerIdPolicy = isCallerIdGuarded(uriCode) && isCallerIdEnabled; + final boolean isAllowedByContactSearchPolicy = + isContactsSearchGuarded(uriCode) && isContactsSearchPolicyEnabled; + final boolean isAllowedByBluetoothSharingPolicy = + isBluetoothContactSharing(uriCode) && isBluetoothContactSharingEnabled + // Only allow apps with INTERACT_ACROSS_USERS to access the bluetooth APIs + && mPm.checkPermission(INTERACT_ACROSS_USERS, callingPackage) + == PERMISSION_GRANTED; // If either guard policy allows access, return true. - return (isCallerIdGuarded(uriCode) && isCallerIdEnabled) - || (isContactsSearchGuarded(uriCode) && isContactsSearchPolicyEnabled) - || (isBluetoothContactSharing(uriCode) && isBluetoothContactSharingEnabled); + return isAllowedByCallerIdPolicy || isAllowedByContactSearchPolicy + || isAllowedByBluetoothSharingPolicy; } private boolean isUriWhitelisted(int uriCode) { @@ -148,7 +169,9 @@ public class EnterprisePolicyGuard { switch (uriCode) { case ContactsProvider2.PHONE_LOOKUP_ENTERPRISE: case ContactsProvider2.EMAILS_LOOKUP_ENTERPRISE: + case ContactsProvider2.CONTACTS_ENTERPRISE: case ContactsProvider2.CONTACTS_FILTER_ENTERPRISE: + case ContactsProvider2.PHONES_ENTERPRISE: case ContactsProvider2.PHONES_FILTER_ENTERPRISE: case ContactsProvider2.CALLABLES_FILTER_ENTERPRISE: case ContactsProvider2.EMAILS_FILTER_ENTERPRISE: @@ -180,8 +203,10 @@ public class EnterprisePolicyGuard { switch(uriCode) { case ContactsProvider2.DIRECTORIES: case ContactsProvider2.DIRECTORIES_ID: + case ContactsProvider2.CONTACTS: case ContactsProvider2.CONTACTS_FILTER: case ContactsProvider2.CALLABLES_FILTER: + case ContactsProvider2.PHONES: case ContactsProvider2.PHONES_FILTER: case ContactsProvider2.EMAILS_FILTER: case ContactsProvider2.CONTACTS_ID_PHOTO: @@ -208,4 +233,5 @@ public class EnterprisePolicyGuard { mContext.getContentResolver(), MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0) == 1; } + } diff --git a/src/com/android/providers/contacts/util/LogFields.java b/src/com/android/providers/contacts/util/LogFields.java index fc05c847..1672d3db 100644 --- a/src/com/android/providers/contacts/util/LogFields.java +++ b/src/com/android/providers/contacts/util/LogFields.java @@ -37,6 +37,8 @@ public final class LogFields { private int resultCount; + private int uid; + public LogFields( int apiType, int uriType, int taskType, boolean callerIsSyncAdapter, long startNanos) { this.apiType = apiType; @@ -78,6 +80,10 @@ public final class LogFields { return resultCount; } + public int getUid() { + return uid; + } + public static final class Builder { private int apiType; private int uriType; @@ -88,6 +94,8 @@ public final class LogFields { private Uri resultUri; private int resultCount; + private int uid; + private Builder() { } @@ -135,12 +143,18 @@ public final class LogFields { return this; } + public Builder setUid(int uid) { + this.uid = uid; + return this; + } + public LogFields build() { LogFields logFields = new LogFields(apiType, uriType, taskType, callerIsSyncAdapter, startNanos); logFields.resultCount = this.resultCount; logFields.exception = this.exception; logFields.resultUri = this.resultUri; + logFields.uid = this.uid; return logFields; } } diff --git a/src/com/android/providers/contacts/util/LogUtils.java b/src/com/android/providers/contacts/util/LogUtils.java index 23e2b140..41409645 100644 --- a/src/com/android/providers/contacts/util/LogUtils.java +++ b/src/com/android/providers/contacts/util/LogUtils.java @@ -37,6 +37,8 @@ public class LogUtils { int INSERT = 2; int UPDATE = 3; int DELETE = 4; + int CALL = 5; + int GAL_CALL = 6; } // Keep in sync with ContactsProviderStatus#TaskType in @@ -54,7 +56,6 @@ public class LogUtils { private static final int STATSD_LOG_ATOM_ID = 301; - // The write methods must be called in the same order as the order of fields in the // atom (frameworks/proto_logging/stats/atoms.proto) definition. public static void log(LogFields logFields) { @@ -67,6 +68,8 @@ public class LogUtils { .writeInt(logFields.getResultCount()) .writeLong(getLatencyMicros(logFields.getStartNanos())) .writeInt(logFields.getTaskType()) + .writeInt(0) // Not used yet. + .writeInt(logFields.getUid()) .usePooledBuffer() .build()); } @@ -92,5 +95,3 @@ public class LogUtils { return (SystemClock.elapsedRealtimeNanos() - startNanos) / 1000; } } - - diff --git a/src/com/android/providers/contacts/util/UserUtils.java b/src/com/android/providers/contacts/util/UserUtils.java index 31ea41aa..effe8abe 100644 --- a/src/com/android/providers/contacts/util/UserUtils.java +++ b/src/com/android/providers/contacts/util/UserUtils.java @@ -17,9 +17,12 @@ package com.android.providers.contacts.util; import com.android.providers.contacts.ContactsProvider2; +import android.annotation.SuppressLint; 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; @@ -78,4 +81,50 @@ public final class UserUtils { final UserInfo ui = getCorpUserInfo(context); return ui == null ? -1 : ui.id; } + + @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); + return userManager.getProfileParent(context.getUserId()); + } } diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml index cdf0c7da..967614cc 100644 --- a/tests/AndroidTest.xml +++ b/tests/AndroidTest.xml @@ -14,7 +14,7 @@ limitations under the License. --> <configuration description="Runs Contacts Provider Tests."> - <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="test-file-name" value="ContactsProviderTests.apk" /> </target_preparer> diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java index 54984d29..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; @@ -1381,6 +1383,25 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase { assertEquals(timeStamp, time); } + /** + * Asserts the equality of two Uri objects, ignoring the order of the query parameters. + */ + protected static void assertUriEquals(Uri expected, Uri actual) { + assertEquals(expected.getScheme(), actual.getScheme()); + assertEquals(expected.getAuthority(), actual.getAuthority()); + assertEquals(expected.getPath(), actual.getPath()); + assertEquals(expected.getFragment(), actual.getFragment()); + Set<String> expectedParameterNames = expected.getQueryParameterNames(); + Set<String> actualParameterNames = actual.getQueryParameterNames(); + assertEquals(expectedParameterNames.size(), actualParameterNames.size()); + assertTrue(expectedParameterNames.containsAll(actualParameterNames)); + for (String parameterName : expectedParameterNames) { + assertEquals(expected.getQueryParameter(parameterName), + actual.getQueryParameter(parameterName)); + } + + } + protected void setTimeForTest(Long time) { Uri uri = Calls.CONTENT_URI.buildUpon() .appendQueryParameter(CallLogProvider.PARAM_KEY_QUERY_FOR_TESTING, "1") @@ -1400,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 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); + } +} diff --git a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java index fca00afc..a4165ce5 100644 --- a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java +++ b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java @@ -16,8 +16,6 @@ package com.android.providers.contacts; -import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY; - import android.accounts.Account; import android.content.ContentValues; import android.content.Context; @@ -38,10 +36,15 @@ import android.test.mock.MockContentProvider; import android.test.suitebuilder.annotation.MediumTest; import android.util.Log; +import androidx.test.platform.app.InstrumentationRegistry; + import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns; import com.google.android.collect.Lists; +import java.util.Arrays; +import java.util.Set; + /** * Unit tests for {@link ContactDirectoryManager}. Run the test like this: * @@ -634,8 +637,17 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test { return; } + try { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .executeShellCommand("am wait-for-broadcast-idle"); + Thread.sleep(1000); // wait for the system + } catch (Exception ignored) { } + // If installed, getDirectoryProviderPackages() should return it. - assertTrue(ContactDirectoryManager.getDirectoryProviderPackages(pm).contains(googleSync)); + Set<String> dirProviderPackages = ContactDirectoryManager.getDirectoryProviderPackages(pm); + assertTrue(googleSync + " package not found in the list of directory provider packages: " + + Arrays.toString(dirProviderPackages.toArray()), + dirProviderPackages.contains(googleSync)); } protected PackageInfo createProviderPackage(String packageName, String authority) { diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java index e3c606e2..0d7d9b3b 100644 --- a/tests/src/com/android/providers/contacts/ContactsActor.java +++ b/tests/src/com/android/providers/contacts/ContactsActor.java @@ -16,6 +16,10 @@ package com.android.providers.contacts; +import static android.content.pm.UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT; +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.Mockito.when; import android.accounts.Account; @@ -25,6 +29,7 @@ import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OnAccountsUpdateListener; import android.accounts.OperationCanceledException; +import android.annotation.NonNull; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentUris; @@ -37,6 +42,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.UserInfo; +import android.content.pm.UserProperties; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; @@ -170,6 +176,8 @@ public class ContactsActor { public static final UserInfo CORP_USER = createUserInfo("corp", 10, 0, UserInfo.FLAG_MANAGED_PROFILE); public static final UserInfo SECONDARY_USER = createUserInfo("2nd", 11, 11, 0); + public static final UserInfo CLONE_PROFILE_USER = createUserInfo("clone", 12, 0, + UserInfo.FLAG_PROFILE); /** "My" user. Set it to change the current user. */ public int myUser = DEFAULT_USER_ID; @@ -253,6 +261,18 @@ public class ContactsActor { public boolean isUserRunning(int userId) { return true; } + + @Override + public UserProperties getUserProperties(@NonNull UserHandle userHandle) { + if (CLONE_PROFILE_USER.getUserHandle().equals(userHandle)) { + return new UserProperties.Builder() + .setUseParentsContacts(true) + .setShowInLauncher(SHOW_IN_LAUNCHER_WITH_PARENT) + .setStartWithParent(true) + .build(); + } + return new UserProperties.Builder().build(); + } } private MockTelephonyManager mMockTelephonyManager; @@ -411,6 +431,15 @@ public class ContactsActor { } @Override + public UserHandle getUser() { + if (mockUserManager != null && + mockUserManager.getProcessUserId() == CLONE_PROFILE_USER.id) { + return CLONE_PROFILE_USER.getUserHandle(); + } + return PRIMARY_USER.getUserHandle(); + } + + @Override public void sendBroadcast(Intent intent, String receiverPermission) { // Ignore. } diff --git a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java index a9420dda..62f17eab 100644 --- a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java +++ b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java @@ -24,6 +24,7 @@ import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.os.Binder; +import android.os.UserHandle; import android.test.mock.MockPackageManager; import java.util.ArrayList; @@ -149,4 +150,15 @@ public class ContactsMockPackageManager extends MockPackageManager { } return ret; } + + @Override + public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, ResolveInfoFlags flags, + UserHandle user) { + return new ArrayList<>(); + } + + @Override + public int getPackageUid(String packageName, int flags) throws NameNotFoundException { + return 123; + } } diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java index 09ea19fc..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)); @@ -9908,24 +9843,4 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { } return false; } - - - /** - * Asserts the equality of two Uri objects, ignoring the order of the query parameters. - */ - public static void assertUriEquals(Uri expected, Uri actual) { - assertEquals(expected.getScheme(), actual.getScheme()); - assertEquals(expected.getAuthority(), actual.getAuthority()); - assertEquals(expected.getPath(), actual.getPath()); - assertEquals(expected.getFragment(), actual.getFragment()); - Set<String> expectedParameterNames = expected.getQueryParameterNames(); - Set<String> actualParameterNames = actual.getQueryParameterNames(); - assertEquals(expectedParameterNames.size(), actualParameterNames.size()); - assertTrue(expectedParameterNames.containsAll(actualParameterNames)); - for (String parameterName : expectedParameterNames) { - assertEquals(expected.getQueryParameter(parameterName), - actual.getQueryParameter(parameterName)); - } - - } } diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java index 848c2379..ca8cb669 100644 --- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java +++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java @@ -179,6 +179,11 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 { } @Override + protected boolean isContactSharingEnabledForCloneProfile() { + return true; + } + + @Override public boolean isWritableAccountWithDataSet(String accountType) { return !READ_ONLY_ACCOUNT_TYPE.equals(accountType); } diff --git a/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java b/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java index 2e5241fb..5aedd8d0 100644 --- a/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java +++ b/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java @@ -17,6 +17,7 @@ package com.android.providers.contacts.enterprise; import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.net.Uri; import android.os.UserHandle; @@ -31,6 +32,9 @@ import org.mockito.Matchers; import java.util.Arrays; import java.util.List; +import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -49,6 +53,7 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase { private static final String CONTACT_EMAIL = "david.green@android.com"; private static final String CONTACT_PHONE = "+1234567890"; private static final long DIRECTORY_ID = Directory.ENTERPRISE_DEFAULT; + private static final String CALLING_PACKAGE = "package"; private static final Uri URI_CONTACTS_ID_PHOTO = Uri.parse("content://com.android.contacts/contacts/" + CONTACT_ID + "/photo"); @@ -179,6 +184,32 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase { checkCrossProfile(guard, URI_CONTACTS_ID_PHOTO, false); checkCrossProfile(guard, URI_CONTACTS_ID_DISPLAY_PHOTO, false); checkCrossProfile(guard, URI_OTHER, false); + + // ManagedProfile is paused + context = getMockContext(true, true, false); + guard = new EnterprisePolicyGuardTestable(context, true); + checkCrossProfile(guard, appendRemoteDirectoryId(URI_PHONE_LOOKUP), false); + checkCrossProfile(guard, appendRemoteDirectoryId(URI_EMAILS_LOOKUP), false); + checkCrossProfile(guard, appendRemoteDirectoryId(URI_CONTACTS_FILTER), false); + checkCrossProfile(guard, appendRemoteDirectoryId(URI_PHONES_FILTER), false); + checkCrossProfile(guard, appendRemoteDirectoryId(URI_CALLABLES_FILTER), false); + checkCrossProfile(guard, appendRemoteDirectoryId(URI_EMAILS_FILTER), false); + checkCrossProfile(guard, URI_DIRECTORY_FILE, false); + + // Always allow uri with no directory support. + checkCrossProfile(guard, URI_DIRECTORIES, true); + checkCrossProfile(guard, URI_DIRECTORIES_ID, true); + checkCrossProfile(guard, URI_CONTACTS_ID_PHOTO, true); + checkCrossProfile(guard, URI_CONTACTS_ID_DISPLAY_PHOTO, true); + checkCrossProfile(guard, URI_OTHER, false); + + // Always allow uri with no remote directory id. + checkCrossProfile(guard, URI_PHONE_LOOKUP, true); + checkCrossProfile(guard, URI_EMAILS_LOOKUP, true); + checkCrossProfile(guard, URI_CONTACTS_FILTER, true); + checkCrossProfile(guard, URI_PHONES_FILTER, true); + checkCrossProfile(guard, URI_CALLABLES_FILTER, true); + checkCrossProfile(guard, URI_EMAILS_FILTER, true); } public void testCrossProfile_userSettingOff() { @@ -210,6 +241,7 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase { checkCrossProfile(guard, URI_EMAILS_FILTER, true); } + private static Uri appendRemoteDirectoryId(Uri uri) { return appendDirectoryId(uri, REMOTE_DIRECTORY_ID); } @@ -231,13 +263,13 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase { } } - private static void checkCrossProfile(EnterprisePolicyGuard guard, Uri uri, boolean expected) { + private void checkCrossProfile(EnterprisePolicyGuard guard, Uri uri, boolean expected) { if (expected) { assertTrue("Expected true but got false for uri: " + uri, - guard.isCrossProfileAllowed(uri)); + guard.isCrossProfileAllowed(uri, CALLING_PACKAGE)); } else { assertFalse("Expected false but got true for uri: " + uri, - guard.isCrossProfileAllowed(uri)); + guard.isCrossProfileAllowed(uri, CALLING_PACKAGE)); } } @@ -256,11 +288,16 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase { private Context getMockContext(boolean isCallerIdEnabled, boolean isContactsSearchEnabled) { + return getMockContext(isCallerIdEnabled, isContactsSearchEnabled, true); + } + + private Context getMockContext(boolean isCallerIdEnabled, boolean isContactsSearchEnabled, + boolean isManagedProfileEnabled) { DevicePolicyManager mockDpm = mock(DevicePolicyManager.class); - when(mockDpm.getCrossProfileCallerIdDisabled(Matchers.<UserHandle>any())) - .thenReturn(!isCallerIdEnabled); - when(mockDpm.getCrossProfileContactsSearchDisabled(Matchers.<UserHandle>any())) - .thenReturn(!isContactsSearchEnabled); + when(mockDpm.hasManagedProfileCallerIdAccess(Matchers.any(),Matchers.any())) + .thenReturn(isCallerIdEnabled); + when(mockDpm.hasManagedProfileContactsAccess(Matchers.any(),Matchers.any())) + .thenReturn(isContactsSearchEnabled); List<UserInfo> userInfos = MANAGED_USERINFO_LIST; UserManager mockUm = mock(UserManager.class); @@ -268,8 +305,14 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase { when(mockUm.getUsers()).thenReturn(userInfos); when(mockUm.getProfiles(Matchers.anyInt())).thenReturn(userInfos); when(mockUm.getProfileParent(WORK_USER_ID)).thenReturn(CURRENT_USER_INFO); + when(mockUm.isQuietModeEnabled(UserHandle.of(WORK_USER_ID))) + .thenReturn(!isManagedProfileEnabled); - Context mockContext = new TestMockContext(getContext(), mockDpm, mockUm); + PackageManager mockPm = mock(PackageManager.class); + when(mockPm.checkPermission(INTERACT_ACROSS_USERS, CALLING_PACKAGE)) + .thenReturn(PERMISSION_GRANTED); + + Context mockContext = new TestMockContext(getContext(), mockDpm, mockUm, mockPm); return mockContext; } @@ -278,11 +321,19 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase { private Context mRealContext; private DevicePolicyManager mDpm; private UserManager mUm; + private PackageManager mPm; - public TestMockContext(Context realContext, DevicePolicyManager dpm, UserManager um) { + public TestMockContext( + Context realContext, DevicePolicyManager dpm, UserManager um, PackageManager pm) { mRealContext = realContext; mDpm = dpm; mUm = um; + mPm = pm; + } + + @Override + public PackageManager getPackageManager() { + return mPm; } public Object getSystemService(String name) { diff --git a/tests/src/com/android/providers/contacts/util/UserUtilsTest.java b/tests/src/com/android/providers/contacts/util/UserUtilsTest.java index 93613cf5..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; @@ -77,5 +78,82 @@ public class UserUtilsTest extends FixedAndroidTestCase { um.myUser = MockUserManager.SECONDARY_USER.id; assertEquals(-1, UserUtils.getCorpUserId(c)); + + // Primary + clone + corp + um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.CLONE_PROFILE_USER, + MockUserManager.CORP_USER); + + um.myUser = MockUserManager.PRIMARY_USER.id; + assertEquals(MockUserManager.CORP_USER.id, UserUtils.getCorpUserId(c)); + + um.myUser = MockUserManager.CLONE_PROFILE_USER.id; + assertEquals(-1, UserUtils.getCorpUserId(c)); + + um.myUser = MockUserManager.CORP_USER.id; + assertEquals(-1, UserUtils.getCorpUserId(c)); + } + + public void testShouldUseParentsContacts() { + 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; + 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)); } } |