aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 01:09:46 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 01:09:46 +0000
commit5f0c493c7a2387c037f8f2a91cfd49a826222b53 (patch)
tree32871fc827edade0e211ebbcf7db5972cc7d0dcc
parentec4db5956bd82404c9ecc14ff500a5759d011c49 (diff)
parenta14e75b24a083007bb01ea72b1dcbc3f50be982e (diff)
downloadContactsProvider-android14-mainline-resolv-release.tar.gz
Snap for 10447354 from a14e75b24a083007bb01ea72b1dcbc3f50be982e to mainline-resolv-releaseaml_res_341410010aml_res_341311030aml_res_341110000aml_res_340912000android14-mainline-resolv-release
Change-Id: I10546e568ddbebc0579e18e48d8c85a10fa7151f
-rw-r--r--res/values-as/strings.xml2
-rw-r--r--res/values-en-rCA/strings.xml4
-rw-r--r--res/values-et/strings.xml2
-rw-r--r--res/values-ne/strings.xml4
-rw-r--r--src/com/android/providers/contacts/AbstractContactsProvider.java4
-rw-r--r--src/com/android/providers/contacts/CallLogDatabaseHelper.java93
-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/src/com/android/providers/contacts/BaseContactsProvider2Test.java86
-rw-r--r--tests/src/com/android/providers/contacts/CallLogMigrationTest.java45
-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.java10
-rw-r--r--tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java69
-rw-r--r--tests/src/com/android/providers/contacts/util/UserUtilsTest.java78
23 files changed, 1294 insertions, 292 deletions
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index dff8c453..7843081c 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core এপসমূহ"</string>
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core এপ্‌সমূহ"</string>
<string name="app_label" msgid="3389954322874982620">"সম্পৰ্কসমূহৰ ষ্ট’ৰেজ"</string>
<string name="provider_label" msgid="6012150850819899907">"সম্পর্কবোৰ"</string>
<string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"সম্পৰ্কসূচী আপগ্ৰেড কৰিবলৈ অধিক মেম\'ৰিৰ প্ৰয়োজন।"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index c62b699a..4b621199 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -19,7 +19,7 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
<string name="app_label" msgid="3389954322874982620">"Contacts Storage"</string>
<string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
- <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Contact upgrade needs more memory."</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Contacts upgrade needs more memory."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Upgrading storage for contacts"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="2581831842693151968">"Tap to complete the upgrade."</string>
<string name="default_directory" msgid="93961630309570294">"Contacts"</string>
@@ -29,7 +29,7 @@
<string name="debug_dump_database_message" msgid="406438635002392290">"You are about to 1) make a copy of your database which includes all contacts related information and all call log to the internal storage, and 2) email it. Remember to delete the copy as soon as you have successfully copied it off the device or the email is received."</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"Delete now"</string>
<string name="debug_dump_start_button" msgid="2837506913757600001">"Start"</string>
- <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Choose a programme to send your file"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Choose a program to send your file"</string>
<string name="debug_dump_email_subject" msgid="108188398416385976">"Contacts Db attached"</string>
<string name="debug_dump_email_body" msgid="4577749800871444318">"Attached is my contacts database with all my contacts information. Handle with care."</string>
</resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index d5dbf950..bcaedf7a 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -19,7 +19,7 @@
<string name="sharedUserLabel" msgid="8024311725474286801">"Androidi tuumrakendused"</string>
<string name="app_label" msgid="3389954322874982620">"Kontaktiruum"</string>
<string name="provider_label" msgid="6012150850819899907">"Kontaktid"</string>
- <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktisikute uuendamiseks on vaja rohkem mälu"</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktisikute uuendamiseks on vaja rohkem mäluruumi."</string>
<string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kontaktide salvestusruumi uuendamine"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="2581831842693151968">"Puudutage täiendamise lõpetamiseks."</string>
<string name="default_directory" msgid="93961630309570294">"Kontaktid"</string>
diff --git a/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/AbstractContactsProvider.java b/src/com/android/providers/contacts/AbstractContactsProvider.java
index e00bd862..38daa50c 100644
--- a/src/com/android/providers/contacts/AbstractContactsProvider.java
+++ b/src/com/android/providers/contacts/AbstractContactsProvider.java
@@ -228,7 +228,7 @@ public abstract class AbstractContactsProvider extends ContentProvider
if (++opCount >= BULK_INSERTS_PER_YIELD_POINT) {
opCount = 0;
try {
- yield(transaction);
+ this.yield(transaction);
} catch (RuntimeException re) {
transaction.markYieldFailed();
throw re;
@@ -274,7 +274,7 @@ public abstract class AbstractContactsProvider extends ContentProvider
}
opCount = 0;
try {
- if (yield(transaction)) {
+ if (this.yield(transaction)) {
ypCount++;
}
} catch (RuntimeException re) {
diff --git a/src/com/android/providers/contacts/CallLogDatabaseHelper.java b/src/com/android/providers/contacts/CallLogDatabaseHelper.java
index c5052d70..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;
@@ -203,7 +204,6 @@ public class CallLogDatabaseHelper {
VoicemailContract.Status.SOURCE_TYPE + " TEXT" +
");");
- migrateFromLegacyTables(db);
}
@Override
@@ -532,87 +532,6 @@ public class CallLogDatabaseHelper {
mPhoneAccountHandleMigrationUtils.migrateIccIdToSubId(db);
}
- /**
- * Perform the migration from the contacts2.db (of the latest version) to the current calllog/
- * voicemail status tables.
- */
- private void migrateFromLegacyTables(SQLiteDatabase calllog) {
- final SQLiteDatabase contacts = getContactsWritableDatabaseForMigration();
-
- if (contacts == null) {
- Log.w(TAG, "Contacts DB == null, skipping migration. (running tests?)");
- return;
- }
- if (DEBUG) {
- Log.d(TAG, "migrateFromLegacyTables");
- }
-
- if ("1".equals(PropertyUtils.getProperty(calllog, DbProperties.DATA_MIGRATED, ""))) {
- return;
- }
-
- Log.i(TAG, "Migrating from old tables...");
-
- contacts.beginTransaction();
- try {
- if (!tableExists(contacts, LegacyConstants.CALLS_LEGACY)
- || !tableExists(contacts, LegacyConstants.VOICEMAIL_STATUS_LEGACY)) {
- // This is fine on new devices. (or after a "clear data".)
- Log.i(TAG, "Source tables don't exist.");
- return;
- }
- calllog.beginTransaction();
- try {
-
- final ContentValues cv = new ContentValues();
-
- try (Cursor source = contacts.rawQuery(
- "SELECT * FROM " + LegacyConstants.CALLS_LEGACY, null)) {
- while (source.moveToNext()) {
- cv.clear();
-
- DatabaseUtils.cursorRowToContentValues(source, cv);
-
- calllog.insertOrThrow(Tables.CALLS, null, cv);
- }
- }
-
- try (Cursor source = contacts.rawQuery("SELECT * FROM " +
- LegacyConstants.VOICEMAIL_STATUS_LEGACY, null)) {
- while (source.moveToNext()) {
- cv.clear();
-
- DatabaseUtils.cursorRowToContentValues(source, cv);
-
- calllog.insertOrThrow(Tables.VOICEMAIL_STATUS, null, cv);
- }
- }
-
- contacts.execSQL("DROP TABLE " + LegacyConstants.CALLS_LEGACY + ";");
- contacts.execSQL("DROP TABLE " + LegacyConstants.VOICEMAIL_STATUS_LEGACY + ";");
-
- // Also copy the last sync time.
- PropertyUtils.setProperty(calllog, DbProperties.CALL_LOG_LAST_SYNCED,
- PropertyUtils.getProperty(contacts,
- LegacyConstants.CALL_LOG_LAST_SYNCED_LEGACY, null));
-
- Log.i(TAG, "Migration completed.");
-
- calllog.setTransactionSuccessful();
- } finally {
- calllog.endTransaction();
- }
-
- contacts.setTransactionSuccessful();
- } catch (RuntimeException e) {
- // We don't want to be stuck here, so we just swallow exceptions...
- Log.w(TAG, "Exception caught during migration", e);
- } finally {
- contacts.endTransaction();
- }
- PropertyUtils.setProperty(calllog, DbProperties.DATA_MIGRATED, "1");
- }
-
@VisibleForTesting
static boolean tableExists(SQLiteDatabase db, String table) {
return DatabaseUtils.longForQuery(db,
@@ -621,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/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/CallLogMigrationTest.java b/tests/src/com/android/providers/contacts/CallLogMigrationTest.java
index d1e80035..74e5d0a5 100644
--- a/tests/src/com/android/providers/contacts/CallLogMigrationTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogMigrationTest.java
@@ -93,51 +93,6 @@ public class CallLogMigrationTest extends FixedAndroidTestCase {
+ " = 1", null));
}
- public void testMigration() throws IOException {
- final File sourceDbFile = new File(getTestContext().getCacheDir(), "contacts2src.db");
- writeAssetFileToDisk("calllogmigration/contacts2.db", sourceDbFile);
-
- try (final SQLiteDatabase sourceDb = SQLiteDatabase.openDatabase(
- sourceDbFile.getAbsolutePath(), /* cursorFactory=*/ null,
- SQLiteDatabase.OPEN_READWRITE)) {
-
- // Make sure the source tables exist initially.
- assertTrue(CallLogDatabaseHelper.tableExists(sourceDb, "calls"));
- assertTrue(CallLogDatabaseHelper.tableExists(sourceDb, "voicemail_status"));
- // Create the calllog DB to perform the migration.
- final CallLogDatabaseHelperTestable dbh =
- new CallLogDatabaseHelperTestable(getTestContext(), sourceDb);
-
- final SQLiteDatabase db = dbh.getReadableDatabase();
-
- // Check the content:
- // Note what we worry here is basically insertion error due to additional constraints,
- // renames, etc. So here, we just check the number of rows and don't check the content.
- assertEquals(3, DatabaseUtils.longForQuery(db, "select count(*) from " +
- CallLogDatabaseHelper.Tables.CALLS, null));
-
- assertEquals(2, DatabaseUtils.longForQuery(db, "select count(*) from " +
- CallLogDatabaseHelper.Tables.VOICEMAIL_STATUS, null));
-
- assertEquals("123456",
- dbh.getProperty(CallLogDatabaseHelper.DbProperties.CALL_LOG_LAST_SYNCED, ""));
-
- // Test onCreate() step, check each entry with TelephonyComponent in the CALLS has
- // a new coloumn of Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING.
- assertEquals(3,
- DatabaseUtils.longForQuery(db, "select count(*) from "
- + CallLogDatabaseHelper.Tables.CALLS + " where "
- + Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING + " = 0", null));
-
- // Also, the source table should have been removed.
- assertFalse(CallLogDatabaseHelper.tableExists(sourceDb, "calls"));
- assertFalse(CallLogDatabaseHelper.tableExists(sourceDb, "voicemail_status"));
-
- assertEquals("1",
- dbh.getProperty(CallLogDatabaseHelper.DbProperties.DATA_MIGRATED, ""));
- }
- }
-
public static final class InMemoryCallLogProviderDbHelperV1 extends SQLiteOpenHelper {
public InMemoryCallLogProviderDbHelperV1(Context context, int databaseVersion) {
super(context,
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 c2ab74fc..ca8cb669 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -32,7 +32,8 @@ import java.util.Locale;
public class SynchronousContactsProvider2 extends ContactsProvider2 {
public static final String READ_ONLY_ACCOUNT_TYPE = "ro";
- private static Boolean sDataWiped = false;
+ private static final Object sDataWipedLock = new Object();
+ private static boolean sDataWiped = false;
private static ContactsDatabaseHelper sDbHelper;
private Account mAccount;
private boolean mNetworkNotified;
@@ -93,7 +94,7 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
@Override
public boolean onCreate() {
boolean created = super.onCreate();
- synchronized (sDataWiped) {
+ synchronized (sDataWipedLock) {
if (!sDataWiped) {
sDataWiped = true;
wipeData();
@@ -178,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));
}
}