aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--res/values-as/strings.xml2
-rw-r--r--res/values-my/strings.xml2
-rw-r--r--res/values-pt-rBR/strings.xml2
-rw-r--r--res/values-pt/strings.xml2
-rw-r--r--res/values-te/strings.xml4
-rw-r--r--src/com/android/providers/contacts/CallLogDatabaseHelper.java11
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java388
-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/LogUtils.java3
-rw-r--r--src/com/android/providers/contacts/util/UserUtils.java49
-rw-r--r--tests/AndroidTest.xml2
-rw-r--r--tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java86
-rw-r--r--tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java504
-rw-r--r--tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java36
-rw-r--r--tests/src/com/android/providers/contacts/ContactsActor.java29
-rw-r--r--tests/src/com/android/providers/contacts/ContactsMockPackageManager.java12
-rw-r--r--tests/src/com/android/providers/contacts/ContactsProvider2Test.java85
-rw-r--r--tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java5
-rw-r--r--tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java69
-rw-r--r--tests/src/com/android/providers/contacts/util/UserUtilsTest.java78
22 files changed, 1268 insertions, 157 deletions
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index c1f5b360..dff8c453 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -26,7 +26,7 @@
<string name="local_invisible_directory" msgid="705244318477396120">"অন্যান্য"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"ইয়াৰ পৰা অহা ভইচমেল "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"সম্পৰ্কসূচীৰ ডেটাবেছ প্ৰতিলিপি কৰক"</string>
- <string name="debug_dump_database_message" msgid="406438635002392290">"আপুনি এই কাৰ্যবোৰ কৰিবলৈ লৈছে ১) আটাইবোৰ সম্পৰ্ক সম্বন্ধীয় তথ্য আৰু কল লগ সন্নিবিষ্ট থকা আপোনাৰ ডেটাবেছক আভ্য়ন্তৰীণ ষ্ট’ৰেজলৈ প্ৰতিলিপি কৰা আৰু ২) ইয়াক ইমেইল কৰা। ডিভাইচৰ পৰা সফলতাৰে প্ৰতিলিপি কৰাৰ বা ইমেইল পোৱাৰ পিছত উক্ত প্ৰতিলিপি মচিবলৈ নাপাহৰিব।"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"আপুনি এই কাৰ্যবোৰ কৰিবলৈ লৈছে ১) আটাইবোৰ সম্পৰ্ক সম্বন্ধীয় তথ্য আৰু কল লগ সন্নিবিষ্ট থকা আপোনাৰ ডেটাবেছক আভ্য়ন্তৰীণ ষ্ট’ৰেজলৈ প্ৰতিলিপি কৰা আৰু ২) ইয়াক ইমেইল কৰা। ডিভাইচৰ পৰা সফলতাৰে প্ৰতিলিপি কৰাৰ বা ইমেইল পোৱাৰ পাছত উক্ত প্ৰতিলিপি মচিবলৈ নাপাহৰিব।"</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"এতিয়াই মচক"</string>
<string name="debug_dump_start_button" msgid="2837506913757600001">"আৰম্ভ কৰক"</string>
<string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"আপোনাৰ ফাইল পঠাবলৈ কোনো প্ৰ\'গ্ৰাম বাছনি কৰক"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 02cb4d54..1efa6e93 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -26,7 +26,7 @@
<string name="local_invisible_directory" msgid="705244318477396120">"တစ်ခြား"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"မှ အသံစာ "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"လိပ်စာဒေတာဘေ့စ်ကို ကူးရန်"</string>
- <string name="debug_dump_database_message" msgid="406438635002392290">"သင့်အနေဖြင့် ၁) လိပ်စာအားလုံးနဲ့ဆိုင်သော အချက်အလက်များ၊ ဖုန်းခေါ်မှု မှတ်တမ်းများ ပါဝင်သော ဒေတာဘေ့စ်ကို စက်အတွင်းသိမ်းဆည်းသော ကော်ပီတစ်ခု ပြုလုပ်ပြီး ၂) အီးမေးလ်ပို့ခြင်း လုပ်တော့မည် ဖြစ်ပါသည်။ ဒီကော်ပီကို စက်ထဲမှ ထုတ်ပြီးပြီးချင်း သို့မဟုတ် အီးမေးလ်ရောက်ရှိပြီးချင်းချင်း ဖျက်ပစ်ဖို့ မမေ့ပါနှင့်။"</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"သင်သည် ၁) အဆက်အသွယ်အားလုံးနှင့်ဆိုင်သော အချက်အလက်များ၊ ဖုန်းခေါ်မှု မှတ်တမ်းများ ပါဝင်သည့် ဒေတာဘေ့စ်ကို စက်တွင်းသိုလှောင်ခန်းသို့ မိတ္တူကူးပြီး ၂) အီးမေးလ်ပို့ပါတော့မည်။ စက်တွင်းမှ မိတ္တူကူးယူပြီးသည်နှင့်၊ သို့မဟုတ် အီးမေးလ်ရောက်ရှိသည်နှင့် ဤမိတ္တူကို ဖျက်ရန် မမေ့ပါနှင့်။"</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"အခုဖျက်ပါ"</string>
<string name="debug_dump_start_button" msgid="2837506913757600001">"စတင်ရန်"</string>
<string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"သင့်ဖိုင်အား ပို့ရန် ပရိုဂရမ် ရွေးပါ"</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index 3ffd077f..5cee4f36 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -26,7 +26,7 @@
<string name="local_invisible_directory" msgid="705244318477396120">"Outros"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"Correio de voz de "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"Copiar banco de dados de contatos"</string>
- <string name="debug_dump_database_message" msgid="406438635002392290">"Você está prestes a 1) fazer uma cópia de seu banco de dados no armazenamento interno, com todas as informações relacionadas aos contatos e todo o histórico de chamadas e 2) enviar essa cópia por e-mail. Lembre-se de excluir a cópia, logo que você a tiver copiado do dispositivo ou que o e-mail for recebido."</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"Você está prestes a 1) fazer uma cópia de seu banco de dados no armazenamento interno, com todas as informações relacionadas aos contatos e todo o histórico de ligações e 2) enviar essa cópia por e-mail. Lembre-se de excluir a cópia, logo que você a tiver copiado do dispositivo ou que o e-mail for recebido."</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"Excluir agora"</string>
<string name="debug_dump_start_button" msgid="2837506913757600001">"Iniciar"</string>
<string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Escolha um programa para enviar o arquivo"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 3ffd077f..5cee4f36 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -26,7 +26,7 @@
<string name="local_invisible_directory" msgid="705244318477396120">"Outros"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"Correio de voz de "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"Copiar banco de dados de contatos"</string>
- <string name="debug_dump_database_message" msgid="406438635002392290">"Você está prestes a 1) fazer uma cópia de seu banco de dados no armazenamento interno, com todas as informações relacionadas aos contatos e todo o histórico de chamadas e 2) enviar essa cópia por e-mail. Lembre-se de excluir a cópia, logo que você a tiver copiado do dispositivo ou que o e-mail for recebido."</string>
+ <string name="debug_dump_database_message" msgid="406438635002392290">"Você está prestes a 1) fazer uma cópia de seu banco de dados no armazenamento interno, com todas as informações relacionadas aos contatos e todo o histórico de ligações e 2) enviar essa cópia por e-mail. Lembre-se de excluir a cópia, logo que você a tiver copiado do dispositivo ou que o e-mail for recebido."</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"Excluir agora"</string>
<string name="debug_dump_start_button" msgid="2837506913757600001">"Iniciar"</string>
<string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Escolha um programa para enviar o arquivo"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 2b6fa1aa..951bad9c 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -27,8 +27,8 @@
<string name="voicemail_from_column" msgid="435732568832121444">"దీని నుండి వాయిస్ మెయిల్ "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"కాంటాక్ట్‌ల డేటాబేస్‌ను కాపీ చేయండి"</string>
<string name="debug_dump_database_message" msgid="406438635002392290">"మీరు 1) అన్ని కాంటాక్ట్‌లకు సంబంధించిన సమాచారాన్ని మరియు మొత్తం కాల్ లాగ్‌ను కలిగి ఉండే మీ డేటాబేస్ యొక్క కాపీని అంతర్గత స్టోరేజ్‌లో రూపొందించి 2) దాన్ని ఈమెయిల్‌ చేయబోతున్నారు. మీరు దాన్ని పరికరం నుండి విజయవంతంగా కాపీ చేసిన తర్వాత లేదా ఈమెయిల్‌ను స్వీకరించిన తర్వాత కాపీని తొలగించడం మర్చిపోవద్దు."</string>
- <string name="debug_dump_delete_button" msgid="7832879421132026435">"ఇప్పుడే తొలగించు"</string>
- <string name="debug_dump_start_button" msgid="2837506913757600001">"ప్రారంభించు"</string>
+ <string name="debug_dump_delete_button" msgid="7832879421132026435">"ఇప్పుడే తొలగించండి"</string>
+ <string name="debug_dump_start_button" msgid="2837506913757600001">"ప్రారంభించండి"</string>
<string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"మీ పైల్‌ను పంపడానికి ప్రోగ్రామ్‌ను ఎంచుకోండి"</string>
<string name="debug_dump_email_subject" msgid="108188398416385976">"కాంటాక్ట్‌ల Db జోడించబడింది"</string>
<string name="debug_dump_email_body" msgid="4577749800871444318">"జోడింపు నా అన్ని కాంటాక్ట్‌ల సమాచారాన్ని కలిగి ఉన్న నా కాంటాక్ట్‌ల డేటాబేస్. జాగ్రత్తగా వ్యవహరించండి."</string>
diff --git a/src/com/android/providers/contacts/CallLogDatabaseHelper.java b/src/com/android/providers/contacts/CallLogDatabaseHelper.java
index c5052d70..0b29d991 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;
@@ -621,9 +622,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..4f496dfd 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -18,15 +18,18 @@ package com.android.providers.contacts;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.READ_CALL_LOG;
+import static android.Manifest.permission.WRITE_CONTACTS;
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.SearchManager;
@@ -47,6 +50,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 +73,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 +134,13 @@ 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.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;
@@ -179,7 +187,6 @@ import com.android.providers.contacts.util.DbQueryUtils;
import com.android.providers.contacts.util.LogFields;
import com.android.providers.contacts.util.LogUtils;
import com.android.providers.contacts.util.NeededForTesting;
-import com.android.providers.contacts.util.PhoneAccountHandleMigrationUtils;
import com.android.providers.contacts.util.UserUtils;
import com.android.vcard.VCardComposer;
import com.android.vcard.VCardConfig;
@@ -282,6 +289,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 +356,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 +1222,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 +1372,7 @@ public class ContactsProvider2 extends AbstractContactsProvider
String authority;
String accountName;
String accountType;
+ String packageName;
}
/**
@@ -1368,6 +1386,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 +1461,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 +1523,9 @@ public class ContactsProvider2 extends AbstractContactsProvider
private long mLastDanglingContactsCleanup = 0;
+ @GuardedBy("mLaunchableCloneAppsCache")
+ private long mLastLaunchableCloneAppsCacheCleanup = 0;
+
private FastScrollingIndexCache mFastScrollingIndexCache;
// Stats about FastScrollingIndex.
@@ -1493,6 +1538,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 +1615,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 +1627,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 +1768,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 +2193,19 @@ public class ContactsProvider2 extends AbstractContactsProvider
}
/**
+ * Returned whether the feature flag for contacts sharing for clone profile is set. If true,
+ * the clone contacts provider would use the parent contacts providers contacts data to serve
+ * its requests.
+ * @return true/false if contact sharing is enabled/disabled
+ */
+ @VisibleForTesting
+ protected boolean isContactSharingEnabledForCloneProfile() {
+ // TODO(b/253449368): This method should also check for the config controlling
+ // all app-cloning features.
+ return mAppCloningDeviceConfigHelper.getEnableAppCloningBuildingBlocks();
+ }
+
+ /**
* Maximum dimension (height or width) of photo thumbnails.
*/
public int getMaxThumbnailDim() {
@@ -2305,6 +2373,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);
@@ -2339,6 +2413,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);
@@ -2370,6 +2450,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);
@@ -2389,6 +2475,11 @@ public class ContactsProvider2 extends AbstractContactsProvider
@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);
@@ -2607,6 +2698,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);
}
@@ -4212,7 +4309,6 @@ public class ContactsProvider2 extends AbstractContactsProvider
@Override
protected int updateInTransaction(
Uri uri, ContentValues values, String selection, String[] selectionArgs) {
-
if (VERBOSE_LOGGING) {
Log.v(TAG, "updateInTransaction: uri=" + uri +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
@@ -5590,10 +5686,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 +5714,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 +5783,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 +5808,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 +5841,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 +5901,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();
}
}
@@ -5760,6 +5958,22 @@ public class ContactsProvider2 extends AbstractContactsProvider
" Caller=" + getCallingPackage() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
+ final String packageName = directoryInfo.packageName;
+ // enforce permissions
+ final int queryType = sUriMatcher.match(uri);
+ final PackageManager pm = getContext().getPackageManager();
+ if (queryType == PHONE_LOOKUP || queryType == PHONES_FILTER) {
+ if (pm.checkPermission(READ_CALL_LOG, packageName) != PERMISSION_GRANTED) {
+ Log.w(TAG, "Package " + packageName
+ + " does not have permission for phone lookup queries.");
+ return null;
+ }
+ }
+ if (pm.checkPermission(WRITE_CONTACTS, packageName) != PERMISSION_GRANTED) {
+ Log.w(TAG, "Package " + packageName
+ + " does not have permission for contact lookup queries.");
+ return null;
+ }
cursor = getContext().getContentResolver().query(
directoryUri, projection, selection, selectionArgs, sortOrder);
if (cursor == null) {
@@ -5804,11 +6018,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 +6031,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 +6135,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 +6162,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 +6617,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 +6784,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 +7550,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 +7564,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 +7615,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 +7624,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 +8994,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 +9326,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 +9381,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/LogUtils.java b/src/com/android/providers/contacts/util/LogUtils.java
index 23e2b140..a2301006 100644
--- a/src/com/android/providers/contacts/util/LogUtils.java
+++ b/src/com/android/providers/contacts/util/LogUtils.java
@@ -54,7 +54,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) {
@@ -92,5 +91,3 @@ public class LogUtils {
return (SystemClock.elapsedRealtimeNanos() - startNanos) / 1000;
}
}
-
-
diff --git a/src/com/android/providers/contacts/util/UserUtils.java b/src/com/android/providers/contacts/util/UserUtils.java
index 31ea41aa..effe8abe 100644
--- a/src/com/android/providers/contacts/util/UserUtils.java
+++ b/src/com/android/providers/contacts/util/UserUtils.java
@@ -17,9 +17,12 @@ package com.android.providers.contacts.util;
import com.android.providers.contacts.ContactsProvider2;
+import android.annotation.SuppressLint;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
+import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
@@ -78,4 +81,50 @@ public final class UserUtils {
final UserInfo ui = getCorpUserInfo(context);
return ui == null ? -1 : ui.id;
}
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public static boolean shouldUseParentsContacts(Context context) {
+ try {
+ final UserManager userManager = getUserManager(context);
+ final UserProperties userProperties = userManager.getUserProperties(context.getUser());
+ return userProperties.getUseParentsContacts();
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Trying to fetch user properties for non-existing/partial user "
+ + context.getUser());
+ return false;
+ }
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public static boolean shouldUseParentsContacts(Context context, UserHandle userHandle) {
+ try {
+ final UserManager userManager = getUserManager(context);
+ final UserProperties userProperties = userManager.getUserProperties(userHandle);
+ return userProperties.getUseParentsContacts();
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Trying to fetch user properties for non-existing/partial user "
+ + userHandle);
+ return false;
+ }
+ }
+
+ /**
+ * Checks if the input profile user is the parent of the other user
+ * @return True if user1 is the parent profile of user2, false otherwise
+ */
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public static boolean isParentUser(Context context, UserHandle user1, UserHandle user2) {
+ if (user1 == null || user2 == null) return false;
+ final UserManager userManager = getUserManager(context);
+ UserInfo parentUserInfo = userManager.getProfileParent(user2.getIdentifier());
+ return parentUserInfo != null
+ && parentUserInfo.getUserHandle() != null
+ && parentUserInfo.getUserHandle().equals(user1);
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public static UserInfo getProfileParentUser(Context context) {
+ final UserManager userManager = getUserManager(context);
+ return userManager.getProfileParent(context.getUserId());
+ }
}
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index cdf0c7da..967614cc 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
<configuration description="Runs Contacts Provider Tests.">
- <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="ContactsProviderTests.apk" />
</target_preparer>
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 54984d29..e8920530 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -70,6 +70,8 @@ import com.android.providers.contacts.util.Hex;
import com.android.providers.contacts.util.MockClock;
import com.google.android.collect.Sets;
+import java.io.FileInputStream;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
@@ -1381,6 +1383,25 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
assertEquals(timeStamp, time);
}
+ /**
+ * Asserts the equality of two Uri objects, ignoring the order of the query parameters.
+ */
+ protected static void assertUriEquals(Uri expected, Uri actual) {
+ assertEquals(expected.getScheme(), actual.getScheme());
+ assertEquals(expected.getAuthority(), actual.getAuthority());
+ assertEquals(expected.getPath(), actual.getPath());
+ assertEquals(expected.getFragment(), actual.getFragment());
+ Set<String> expectedParameterNames = expected.getQueryParameterNames();
+ Set<String> actualParameterNames = actual.getQueryParameterNames();
+ assertEquals(expectedParameterNames.size(), actualParameterNames.size());
+ assertTrue(expectedParameterNames.containsAll(actualParameterNames));
+ for (String parameterName : expectedParameterNames) {
+ assertEquals(expected.getQueryParameter(parameterName),
+ actual.getQueryParameter(parameterName));
+ }
+
+ }
+
protected void setTimeForTest(Long time) {
Uri uri = Calls.CONTENT_URI.buildUpon()
.appendQueryParameter(CallLogProvider.PARAM_KEY_QUERY_FOR_TESTING, "1")
@@ -1400,6 +1421,71 @@ public abstract class BaseContactsProvider2Test extends PhotoLoadingTestCase {
getContactsProvider().getProfileProviderForTest().getDatabaseHelper(), values);
}
+ protected class VCardTestUriCreator {
+ private String mLookup1;
+ private String mLookup2;
+
+ public VCardTestUriCreator(String lookup1, String lookup2) {
+ super();
+ mLookup1 = lookup1;
+ mLookup2 = lookup2;
+ }
+
+ public Uri getUri1() {
+ return Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, mLookup1);
+ }
+
+ public Uri getUri2() {
+ return Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, mLookup2);
+ }
+
+ public Uri getCombinedUri() {
+ return Uri.withAppendedPath(Contacts.CONTENT_MULTI_VCARD_URI,
+ Uri.encode(mLookup1 + ":" + mLookup2));
+ }
+ }
+
+ protected VCardTestUriCreator createVCardTestContacts() {
+ final long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount,
+ RawContacts.SOURCE_ID, "4:12");
+ DataUtil.insertStructuredName(mResolver, rawContactId1, "John", "Doe");
+
+ final long rawContactId2 = RawContactUtil.createRawContact(mResolver, mAccount,
+ RawContacts.SOURCE_ID, "3:4%121");
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Jane", "Doh");
+
+ final long contactId1 = queryContactId(rawContactId1);
+ final long contactId2 = queryContactId(rawContactId2);
+ final Uri contact1Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1);
+ final Uri contact2Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId2);
+ final String lookup1 =
+ Uri.encode(Contacts.getLookupUri(mResolver, contact1Uri).getPathSegments().get(2));
+ final String lookup2 =
+ Uri.encode(Contacts.getLookupUri(mResolver, contact2Uri).getPathSegments().get(2));
+ return new VCardTestUriCreator(lookup1, lookup2);
+ }
+
+ protected String readToEnd(FileInputStream inputStream) {
+ try {
+ System.out.println("DECLARED INPUT STREAM LENGTH: " + inputStream.available());
+ int ch;
+ StringBuilder stringBuilder = new StringBuilder();
+ int index = 0;
+ while (true) {
+ ch = inputStream.read();
+ System.out.println("READ CHARACTER: " + index + " " + ch);
+ if (ch == -1) {
+ break;
+ }
+ stringBuilder.append((char)ch);
+ index++;
+ }
+ return stringBuilder.toString();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
/**
* A contact in the database, and the attributes used to create it. Construct using
* {@link GoldenContactBuilder#build()}.
diff --git a/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java b/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java
new file mode 100644
index 00000000..26a78143
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/CloneContactsProvider2Test.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.contacts;
+
+import static com.android.providers.contacts.ContactsActor.MockUserManager.CLONE_PROFILE_USER;
+import static com.android.providers.contacts.ContactsActor.MockUserManager.PRIMARY_USER;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.spy;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.util.SparseArray;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Assert;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Set;
+
+@MediumTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+public class CloneContactsProvider2Test extends BaseContactsProvider2Test {
+
+ private ContactsActor mCloneContactsActor;
+ private SynchronousContactsProvider2 mCloneContactsProvider;
+
+ private SynchronousContactsProvider2 getCloneContactsProvider() {
+ return (SynchronousContactsProvider2) mCloneContactsActor.provider;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mCloneContactsActor = new ContactsActor(
+ new ContactsActor.AlteringUserContext(getContext(), CLONE_PROFILE_USER.id),
+ getContextPackageName(), SynchronousContactsProvider2.class, getAuthority());
+ mActor.mockUserManager.setUsers(ContactsActor.MockUserManager.PRIMARY_USER,
+ CLONE_PROFILE_USER);
+ mCloneContactsActor.mockUserManager.setUsers(ContactsActor.MockUserManager.PRIMARY_USER,
+ CLONE_PROFILE_USER);
+ mCloneContactsActor.mockUserManager.myUser = CLONE_PROFILE_USER.id;
+ mCloneContactsProvider = spy(getCloneContactsProvider());
+ mCloneContactsProvider.wipeData();
+ }
+
+ private ContentValues getSampleContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(ContactsContract.RawContacts.ACCOUNT_NAME, "test@test.com");
+ values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, "test.com");
+ values.put(ContactsContract.RawContacts.CUSTOM_RINGTONE, "custom");
+ values.put(ContactsContract.RawContacts.STARRED, "1");
+ return values;
+ }
+
+ private void getCloneContactsProviderWithMockedCallToParent(Uri uri) {
+ Cursor primaryProfileCursor = mActor.provider.query(uri,
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(primaryProfileCursor);
+ doReturn(primaryProfileCursor).when(mCloneContactsProvider)
+ .queryContactsProviderForUser(eq(uri), any(), any(), any(), any(),
+ any(), eq(PRIMARY_USER));
+ }
+
+ private void getCloneContactsProviderWithMockedOpenAssetFileCall(Uri uri)
+ throws FileNotFoundException {
+ AssetFileDescriptor fileDescriptor = mActor.provider.openAssetFile(uri, "r");
+ doReturn(fileDescriptor).when(mCloneContactsProvider)
+ .openAssetFileThroughParentProvider(eq(uri), eq("r"));
+ }
+
+ private String getCursorValue(Cursor c, String columnName) {
+ return c.getString(c.getColumnIndex(columnName));
+ }
+
+ private void assertEqualContentValues(ContentValues contentValues, Cursor cursor) {
+ for (String key: contentValues.getValues().keySet()) {
+ assertEquals(contentValues.get(key), getCursorValue(cursor, key));
+ }
+ }
+
+ private void assertRawContactsCursorEquals(Cursor expectedCursor, Cursor actualCursor,
+ Set<String> columnNames) {
+ assertNotNull(actualCursor);
+ assertEquals(expectedCursor.getCount(), actualCursor.getCount());
+ while (actualCursor.moveToNext()) {
+ expectedCursor.moveToNext();
+ for (String key: columnNames) {
+ assertEquals(getCursorValue(expectedCursor, key),
+ getCursorValue(actualCursor, key));
+ }
+ }
+ }
+
+ private void assertRejectedApplyBatchResults(ContentProviderResult[] res,
+ ArrayList<ContentProviderOperation> ops) {
+ assertEquals(ops.size(), res.length);
+ for (int i = 0;i < ops.size();i++) {
+ Uri expectedUri = ops.get(i).getUri()
+ .buildUpon()
+ .appendPath("0")
+ .build();
+ assertUriEquals(expectedUri, res[i].uri);
+ }
+ }
+
+ /**
+ * Asserts that no contacts are returned when queried by the given contacts provider
+ */
+ private void assertContactsProviderEmpty(ContactsProvider2 contactsProvider2) {
+ Cursor cursor = contactsProvider2.query(ContactsContract.RawContacts.CONTENT_URI,
+ new String[]{ContactsContract.RawContactsEntity._ID},
+ null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(cursor);
+ assertEquals(cursor.getCount(), 0);
+ }
+
+ private long insertRawContactsThroughPrimaryProvider(ContentValues values) {
+ Uri resultUri = mActor.resolver.insert(ContactsContract.RawContacts.CONTENT_URI,
+ values);
+ assertNotNull(resultUri);
+ return ContentUris.parseId(resultUri);
+ }
+
+ public void testAreContactWritesEnabled() {
+ // Check that writes are disabled for clone CP2
+ ContactsProvider2 cloneContactsProvider =
+ (ContactsProvider2) mCloneContactsActor.provider;
+ assertFalse(cloneContactsProvider.areContactWritesEnabled());
+
+ // Check that writes are enabled for primary CP2
+ ContactsProvider2 primaryContactsProvider = (ContactsProvider2) getProvider();
+ assertTrue(primaryContactsProvider.areContactWritesEnabled());
+ }
+
+ public void testCloneContactsProviderInsert() {
+ Uri resultUri =
+ mCloneContactsActor.resolver.insert(ContactsContract.RawContacts.CONTENT_URI,
+ getSampleContentValues());
+
+ // Here we expect a fakeUri returned to fail silently
+ Uri expectedUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon()
+ .appendPath("0")
+ .build();
+ assertUriEquals(expectedUri, resultUri);
+ // No contacts should be present in both clone and primary providers
+ assertContactsProviderEmpty(getContactsProvider());
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+ assertContactsProviderEmpty(mCloneContactsProvider);
+
+ }
+
+ public void testPrimaryContactsProviderInsert() {
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+ Cursor cursor = mActor.resolver.query(ContentUris.withAppendedId(
+ ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals(rawContactId,
+ cursor.getLong(cursor.getColumnIndex(ContactsContract.RawContacts._ID)));
+ assertEqualContentValues(inputContentValues, cursor);
+ }
+
+ public void testCloneContactsProviderUpdate() {
+ // Insert contact through the primary clone provider
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+
+ // Update display name in the input content values
+ ContentValues updatedContentValues = getSampleContentValues();
+ updatedContentValues.put(ContactsContract.RawContacts.STARRED,
+ "0");
+ updatedContentValues.put(ContactsContract.RawContacts.CUSTOM_RINGTONE,
+ "beethoven5");
+
+ // Call clone contacts provider update method to update the raw contact inserted earlier
+ int updateResult = mCloneContactsActor.resolver.update(
+ ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ updatedContentValues, null /* extras */);
+
+ // Check results, no rows should have been affected
+ assertEquals(0, updateResult);
+
+ // Check values associated with rawContactId by querying the database
+ Cursor cursor = mActor.resolver.query(ContentUris.withAppendedId(
+ ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEqualContentValues(inputContentValues, cursor);
+ }
+
+ public void testCloneContactsProviderDelete() {
+ // Insert contact through the primary clone provider
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+
+ // Delete the inserted row through clone provider
+ int deleteResult = mCloneContactsActor.resolver.delete(
+ ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ null);
+
+ // Check results, no rows should have been affected
+ assertEquals(0, deleteResult);
+
+ // Check that contact is present in the primary CP2 database
+ Cursor cursor = mActor.resolver.query(ContentUris.withAppendedId(
+ ContactsContract.RawContacts.CONTENT_URI, rawContactId),
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEqualContentValues(inputContentValues, cursor);
+ }
+
+ public void testCloneContactsProviderBulkInsert() {
+ int bulkInsertResult =
+ mCloneContactsActor.resolver.bulkInsert(ContactsContract.RawContacts.CONTENT_URI,
+ new ContentValues[]{ getSampleContentValues() });
+
+ // Check results, no rows should have been affected
+ assertEquals(0, bulkInsertResult);
+ // No contacts should be present in both clone and primary providers
+ assertContactsProviderEmpty(getContactsProvider());
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+ assertContactsProviderEmpty(mCloneContactsProvider);
+ }
+
+ public void testCloneContactsApplyBatch()
+ throws RemoteException, OperationApplicationException {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null /* value */)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null /* value */).build());
+
+ // Phone Number
+ ops.add(ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, "7XXXXXXXXXX")
+ .withValue(ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, "1").build());
+
+ // Display name/Contact name
+ ops.add(ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Name")
+ .build());
+
+ // Check results, fake uris should be returned for each of the insert operation
+ ContentProviderResult[] res = mCloneContactsActor.resolver.applyBatch(
+ ContactsContract.AUTHORITY, ops);
+ assertRejectedApplyBatchResults(res, ops);
+
+ // No contacts should be present in both clone and primary providers
+ assertContactsProviderEmpty(getContactsProvider());
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+ assertContactsProviderEmpty(mCloneContactsProvider);
+ }
+
+ public void testCloneContactsCallOperation() {
+ // Query Account Operation
+ Bundle response = mCloneContactsActor.resolver.call(ContactsContract.AUTHORITY_URI,
+ ContactsContract.Settings.QUERY_DEFAULT_ACCOUNT_METHOD, null /* arg */,
+ null /* extras */);
+ assertNotNull(response);
+ assertEquals(Bundle.EMPTY, response);
+
+ // Set account operation
+ Bundle bundle = new Bundle();
+ bundle.putString(ContactsContract.Settings.ACCOUNT_NAME, "test@test.com");
+ bundle.putString(ContactsContract.Settings.ACCOUNT_TYPE, "test.com");
+ Bundle setAccountResponse =
+ mCloneContactsActor.resolver.call(ContactsContract.AUTHORITY_URI,
+ ContactsContract.Settings.SET_DEFAULT_ACCOUNT_METHOD, null /* arg */, bundle);
+ assertNotNull(setAccountResponse);
+ assertEquals(Bundle.EMPTY, response);
+
+ // Authorization URI
+ Uri testUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, 1);
+ final Bundle uriBundle = new Bundle();
+ uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, testUri);
+ final Bundle authResponse = mCloneContactsActor.resolver.call(
+ ContactsContract.AUTHORITY_URI,
+ ContactsContract.Authorization.AUTHORIZATION_METHOD,
+ null /* arg */,
+ uriBundle);
+ assertNotNull(authResponse);
+ assertEquals(Bundle.EMPTY, authResponse);
+ }
+
+ public void testCloneContactsProviderReads_callerNotInAllowlist() {
+ // Insert raw contact through the primary clone provider
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId);
+
+ // Mock call to parent profile contacts provider to return the correct result containing all
+ // contacts in the parent profile.
+ getCloneContactsProviderWithMockedCallToParent(uri);
+
+ // Mock call to ensure the caller package is not in the app-cloning allowlist
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+
+ // Test clone contacts provider read with the uri of the contact added above
+ mCloneContactsProvider.query(uri,
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+
+ // Check that the call passed through to the local query instead of redirecting to the
+ // parent provider
+ verify(mCloneContactsProvider, times(1))
+ .queryDirectoryIfNecessary(any(), any(), any(), any(), any(), any());
+ }
+
+ public void testContactsProviderReads_callerInAllowlist() {
+ // Insert raw contact through the primary clone provider
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId);
+
+ // Mock call to parent profile contacts provider to return the correct result containing all
+ // contacts in the parent profile.
+ getCloneContactsProviderWithMockedCallToParent(uri);
+
+ // Mock call to ensure the caller package is in the app-cloning allowlist
+ doReturn(true)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+
+ // Test clone contacts provider read with the uri of the contact added above
+ Cursor cursor = mCloneContactsProvider.query(uri,
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+
+ // Check that the call did not pass through to the local query and instead redirected to the
+ // parent provider
+ verify(mCloneContactsProvider, times(0))
+ .queryDirectoryIfNecessary(any(), any(), any(), any(), any(), any());
+ assertNotNull(cursor);
+ Cursor primaryProfileCursor = mActor.provider.query(uri,
+ null /* projection */, null /* queryArgs */, null /* cancellationSignal */);
+ assertNotNull(primaryProfileCursor);
+ assertRawContactsCursorEquals(primaryProfileCursor, cursor,
+ inputContentValues.getValues().keySet());
+ }
+
+ public void testQueryPrimaryProfileProvider_callingFromParentUser() {
+ ContentValues inputContentValues = getSampleContentValues();
+ long rawContactId = insertRawContactsThroughPrimaryProvider(inputContentValues);
+ Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+ rawContactId);
+
+ // Fetch primary contacts provider and call method to redirect to parent provider
+ final ContactsProvider2 primaryCP2 = (ContactsProvider2) getProvider();
+ Cursor cursor = primaryCP2.queryParentProfileContactsProvider(uri,
+ null /* projection */, null /* selection */, null /* selectionArgs */,
+ null /* sortOrder */, null /* cancellationSignal */);
+
+ // Assert that empty cursor is returned
+ assertNotNull(cursor);
+ assertEquals(0, cursor.getCount());
+ }
+
+ public void testQueryPrimaryProfileProvider_incorrectAuthority() {
+ ContentValues inputContentValues = getSampleContentValues();
+ insertRawContactsThroughPrimaryProvider(inputContentValues);
+
+ Assert.assertThrows(IllegalArgumentException.class, () ->
+ mCloneContactsProvider.queryParentProfileContactsProvider(CallLog.CONTENT_URI,
+ null /* projection */, null /* selection */, null /* selectionArgs */,
+ null /* sortOrder */, null /* cancellationSignal */));
+ }
+
+ public void testOpenAssetFileMultiVCard() throws IOException {
+ final VCardTestUriCreator contacts = createVCardTestContacts();
+
+ // Mock call to parent profile contacts provider to return the correct asset file
+ getCloneContactsProviderWithMockedOpenAssetFileCall(contacts.getCombinedUri());
+
+ // Mock call to ensure the caller package is in the app-cloning allowlist
+ doReturn(true)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+
+ final AssetFileDescriptor descriptor =
+ mCloneContactsProvider.openAssetFile(contacts.getCombinedUri(), "r");
+ final FileInputStream inputStream = descriptor.createInputStream();
+ String data = readToEnd(inputStream);
+ inputStream.close();
+ descriptor.close();
+
+ // Ensure that the resulting VCard has both contacts
+ assertTrue(data.contains("N:Doe;John;;;"));
+ assertTrue(data.contains("N:Doh;Jane;;;"));
+ }
+
+ public void testOpenAssetFileMultiVCard_callerNotInAllowlist() throws IOException {
+ final VCardTestUriCreator contacts = createVCardTestContacts();
+
+ // Mock call to parent profile contacts provider to return the correct asset file
+ getCloneContactsProviderWithMockedOpenAssetFileCall(contacts.getCombinedUri());
+
+ // Mock call to ensure the caller package is not in the app-cloning allowlist
+ doReturn(false)
+ .when(mCloneContactsProvider).isAppAllowedToUseParentUsersContacts(any());
+
+ final AssetFileDescriptor descriptor =
+ mCloneContactsProvider.openAssetFile(contacts.getCombinedUri(), "r");
+
+ // Check that the call passed through to the local call instead of redirecting to the
+ // parent provider
+ verify(mCloneContactsProvider, times(1))
+ .openAssetFile(eq(contacts.getCombinedUri()), any());
+ }
+
+ public void testIsAppAllowedToUseParentUsersContacts_AppInAllowlistCacheEmpty()
+ throws InterruptedException {
+ String testPackageName = mCloneContactsActor.packageName;
+ int processUid = Binder.getCallingUid();
+ doReturn(true)
+ .when(mCloneContactsProvider)
+ .doesPackageHaveALauncherActivity(eq(testPackageName), any());
+
+ SparseArray<ContactsProvider2.LaunchableCloneAppsCacheEntry> launchableCloneAppsCache =
+ mCloneContactsProvider.getLaunchableCloneAppsCacheForTesting();
+ launchableCloneAppsCache.clear();
+ boolean appAllowedToUseParentUsersContacts =
+ mCloneContactsProvider.isAppAllowedToUseParentUsersContacts(testPackageName);
+ assertTrue(appAllowedToUseParentUsersContacts);
+
+ // Check that the cache has been updated with an entry corresponding to current app uid
+ ContactsProvider2.LaunchableCloneAppsCacheEntry cacheEntry =
+ launchableCloneAppsCache.get(processUid);
+ assertNotNull(cacheEntry);
+ assertEquals(1, launchableCloneAppsCache.size());
+ assertTrue(cacheEntry.doesAppHaveLaunchableActivity);
+ }
+
+ public void testIsAppAllowedToUseParentUsersContacts_AppNotInAllowlistCacheEmtpy() {
+ String testPackageName = mCloneContactsActor.packageName;
+ int processUid = Binder.getCallingUid();
+
+ SparseArray<ContactsProvider2.LaunchableCloneAppsCacheEntry> launchableCloneAppsCache =
+ mCloneContactsProvider.getLaunchableCloneAppsCacheForTesting();
+ launchableCloneAppsCache.clear();
+ assertFalse(mCloneContactsProvider.isAppAllowedToUseParentUsersContacts(testPackageName));
+
+ // Check that the cache has been updated with an entry corresponding to current app uid
+ ContactsProvider2.LaunchableCloneAppsCacheEntry cacheEntry =
+ launchableCloneAppsCache.get(processUid);
+ assertNotNull(cacheEntry);
+ assertEquals(1, launchableCloneAppsCache.size());
+ assertFalse(cacheEntry.doesAppHaveLaunchableActivity);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
index fca00afc..7e8a7ad5 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;
@@ -29,6 +27,7 @@ import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Process;
import android.provider.ContactsContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
@@ -38,10 +37,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:
*
@@ -548,6 +552,11 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
mPackageManager.setInstalledPackages(
Lists.newArrayList(createProviderPackage("test.package1", "authority1"),
createPackage("test.packageX", "authorityX", false)));
+// mPackageManager.grantRuntimePermission("test.package1", "android.permission.WRITE_CONTACTS",
+// Process.myUserHandle());
+// mPackageManager.grantRuntimePermission("test.package1", "android.permission.READ_CALL_LOG",
+// Process.myUserHandle());
+
MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
MockContactDirectoryProvider.class, "authority1");
@@ -582,12 +591,20 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
assertEquals("account-name1", cursor.getString(cursor.getColumnIndex("accountName")));
assertEquals("account-type1", cursor.getString(cursor.getColumnIndex("accountType")));
cursor.close();
+// mPackageManager.revokeRuntimePermission("test.package1", "android.permission.WRITE_CONTACTS",
+// Process.myUserHandle());
+// mPackageManager.revokeRuntimePermission("test.package1", "android.permission.READ_CALL_LOG",
+// Process.myUserHandle());
}
public void testProjectionPopulated() throws Exception {
mPackageManager.setInstalledPackages(
Lists.newArrayList(createProviderPackage("test.package1", "authority1"),
createPackage("test.packageX", "authorityX", false)));
+// mPackageManager.grantRuntimePermission("test.package1", "android.permission.WRITE_CONTACTS",
+// Process.myUserHandle());
+// mPackageManager.grantRuntimePermission("test.package1", "android.permission.READ_CALL_LOG",
+// Process.myUserHandle());
MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
MockContactDirectoryProvider.class, "authority1");
@@ -616,6 +633,10 @@ public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
AggregationExceptions.RAW_CONTACT_ID1,
AggregationExceptions.RAW_CONTACT_ID2,
});
+// mPackageManager.revokeRuntimePermission("test.package1", "android.permission.WRITE_CONTACTS",
+// Process.myUserHandle());
+// mPackageManager.revokeRuntimePermission("test.package1", "android.permission.READ_CALL_LOG",
+// Process.myUserHandle());
}
/**
@@ -634,8 +655,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..c583e1df 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 int checkPermission(String permission, String packageName) {
+ return PERMISSION_GRANTED;
+ }
+
+ @Override
+ public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, ResolveInfoFlags flags,
+ UserHandle user) {
+ return new ArrayList<>();
+ }
}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 09ea19fc..69ae0fb2 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -8163,50 +8163,6 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
assertEquals("default", helper.getProperty("existent1", "default"));
}
- private class VCardTestUriCreator {
- private String mLookup1;
- private String mLookup2;
-
- public VCardTestUriCreator(String lookup1, String lookup2) {
- super();
- mLookup1 = lookup1;
- mLookup2 = lookup2;
- }
-
- public Uri getUri1() {
- return Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, mLookup1);
- }
-
- public Uri getUri2() {
- return Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, mLookup2);
- }
-
- public Uri getCombinedUri() {
- return Uri.withAppendedPath(Contacts.CONTENT_MULTI_VCARD_URI,
- Uri.encode(mLookup1 + ":" + mLookup2));
- }
- }
-
- private VCardTestUriCreator createVCardTestContacts() {
- final long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount,
- RawContacts.SOURCE_ID, "4:12");
- DataUtil.insertStructuredName(mResolver, rawContactId1, "John", "Doe");
-
- final long rawContactId2 = RawContactUtil.createRawContact(mResolver, mAccount,
- RawContacts.SOURCE_ID, "3:4%121");
- DataUtil.insertStructuredName(mResolver, rawContactId2, "Jane", "Doh");
-
- final long contactId1 = queryContactId(rawContactId1);
- final long contactId2 = queryContactId(rawContactId2);
- final Uri contact1Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1);
- final Uri contact2Uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId2);
- final String lookup1 =
- Uri.encode(Contacts.getLookupUri(mResolver, contact1Uri).getPathSegments().get(2));
- final String lookup2 =
- Uri.encode(Contacts.getLookupUri(mResolver, contact2Uri).getPathSegments().get(2));
- return new VCardTestUriCreator(lookup1, lookup2);
- }
-
public void testQueryMultiVCard() {
// No need to create any contacts here, because the query for multiple vcards
// does not go into the database at all
@@ -9688,27 +9644,6 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
return c;
}
- private String readToEnd(FileInputStream inputStream) {
- try {
- System.out.println("DECLARED INPUT STREAM LENGTH: " + inputStream.available());
- int ch;
- StringBuilder stringBuilder = new StringBuilder();
- int index = 0;
- while (true) {
- ch = inputStream.read();
- System.out.println("READ CHARACTER: " + index + " " + ch);
- if (ch == -1) {
- break;
- }
- stringBuilder.append((char)ch);
- index++;
- }
- return stringBuilder.toString();
- } catch (IOException e) {
- return null;
- }
- }
-
private void assertQueryParameter(String uriString, String parameter, String expectedValue) {
assertEquals(expectedValue, ContactsProvider2.getQueryParameter(
Uri.parse(uriString), parameter));
@@ -9908,24 +9843,4 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test {
}
return false;
}
-
-
- /**
- * Asserts the equality of two Uri objects, ignoring the order of the query parameters.
- */
- public static void assertUriEquals(Uri expected, Uri actual) {
- assertEquals(expected.getScheme(), actual.getScheme());
- assertEquals(expected.getAuthority(), actual.getAuthority());
- assertEquals(expected.getPath(), actual.getPath());
- assertEquals(expected.getFragment(), actual.getFragment());
- Set<String> expectedParameterNames = expected.getQueryParameterNames();
- Set<String> actualParameterNames = actual.getQueryParameterNames();
- assertEquals(expectedParameterNames.size(), actualParameterNames.size());
- assertTrue(expectedParameterNames.containsAll(actualParameterNames));
- for (String parameterName : expectedParameterNames) {
- assertEquals(expected.getQueryParameter(parameterName),
- actual.getQueryParameter(parameterName));
- }
-
- }
}
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index 848c2379..ca8cb669 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -179,6 +179,11 @@ public class SynchronousContactsProvider2 extends ContactsProvider2 {
}
@Override
+ protected boolean isContactSharingEnabledForCloneProfile() {
+ return true;
+ }
+
+ @Override
public boolean isWritableAccountWithDataSet(String accountType) {
return !READ_ONLY_ACCOUNT_TYPE.equals(accountType);
}
diff --git a/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java b/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java
index 2e5241fb..5aedd8d0 100644
--- a/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java
+++ b/tests/src/com/android/providers/contacts/enterprise/EnterprisePolicyGuardTest.java
@@ -17,6 +17,7 @@ package com.android.providers.contacts.enterprise;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.net.Uri;
import android.os.UserHandle;
@@ -31,6 +32,9 @@ import org.mockito.Matchers;
import java.util.Arrays;
import java.util.List;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -49,6 +53,7 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase {
private static final String CONTACT_EMAIL = "david.green@android.com";
private static final String CONTACT_PHONE = "+1234567890";
private static final long DIRECTORY_ID = Directory.ENTERPRISE_DEFAULT;
+ private static final String CALLING_PACKAGE = "package";
private static final Uri URI_CONTACTS_ID_PHOTO =
Uri.parse("content://com.android.contacts/contacts/" + CONTACT_ID + "/photo");
@@ -179,6 +184,32 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase {
checkCrossProfile(guard, URI_CONTACTS_ID_PHOTO, false);
checkCrossProfile(guard, URI_CONTACTS_ID_DISPLAY_PHOTO, false);
checkCrossProfile(guard, URI_OTHER, false);
+
+ // ManagedProfile is paused
+ context = getMockContext(true, true, false);
+ guard = new EnterprisePolicyGuardTestable(context, true);
+ checkCrossProfile(guard, appendRemoteDirectoryId(URI_PHONE_LOOKUP), false);
+ checkCrossProfile(guard, appendRemoteDirectoryId(URI_EMAILS_LOOKUP), false);
+ checkCrossProfile(guard, appendRemoteDirectoryId(URI_CONTACTS_FILTER), false);
+ checkCrossProfile(guard, appendRemoteDirectoryId(URI_PHONES_FILTER), false);
+ checkCrossProfile(guard, appendRemoteDirectoryId(URI_CALLABLES_FILTER), false);
+ checkCrossProfile(guard, appendRemoteDirectoryId(URI_EMAILS_FILTER), false);
+ checkCrossProfile(guard, URI_DIRECTORY_FILE, false);
+
+ // Always allow uri with no directory support.
+ checkCrossProfile(guard, URI_DIRECTORIES, true);
+ checkCrossProfile(guard, URI_DIRECTORIES_ID, true);
+ checkCrossProfile(guard, URI_CONTACTS_ID_PHOTO, true);
+ checkCrossProfile(guard, URI_CONTACTS_ID_DISPLAY_PHOTO, true);
+ checkCrossProfile(guard, URI_OTHER, false);
+
+ // Always allow uri with no remote directory id.
+ checkCrossProfile(guard, URI_PHONE_LOOKUP, true);
+ checkCrossProfile(guard, URI_EMAILS_LOOKUP, true);
+ checkCrossProfile(guard, URI_CONTACTS_FILTER, true);
+ checkCrossProfile(guard, URI_PHONES_FILTER, true);
+ checkCrossProfile(guard, URI_CALLABLES_FILTER, true);
+ checkCrossProfile(guard, URI_EMAILS_FILTER, true);
}
public void testCrossProfile_userSettingOff() {
@@ -210,6 +241,7 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase {
checkCrossProfile(guard, URI_EMAILS_FILTER, true);
}
+
private static Uri appendRemoteDirectoryId(Uri uri) {
return appendDirectoryId(uri, REMOTE_DIRECTORY_ID);
}
@@ -231,13 +263,13 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase {
}
}
- private static void checkCrossProfile(EnterprisePolicyGuard guard, Uri uri, boolean expected) {
+ private void checkCrossProfile(EnterprisePolicyGuard guard, Uri uri, boolean expected) {
if (expected) {
assertTrue("Expected true but got false for uri: " + uri,
- guard.isCrossProfileAllowed(uri));
+ guard.isCrossProfileAllowed(uri, CALLING_PACKAGE));
} else {
assertFalse("Expected false but got true for uri: " + uri,
- guard.isCrossProfileAllowed(uri));
+ guard.isCrossProfileAllowed(uri, CALLING_PACKAGE));
}
}
@@ -256,11 +288,16 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase {
private Context getMockContext(boolean isCallerIdEnabled, boolean isContactsSearchEnabled) {
+ return getMockContext(isCallerIdEnabled, isContactsSearchEnabled, true);
+ }
+
+ private Context getMockContext(boolean isCallerIdEnabled, boolean isContactsSearchEnabled,
+ boolean isManagedProfileEnabled) {
DevicePolicyManager mockDpm = mock(DevicePolicyManager.class);
- when(mockDpm.getCrossProfileCallerIdDisabled(Matchers.<UserHandle>any()))
- .thenReturn(!isCallerIdEnabled);
- when(mockDpm.getCrossProfileContactsSearchDisabled(Matchers.<UserHandle>any()))
- .thenReturn(!isContactsSearchEnabled);
+ when(mockDpm.hasManagedProfileCallerIdAccess(Matchers.any(),Matchers.any()))
+ .thenReturn(isCallerIdEnabled);
+ when(mockDpm.hasManagedProfileContactsAccess(Matchers.any(),Matchers.any()))
+ .thenReturn(isContactsSearchEnabled);
List<UserInfo> userInfos = MANAGED_USERINFO_LIST;
UserManager mockUm = mock(UserManager.class);
@@ -268,8 +305,14 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase {
when(mockUm.getUsers()).thenReturn(userInfos);
when(mockUm.getProfiles(Matchers.anyInt())).thenReturn(userInfos);
when(mockUm.getProfileParent(WORK_USER_ID)).thenReturn(CURRENT_USER_INFO);
+ when(mockUm.isQuietModeEnabled(UserHandle.of(WORK_USER_ID)))
+ .thenReturn(!isManagedProfileEnabled);
- Context mockContext = new TestMockContext(getContext(), mockDpm, mockUm);
+ PackageManager mockPm = mock(PackageManager.class);
+ when(mockPm.checkPermission(INTERACT_ACROSS_USERS, CALLING_PACKAGE))
+ .thenReturn(PERMISSION_GRANTED);
+
+ Context mockContext = new TestMockContext(getContext(), mockDpm, mockUm, mockPm);
return mockContext;
}
@@ -278,11 +321,19 @@ public class EnterprisePolicyGuardTest extends FixedAndroidTestCase {
private Context mRealContext;
private DevicePolicyManager mDpm;
private UserManager mUm;
+ private PackageManager mPm;
- public TestMockContext(Context realContext, DevicePolicyManager dpm, UserManager um) {
+ public TestMockContext(
+ Context realContext, DevicePolicyManager dpm, UserManager um, PackageManager pm) {
mRealContext = realContext;
mDpm = dpm;
mUm = um;
+ mPm = pm;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPm;
}
public Object getSystemService(String name) {
diff --git a/tests/src/com/android/providers/contacts/util/UserUtilsTest.java b/tests/src/com/android/providers/contacts/util/UserUtilsTest.java
index 93613cf5..c672697a 100644
--- a/tests/src/com/android/providers/contacts/util/UserUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/util/UserUtilsTest.java
@@ -18,6 +18,7 @@ package com.android.providers.contacts.util;
import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
import android.content.Context;
+import android.os.UserHandle;
import android.provider.ContactsContract;
import android.test.suitebuilder.annotation.SmallTest;
@@ -77,5 +78,82 @@ public class UserUtilsTest extends FixedAndroidTestCase {
um.myUser = MockUserManager.SECONDARY_USER.id;
assertEquals(-1, UserUtils.getCorpUserId(c));
+
+ // Primary + clone + corp
+ um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.CLONE_PROFILE_USER,
+ MockUserManager.CORP_USER);
+
+ um.myUser = MockUserManager.PRIMARY_USER.id;
+ assertEquals(MockUserManager.CORP_USER.id, UserUtils.getCorpUserId(c));
+
+ um.myUser = MockUserManager.CLONE_PROFILE_USER.id;
+ assertEquals(-1, UserUtils.getCorpUserId(c));
+
+ um.myUser = MockUserManager.CORP_USER.id;
+ assertEquals(-1, UserUtils.getCorpUserId(c));
+ }
+
+ public void testShouldUseParentsContacts() {
+ final Context c = mActor.getProviderContext();
+ final MockUserManager um = mActor.mockUserManager;
+
+ um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.SECONDARY_USER,
+ MockUserManager.CLONE_PROFILE_USER, MockUserManager.CORP_USER);
+
+ um.myUser = MockUserManager.PRIMARY_USER.id;
+ assertFalse(UserUtils.shouldUseParentsContacts(c));
+ assertFalse(UserUtils.shouldUseParentsContacts(c,
+ MockUserManager.PRIMARY_USER.getUserHandle()));
+
+ um.myUser = MockUserManager.SECONDARY_USER.id;
+ assertFalse(UserUtils.shouldUseParentsContacts(c));
+ assertFalse(UserUtils.shouldUseParentsContacts(c,
+ MockUserManager.SECONDARY_USER.getUserHandle()));
+
+ um.myUser = MockUserManager.CORP_USER.id;
+ assertFalse(UserUtils.shouldUseParentsContacts(c));
+ assertFalse(UserUtils.shouldUseParentsContacts(c,
+ MockUserManager.CORP_USER.getUserHandle()));
+
+ um.myUser = MockUserManager.CLONE_PROFILE_USER.id;
+ assertTrue(UserUtils.shouldUseParentsContacts(c));
+ assertTrue(UserUtils.shouldUseParentsContacts(c,
+ MockUserManager.CLONE_PROFILE_USER.getUserHandle()));
+
+ }
+
+ public void testIsParentUser() {
+ final Context c = mActor.getProviderContext();
+ final MockUserManager um = mActor.mockUserManager;
+ um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.SECONDARY_USER,
+ MockUserManager.CLONE_PROFILE_USER, MockUserManager.CORP_USER);
+
+ UserHandle primaryProfileUserHandle = MockUserManager.PRIMARY_USER.getUserHandle();
+ UserHandle cloneUserHandle = MockUserManager.CLONE_PROFILE_USER.getUserHandle();
+ UserHandle corpUserHandle = MockUserManager.CORP_USER.getUserHandle();
+
+ assertTrue(UserUtils.isParentUser(c, primaryProfileUserHandle, cloneUserHandle));
+ assertTrue(UserUtils.isParentUser(c, primaryProfileUserHandle, corpUserHandle));
+ assertFalse(UserUtils.isParentUser(c, primaryProfileUserHandle, primaryProfileUserHandle));
+ assertFalse(UserUtils.isParentUser(c, cloneUserHandle, cloneUserHandle));
+ assertFalse(UserUtils.isParentUser(c, cloneUserHandle, primaryProfileUserHandle));
+ assertFalse(UserUtils.isParentUser(c, corpUserHandle, primaryProfileUserHandle));
+ }
+
+ public void testGetProfileParent() {
+ final Context c = mActor.getProviderContext();
+ final MockUserManager um = mActor.mockUserManager;
+
+ um.setUsers(MockUserManager.PRIMARY_USER, MockUserManager.SECONDARY_USER,
+ MockUserManager.CLONE_PROFILE_USER, MockUserManager.CORP_USER);
+
+ um.myUser = MockUserManager.PRIMARY_USER.id;
+ assertNull(UserUtils.getProfileParentUser(c));
+
+ um.myUser = MockUserManager.CLONE_PROFILE_USER.id;
+ assertEquals(MockUserManager.PRIMARY_USER, UserUtils.getProfileParentUser(c));
+
+ um.myUser = MockUserManager.CORP_USER.id;
+ assertEquals(MockUserManager.PRIMARY_USER, UserUtils.getProfileParentUser(c));
}
}