Merge tag android-5.1.0_r1 into AOSP_5.1_MERGE
Change-Id: I1753a6211411ee253a8ab3362c3ac252c1ebca6e
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 496c990..21ea8cb 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -17,17 +17,17 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
- <string name="app_label" msgid="3389954322874982620">"संपर्क मेमोरी"</string>
+ <string name="app_label" msgid="3389954322874982620">"संपर्क संग्रहण"</string>
<string name="provider_label" msgid="6012150850819899907">"संपर्क"</string>
<string name="upgrade_msg" msgid="8640807392794309950">"संपर्क डेटाबेस अपग्रेड हो रहा है."</string>
- <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"संपर्क अपग्रेड के लिए अधिक मेमोरी की आवश्यकता होती है."</string>
- <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"संपर्कों के लिए मेमोरी अपग्रेड करना"</string>
+ <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"संपर्क अपग्रेड के लिए अधिक स्मृति की आवश्यकता होती है."</string>
+ <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"संपर्कों के लिए संग्रहण अपग्रेड करना"</string>
<string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"अपग्रेड पूर्ण करने के लिए स्पर्श करें."</string>
<string name="default_directory" msgid="93961630309570294">"संपर्क"</string>
<string name="local_invisible_directory" msgid="705244318477396120">"अन्य"</string>
<string name="voicemail_from_column" msgid="435732568832121444">"इनका ध्वनिमेल: "</string>
<string name="debug_dump_title" msgid="4916885724165570279">"संपर्क डेटाबेस की प्रतिलिपि बनाएं"</string>
- <string name="debug_dump_database_message" msgid="406438635002392290">"आप 1) मोबाइल मेमोरी में अपने उस डेटाबेस की प्रतिलिपि बनाने वाले हैं जिसमें सभी संपर्कों संबंधी जानकारी और सभी कॉल लॉग शामिल हैं, और 2) उसे ईमेल करने वाले हैं. जैसे ही आप डिवाइस से इसकी प्रतिलिपि सफलतापूर्वक बना लें या ईमेल प्राप्त हो जाए तो प्रतिलिपि को हटाना न भूलें."</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_email_sender_picker" msgid="3534420908672176460">"अपनी फ़ाइल भेजने के लिए कोई प्रोग्राम चुनें"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index db4efe9..4ca69a9 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -27,7 +27,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">"אתה עומד 1) ליצור עותק באחסון הפנימי של מסד הנתונים שכולל את כל המידע הקשור לאנשי הקשר וכל יומני השיחות, 2) לשלוח אותו באימייל. זכור למחוק את העותק מיד לאחר שתעתיק אותו בהצלחה מהמכשיר או כשהודעת האימייל מתקבלת."</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_email_sender_picker" msgid="3534420908672176460">"בחר תכנית לשליחת הקובץ"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 0dba728..2552edf 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="sharedUserLabel" msgid="8024311725474286801">"Principais apps do Android"</string>
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Principais aplicativos do Android"</string>
<string name="app_label" msgid="3389954322874982620">"Armazenamento de contatos"</string>
<string name="provider_label" msgid="6012150850819899907">"Contatos"</string>
<string name="upgrade_msg" msgid="8640807392794309950">"Atualizando o banco de dados de contatos."</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index d3afc16..6c0cf53 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -16,7 +16,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="sharedUserLabel" msgid="8024311725474286801">"Osrednje aplikacije sistema Android"</string>
+ <string name="sharedUserLabel" msgid="8024311725474286801">"Osrednji programi sistema Android"</string>
<string name="app_label" msgid="3389954322874982620">"Shramba za stike"</string>
<string name="provider_label" msgid="6012150850819899907">"Stiki"</string>
<string name="upgrade_msg" msgid="8640807392794309950">"Nadgradnja zbirke podatkov stikov."</string>
@@ -30,7 +30,7 @@
<string name="debug_dump_database_message" msgid="406438635002392290">"V naslednjem koraku boste 1) naredili kopijo zbirke podatkov, ki vključuje vse informacije o stikih in celoten dnevnik klicev, v notranji pomnilnik ter 2) jo poslali. Ne pozabite izbrisati kopije iz naprave, ko jo boste uspešno kopirali oziroma ko jo boste prejeli po e-pošti."</string>
<string name="debug_dump_delete_button" msgid="7832879421132026435">"Izbriši zdaj"</string>
<string name="debug_dump_start_button" msgid="2837506913757600001">"Začni"</string>
- <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Izberite aplikacijo za pošiljanje datoteke"</string>
+ <string name="debug_dump_email_sender_picker" msgid="3534420908672176460">"Izberite program za pošiljanje datoteke"</string>
<string name="debug_dump_email_subject" msgid="108188398416385976">"Priložena zbirka podatkov o stikih"</string>
<string name="debug_dump_email_body" msgid="4577749800871444318">"Priložena je zbirka podatkov z vsemi informacijami o stikih. Z e-pošto ravnajte previdno."</string>
</resources>
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index e6a4b66..8589b9d 100755
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -30,6 +30,10 @@
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.CallLog;
@@ -45,6 +49,7 @@
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
/**
* Call log content provider.
@@ -52,6 +57,8 @@
public class CallLogProvider extends ContentProvider {
private static final String TAG = CallLogProvider.class.getSimpleName();
+ private static final int BACKGROUND_TASK_INITIALIZE = 0;
+
/** Selection clause for selecting all calls that were made after a certain time */
private static final String MORE_RECENT_THAN_SELECTION = Calls.DATE + "> ?";
/** Selection clause to use to exclude voicemail records. */
@@ -117,6 +124,10 @@
sCallsProjectionMap.put(Calls.CACHED_FORMATTED_NUMBER, Calls.CACHED_FORMATTED_NUMBER);
}
+ private HandlerThread mBackgroundThread;
+ private Handler mBackgroundHandler;
+ private volatile CountDownLatch mReadAccessLatch;
+
private ContactsDatabaseHelper mDbHelper;
private DatabaseUtils.InsertHelper mCallsInserter;
private boolean mUseStrictPhoneNumberComparation;
@@ -136,11 +147,20 @@
com.android.internal.R.bool.config_use_strict_phone_number_comparation);
mVoicemailPermissions = new VoicemailPermissions(context);
mCallLogInsertionHelper = createCallLogInsertionHelper(context);
- final UserManager userManager = UserUtils.getUserManager(context);
- if (userManager != null &&
- !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS)) {
- syncEntriesFromPrimaryUser(userManager);
- }
+
+ mBackgroundThread = new HandlerThread("CallLogProviderWorker",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ mBackgroundThread.start();
+ mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ performBackgroundTask(msg.what);
+ }
+ };
+
+ mReadAccessLatch = new CountDownLatch(1);
+
+ scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate finish");
@@ -161,6 +181,7 @@
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
+ waitForAccess(mReadAccessLatch);
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(Tables.CALLS);
qb.setProjectionMap(sCallsProjectionMap);
@@ -256,6 +277,7 @@
@Override
public Uri insert(Uri uri, ContentValues values) {
+ waitForAccess(mReadAccessLatch);
checkForSupportedColumns(sCallsProjectionMap, values);
// Inserting a voicemail record through call_log requires the voicemail
// permission and also requires the additional voicemail param set.
@@ -282,6 +304,7 @@
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ waitForAccess(mReadAccessLatch);
checkForSupportedColumns(sCallsProjectionMap, values);
// Request that involves changing record type to voicemail requires the
// voicemail param set in the uri.
@@ -312,6 +335,7 @@
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
+ waitForAccess(mReadAccessLatch);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder, false /*isQuery*/);
@@ -507,4 +531,42 @@
private void setLastTimeSynced(long time) {
mDbHelper.setProperty(DbProperties.CALL_LOG_LAST_SYNCED, String.valueOf(time));
}
+
+ private static void waitForAccess(CountDownLatch latch) {
+ if (latch == null) {
+ return;
+ }
+
+ while (true) {
+ try {
+ latch.await();
+ return;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private void scheduleBackgroundTask(int task) {
+ mBackgroundHandler.sendEmptyMessage(task);
+ }
+
+ private void performBackgroundTask(int task) {
+ if (task == BACKGROUND_TASK_INITIALIZE) {
+ try {
+ final Context context = getContext();
+ if (context != null) {
+ final UserManager userManager = UserUtils.getUserManager(context);
+ if (userManager != null &&
+ !userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS)) {
+ syncEntriesFromPrimaryUser(userManager);
+ }
+ }
+ } finally {
+ mReadAccessLatch.countDown();
+ mReadAccessLatch = null;
+ }
+ }
+
+ }
}
diff --git a/src/com/android/providers/contacts/ContactDirectoryManager.java b/src/com/android/providers/contacts/ContactDirectoryManager.java
index f243e79..530a31b 100644
--- a/src/com/android/providers/contacts/ContactDirectoryManager.java
+++ b/src/com/android/providers/contacts/ContactDirectoryManager.java
@@ -199,6 +199,7 @@
@VisibleForTesting
static boolean isDirectoryProvider(ProviderInfo provider) {
+ if (provider == null) return false;
Bundle metaData = provider.metaData;
if (metaData == null) return false;
@@ -213,17 +214,26 @@
static Set<String> getDirectoryProviderPackages(PackageManager pm) {
final Set<String> ret = Sets.newHashSet();
- // Note to 3rd party developers:
- // queryContentProviders() is a public API but this method doesn't officially support
- // the GET_META_DATA flag. Don't use it in your app.
- final List<ProviderInfo> providers = pm.queryContentProviders(null, 0,
- PackageManager.GET_META_DATA);
- if (providers == null) {
+ final List<PackageInfo> packages = pm.getInstalledPackages(PackageManager.GET_PROVIDERS
+ | PackageManager.GET_META_DATA);
+ if (packages == null) {
return ret;
}
- for (ProviderInfo provider : providers) {
- if (isDirectoryProvider(provider)) {
- ret.add(provider.packageName);
+ for (PackageInfo packageInfo : packages) {
+ if (DEBUG) {
+ Log.d(TAG, "package=" + packageInfo.packageName);
+ }
+ if (packageInfo.providers == null) {
+ continue;
+ }
+ for (ProviderInfo provider : packageInfo.providers) {
+ if (DEBUG) {
+ Log.d(TAG, "provider=" + provider.authority);
+ }
+ if (isDirectoryProvider(provider)) {
+ Log.d(TAG, "Found " + provider.authority);
+ ret.add(provider.packageName);
+ }
}
}
if (DEBUG) {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index f6e472d..be3a497 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -260,7 +260,7 @@
private static final String PREF_LOCALE = "locale";
- private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 3;
+ private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 4;
private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
diff --git a/src/com/android/providers/contacts/PhotoPriorityResolver.java b/src/com/android/providers/contacts/PhotoPriorityResolver.java
index 150811c..bbf83c5 100644
--- a/src/com/android/providers/contacts/PhotoPriorityResolver.java
+++ b/src/com/android/providers/contacts/PhotoPriorityResolver.java
@@ -19,9 +19,10 @@
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.content.Context;
-import android.content.pm.PackageInfo;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.XmlResourceParser;
import android.util.Log;
@@ -34,6 +35,7 @@
import java.io.IOException;
import java.util.HashMap;
+import java.util.List;
/**
* Maintains a cache of photo priority per account type. During contact aggregation
@@ -46,10 +48,21 @@
public static final int DEFAULT_PRIORITY = 7;
+ private static final String SYNC_META_DATA = "android.content.SyncAdapter";
+
/**
- * Meta-data key for the contacts configuration associated with a sync service.
+ * The metadata name for so-called "contacts.xml".
+ *
+ * On LMP and later, we also accept the "alternate" name.
+ * This is to allow sync adapters to have a contacts.xml without making it visible on older
+ * platforms. If you modify this also update the matching list in
+ * ContactsCommon/ExternalAccountType.
*/
- private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
+ private static final String[] METADATA_CONTACTS_NAMES = new String[] {
+ "android.provider.ALTERNATE_CONTACTS_STRUCTURE",
+ "android.provider.CONTACTS_STRUCTURE"
+ };
+
/**
* The XML tag capturing the picture priority. The syntax is:
@@ -107,19 +120,24 @@
*/
/* package */ int resolvePhotoPriorityFromMetaData(String packageName) {
final PackageManager pm = mContext.getPackageManager();
- try {
- PackageInfo pi = pm.getPackageInfo(packageName, PackageManager.GET_SERVICES
- | PackageManager.GET_META_DATA);
- if (pi != null && pi.services != null) {
- for (ServiceInfo si : pi.services) {
- final XmlResourceParser parser = si.loadXmlMetaData(pm, METADATA_CONTACTS);
+ final Intent intent = new Intent(SYNC_META_DATA).setPackage(packageName);
+ final List<ResolveInfo> intentServices = pm.queryIntentServices(intent,
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+
+ if (intentServices != null) {
+ for (final ResolveInfo resolveInfo : intentServices) {
+ final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo == null) {
+ continue;
+ }
+ for (String metadataName : METADATA_CONTACTS_NAMES) {
+ final XmlResourceParser parser = serviceInfo.loadXmlMetaData(
+ pm, metadataName);
if (parser != null) {
return loadPhotoPriorityFromXml(mContext, parser);
}
}
}
- } catch (NameNotFoundException e) {
- Log.w(TAG, "Problem loading photo priorities: " + e.toString());
}
return DEFAULT_PRIORITY;
}
diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator.java b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
index 390871c..d030687 100644
--- a/src/com/android/providers/contacts/aggregation/ContactAggregator.java
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
@@ -40,6 +40,7 @@
import android.util.EventLog;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.providers.contacts.ContactLookupKey;
import com.android.providers.contacts.ContactsDatabaseHelper;
import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
@@ -68,6 +69,9 @@
import com.android.providers.contacts.util.Clock;
import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.HashMultimap;
import java.util.ArrayList;
import java.util.Collections;
@@ -76,6 +80,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
/**
* ContactAggregator deals with aggregating contact information coming from different sources.
@@ -131,10 +136,21 @@
private static final int SECONDARY_HIT_LIMIT = 20;
private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
+ // If we encounter no less than this many raw contacts in the best matching contact during
+ // aggregation, don't attempt to aggregate - this is likely an error or a shared corporate
+ // data element.
+ @VisibleForTesting
+ static final int AGGREGATION_CONTACT_SIZE_LIMIT = 50;
+
// If we encounter more than this many contacts with matching name during aggregation
// suggestion lookup, ignore the remaining results.
private static final int FIRST_LETTER_SUGGESTION_HIT_LIMIT = 100;
+ // Return code for the canJoinIntoContact method.
+ private static final int JOIN = 1;
+ private static final int KEEP_SEPARATE = 0;
+ private static final int RE_AGGREGATE = -1;
+
private final ContactsProvider2 mContactsProvider;
private final ContactsDatabaseHelper mDbHelper;
private PhotoPriorityResolver mPhotoPriorityResolver;
@@ -165,7 +181,7 @@
private String[] mSelectionArgs1 = new String[1];
private String[] mSelectionArgs2 = new String[2];
- private String[] mSelectionArgs3 = new String[3];
+
private long mMimeTypeIdIdentity;
private long mMimeTypeIdEmail;
private long mMimeTypeIdPhoto;
@@ -730,8 +746,10 @@
}
long contactId = -1; // Best matching contact ID.
- long contactIdToSplit = -1;
+ boolean needReaggregate = false;
+ final Set<Long> rawContactIdsInSameAccount = new HashSet<Long>();
+ final Set<Long> rawContactIdsInOtherAccount = new HashSet<Long>();
if (aggregationMode == RawContacts.AGGREGATION_MODE_DEFAULT) {
candidates.clear();
matcher.clear();
@@ -746,13 +764,55 @@
contactId = pickBestMatchBasedOnData(db, rawContactId, candidates, matcher);
}
- // If we found an aggregate to join, but it already contains raw contacts from
- // the same account, not only will we not join it, but also we will split
- // that other aggregate
- if (contactId != -1 && contactId != currentContactId &&
- !canJoinIntoContact(db, contactId, rawContactId, accountId)) {
- contactIdToSplit = contactId;
- contactId = -1;
+ // If we found an best matched contact, find out if the raw contact can be joined
+ // into it
+ if (contactId != -1 && contactId != currentContactId) {
+ // List all raw contact ID and their account ID mappings in contact
+ // [contactId] excluding raw_contact [rawContactId].
+
+ // Based on the mapping, create two sets of raw contact IDs in
+ // [rawContactAccountId] and not in [rawContactAccountId]. We don't always
+ // need them, so lazily initialize them.
+ mSelectionArgs2[0] = String.valueOf(contactId);
+ mSelectionArgs2[1] = String.valueOf(rawContactId);
+ final Cursor rawContactsToAccountsCursor = db.rawQuery(
+ "SELECT " + RawContacts._ID + ", " + RawContactsColumns.ACCOUNT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=?" +
+ " AND " + RawContacts._ID + "!=?",
+ mSelectionArgs2);
+ try {
+ rawContactsToAccountsCursor.moveToPosition(-1);
+ while (rawContactsToAccountsCursor.moveToNext()) {
+ final long rcId = rawContactsToAccountsCursor.getLong(0);
+ final long rc_accountId = rawContactsToAccountsCursor.getLong(1);
+ if (rc_accountId == accountId) {
+ rawContactIdsInSameAccount.add(rcId);
+ } else {
+ rawContactIdsInOtherAccount.add(rcId);
+ }
+ }
+ } finally {
+ rawContactsToAccountsCursor.close();
+ }
+ final int actionCode;
+ final int totalNumOfRawContactsInCandidate = rawContactIdsInSameAccount.size()
+ + rawContactIdsInOtherAccount.size();
+ if (totalNumOfRawContactsInCandidate >= AGGREGATION_CONTACT_SIZE_LIMIT) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Too many raw contacts (" + totalNumOfRawContactsInCandidate
+ + ") in the best matching contact, so skip aggregation");
+ }
+ actionCode = KEEP_SEPARATE;
+ } else {
+ actionCode = canJoinIntoContact(db, rawContactId,
+ rawContactIdsInSameAccount, rawContactIdsInOtherAccount);
+ }
+ if (actionCode == KEEP_SEPARATE) {
+ contactId = -1;
+ } else if (actionCode == RE_AGGREGATE) {
+ needReaggregate = true;
+ }
}
}
} else if (aggregationMode == RawContacts.AGGREGATION_MODE_DISABLED) {
@@ -781,12 +841,32 @@
if (contactId == currentContactId) {
// Aggregation unchanged
markAggregated(rawContactId);
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Aggregation unchanged");
+ }
} else if (contactId == -1) {
- // Splitting an aggregate
- createNewContactForRawContact(txContext, db, rawContactId);
+ // create new contact for [rawContactId]
+ createContactForRawContacts(db, txContext, Sets.newHashSet(rawContactId), null);
if (currentContactContentsCount > 0) {
updateAggregateData(txContext, currentContactId);
}
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "create new contact for rid=" + rawContactId);
+ }
+ } else if (needReaggregate) {
+ // re-aggregate
+ final Set<Long> allRawContactIdSet = new HashSet<Long>();
+ allRawContactIdSet.addAll(rawContactIdsInSameAccount);
+ allRawContactIdSet.addAll(rawContactIdsInOtherAccount);
+ // If there is no other raw contacts aggregated with the given raw contact currently,
+ // we might as well reuse it.
+ currentContactId = (currentContactId != 0 && currentContactContentsCount == 0)
+ ? currentContactId : 0;
+ reAggregateRawContacts(txContext, db, contactId, currentContactId, rawContactId,
+ allRawContactIdSet);
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Re-aggregating rid=" + rawContactId + " and cid=" + contactId);
+ }
} else {
// Joining with an existing aggregate
if (currentContactContentsCount == 0) {
@@ -797,6 +877,7 @@
mAggregatedPresenceDelete.execute();
}
+ clearSuperPrimarySetting(db, contactId, rawContactId);
setContactIdAndMarkAggregated(rawContactId, contactId);
computeAggregateData(db, contactId, mContactUpdate);
mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
@@ -807,218 +888,402 @@
if (currentContactId != 0) {
updateAggregateData(txContext, currentContactId);
}
- }
-
- if (contactIdToSplit != -1) {
- splitAutomaticallyAggregatedRawContacts(txContext, db, contactIdToSplit);
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Join rid=" + rawContactId + " with cid=" + contactId);
+ }
}
}
/**
- * @return true if the raw contact of {@code rawContactId} can be joined into the existing
- * contact of {@code of contactId}.
- *
- * Now a raw contact can be merged into a contact containing raw contacts from
- * the same account if there's at least one raw contact in those raw contacts
- * that shares at least one email address, phone number, or identity.
+ * Find out which mime-types are shared by raw contact of {@code rawContactId} and raw contacts
+ * of {@code contactId}. Clear the is_super_primary settings for these mime-types.
*/
- private boolean canJoinIntoContact(SQLiteDatabase db, long contactId,
- long rawContactId, long rawContactAccountId) {
- // First, list all raw contact IDs in contact [contactId] on account [rawContactAccountId],
- // excluding raw_contact [rawContactId].
+ private void clearSuperPrimarySetting(SQLiteDatabase db, long contactId, long rawContactId) {
+ final String[] args = {String.valueOf(contactId), String.valueOf(rawContactId)};
- // Append all found raw contact IDs into this SB to create a comma separated list of
- // the IDs.
- // We don't always need it, so lazily initialize it.
- StringBuilder rawContactIdsBuilder;
+ // Find out which mime-types are shared by raw contact of rawContactId and raw contacts
+ // of contactId
+ int index = 0;
+ final StringBuilder mimeTypeCondition = new StringBuilder();
+ mimeTypeCondition.append(" AND " + DataColumns.MIMETYPE_ID + " IN (");
- mSelectionArgs3[0] = String.valueOf(contactId);
- mSelectionArgs3[1] = String.valueOf(rawContactId);
- mSelectionArgs3[2] = String.valueOf(rawContactAccountId);
- final Cursor duplicatesCursor = db.rawQuery(
- "SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts.CONTACT_ID + "=?" +
- " AND " + RawContacts._ID + "!=?" +
- " AND " + RawContactsColumns.ACCOUNT_ID +"=?",
- mSelectionArgs3);
+ final Cursor c = db.rawQuery(
+ "SELECT DISTINCT(a." + DataColumns.MIMETYPE_ID + ")" +
+ " FROM (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE " +
+ Data.RAW_CONTACT_ID + " IN (SELECT " + RawContacts._ID + " FROM " +
+ Tables.RAW_CONTACTS + " WHERE " + RawContacts.CONTACT_ID + "=?1)) AS a" +
+ " JOIN (SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA + " WHERE "
+ + Data.RAW_CONTACT_ID + "=?2) AS b" +
+ " ON a." + DataColumns.MIMETYPE_ID + "=b." + DataColumns.MIMETYPE_ID,
+ args);
try {
- final int duplicateCount = duplicatesCursor.getCount();
- if (duplicateCount == 0) {
- return true; // No duplicates -- common case -- bail early.
- }
- if (VERBOSE_LOGGING) {
- Log.v(TAG, "canJoinIntoContact: " + duplicateCount + " duplicate(s) found");
- }
-
- rawContactIdsBuilder = new StringBuilder();
-
- duplicatesCursor.moveToPosition(-1);
- while (duplicatesCursor.moveToNext()) {
- if (rawContactIdsBuilder.length() > 0) {
- rawContactIdsBuilder.append(',');
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ if (index > 0) {
+ mimeTypeCondition.append(',');
}
- rawContactIdsBuilder.append(duplicatesCursor.getLong(0));
+ mimeTypeCondition.append(c.getLong((0)));
+ index++;
}
} finally {
- duplicatesCursor.close();
+ c.close();
}
- // Comma separated raw_contacts IDs.
- final String rawContactIds = rawContactIdsBuilder.toString();
+ if (index == 0) {
+ return;
+ }
- // See if there's any raw_contacts that share an email address, a phone number, or
- // an identity with raw_contact [rawContactId].
+ // Clear is_super_primary setting for all the mime-types exist in both raw contact
+ // of rawContactId and raw contacts of contactId
+ String superPrimaryUpdateSql = "UPDATE " + Tables.DATA +
+ " SET " + Data.IS_SUPER_PRIMARY + "=0" +
+ " WHERE (" + Data.RAW_CONTACT_ID +
+ " IN (SELECT " + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=?1)" +
+ " OR " + Data.RAW_CONTACT_ID + "=?2)";
- // First, check for the email address.
- mSelectionArgs2[0] = String.valueOf(mMimeTypeIdEmail);
- mSelectionArgs2[1] = String.valueOf(rawContactId);
- if (isFirstColumnGreaterThanZero(db,
- "SELECT count(*)" +
- " FROM " + Tables.DATA + " AS d1" +
- " JOIN " + Tables.DATA + " AS d2"
- + " ON (d1." + Email.ADDRESS + " = d2." + Email.ADDRESS + ")" +
- " WHERE d1." + DataColumns.MIMETYPE_ID + " = ?1" +
- " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" +
- " AND d1." + Data.RAW_CONTACT_ID + " = ?2" +
- " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")",
- mSelectionArgs2)) {
+ mimeTypeCondition.append(')');
+ superPrimaryUpdateSql += mimeTypeCondition.toString();
+ db.execSQL(superPrimaryUpdateSql, args);
+ }
+
+ /**
+ * @return JOIN if the raw contact of {@code rawContactId} can be joined into the existing
+ * contact of {@code contactId}. KEEP_SEPARATE if the raw contact of {@code rawContactId}
+ * cannot be joined into the existing contact of {@code contactId}. RE_AGGREGATE if raw contact
+ * of {@code rawContactId} and all the raw contacts of contact of {@code contactId} need to be
+ * re-aggregated.
+ *
+ * If contact of {@code contactId} doesn't contain any raw contacts from the same account as
+ * raw contact of {@code rawContactId}, join raw contact with contact if there is no identity
+ * mismatch between them on the same namespace, otherwise, keep them separate.
+ *
+ * If contact of {@code contactId} contains raw contacts from the same account as raw contact of
+ * {@code rawContactId}, join raw contact with contact if there's at least one raw contact in
+ * those raw contacts that shares at least one email address, phone number, or identity;
+ * otherwise, re-aggregate raw contact and all the raw contacts of contact.
+ */
+ private int canJoinIntoContact(SQLiteDatabase db, long rawContactId,
+ Set<Long> rawContactIdsInSameAccount, Set<Long> rawContactIdsInOtherAccount ) {
+
+ if (rawContactIdsInSameAccount.isEmpty()) {
+ final String rid = String.valueOf(rawContactId);
+ final String ridsInOtherAccts = TextUtils.join(",", rawContactIdsInOtherAccount);
+ // If there is no identity match between raw contact of [rawContactId] and
+ // any raw contact in other accounts on the same namespace, and there is at least
+ // one identity mismatch exist, keep raw contact separate from contact.
+ if (DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts,
+ /* isIdentityMatching =*/ true, /* countOnly =*/ true), null) == 0 &&
+ DatabaseUtils.longForQuery(db, buildIdentityMatchingSql(rid, ridsInOtherAccts,
+ /* isIdentityMatching =*/ false, /* countOnly =*/ true), null) > 0) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: no duplicates, but has no matching identity " +
+ "and has mis-matching identity on the same namespace between rid=" +
+ rid + " and ridsInOtherAccts=" + ridsInOtherAccts);
+ }
+ return KEEP_SEPARATE; // has identity and identity doesn't match
+ } else {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: can join the first raw contact from the same " +
+ "account without any identity mismatch.");
+ }
+ return JOIN; // no identity or identity match
+ }
+ }
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: " + rawContactIdsInSameAccount.size() +
+ " duplicate(s) found");
+ }
+
+
+ final Set<Long> rawContactIdSet = new HashSet<Long>();
+ rawContactIdSet.add(rawContactId);
+ if (rawContactIdsInSameAccount.size() > 0 &&
+ isDataMaching(db, rawContactIdSet, rawContactIdsInSameAccount)) {
if (VERBOSE_LOGGING) {
- Log.v(TAG, "Relaxing rule SA: email match found for rid=" + rawContactId);
+ Log.v(TAG, "canJoinIntoContact: join if there is a data matching found in the " +
+ "same account");
+ }
+ return JOIN;
+ } else {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: re-aggregate rid=" + rawContactId +
+ " with its best matching contact to connected component");
+ }
+ return RE_AGGREGATE;
+ }
+ }
+
+ private interface RawContactMatchingSelectionStatement {
+ String SELECT_COUNT = "SELECT count(*) " ;
+ String SELECT_ID = "SELECT d1." + Data.RAW_CONTACT_ID + ",d2." + Data.RAW_CONTACT_ID ;
+ }
+
+ /**
+ * Build sql to check if there is any identity match/mis-match between two sets of raw contact
+ * ids on the same namespace.
+ */
+ private String buildIdentityMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+ boolean isIdentityMatching, boolean countOnly) {
+ final String identityType = String.valueOf(mMimeTypeIdIdentity);
+ final String matchingOperator = (isIdentityMatching) ? "=" : "!=";
+ final String sql =
+ " FROM " + Tables.DATA + " AS d1" +
+ " JOIN " + Tables.DATA + " AS d2" +
+ " ON (d1." + Identity.IDENTITY + matchingOperator +
+ " d2." + Identity.IDENTITY + " AND" +
+ " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" +
+ " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + identityType +
+ " AND d2." + DataColumns.MIMETYPE_ID + " = " + identityType +
+ " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+ " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
+ return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+ RawContactMatchingSelectionStatement.SELECT_ID + sql;
+ }
+
+ private String buildEmailMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+ boolean countOnly) {
+ final String emailType = String.valueOf(mMimeTypeIdEmail);
+ final String sql =
+ " FROM " + Tables.DATA + " AS d1" +
+ " JOIN " + Tables.DATA + " AS d2" +
+ " ON lower(d1." + Email.ADDRESS + ")= lower(d2." + Email.ADDRESS + ")" +
+ " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + emailType +
+ " AND d2." + DataColumns.MIMETYPE_ID + " = " + emailType +
+ " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+ " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")";
+ return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+ RawContactMatchingSelectionStatement.SELECT_ID + sql;
+ }
+
+ private String buildPhoneMatchingSql(String rawContactIdSet1, String rawContactIdSet2,
+ boolean countOnly) {
+ // It's a bit tricker because it has to be consistent with
+ // updateMatchScoresBasedOnPhoneMatches().
+ final String phoneType = String.valueOf(mMimeTypeIdPhone);
+ final String sql =
+ " FROM " + Tables.PHONE_LOOKUP + " AS p1" +
+ " JOIN " + Tables.DATA + " AS d1 ON " +
+ "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" +
+ " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH +
+ "=p2." + PhoneLookupColumns.MIN_MATCH + ")" +
+ " JOIN " + Tables.DATA + " AS d2 ON " +
+ "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" +
+ " WHERE d1." + DataColumns.MIMETYPE_ID + " = " + phoneType +
+ " AND d2." + DataColumns.MIMETYPE_ID + " = " + phoneType +
+ " AND d1." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet1 + ")" +
+ " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIdSet2 + ")" +
+ " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + "," +
+ String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter()) +
+ ")";
+ return (countOnly) ? RawContactMatchingSelectionStatement.SELECT_COUNT + sql :
+ RawContactMatchingSelectionStatement.SELECT_ID + sql;
+ }
+
+ private String buildExceptionMatchingSql(String rawContactIdSet1, String rawContactIdSet2) {
+ return "SELECT " + AggregationExceptions.RAW_CONTACT_ID1 + ", " +
+ AggregationExceptions.RAW_CONTACT_ID2 +
+ " FROM " + Tables.AGGREGATION_EXCEPTIONS +
+ " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 + " IN (" +
+ rawContactIdSet1 + ")" +
+ " AND " + AggregationExceptions.RAW_CONTACT_ID2 + " IN (" + rawContactIdSet2 + ")" +
+ " AND " + AggregationExceptions.TYPE + "=" +
+ AggregationExceptions.TYPE_KEEP_TOGETHER ;
+ }
+
+ private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query) {
+ return DatabaseUtils.longForQuery(db, query, null) > 0;
+ }
+
+ /**
+ * If there's any identity, email address or a phone number matching between two raw contact
+ * sets.
+ */
+ private boolean isDataMaching(SQLiteDatabase db, Set<Long> rawContactIdSet1,
+ Set<Long> rawContactIdSet2) {
+ final String rawContactIds1 = TextUtils.join(",", rawContactIdSet1);
+ final String rawContactIds2 = TextUtils.join(",", rawContactIdSet2);
+ // First, check for the identity
+ if (isFirstColumnGreaterThanZero(db, buildIdentityMatchingSql(
+ rawContactIds1, rawContactIds2, /* isIdentityMatching =*/ true,
+ /* countOnly =*/true))) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "canJoinIntoContact: identity match found between " + rawContactIds1 +
+ " and " + rawContactIds2);
}
return true;
}
- // Next, check for the identity.
- mSelectionArgs2[0] = String.valueOf(mMimeTypeIdIdentity);
- mSelectionArgs2[1] = String.valueOf(rawContactId);
+ // Next, check for the email address.
if (isFirstColumnGreaterThanZero(db,
- "SELECT count(*)" +
- " FROM " + Tables.DATA + " AS d1" +
- " JOIN " + Tables.DATA + " AS d2"
- + " ON (d1." + Identity.IDENTITY + " = d2." + Identity.IDENTITY + " AND" +
- " d1." + Identity.NAMESPACE + " = d2." + Identity.NAMESPACE + " )" +
- " WHERE d1." + DataColumns.MIMETYPE_ID + " = ?1" +
- " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" +
- " AND d1." + Data.RAW_CONTACT_ID + " = ?2" +
- " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")",
- mSelectionArgs2)) {
+ buildEmailMatchingSql(rawContactIds1, rawContactIds2, true))) {
if (VERBOSE_LOGGING) {
- Log.v(TAG, "Relaxing rule SA: identity match found for rid=" + rawContactId);
+ Log.v(TAG, "canJoinIntoContact: email match found between " + rawContactIds1 +
+ " and " + rawContactIds2);
}
return true;
}
// Lastly, the phone number.
- // It's a bit tricker because it has to be consistent with
- // updateMatchScoresBasedOnPhoneMatches().
- mSelectionArgs3[0] = String.valueOf(mMimeTypeIdPhone);
- mSelectionArgs3[1] = String.valueOf(rawContactId);
- mSelectionArgs3[2] = String.valueOf(mDbHelper.getUseStrictPhoneNumberComparisonParameter());
-
if (isFirstColumnGreaterThanZero(db,
- "SELECT count(*)" +
- " FROM " + Tables.PHONE_LOOKUP + " AS p1" +
- " JOIN " + Tables.DATA + " AS d1 ON " +
- "(d1." + Data._ID + "=p1." + PhoneLookupColumns.DATA_ID + ")" +
- " JOIN " + Tables.PHONE_LOOKUP + " AS p2 ON (p1." + PhoneLookupColumns.MIN_MATCH +
- "=p2." + PhoneLookupColumns.MIN_MATCH + ")" +
- " JOIN " + Tables.DATA + " AS d2 ON " +
- "(d2." + Data._ID + "=p2." + PhoneLookupColumns.DATA_ID + ")" +
- " WHERE d1." + DataColumns.MIMETYPE_ID + " = ?1" +
- " AND d2." + DataColumns.MIMETYPE_ID + " = ?1" +
- " AND d1." + Data.RAW_CONTACT_ID + " = ?2" +
- " AND d2." + Data.RAW_CONTACT_ID + " IN (" + rawContactIds + ")" +
- " AND PHONE_NUMBERS_EQUAL(d1." + Phone.NUMBER + ",d2." + Phone.NUMBER + ",?3)",
- mSelectionArgs3)) {
+ buildPhoneMatchingSql(rawContactIds1, rawContactIds2, true))) {
if (VERBOSE_LOGGING) {
- Log.v(TAG, "Relaxing rule SA: phone match found for rid=" + rawContactId);
+ Log.v(TAG, "canJoinIntoContact: phone match found between " + rawContactIds1 +
+ " and " + rawContactIds2);
}
return true;
}
- if (VERBOSE_LOGGING) {
- Log.v(TAG, "Rule SA splitting up cid=" + contactId + " for rid=" + rawContactId);
- }
return false;
}
- private boolean isFirstColumnGreaterThanZero(SQLiteDatabase db, String query,
- String[] selectionArgs) {
- final Cursor cursor = db.rawQuery(query, selectionArgs);
- try {
- return cursor.moveToFirst() && (cursor.getInt(0) > 0);
- } finally {
- cursor.close();
+ /**
+ * Re-aggregate rawContact of {@code rawContactId} and all the raw contacts of
+ * {@code existingRawContactIds} into connected components. This only happens when a given
+ * raw contacts cannot be joined with its best matching contacts directly.
+ *
+ * Two raw contacts are considered connected if they share at least one email address, phone
+ * number or identity. Create new contact for each connected component except the very first
+ * one that doesn't contain rawContactId of {@code rawContactId}.
+ */
+ private void reAggregateRawContacts(TransactionContext txContext, SQLiteDatabase db,
+ long contactId, long currentContactId, long rawContactId,
+ Set<Long> existingRawContactIds) {
+ // Find the connected component based on the aggregation exceptions or
+ // identity/email/phone matching for all the raw contacts of [contactId] and the give
+ // raw contact.
+ final Set<Long> allIds = new HashSet<Long>();
+ allIds.add(rawContactId);
+ allIds.addAll(existingRawContactIds);
+ final Set<Set<Long>> connectedRawContactSets = findConnectedRawContacts(db, allIds);
+
+ if (connectedRawContactSets.size() == 1) {
+ // If everything is connected, create one contact with [contactId]
+ createContactForRawContacts(db, txContext, connectedRawContactSets.iterator().next(),
+ contactId);
+ } else {
+ for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
+ if (connectedRawContactIds.contains(rawContactId)) {
+ // crate contact for connect component containing [rawContactId], reuse
+ // [currentContactId] if possible.
+ createContactForRawContacts(db, txContext, connectedRawContactIds,
+ currentContactId == 0 ? null : currentContactId);
+ connectedRawContactSets.remove(connectedRawContactIds);
+ break;
+ }
+ }
+ // Create new contact for each connected component except the last one. The last one
+ // will reuse [contactId]. Only the last one can reuse [contactId] when all other raw
+ // contacts has already been assigned new contact Id, so that the contact aggregation
+ // stats could be updated correctly.
+ int index = connectedRawContactSets.size();
+ for (Set<Long> connectedRawContactIds : connectedRawContactSets) {
+ if (index > 1) {
+ createContactForRawContacts(db, txContext, connectedRawContactIds, null);
+ index--;
+ } else {
+ createContactForRawContacts(db, txContext, connectedRawContactIds, contactId);
+ }
+ }
}
}
/**
- * Breaks up an existing aggregate when a new raw contact is inserted that has
- * come from the same account as one of the raw contacts in this aggregate.
+ * Partition the given raw contact Ids to connected component based on aggregation exception,
+ * identity matching, email matching or phone matching.
*/
- private void splitAutomaticallyAggregatedRawContacts(
- TransactionContext txContext, SQLiteDatabase db, long contactId) {
- mSelectionArgs1[0] = String.valueOf(contactId);
- int count = (int) DatabaseUtils.longForQuery(db,
- "SELECT COUNT(" + RawContacts._ID + ")" +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
- if (count < 2) {
- // A single-raw-contact aggregate does not need to be split up
- return;
+ private Set<Set<Long>> findConnectedRawContacts(SQLiteDatabase db, Set<Long> rawContactIdSet) {
+ // Connections between two raw contacts
+ final Multimap<Long, Long> matchingRawIdPairs = HashMultimap.create();
+ String rawContactIds = TextUtils.join(",", rawContactIdSet);
+ findIdPairs(db, buildExceptionMatchingSql(rawContactIds, rawContactIds),
+ matchingRawIdPairs);
+ findIdPairs(db, buildIdentityMatchingSql(rawContactIds, rawContactIds,
+ /* isIdentityMatching =*/ true, /* countOnly =*/false), matchingRawIdPairs);
+ findIdPairs(db, buildEmailMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
+ matchingRawIdPairs);
+ findIdPairs(db, buildPhoneMatchingSql(rawContactIds, rawContactIds, /* countOnly =*/false),
+ matchingRawIdPairs);
+
+ return findConnectedComponents(rawContactIdSet, matchingRawIdPairs);
+ }
+
+ /**
+ * Given a set of raw contact ids {@code rawContactIdSet} and the connection among them
+ * {@code matchingRawIdPairs}, find the connected components.
+ */
+ @VisibleForTesting
+ static Set<Set<Long>> findConnectedComponents(Set<Long> rawContactIdSet, Multimap<Long,
+ Long> matchingRawIdPairs) {
+ Set<Set<Long>> connectedRawContactSets = new HashSet<Set<Long>>();
+ Set<Long> visited = new HashSet<Long>();
+ for (Long id : rawContactIdSet) {
+ if (!visited.contains(id)) {
+ Set<Long> set = new HashSet<Long>();
+ findConnectedComponentForRawContact(matchingRawIdPairs, visited, id, set);
+ connectedRawContactSets.add(set);
+ }
}
+ return connectedRawContactSets;
+ }
- // Find all constituent raw contacts that are not held together by
- // an explicit aggregation exception
- String query =
- "SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts.CONTACT_ID + "=?" +
- " AND " + RawContacts._ID + " NOT IN " +
- "(SELECT " + AggregationExceptions.RAW_CONTACT_ID1 +
- " FROM " + Tables.AGGREGATION_EXCEPTIONS +
- " WHERE " + AggregationExceptions.TYPE + "="
- + AggregationExceptions.TYPE_KEEP_TOGETHER +
- " UNION SELECT " + AggregationExceptions.RAW_CONTACT_ID2 +
- " FROM " + Tables.AGGREGATION_EXCEPTIONS +
- " WHERE " + AggregationExceptions.TYPE + "="
- + AggregationExceptions.TYPE_KEEP_TOGETHER +
- ")";
+ private static void findConnectedComponentForRawContact(Multimap<Long, Long> connections,
+ Set<Long> visited, Long rawContactId, Set<Long> results) {
+ visited.add(rawContactId);
+ results.add(rawContactId);
+ for (long match : connections.get(rawContactId)) {
+ if (!visited.contains(match)) {
+ findConnectedComponentForRawContact(connections, visited, match, results);
+ }
+ }
+ }
- Cursor cursor = db.rawQuery(query, mSelectionArgs1);
+ /**
+ * Given a query which will return two non-null IDs in the first two columns as results, this
+ * method will put two entries into the given result map for each pair of different IDs, one
+ * keyed by each ID.
+ */
+ private void findIdPairs(SQLiteDatabase db, String query, Multimap<Long, Long> results) {
+ Cursor cursor = db.rawQuery(query, null);
try {
- // Process up to count-1 raw contact, leaving the last one alone.
- for (int i = 0; i < count - 1; i++) {
- if (!cursor.moveToNext()) {
- break;
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ long idA = cursor.getLong(0);
+ long idB = cursor.getLong(1);
+ if (idA != idB) {
+ results.put(idA, idB);
+ results.put(idB, idA);
}
- long rawContactId = cursor.getLong(0);
- createNewContactForRawContact(txContext, db, rawContactId);
}
} finally {
cursor.close();
}
- if (contactId > 0) {
- updateAggregateData(txContext, contactId);
- }
}
/**
- * Creates a stand-alone Contact for the given raw contact ID. This is only called
- * when splitting an existing merged contact into separate raw contacts.
+ * Creates a new Contact for a given set of the raw contacts of {@code rawContactIds} if the
+ * given contactId is null. Otherwise, regroup them into contact with {@code contactId}.
*/
- private void createNewContactForRawContact(
- TransactionContext txContext, SQLiteDatabase db, long rawContactId) {
- // All split contacts should automatically be unpinned.
- unpinRawContact(rawContactId);
- mSelectionArgs1[0] = String.valueOf(rawContactId);
- computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
- mContactInsert);
- long contactId = mContactInsert.executeInsert();
- setContactIdAndMarkAggregated(rawContactId, contactId);
- mDbHelper.updateContactVisible(txContext, contactId);
- setPresenceContactId(rawContactId, contactId);
- updateAggregatedStatusUpdate(contactId);
+ private void createContactForRawContacts(SQLiteDatabase db, TransactionContext txContext,
+ Set<Long> rawContactIds, Long contactId) {
+ if (rawContactIds.isEmpty()) {
+ // No raw contact id is provided.
+ return;
+ }
+
+ // If contactId is not provided, generates a new one.
+ if (contactId == null) {
+ mSelectionArgs1[0]= String.valueOf(rawContactIds.iterator().next());
+ computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
+ mContactInsert);
+ contactId = mContactInsert.executeInsert();
+ }
+ for (Long rawContactId : rawContactIds) {
+ // Regrouped contacts should automatically be unpinned.
+ unpinRawContact(rawContactId);
+ setContactIdAndMarkAggregated(rawContactId, contactId);
+ setPresenceContactId(rawContactId, contactId);
+ }
+ updateAggregateData(txContext, contactId);
}
private static class RawContactIdQuery {
@@ -1563,7 +1828,7 @@
private interface EmailLookupQuery {
String TABLE = Tables.DATA + " dataA"
+ " JOIN " + Tables.DATA + " dataB" +
- " ON (" + "dataA." + Email.DATA + "=dataB." + Email.DATA + ")"
+ " ON lower(" + "dataA." + Email.DATA + ")=lower(dataB." + Email.DATA + ")"
+ " JOIN " + Tables.RAW_CONTACTS +
" ON (dataB." + Data.RAW_CONTACT_ID + " = "
+ Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
diff --git a/src/com/android/providers/contacts/util/UserUtils.java b/src/com/android/providers/contacts/util/UserUtils.java
index 0df5a24..74fd2e7 100644
--- a/src/com/android/providers/contacts/util/UserUtils.java
+++ b/src/com/android/providers/contacts/util/UserUtils.java
@@ -61,16 +61,6 @@
Log.v(TAG, "getCorpUserId: myUser=" + myUser);
}
- // TODO DevicePolicyManager is not mockable -- the constructor is private.
- // Test it somehow.
- if (getDevicePolicyManager(context)
- .getCrossProfileCallerIdDisabled(new UserHandle(myUser))) {
- if (VERBOSE_LOGGING) {
- Log.v(TAG, "Enterprise caller-id disabled.");
- }
- return -1;
- }
-
// Check each user.
for (UserInfo ui : um.getUsers()) {
if (!ui.isManagedProfile()) {
@@ -82,10 +72,21 @@
}
// Check if it's linked to the current user.
if (parent.id == myUser) {
- if (VERBOSE_LOGGING) {
- Log.v(TAG, "Corp user=" + ui.id);
+ // Check if profile is blocking calling id.
+ // TODO DevicePolicyManager is not mockable -- the constructor is private.
+ // Test it somehow.
+ if (getDevicePolicyManager(context)
+ .getCrossProfileCallerIdDisabled(ui.getUserHandle())) {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Enterprise caller-id disabled for user " + ui.id);
+ }
+ return -1;
+ } else {
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Corp user=" + ui.id);
+ }
+ return ui.id;
}
- return ui.id;
}
}
if (VERBOSE_LOGGING) {
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index b90e88b..3778380 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -232,16 +232,24 @@
}
protected Uri insertOrganization(long rawContactId, ContentValues values) {
- return insertOrganization(rawContactId, values, false);
+ return insertOrganization(rawContactId, values, false, false);
}
protected Uri insertOrganization(long rawContactId, ContentValues values, boolean primary) {
+ return insertOrganization(rawContactId, values, primary, false);
+ }
+
+ protected Uri insertOrganization(long rawContactId, ContentValues values, boolean primary,
+ boolean superPrimary) {
values.put(Data.RAW_CONTACT_ID, rawContactId);
values.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
values.put(Organization.TYPE, Organization.TYPE_WORK);
if (primary) {
values.put(Data.IS_PRIMARY, 1);
}
+ if (superPrimary) {
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ }
Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
return resultUri;
@@ -252,11 +260,21 @@
}
protected Uri insertPhoneNumber(long rawContactId, String phoneNumber, boolean primary) {
- return insertPhoneNumber(rawContactId, phoneNumber, primary, Phone.TYPE_HOME);
+ return insertPhoneNumber(rawContactId, phoneNumber, primary, false, Phone.TYPE_HOME);
+ }
+
+ protected Uri insertPhoneNumber(long rawContactId, String phoneNumber, boolean primary,
+ boolean superPrimary) {
+ return insertPhoneNumber(rawContactId, phoneNumber, primary, superPrimary, Phone.TYPE_HOME);
}
protected Uri insertPhoneNumber(long rawContactId, String phoneNumber, boolean primary,
int type) {
+ return insertPhoneNumber(rawContactId, phoneNumber, primary, false, type);
+ }
+
+ protected Uri insertPhoneNumber(long rawContactId, String phoneNumber, boolean primary,
+ boolean superPrimary, int type) {
ContentValues values = new ContentValues();
values.put(Data.RAW_CONTACT_ID, rawContactId);
values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
@@ -265,6 +283,9 @@
if (primary) {
values.put(Data.IS_PRIMARY, 1);
}
+ if (superPrimary) {
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ }
Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
return resultUri;
@@ -524,6 +545,16 @@
assertEquals(1, mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null));
}
+ protected void setRawContactCustomization(long rawContactId, int starred, int sendToVoiceMail) {
+ ContentValues values = new ContentValues();
+
+ values.put(RawContacts.STARRED, starred);
+ values.put(RawContacts.SEND_TO_VOICEMAIL, sendToVoiceMail);
+
+ assertEquals(1, mResolver.update(ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, rawContactId), values, null, null));
+ }
+
protected void markInvisible(long contactId) {
// There's no api for this, so we just tweak the DB directly.
SQLiteDatabase db = ((ContactsProvider2) getProvider()).getDatabaseHelper()
@@ -712,6 +743,20 @@
}
}
+ protected void assertSuperPrimary(Long dataId, boolean isSuperPrimary) {
+ final String[] projection = new String[]{Data.MIMETYPE, Data._ID, Data.IS_SUPER_PRIMARY};
+ Cursor c = mResolver.query(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
+ projection, null, null, null);
+
+ c.moveToFirst();
+ if (isSuperPrimary) {
+ assertEquals(1, c.getInt(c.getColumnIndexOrThrow(Data.IS_SUPER_PRIMARY)));
+ } else {
+ assertEquals(0, c.getInt(c.getColumnIndexOrThrow(Data.IS_SUPER_PRIMARY)));
+ }
+
+ }
+
protected void assertDataRow(ContentValues actual, String expectedMimetype,
Object... expectedArguments) {
assertEquals(actual.toString(), expectedMimetype, actual.getAsString(Data.MIMETYPE));
diff --git a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
index be14f45..c5bc6f6 100644
--- a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
+++ b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
@@ -122,6 +122,9 @@
public void testIsDirectoryProvider() {
ProviderInfo provider = new ProviderInfo();
+ // Null -- just return false.
+ assertFalse(ContactDirectoryManager.isDirectoryProvider(null));
+
// No metadata
assertFalse(ContactDirectoryManager.isDirectoryProvider(provider));
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 7185368..bad7d5d 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -66,6 +66,7 @@
import android.util.Log;
import com.android.providers.contacts.util.MockSharedPreferences;
+
import com.google.android.collect.Sets;
import java.io.File;
@@ -220,6 +221,16 @@
public Bundle getUserRestrictions(UserHandle userHandle) {
return new Bundle();
}
+
+ @Override
+ public boolean hasUserRestriction(String restrictionKey) {
+ return false;
+ }
+
+ @Override
+ public boolean hasUserRestriction(String restrictionKey, UserHandle userHandle) {
+ return false;
+ }
}
/**
diff --git a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
index 694f0f3..a5aa7c7 100644
--- a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
+++ b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
@@ -96,17 +96,4 @@
public Resources getResourcesForApplication(String appPackageName) {
return new ContactsMockResources();
}
-
- @Override
- public List<ProviderInfo> queryContentProviders(String processName, int uid, int flags) {
- final List<ProviderInfo> ret = Lists.newArrayList();
- if (mPackages == null) return ret;
- for (PackageInfo packageInfo : mPackages) {
- if (packageInfo.providers == null) continue;
- for (ProviderInfo providerInfo : packageInfo.providers) {
- ret.add(providerInfo);
- }
- }
- return ret;
- }
}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 0145fef..0e4d9d6 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -6430,12 +6430,12 @@
setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
rawContactId1, rawContactId2);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+ rawContactId1, rawContactId2);
ContentValues values = new ContentValues();
values.put(Data.IS_SUPER_PRIMARY, 1);
mResolver.update(photoUri2, values, null, null);
- setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
- rawContactId1, rawContactId2);
contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
queryContactId(rawContactId1));
assertStoredValue(contactUri, Contacts.PHOTO_ID, photoId2);
diff --git a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
index cb0766e..0e0264c 100644
--- a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
@@ -43,6 +43,12 @@
import com.android.providers.contacts.testutil.RawContactUtil;
import com.google.android.collect.Lists;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
/**
* Unit tests for {@link ContactAggregator}.
@@ -589,7 +595,7 @@
assertNotAggregated(rawContactId1, rawContactId2);
}
- public void testSplitBecauseOfMultipleAffinities() {
+ public void testReaggregateBecauseOfMultipleAffinities() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
ACCOUNT_1);
long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
@@ -597,7 +603,8 @@
assertAggregated(rawContactId1, rawContactId2);
// The aggregate this raw contact could join has a raw contact from the same account,
- // let's not aggregate and break up the existing aggregate because of the ambiguity
+ // The ambiguity will trigger re-aggregation. And since no data matching exists, all
+ // three raw contacts are broken-up.
long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
ACCOUNT_1);
assertNotAggregated(rawContactId1, rawContactId3);
@@ -673,10 +680,9 @@
long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
- insertPhoneNumber(rawContactId1, "1234567890");
long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
- insertPhoneNumber(rawContactId2, "1234567890");
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
@@ -733,7 +739,7 @@
assertAggregated(rawContactId1, rawContactId3);
// The aggregate this raw contact could join has a raw contact from the same account,
- // let's not aggregate and break up the existing aggregate because of the ambiguity
+ // Let's re-aggregate the existing aggregate because of the ambiguity
long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
ACCOUNT_1);
assertAggregated(rawContactId1, rawContactId2); // Aggregation exception
@@ -742,7 +748,85 @@
assertNotAggregated(rawContactId3, rawContactId4);
}
- public void testNonAggregationFromSameAccount() {
+ public void testNonSplitWhenIdentityMatch() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertIdentity(rawContactId1, "iden", "namespace");
+ insertIdentity(rawContactId1, "iden2", "namespace");
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_2);
+ insertIdentity(rawContactId2, "iden", "namespace");
+ assertAggregated(rawContactId1, rawContactId2);
+
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ assertAggregated(rawContactId1, rawContactId2);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId2, rawContactId3);
+ }
+
+ public void testReAggregateToConnectedComponent() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertPhoneNumber(rawContactId1, "111");
+ setRawContactCustomization(rawContactId1, 1, 1);
+ long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_2);
+ insertPhoneNumber(rawContactId2, "111");
+ setRawContactCustomization(rawContactId2, 1, 1);
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_3);
+ insertIdentity(rawContactId3, "iden", "namespace");
+ long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ new Account("account_name_4", "account_type_4"));
+ insertIdentity(rawContactId4, "iden", "namespace");
+
+ assertAggregated(rawContactId1, rawContactId2);
+ assertAggregated(rawContactId1, rawContactId3);
+ assertAggregated(rawContactId1, rawContactId4);
+ assertStoredValue(getContactUriForRawContact(rawContactId1),
+ Contacts.STARRED, 1);
+ assertStoredValue(getContactUriForRawContact(rawContactId4),
+ Contacts.SEND_TO_VOICEMAIL, 0);
+
+ long rawContactId5 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+
+ assertAggregated(rawContactId1, rawContactId2);
+ assertAggregated(rawContactId3, rawContactId4);
+ assertNotAggregated(rawContactId1, rawContactId3);
+ assertNotAggregated(rawContactId1, rawContactId5);
+ assertNotAggregated(rawContactId3, rawContactId5);
+ assertStoredValue(getContactUriForRawContact(rawContactId1),
+ Contacts.STARRED, 1);
+ assertStoredValue(getContactUriForRawContact(rawContactId1),
+ Contacts.SEND_TO_VOICEMAIL, 1);
+
+ assertStoredValue(getContactUriForRawContact(rawContactId3),
+ Contacts.STARRED, 0);
+ assertStoredValue(getContactUriForRawContact(rawContactId3),
+ Contacts.SEND_TO_VOICEMAIL, 0);
+
+ assertStoredValue(getContactUriForRawContact(rawContactId5),
+ Contacts.STARRED, 0);
+ assertStoredValue(getContactUriForRawContact(rawContactId5),
+ Contacts.SEND_TO_VOICEMAIL, 0);
+ }
+
+ public void testNonAggregationFromDifferentAccountWithIdentityMisMatch() {
+ long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+ ACCOUNT_1);
+ insertIdentity(rawContactId1, "iden1", "namespace");
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ insertIdentity(rawContactId2, "iden2", "namespace");
+ DataUtil.insertStructuredName(mResolver, rawContactId2, "John", "Doe");
+
+ // rawContact1 and rawContact2 have different identities on the same namespace,
+ // which prevent them to aggregate.
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
+ public void testNonAggregationFromSameAccountWithoutAnyDataMatching() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
ACCOUNT_1);
long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
@@ -766,7 +850,7 @@
assertNotAggregated(rawContactId1, rawContactId2);
}
- public void testAggregationFromSameAccountEmailSame() {
+ public void testAggregationFromSameAccountEmailSame_IgnoreCase() {
long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
ACCOUNT_1);
insertEmail(rawContactId1, "lightning@android.com");
@@ -776,6 +860,16 @@
insertEmail(rawContactId2, "lightning@android.com");
assertAggregated(rawContactId1, rawContactId2);
+
+ long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "Jane", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId3, "jane@android.com");
+
+ long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "Jane", "Doe",
+ ACCOUNT_1);
+ insertEmail(rawContactId4, "JANE@ANDROID.COM");
+
+ assertAggregated(rawContactId3, rawContactId4);
}
public void testNonAggregationFromSameAccountEmailDifferent() {
@@ -905,7 +999,7 @@
long rawContactId2 = RawContactUtil.createRawContact(mResolver);
DataUtil.insertStructuredName(mResolver, rawContactId2, "Charles", "Muntz");
- insertEmail(rawContactId2, "up@android.com");
+ insertEmail(rawContactId2, "Up@Android.com");
long contactId1 = queryContactId(rawContactId1);
long contactId2 = queryContactId(rawContactId2);
@@ -1486,6 +1580,121 @@
cursor.close();
}
+ public void testAggregation_clearSuperPrimary() {
+ // Three types of mime-type super primary merging are tested here
+ // 1. both raw contacts have super primary phone numbers
+ // 2. both raw contacts have emails, but only one has super primary email
+ // 3. only raw contact1 has organizations and it has set the super primary organization
+ ContentValues values = new ContentValues();
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ Uri uri_phone1 = insertPhoneNumber(rawContactId1, "(222)222-2222", false, false);
+ Uri uri_email1 = insertEmail(rawContactId1, "one@gmail.com", true, true);
+ values.clear();
+ values.put(Organization.COMPANY, "Monsters Inc");
+ Uri uri_org1 = insertOrganization(rawContactId1, values, true, true);
+ values.clear();
+ values.put(Organization.TITLE, "CEO");
+ Uri uri_org2 = insertOrganization(rawContactId1, values, false, false);
+
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+ Uri uri_phone2 = insertPhoneNumber(rawContactId2, "(333)333-3333", false, false);
+ Uri uri_email2 = insertEmail(rawContactId2, "two@gmail.com", false, false);
+
+ // Two raw contacts with same phone number will trigger the aggregation
+ Uri uri_phone3 = insertPhoneNumber(rawContactId1, "(111)111-1111", true, true);
+ Uri uri_phone4 = insertPhoneNumber(rawContactId2, "1(111)111-1111", true, true);
+
+ // After aggregation, the super primary flag should be cleared for both case 1 and case 2,
+ // i.e., phone and email mime-types. Only case 3, i.e. organization mime-type, has the
+ // super primary flag unchanged.
+ assertAggregated(rawContactId1, rawContactId2);
+ assertSuperPrimary(ContentUris.parseId(uri_phone1), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone2), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone3), false);
+ assertSuperPrimary(ContentUris.parseId(uri_phone4), false);
+
+ assertSuperPrimary(ContentUris.parseId(uri_email1), false);
+ assertSuperPrimary(ContentUris.parseId(uri_email2), false);
+
+ assertSuperPrimary(ContentUris.parseId(uri_org1), true);
+ assertSuperPrimary(ContentUris.parseId(uri_org2), false);
+ }
+
+ public void testAggregation_clearSuperPrimarySingleMimetype() {
+ // Setup: two raw contacts, each has a single name. One of the names is super primary.
+ long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+ final Uri uri = DataUtil.insertStructuredName(mResolver, rawContactId1, "name1",
+ null, null, /* isSuperPrimary = */ true);
+
+ // Sanity check.
+ assertStoredValue(uri, Data.IS_SUPER_PRIMARY, 1);
+
+ // Action: aggregate
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+
+ // Verify: name is still super primary
+ assertStoredValue(uri, Data.IS_SUPER_PRIMARY, 1);
+ }
+
+ public void testNotAggregate_TooManyRawContactsInCandidate() {
+ long preId= 0;
+ for (int i = 0; i < ContactAggregator.AGGREGATION_CONTACT_SIZE_LIMIT; i++) {
+ long id = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
+ if (i > 0) {
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, preId, id);
+ }
+ preId = id;
+ }
+ // Although the newly added raw contact matches the names with other raw contacts,
+ // but the best matching contact has already meets the size limit, so keep the new raw
+ // contact separate from other raw contacts.
+ long newId = RawContactUtil.createRawContact(mResolver,
+ new Account("account_new", "new account type"));
+ DataUtil.insertStructuredName(mResolver, newId, "John", "Doe");
+ assertNotAggregated(preId, newId);
+ assertTrue(queryContactId(newId) > 0);
+ }
+
+ public void testFindConnectedRawContacts() {
+ Set<Long> rawContactIdSet = new HashSet<Long>();
+ rawContactIdSet.addAll(Arrays.asList(1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l));
+
+ Multimap<Long, Long> matchingrawIdPairs = HashMultimap.create();
+ matchingrawIdPairs.put(1l, 2l);
+ matchingrawIdPairs.put(2l, 1l);
+
+ matchingrawIdPairs.put(1l, 7l);
+ matchingrawIdPairs.put(7l, 1l);
+
+ matchingrawIdPairs.put(2l, 3l);
+ matchingrawIdPairs.put(3l, 2l);
+
+ matchingrawIdPairs.put(2l, 8l);
+ matchingrawIdPairs.put(8l, 2l);
+
+ matchingrawIdPairs.put(8l, 9l);
+ matchingrawIdPairs.put(9l, 8l);
+
+ matchingrawIdPairs.put(4l, 5l);
+ matchingrawIdPairs.put(5l, 4l);
+
+ Set<Set<Long>> actual = ContactAggregator.findConnectedComponents(rawContactIdSet,
+ matchingrawIdPairs);
+
+ Set<Set<Long>> expected = new HashSet<Set<Long>>();
+ Set<Long> result1 = new HashSet<Long>();
+ result1.addAll(Arrays.asList(1l, 2l, 3l, 7l, 8l, 9l));
+ Set<Long> result2 = new HashSet<Long>();
+ result2.addAll(Arrays.asList(4l, 5l));
+ Set<Long> result3 = new HashSet<Long>();
+ result3.addAll(Arrays.asList(6l));
+ expected.addAll(Arrays.asList(result1, result2, result3));
+
+ assertEquals(expected, actual);
+ }
+
private void assertSuggestions(long contactId, long... suggestions) {
final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
Uri uri = Uri.withAppendedPath(aggregateUri,
diff --git a/tests/src/com/android/providers/contacts/testutil/DataUtil.java b/tests/src/com/android/providers/contacts/testutil/DataUtil.java
index 194e67d..1f4f35a 100644
--- a/tests/src/com/android/providers/contacts/testutil/DataUtil.java
+++ b/tests/src/com/android/providers/contacts/testutil/DataUtil.java
@@ -22,6 +22,7 @@
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
import android.test.mock.MockContentResolver;
/**
@@ -59,6 +60,13 @@
public static Uri insertStructuredName(
ContentResolver resolver, long rawContactId, String givenName, String familyName,
String phoneticGiven) {
+ return insertStructuredName(resolver, rawContactId, givenName, familyName, phoneticGiven,
+ /* isSuperPrimary = true */ false);
+ }
+
+ public static Uri insertStructuredName(
+ ContentResolver resolver, long rawContactId, String givenName, String familyName,
+ String phoneticGiven, boolean isSuperPrimary) {
ContentValues values = new ContentValues();
StringBuilder sb = new StringBuilder();
if (givenName != null) {
@@ -79,7 +87,11 @@
if (phoneticGiven != null) {
values.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticGiven);
}
+ if (isSuperPrimary) {
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ }
return insertStructuredName(resolver, rawContactId, values);
}
-}
+}
\ No newline at end of file