aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2023-10-05 15:42:48 -0700
committerXin Li <delphij@google.com>2023-10-05 15:42:48 -0700
commit0aa88f803ee6411849b7162fa3094d26bbb8742f (patch)
tree1b195ae79d25fbf7093326819453f7df242a52f3
parenta631f3aade06e25bd170c0a372f7617ec6ed2261 (diff)
parente8afe56b5db8d7b32bca56894218efc43b0368bc (diff)
downloadContactsProvider-0aa88f803ee6411849b7162fa3094d26bbb8742f.tar.gz
Merge Android 14
Bug: 298295554 Merged-In: I0a92ef586db9c5a383f70274839daca17fd74f6d Change-Id: Idb318d89eba83157b46846e29458d6e3b6e562d4
-rw-r--r--res/values-ne/strings.xml4
-rw-r--r--src/com/android/providers/contacts/CallLogDatabaseHelper.java11
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java415
-rw-r--r--src/com/android/providers/contacts/DataRowHandlerForNickname.java6
-rw-r--r--src/com/android/providers/contacts/DataRowHandlerForOrganization.java8
-rw-r--r--src/com/android/providers/contacts/enterprise/EnterprisePolicyGuard.java42
-rw-r--r--src/com/android/providers/contacts/util/LogFields.java14
-rw-r--r--src/com/android/providers/contacts/util/LogUtils.java7
-rw-r--r--src/com/android/providers/contacts/util/UserUtils.java49
-rw-r--r--tests/AndroidTest.xml2
-rw-r--r--tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java86
-rw-r--r--tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java504
-rw-r--r--tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java18
-rw-r--r--tests/src/com/android/providers/contacts/ContactsActor.java29
-rw-r--r--tests/src/com/android/providers/contacts/ContactsMockPackageManager.java12
-rw-r--r--tests/src/com/android/providers/contacts/ContactsProvider2Test.java85
-rw-r--r--tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java5
-rw-r--r--tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java69
-rw-r--r--tests/src/com/android/providers/contacts/util/UserUtilsTest.java78
19 files changed, 1286 insertions, 158 deletions
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index fd716daf..4fd36c1c 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -18,11 +18,11 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="sharedUserLabel" msgid="8024311725474286801">"एन्ड्रोइड कोर एपहरू"</string>
<string name="app_label" msgid="3389954322874982620">"सम्पर्कहरू भण्डारण"</string>
- <string name="provider_label" msgid="6012150850819899907">"सम्पर्कहरू"</string>
+ <string name="provider_label" msgid="6012150850819899907">"कन्ट्याक्टहरू"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"सम्पर्क अद्यावधिकका लागि अझै धेरै मेमोरी चाहिन्छ।"</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"सम्पर्कका लागि भणडारण अद्यावधिक गर्दै"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="2581831842693151968">"स्तरवृद्धि पूरा गर्न ट्याप गर्नुहोस्।"</string>
- <string name="default_directory" msgid="93961630309570294">"सम्पर्कहरू"</string>
+ <string name="default_directory" msgid="93961630309570294">"कन्ट्याक्टहरू"</string>
<string name="local_invisible_directory" msgid="705244318477396120">"अन्य"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"बाट भ्वाइसमेल "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"सम्पर्क डेटाबेस प्रतिलिप गर्नुहोस्"</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));
}
}