aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/providers/contacts/ContactsProvider2.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/contacts/ContactsProvider2.java')
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java415
1 files changed, 370 insertions, 45 deletions
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.