am b3c80626: am a72b3934: reconcile main tree with open-source eclair
Merge commit 'b3c80626606fd3cad8df7e34d48832bddaead8cd'
* commit 'b3c80626606fd3cad8df7e34d48832bddaead8cd':
android-2.1_r1 snapshot
diff --git a/Android.mk b/Android.mk
index e963f99..79ff490 100644
--- a/Android.mk
+++ b/Android.mk
@@ -1,7 +1,7 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := user
+LOCAL_MODULE_TAGS := optional
# Only compile source java files in this apk.
LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/src/com/android/providers/contacts/ContactAggregationScheduler.java b/src/com/android/providers/contacts/ContactAggregationScheduler.java
deleted file mode 100644
index 497b6a3..0000000
--- a/src/com/android/providers/contacts/ContactAggregationScheduler.java
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright (C) 2009 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 android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.util.Log;
-
-/**
- * A scheduler for asynchronous aggregation of contacts. Aggregation will start after
- * a short delay after it is scheduled, unless it is scheduled again, in which case the
- * aggregation pass is delayed. There is an upper boundary on how long aggregation can
- * be delayed.
- */
-public class ContactAggregationScheduler {
-
- private static final String TAG = "ContactAggregator";
-
- public interface Aggregator {
-
- /**
- * Performs an aggregation run.
- */
- void run();
-
- /**
- * Interrupts aggregation.
- */
- void interrupt();
- }
-
- // Message ID used for communication with the aggregator
- private static final int START_AGGREGATION_MESSAGE_ID = 1;
-
- // Aggregation is delayed by this many milliseconds to allow changes to accumulate
- public static final int AGGREGATION_DELAY = 1000;
-
- // Maximum delay of aggregation from the initial aggregation request
- public static final int MAX_AGGREGATION_DELAY = 10000;
-
- // Minimum gap between requests that should cause a delay of aggregation
- public static final int DELAYED_EXECUTION_TIMEOUT = 500;
-
- public static final int STATUS_STAND_BY = 0;
- public static final int STATUS_SCHEDULED = 1;
- public static final int STATUS_RUNNING = 2;
- public static final int STATUS_INTERRUPTED = 3;
-
-
- private Aggregator mAggregator;
-
- // Aggregation status
- private int mStatus = STATUS_STAND_BY;
-
- // If true, we need to automatically reschedule aggregation after the current pass is done
- private boolean mRescheduleWhenComplete;
-
- // The time when aggregation was requested for the first time.
- // Reset when aggregation is completed
- private long mInitialRequestTimestamp;
-
- // Last time aggregation was requested
- private long mLastAggregationEndedTimestamp;
-
- private HandlerThread mHandlerThread;
- private Handler mMessageHandler;
-
- public void setAggregator(Aggregator aggregator) {
- mAggregator = aggregator;
- }
-
- public void start() {
- mHandlerThread = new HandlerThread("ContactAggregator", Process.THREAD_PRIORITY_BACKGROUND);
- mHandlerThread.start();
- mMessageHandler = new Handler(mHandlerThread.getLooper()) {
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case START_AGGREGATION_MESSAGE_ID:
- run();
- break;
-
- default:
- throw new IllegalStateException("Unhandled message: " + msg.what);
- }
- }
- };
- }
-
- public void stop() {
- mAggregator.interrupt();
- Looper looper = mHandlerThread.getLooper();
- if (looper != null) {
- looper.quit();
- }
- }
-
- /**
- * Schedules an aggregation pass after a short delay.
- */
- public synchronized void schedule() {
-
- switch (mStatus) {
- case STATUS_STAND_BY: {
-
- mInitialRequestTimestamp = currentTime();
- mStatus = STATUS_SCHEDULED;
- if (mInitialRequestTimestamp - mLastAggregationEndedTimestamp <
- DELAYED_EXECUTION_TIMEOUT) {
- runDelayed();
- } else {
- runNow();
- }
- break;
- }
-
- case STATUS_INTERRUPTED: {
-
- // If the previous aggregation run was interrupted, do not reset
- // the initial request timestamp - we don't want a continuous string
- // of interrupted runs
- mStatus = STATUS_SCHEDULED;
- runDelayed();
- break;
- }
-
- case STATUS_SCHEDULED: {
-
- // If it has been less than MAX_AGGREGATION_DELAY millis since the initial request,
- // reschedule the request.
- if (currentTime() - mInitialRequestTimestamp < MAX_AGGREGATION_DELAY) {
- runDelayed();
- }
- break;
- }
-
- case STATUS_RUNNING: {
-
- // If it has been less than MAX_AGGREGATION_DELAY millis since the initial request,
- // interrupt the current pass and reschedule the request.
- if (currentTime() - mInitialRequestTimestamp < MAX_AGGREGATION_DELAY) {
- mAggregator.interrupt();
- mStatus = STATUS_INTERRUPTED;
- }
-
- mRescheduleWhenComplete = true;
- break;
- }
- }
- }
-
- /**
- * Called just before an aggregation pass begins.
- */
- public void run() {
- synchronized (this) {
- mStatus = STATUS_RUNNING;
- mRescheduleWhenComplete = false;
- }
- try {
- mAggregator.run();
- } finally {
- mLastAggregationEndedTimestamp = currentTime();
- synchronized (this) {
- if (mStatus == STATUS_RUNNING) {
- mStatus = STATUS_STAND_BY;
- }
- if (mRescheduleWhenComplete) {
- mRescheduleWhenComplete = false;
- schedule();
- } else {
- Log.w(TAG, "No more aggregation requests");
- }
- }
- }
- }
-
- /* package */ void runNow() {
-
- // If aggregation has already been requested, cancel the previous request
- mMessageHandler.removeMessages(START_AGGREGATION_MESSAGE_ID);
-
- // Schedule aggregation for right now
- mMessageHandler.sendEmptyMessage(START_AGGREGATION_MESSAGE_ID);
- }
-
- /* package */ void runDelayed() {
-
- // If aggregation has already been requested, cancel the previous request
- mMessageHandler.removeMessages(START_AGGREGATION_MESSAGE_ID);
-
- // Schedule aggregation for AGGREGATION_DELAY milliseconds from now
- mMessageHandler.sendEmptyMessageDelayed(
- START_AGGREGATION_MESSAGE_ID, AGGREGATION_DELAY);
- }
-
- /* package */ long currentTime() {
- return System.currentTimeMillis();
- }
-}
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 4475a83..e3a5142 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -20,10 +20,9 @@
import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.DisplayNameSources;
-import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
@@ -36,19 +35,15 @@
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.text.TextUtils;
-import android.text.util.Rfc822Token;
-import android.text.util.Rfc822Tokenizer;
import android.util.EventLog;
import android.util.Log;
@@ -63,91 +58,18 @@
* ContactAggregator deals with aggregating contact information coming from different sources.
* Two John Doe contacts from two disjoint sources are presumed to be the same
* person unless the user declares otherwise.
- * <p>
- * ContactAggregator runs on a separate thread.
*/
-public class ContactAggregator implements ContactAggregationScheduler.Aggregator {
+public class ContactAggregator {
private static final String TAG = "ContactAggregator";
- private interface DataMimetypeQuery {
+ private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
- // Data mime types used in the contact matching algorithm
- String MIMETYPE_SELECTION_IN_CLAUSE = MimetypesColumns.MIMETYPE + " IN ('"
- + Email.CONTENT_ITEM_TYPE + "','"
- + Nickname.CONTENT_ITEM_TYPE + "','"
- + Phone.CONTENT_ITEM_TYPE + "','"
- + StructuredName.CONTENT_ITEM_TYPE + "')";
-
- String[] COLUMNS = new String[] {
- MimetypesColumns.MIMETYPE, Data.DATA1
- };
-
- int MIMETYPE = 0;
- int DATA1 = 1;
- }
-
- private interface DataContactIdQuery {
- String TABLE = Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS;
-
- String[] COLUMNS = new String[] {
- Data.DATA1, RawContacts.CONTACT_ID
- };
-
- int DATA1 = 0;
- int CONTACT_ID = 1;
- }
-
- private interface NameLookupQuery {
- String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
-
- String[] COLUMNS = new String[] {
- RawContacts.CONTACT_ID, NameLookupColumns.NORMALIZED_NAME,
- NameLookupColumns.NAME_TYPE
- };
-
- int CONTACT_ID = 0;
- int NORMALIZED_NAME = 1;
- int NAME_TYPE = 2;
- }
-
- private interface EmailLookupQuery {
- String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
-
- String[] COLUMNS = new String[] {
- RawContacts.CONTACT_ID
- };
-
- int CONTACT_ID = 0;
- }
-
- private interface ContactIdQuery {
- String TABLE = Tables.CONTACTS;
-
- String[] COLUMNS = new String[] {
- Contacts._ID
- };
-
- int _ID = 0;
- }
-
- private static final String[] CONTACT_ID_COLUMN = new String[] { RawContacts._ID };
- private static final String[] CONTACT_ID_COLUMNS = new String[]{ RawContacts.CONTACT_ID };
- private static final int COL_CONTACT_ID = 0;
-
- private static final int MODE_AGGREGATION = 0;
- private static final int MODE_SUGGESTIONS = 1;
-
- /**
- * When yielding the transaction to another thread, sleep for this many milliseconds
- * to allow the other thread to build up a transaction before yielding back.
- */
- private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
-
- /**
- * The maximum number of contacts aggregated in a single transaction.
- */
- private static final int MAX_TRANSACTION_SIZE = 50;
+ private static final String STRUCTURED_NAME_BASED_LOOKUP_SQL =
+ NameLookupColumns.NAME_TYPE + " IN ("
+ + NameLookupType.NAME_EXACT + ","
+ + NameLookupType.NAME_VARIANT + ","
+ + NameLookupType.NAME_COLLATION_KEY + ")";
// From system/core/logcat/event-log-tags
// aggregator [time, count] will be logged for each aggregator cycle.
@@ -156,10 +78,12 @@
// If we encounter more than this many contacts with matching names, aggregate only this many
private static final int PRIMARY_HIT_LIMIT = 15;
+ private static final String PRIMARY_HIT_LIMIT_STRING = String.valueOf(PRIMARY_HIT_LIMIT);
// If we encounter more than this many contacts with matching phone number or email,
// don't attempt to aggregate - this is likely an error or a shared corporate data element.
private static final int SECONDARY_HIT_LIMIT = 20;
+ private static final String SECONDARY_HIT_LIMIT_STRING = String.valueOf(SECONDARY_HIT_LIMIT);
// If we encounter more than this many contacts with matching name during aggregation
// suggestion lookup, ignore the remaining results.
@@ -167,12 +91,8 @@
private final ContactsProvider2 mContactsProvider;
private final ContactsDatabaseHelper mDbHelper;
- private final ContactAggregationScheduler mScheduler;
private boolean mEnabled = true;
- // Set if the current aggregation pass should be interrupted
- private volatile boolean mCancel;
-
/** Precompiled sql statement for setting an aggregated presence */
private SQLiteStatement mAggregatedPresenceReplace;
private SQLiteStatement mPresenceContactIdUpdate;
@@ -188,9 +108,25 @@
private SQLiteStatement mContactIdAndMarkAggregatedUpdate;
private SQLiteStatement mContactIdUpdate;
private SQLiteStatement mMarkAggregatedUpdate;
+ private SQLiteStatement mContactUpdate;
+ private SQLiteStatement mContactInsert;
private HashSet<Long> mRawContactsMarkedForAggregation = new HashSet<Long>();
+ private String[] mSelectionArgs1 = new String[1];
+ private String[] mSelectionArgs2 = new String[2];
+ private String[] mSelectionArgs3 = new String[3];
+ private long mMimeTypeIdEmail;
+ private long mMimeTypeIdPhoto;
+ private long mMimeTypeIdPhone;
+ private String mRawContactsQueryByRawContactId;
+ private String mRawContactsQueryByContactId;
+ private StringBuilder mSb = new StringBuilder();
+ private MatchCandidateList mCandidates = new MatchCandidateList();
+ private ContactMatcher mMatcher = new ContactMatcher();
+ private ContentValues mValues = new ContentValues();
+ private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
+
/**
* Captures a potential match for a given name. The matching algorithm
* constructs a bunch of NameMatchCandidate objects for various potential matches
@@ -233,34 +169,33 @@
}
}
- private class AggregationNameLookupBuilder extends NameLookupBuilder {
+ /**
+ * A convenience class used in the algorithm that figures out which of available
+ * display names to use for an aggregate contact.
+ */
+ private static class DisplayNameCandidate {
+ long rawContactId;
+ String displayName;
+ int displayNameSource;
+ boolean verified;
- private final MatchCandidateList mCandidates;
-
- public AggregationNameLookupBuilder(NameSplitter splitter, MatchCandidateList candidates) {
- super(splitter);
- mCandidates = candidates;
+ public DisplayNameCandidate() {
+ clear();
}
- @Override
- protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
- String name) {
- mCandidates.add(name, lookupType);
- }
-
- @Override
- protected String[] getCommonNicknameClusters(String normalizedName) {
- return mContactsProvider.getCommonNicknameClusters(normalizedName);
+ public void clear() {
+ rawContactId = -1;
+ displayName = null;
+ displayNameSource = DisplayNameSources.UNDEFINED;
+ verified = false;
}
}
/**
- * Constructor. Starts a contact aggregation thread. Call {@link #quit} to kill the
- * aggregation thread. Call {@link #schedule} to kick off the aggregation process after
- * a delay of {@link ContactAggregationScheduler#AGGREGATION_DELAY} milliseconds.
+ * Constructor.
*/
public ContactAggregator(ContactsProvider2 contactsProvider,
- ContactsDatabaseHelper contactsDatabaseHelper, ContactAggregationScheduler scheduler) {
+ ContactsDatabaseHelper contactsDatabaseHelper) {
mContactsProvider = contactsProvider;
mDbHelper = contactsDatabaseHelper;
@@ -303,7 +238,7 @@
mDisplayNameUpdate = db.compileStatement(
"UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.DISPLAY_NAME + "=? " +
+ " SET " + Contacts.NAME_RAW_CONTACT_ID + "=? " +
" WHERE " + Contacts._ID + "=?");
mLookupKeyUpdate = db.compileStatement(
@@ -348,14 +283,21 @@
" SET " + PresenceColumns.CONTACT_ID + "=?" +
" WHERE " + PresenceColumns.RAW_CONTACT_ID + "=?");
- mScheduler = scheduler;
- mScheduler.setAggregator(this);
- mScheduler.start();
+ mContactUpdate = db.compileStatement(ContactReplaceSqlStatement.UPDATE_SQL);
+ mContactInsert = db.compileStatement(ContactReplaceSqlStatement.INSERT_SQL);
- // Perform an aggregation pass in the beginning, which will most of the time
- // do nothing. It will only be useful if the content provider has been killed
- // before completing aggregation.
- mScheduler.schedule();
+ mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhoto = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+
+ // Query used to retrieve data from raw contacts to populate the corresponding aggregate
+ mRawContactsQueryByRawContactId = String.format(
+ RawContactsQuery.SQL_FORMAT_BY_RAW_CONTACT_ID,
+ mMimeTypeIdPhoto, mMimeTypeIdPhone);
+
+ mRawContactsQueryByContactId = String.format(
+ RawContactsQuery.SQL_FORMAT_BY_CONTACT_ID,
+ mMimeTypeIdPhoto, mMimeTypeIdPhone);
}
public void setEnabled(boolean enabled) {
@@ -366,160 +308,17 @@
return mEnabled;
}
- /**
- * Schedules aggregation pass after a short delay. This method should be called every time
- * the {@link RawContacts#CONTACT_ID} field is reset on any record.
- */
- public void schedule() {
- if (mEnabled) {
- mScheduler.schedule();
- }
- }
-
- /**
- * Kills the contact aggregation thread.
- */
- public void quit() {
- mScheduler.stop();
- }
-
- /**
- * Invoked by the scheduler to cancel aggregation.
- */
- public void interrupt() {
- mCancel = true;
- }
-
private interface AggregationQuery {
- String TABLE = Tables.RAW_CONTACTS;
-
- String[] COLUMNS = {
- RawContacts._ID, RawContacts.CONTACT_ID
- };
+ String SQL =
+ "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts._ID + " IN(";
int _ID = 0;
int CONTACT_ID = 1;
-
- String SELECTION = RawContactsColumns.AGGREGATION_NEEDED + "=1"
- + " AND " + RawContacts.AGGREGATION_MODE
- + "=" + RawContacts.AGGREGATION_MODE_DEFAULT
- + " AND " + RawContacts.CONTACT_ID + " NOT NULL";
}
/**
- * Find all contacts that require aggregation and pass them through aggregation one by one.
- * Do not call directly. It is invoked by the scheduler.
- */
- public void run() {
- if (!mEnabled) {
- return;
- }
-
- mCancel = false;
-
- SQLiteDatabase db = mDbHelper.getWritableDatabase();
- MatchCandidateList candidates = new MatchCandidateList();
- ContactMatcher matcher = new ContactMatcher();
- ContentValues values = new ContentValues();
- long rawContactIds[] = new long[MAX_TRANSACTION_SIZE];
- long contactIds[] = new long[MAX_TRANSACTION_SIZE];
- while (!mCancel) {
- if (!aggregateBatch(db, candidates, matcher, values, rawContactIds, contactIds)) {
- break;
- }
- }
- }
-
- /**
- * Takes a batch of contacts and aggregates them. Returns the number of successfully
- * processed raw contacts.
- *
- * @return true if there are possibly more contacts to aggregate
- */
- private boolean aggregateBatch(SQLiteDatabase db, MatchCandidateList candidates,
- ContactMatcher matcher, ContentValues values, long[] rawContactIds, long[] contactIds) {
- boolean lastBatch = false;
- long elapsedTime = 0;
- int aggregatedCount = 0;
- while (!mCancel && aggregatedCount < MAX_TRANSACTION_SIZE) {
- db.beginTransaction();
- try {
-
- long start = System.currentTimeMillis();
- int count = findContactsToAggregate(db, rawContactIds, contactIds,
- MAX_TRANSACTION_SIZE - aggregatedCount);
- if (mCancel || count == 0) {
- lastBatch = true;
- break;
- }
-
- Log.i(TAG, "Contact aggregation: " + count);
- EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION,
- System.currentTimeMillis() - start, -count);
-
- for (int i = 0; i < count; i++) {
- start = System.currentTimeMillis();
- aggregateContact(db, rawContactIds[i], contactIds[i], candidates, matcher,
- values);
- long end = System.currentTimeMillis();
- elapsedTime += (end - start);
- aggregatedCount++;
- if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
-
- // We have yielded the database, so the rawContactIds and contactIds
- // arrays are no longer current - we need to refetch them
- break;
- }
- }
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, aggregatedCount);
- String performance = aggregatedCount == 0 ? "" : ", " + (elapsedTime / aggregatedCount)
- + " ms per contact";
- if (aggregatedCount != 0) {
- Log.i(TAG, "Contact aggregation complete: " + aggregatedCount + performance);
- }
-
- if (aggregatedCount > 0) {
-
- // Aggregation does not affect raw contacts and therefore there is no reason
- // to send a notification to sync adapters. We do need to notify every one else though.
- mContactsProvider.notifyChange(false);
- }
-
- return !lastBatch;
- }
-
- /**
- * Finds a batch of contacts marked for aggregation. The maximum batch size
- * is {@link #MAX_TRANSACTION_SIZE} contacts.
- * @param limit
- */
- private int findContactsToAggregate(SQLiteDatabase db, long[] rawContactIds,
- long[] contactIds, int limit) {
- Cursor c = db.query(AggregationQuery.TABLE, AggregationQuery.COLUMNS,
- AggregationQuery.SELECTION, null, null, null, null,
- String.valueOf(limit));
-
- int count = 0;
- try {
- while (c.moveToNext()) {
- rawContactIds[count] = c.getLong(AggregationQuery._ID);
- contactIds[count] = c.getLong(AggregationQuery.CONTACT_ID);
- count++;
- }
- } finally {
- c.close();
- }
- return count;
- }
-
-
- /**
* Aggregate all raw contacts that were marked for aggregation in the current transaction.
* Call just before committing the transaction.
*/
@@ -530,27 +329,33 @@
}
long start = System.currentTimeMillis();
- Log.i(TAG, "Contact aggregation: " + count);
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "Contact aggregation: " + count);
+ }
+
EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, start, -count);
+ String selectionArgs[] = new String[count];
+
+ int index = 0;
+ mSb.setLength(0);
+ mSb.append(AggregationQuery.SQL);
+ for (long rawContactId : mRawContactsMarkedForAggregation) {
+ if (index > 0) {
+ mSb.append(',');
+ }
+ mSb.append('?');
+ selectionArgs[index++] = String.valueOf(rawContactId);
+ }
+
+ mSb.append(')');
+
long rawContactIds[] = new long[count];
long contactIds[] = new long[count];
-
- StringBuilder sb = new StringBuilder();
- sb.append(RawContacts._ID + " IN(");
- for (long rawContactId : mRawContactsMarkedForAggregation) {
- sb.append(rawContactId);
- sb.append(',');
- }
-
- sb.setLength(sb.length() - 1);
- sb.append(')');
-
- Cursor c = db.query(AggregationQuery.TABLE, AggregationQuery.COLUMNS, sb.toString(), null,
- null, null, null);
-
- int index = 0;
+ Cursor c = db.rawQuery(mSb.toString(), selectionArgs);
try {
+ count = c.getCount();
+ index = 0;
while (c.moveToNext()) {
rawContactIds[index] = c.getLong(AggregationQuery._ID);
contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
@@ -560,18 +365,17 @@
c.close();
}
- MatchCandidateList candidates = new MatchCandidateList();
- ContactMatcher matcher = new ContactMatcher();
- ContentValues values = new ContentValues();
-
for (int i = 0; i < count; i++) {
- aggregateContact(db, rawContactIds[i], contactIds[i], candidates, matcher, values);
+ aggregateContact(db, rawContactIds[i], contactIds[i], mCandidates, mMatcher, mValues);
}
long elapsedTime = System.currentTimeMillis() - start;
EventLog.writeEvent(LOG_SYNC_CONTACTS_AGGREGATION, elapsedTime, count);
- String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact";
- Log.i(TAG, "Contact aggregation complete: " + count + performance);
+
+ if (VERBOSE_LOGGING) {
+ String performance = count == 0 ? "" : ", " + (elapsedTime / count) + " ms per contact";
+ Log.i(TAG, "Contact aggregation complete: " + count + performance);
+ }
}
public void clearPendingAggregations() {
@@ -593,13 +397,10 @@
/**
* Creates a new contact based on the given raw contact. Does not perform aggregation.
*/
- public void insertContact(SQLiteDatabase db, long rawContactId) {
- ContentValues contactValues = new ContentValues();
- contactValues.put(Contacts.DISPLAY_NAME, "");
- contactValues.put(Contacts.IN_VISIBLE_GROUP, false);
- computeAggregateData(db,
- RawContactsColumns.CONCRETE_ID + "=" + rawContactId, contactValues);
- long contactId = db.insert(Tables.CONTACTS, Contacts.DISPLAY_NAME, contactValues);
+ public void onRawContactInsert(SQLiteDatabase db, long rawContactId) {
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
+ long contactId = mContactInsert.executeInsert();
setContactId(rawContactId, contactId);
mDbHelper.updateContactVisible(contactId);
}
@@ -625,9 +426,9 @@
}
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
- final ContentValues values = new ContentValues();
- computeAggregateData(db, contactId, values);
- db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+ computeAggregateData(db, contactId, mContactUpdate);
+ mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+ mContactUpdate.execute();
mDbHelper.updateContactVisible(contactId);
updateAggregatedPresence(contactId);
@@ -675,11 +476,10 @@
markAggregated(rawContactId);
} else if (contactId == -1) {
// Splitting an aggregate
- ContentValues contactValues = new ContentValues();
- contactValues.put(RawContactsColumns.DISPLAY_NAME, "");
- computeAggregateData(db,
- RawContactsColumns.CONCRETE_ID + "=" + rawContactId, contactValues);
- contactId = db.insert(Tables.CONTACTS, Contacts.DISPLAY_NAME, contactValues);
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1,
+ mContactInsert);
+ contactId = mContactInsert.executeInsert();
setContactIdAndMarkAggregated(rawContactId, contactId);
mDbHelper.updateContactVisible(contactId);
@@ -702,8 +502,9 @@
}
setContactIdAndMarkAggregated(rawContactId, contactId);
- computeAggregateData(db, contactId, values);
- db.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null);
+ computeAggregateData(db, contactId, mContactUpdate);
+ mContactUpdate.bindLong(ContactReplaceSqlStatement.CONTACT_ID, contactId);
+ mContactUpdate.execute();
mDbHelper.updateContactVisible(contactId);
updateAggregatedPresence(contactId);
}
@@ -741,6 +542,52 @@
mPresenceContactIdUpdate.execute();
}
+ interface AggregateExceptionPrefetchQuery {
+ String TABLE = Tables.AGGREGATION_EXCEPTIONS;
+
+ String[] COLUMNS = {
+ AggregationExceptions.RAW_CONTACT_ID1,
+ AggregationExceptions.RAW_CONTACT_ID2,
+ };
+
+ int RAW_CONTACT_ID1 = 0;
+ int RAW_CONTACT_ID2 = 1;
+ }
+
+ // A set of raw contact IDs for which there are aggregation exceptions
+ private final HashSet<Long> mAggregationExceptionIds = new HashSet<Long>();
+ private boolean mAggregationExceptionIdsValid;
+
+ public void invalidateAggregationExceptionCache() {
+ mAggregationExceptionIdsValid = false;
+ }
+
+ /**
+ * Finds all raw contact IDs for which there are aggregation exceptions. The list of
+ * ids is used as an optimization in aggregation: there is no point to run a query against
+ * the agg_exceptions table if it is known that there are no records there for a given
+ * raw contact ID.
+ */
+ private void prefetchAggregationExceptionIds(SQLiteDatabase db) {
+ mAggregationExceptionIds.clear();
+ final Cursor c = db.query(AggregateExceptionPrefetchQuery.TABLE,
+ AggregateExceptionPrefetchQuery.COLUMNS,
+ null, null, null, null, null);
+
+ try {
+ while (c.moveToNext()) {
+ long rawContactId1 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID1);
+ long rawContactId2 = c.getLong(AggregateExceptionPrefetchQuery.RAW_CONTACT_ID2);
+ mAggregationExceptionIds.add(rawContactId1);
+ mAggregationExceptionIds.add(rawContactId2);
+ }
+ } finally {
+ c.close();
+ }
+
+ mAggregationExceptionIdsValid = true;
+ }
+
interface AggregateExceptionQuery {
String TABLE = Tables.AGGREGATION_EXCEPTIONS
+ " JOIN raw_contacts raw_contacts1 "
@@ -771,6 +618,16 @@
*/
private long pickBestMatchBasedOnExceptions(SQLiteDatabase db, long rawContactId,
ContactMatcher matcher) {
+ if (!mAggregationExceptionIdsValid) {
+ prefetchAggregationExceptionIds(db);
+ }
+
+ // If there are no aggregation exceptions involving this raw contact, there is no need to
+ // run a query and we can just return -1, which stands for "nothing found"
+ if (!mAggregationExceptionIds.contains(rawContactId)) {
+ return -1;
+ }
+
final Cursor c = db.query(AggregateExceptionQuery.TABLE,
AggregateExceptionQuery.COLUMNS,
AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId
@@ -825,189 +682,233 @@
private long pickBestMatchBasedOnData(SQLiteDatabase db, long rawContactId,
MatchCandidateList candidates, ContactMatcher matcher) {
- updateMatchScoresBasedOnDataMatches(db, rawContactId, MODE_AGGREGATION, candidates, matcher);
-
- // See if we have already found a good match based on name matches alone
- long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+ // Find good matches based on name alone
+ long bestMatch = updateMatchScoresBasedOnDataMatches(db, rawContactId, candidates, matcher);
if (bestMatch == -1) {
// We haven't found a good match on name, see if we have any matches on phone, email etc
- bestMatch = pickBestMatchBasedOnSecondaryData(db, candidates, matcher);
+ bestMatch = pickBestMatchBasedOnSecondaryData(db, rawContactId, candidates, matcher);
}
return bestMatch;
}
+
/**
* Picks the best matching contact based on secondary data matches. The method loads
* structured names for all candidate contacts and recomputes match scores using approximate
* matching.
*/
private long pickBestMatchBasedOnSecondaryData(SQLiteDatabase db,
- MatchCandidateList candidates, ContactMatcher matcher) {
+ long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
List<Long> secondaryContactIds = matcher.prepareSecondaryMatchCandidates(
ContactMatcher.SCORE_THRESHOLD_PRIMARY);
if (secondaryContactIds == null || secondaryContactIds.size() > SECONDARY_HIT_LIMIT) {
return -1;
}
- StringBuilder selection = new StringBuilder();
- selection.append(RawContacts.CONTACT_ID).append(" IN (");
+ loadNameMatchCandidates(db, rawContactId, candidates, true);
+
+ mSb.setLength(0);
+ mSb.append(RawContacts.CONTACT_ID).append(" IN (");
for (int i = 0; i < secondaryContactIds.size(); i++) {
if (i != 0) {
- selection.append(',');
+ mSb.append(',');
}
- selection.append(secondaryContactIds.get(i));
+ mSb.append(secondaryContactIds.get(i));
}
- selection.append(") AND " + MimetypesColumns.MIMETYPE + "='"
- + StructuredName.CONTENT_ITEM_TYPE + "'");
- final Cursor c = db.query(DataContactIdQuery.TABLE, DataContactIdQuery.COLUMNS,
- selection.toString(), null, null, null, null);
+ // We only want to compare structured names to structured names
+ // at this stage, we need to ignore all other sources of name lookup data.
+ mSb.append(") AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL);
- MatchCandidateList nameCandidates = new MatchCandidateList();
- AggregationNameLookupBuilder builder =
- new AggregationNameLookupBuilder(mContactsProvider.getNameSplitter(), nameCandidates);
+ matchAllCandidates(db, mSb.toString(), candidates, matcher,
+ ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE, null);
+
+ return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
+ }
+
+ private interface NameLookupQuery {
+ String TABLE = Tables.NAME_LOOKUP;
+
+ String SELECTION = NameLookupColumns.RAW_CONTACT_ID + "=?";
+ String SELECTION_STRUCTURED_NAME_BASED =
+ SELECTION + " AND " + STRUCTURED_NAME_BASED_LOOKUP_SQL;
+
+ String[] COLUMNS = new String[] {
+ NameLookupColumns.NORMALIZED_NAME,
+ NameLookupColumns.NAME_TYPE
+ };
+
+ int NORMALIZED_NAME = 0;
+ int NAME_TYPE = 1;
+ }
+
+ private void loadNameMatchCandidates(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, boolean structuredNameBased) {
+ candidates.clear();
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
+ structuredNameBased
+ ? NameLookupQuery.SELECTION_STRUCTURED_NAME_BASED
+ : NameLookupQuery.SELECTION,
+ mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
- String name = c.getString(DataContactIdQuery.DATA1);
- long contactId = c.getLong(DataContactIdQuery.CONTACT_ID);
-
- nameCandidates.clear();
- builder.insertNameLookup(0, 0, name);
-
- // Note the N^2 complexity of the following fragment. This is not a huge concern
- // since the number of candidates is very small and in general secondary hits
- // in the absence of primary hits are rare.
- for (int i = 0; i < candidates.mCount; i++) {
- NameMatchCandidate candidate = candidates.mList.get(i);
-
- // We only want to compare structured names to structured names
- // at this stage, we need to ignore all other sources of name lookup data.
- if (NameLookupType.isBasedOnStructuredName(candidate.mLookupType)) {
- for (int j = 0; j < nameCandidates.mCount; j++) {
- NameMatchCandidate nameCandidate = nameCandidates.mList.get(j);
- matcher.matchName(contactId,
- nameCandidate.mLookupType, nameCandidate.mName,
- candidate.mLookupType, candidate.mName,
- ContactMatcher.MATCHING_ALGORITHM_CONSERVATIVE);
- }
- }
- }
+ String normalizedName = c.getString(NameLookupQuery.NORMALIZED_NAME);
+ int type = c.getInt(NameLookupQuery.NAME_TYPE);
+ candidates.add(normalizedName, type);
}
} finally {
c.close();
}
-
- return matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_SECONDARY);
}
/**
* Computes scores for contacts that have matching data rows.
*/
- private void updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
- int mode, MatchCandidateList candidates, ContactMatcher matcher) {
+ private long updateMatchScoresBasedOnDataMatches(SQLiteDatabase db, long rawContactId,
+ MatchCandidateList candidates, ContactMatcher matcher) {
- final Cursor c = db.query(Tables.DATA_JOIN_MIMETYPE_RAW_CONTACTS,
- DataMimetypeQuery.COLUMNS,
- Data.RAW_CONTACT_ID + "=" + rawContactId + " AND ("
- + DataMimetypeQuery.MIMETYPE_SELECTION_IN_CLAUSE + ")",
- null, null, null, null);
+ updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+ long bestMatch = matcher.pickBestMatch(ContactMatcher.SCORE_THRESHOLD_PRIMARY);
+ if (bestMatch != -1) {
+ return bestMatch;
+ }
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+
+ return -1;
+ }
+
+ private interface NameLookupMatchQuery {
+ String TABLE = Tables.NAME_LOOKUP + " nameA"
+ + " JOIN " + Tables.NAME_LOOKUP + " nameB" +
+ " ON (" + "nameA." + NameLookupColumns.NORMALIZED_NAME + "="
+ + "nameB." + NameLookupColumns.NORMALIZED_NAME + ")"
+ + " JOIN " + Tables.RAW_CONTACTS +
+ " ON (nameB." + NameLookupColumns.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "nameA." + NameLookupColumns.RAW_CONTACT_ID + "=?"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
+
+ String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID,
+ "nameA." + NameLookupColumns.NORMALIZED_NAME,
+ "nameA." + NameLookupColumns.NAME_TYPE,
+ "nameB." + NameLookupColumns.NAME_TYPE,
+ };
+
+ int CONTACT_ID = 0;
+ int NAME = 1;
+ int NAME_TYPE_A = 2;
+ int NAME_TYPE_B = 3;
+ }
+
+ private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
+ ContactMatcher matcher) {
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(NameLookupMatchQuery.TABLE, NameLookupMatchQuery.COLUMNS,
+ NameLookupMatchQuery.SELECTION,
+ mSelectionArgs1, null, null, null, PRIMARY_HIT_LIMIT_STRING);
try {
while (c.moveToNext()) {
- String mimeType = c.getString(DataMimetypeQuery.MIMETYPE);
- String data = c.getString(DataMimetypeQuery.DATA1);
- if (mimeType.equals(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
- addMatchCandidatesStructuredName(data, candidates);
- } else if (mimeType.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
- if (!TextUtils.isEmpty(data)) {
- addMatchCandidatesEmail(data, mode, candidates);
- lookupEmailMatches(db, data, matcher);
- }
- } else if (mimeType.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
- if (!TextUtils.isEmpty(data)) {
- lookupPhoneMatches(db, data, matcher);
- }
- } else if (mimeType.equals(CommonDataKinds.Nickname.CONTENT_ITEM_TYPE)) {
- if (!TextUtils.isEmpty(data)) {
- addMatchCandidatesNickname(data, mode, candidates);
- lookupNicknameMatches(db, data, matcher);
- }
+ long contactId = c.getLong(NameLookupMatchQuery.CONTACT_ID);
+ String name = c.getString(NameLookupMatchQuery.NAME);
+ int nameTypeA = c.getInt(NameLookupMatchQuery.NAME_TYPE_A);
+ int nameTypeB = c.getInt(NameLookupMatchQuery.NAME_TYPE_B);
+ matcher.matchName(contactId, nameTypeA, name,
+ nameTypeB, name, ContactMatcher.MATCHING_ALGORITHM_EXACT);
+ if (nameTypeA == NameLookupType.NICKNAME &&
+ nameTypeB == NameLookupType.NICKNAME) {
+ matcher.updateScoreWithNicknameMatch(contactId);
}
}
} finally {
c.close();
}
-
- lookupNameMatches(db, candidates, matcher);
-
- if (mode == MODE_SUGGESTIONS) {
- lookupApproximateNameMatches(db, candidates, matcher);
- }
}
- /**
- * Looks for matches based on the full name.
- */
- private void addMatchCandidatesStructuredName(String name, MatchCandidateList candidates) {
- AggregationNameLookupBuilder builder =
- new AggregationNameLookupBuilder(mContactsProvider.getNameSplitter(), candidates);
- builder.insertNameLookup(0, 0, name);
+ private interface EmailLookupQuery {
+ String TABLE = Tables.DATA + " dataA"
+ + " JOIN " + Tables.DATA + " dataB" +
+ " ON (" + "dataA." + Email.DATA + "=dataB." + Email.DATA + ")"
+ + " JOIN " + Tables.RAW_CONTACTS +
+ " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
+ + " AND dataA." + DataColumns.MIMETYPE_ID + "=?"
+ + " AND dataA." + Email.DATA + " NOT NULL"
+ + " AND dataB." + DataColumns.MIMETYPE_ID + "=?"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
+
+ String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID
+ };
+
+ int CONTACT_ID = 0;
}
- /**
- * Extracts the user name portion from an email address and normalizes it so that it
- * can be matched against names and nicknames.
- */
- private void addMatchCandidatesEmail(String email, int mode, MatchCandidateList candidates) {
- Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
- if (tokens.length == 0) {
- return;
- }
-
- String address = tokens[0].getAddress();
- int at = address.indexOf('@');
- if (at != -1) {
- address = address.substring(0, at);
- }
-
- candidates.add(NameNormalizer.normalize(address), NameLookupType.EMAIL_BASED_NICKNAME);
- }
-
-
- /**
- * Normalizes the nickname and adds it to the list of candidates.
- */
- private void addMatchCandidatesNickname(String nickname, int mode,
- MatchCandidateList candidates) {
- candidates.add(NameNormalizer.normalize(nickname), NameLookupType.NICKNAME);
- }
-
- /**
- * Given a list of {@link NameMatchCandidate}'s, finds all matches and computes their scores.
- */
- private void lookupNameMatches(SQLiteDatabase db, MatchCandidateList candidates,
+ private void updateMatchScoresBasedOnEmailMatches(SQLiteDatabase db, long rawContactId,
ContactMatcher matcher) {
+ mSelectionArgs3[0] = String.valueOf(rawContactId);
+ mSelectionArgs3[1] = mSelectionArgs3[2] = String.valueOf(mMimeTypeIdEmail);
+ Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
+ EmailLookupQuery.SELECTION,
+ mSelectionArgs3, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
+ matcher.updateScoreWithEmailMatch(contactId);
+ }
+ } finally {
+ c.close();
+ }
+ }
- if (candidates.mCount == 0) {
- return;
+ private interface PhoneLookupQuery {
+ String TABLE = Tables.PHONE_LOOKUP + " phoneA"
+ + " JOIN " + Tables.DATA + " dataA"
+ + " ON (dataA." + Data._ID + "=phoneA." + PhoneLookupColumns.DATA_ID + ")"
+ + " JOIN " + Tables.PHONE_LOOKUP + " phoneB"
+ + " ON (phoneA." + PhoneLookupColumns.MIN_MATCH + "="
+ + "phoneB." + PhoneLookupColumns.MIN_MATCH + ")"
+ + " JOIN " + Tables.DATA + " dataB"
+ + " ON (dataB." + Data._ID + "=phoneB." + PhoneLookupColumns.DATA_ID + ")"
+ + " JOIN " + Tables.RAW_CONTACTS
+ + " ON (dataB." + Data.RAW_CONTACT_ID + " = "
+ + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+ String SELECTION = "dataA." + Data.RAW_CONTACT_ID + "=?"
+ + " AND PHONE_NUMBERS_EQUAL(dataA." + Phone.NUMBER + ", "
+ + "dataB." + Phone.NUMBER + ",?)"
+ + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0";
+
+ String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID
+ };
+
+ int CONTACT_ID = 0;
+ }
+
+ private void updateMatchScoresBasedOnPhoneMatches(SQLiteDatabase db, long rawContactId,
+ ContactMatcher matcher) {
+ mSelectionArgs2[0] = String.valueOf(rawContactId);
+ mSelectionArgs2[1] = mDbHelper.getUseStrictPhoneNumberComparisonParameter();
+ Cursor c = db.query(PhoneLookupQuery.TABLE, PhoneLookupQuery.COLUMNS,
+ PhoneLookupQuery.SELECTION,
+ mSelectionArgs2, null, null, null, SECONDARY_HIT_LIMIT_STRING);
+ try {
+ while (c.moveToNext()) {
+ long contactId = c.getLong(PhoneLookupQuery.CONTACT_ID);
+ matcher.updateScoreWithPhoneNumberMatch(contactId);
+ }
+ } finally {
+ c.close();
}
- StringBuilder selection = new StringBuilder();
- selection.append(RawContactsColumns.AGGREGATION_NEEDED + "=0 AND ");
- selection.append(NameLookupColumns.NORMALIZED_NAME);
- selection.append(" IN (");
- for (int i = 0; i < candidates.mCount; i++) {
- DatabaseUtils.appendEscapedSQLString(selection, candidates.mList.get(i).mName);
- selection.append(",");
- }
-
- // Yank the last comma
- selection.setLength(selection.length() - 1);
- selection.append(")");
-
- matchAllCandidates(db, selection.toString(), candidates, matcher,
- ContactMatcher.MATCHING_ALGORITHM_EXACT, String.valueOf(PRIMARY_HIT_LIMIT));
}
/**
@@ -1037,22 +938,38 @@
}
}
+ private interface ContactNameLookupQuery {
+ String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
+
+ String[] COLUMNS = new String[] {
+ RawContacts.CONTACT_ID,
+ NameLookupColumns.NORMALIZED_NAME,
+ NameLookupColumns.NAME_TYPE
+ };
+
+ int CONTACT_ID = 0;
+ int NORMALIZED_NAME = 1;
+ int NAME_TYPE = 2;
+ }
+
/**
* Loads all candidate rows from the name lookup table and updates match scores based
* on that data.
*/
private void matchAllCandidates(SQLiteDatabase db, String selection,
MatchCandidateList candidates, ContactMatcher matcher, int algorithm, String limit) {
- final Cursor c = db.query(NameLookupQuery.TABLE, NameLookupQuery.COLUMNS,
+ final Cursor c = db.query(ContactNameLookupQuery.TABLE, ContactNameLookupQuery.COLUMNS,
selection, null, null, null, null, limit);
try {
while (c.moveToNext()) {
- Long contactId = c.getLong(NameLookupQuery.CONTACT_ID);
- String name = c.getString(NameLookupQuery.NORMALIZED_NAME);
- int nameType = c.getInt(NameLookupQuery.NAME_TYPE);
+ Long contactId = c.getLong(ContactNameLookupQuery.CONTACT_ID);
+ String name = c.getString(ContactNameLookupQuery.NORMALIZED_NAME);
+ int nameType = c.getInt(ContactNameLookupQuery.NAME_TYPE);
- // Determine which candidate produced this match
+ // Note the N^2 complexity of the following fragment. This is not a huge concern
+ // since the number of candidates is very small and in general secondary hits
+ // in the absence of primary hits are rare.
for (int i = 0; i < candidates.mCount; i++) {
NameMatchCandidate candidate = candidates.mList.get(i);
matcher.matchName(contactId, candidate.mLookupType, candidate.mName,
@@ -1064,79 +981,39 @@
}
}
- private void lookupPhoneMatches(SQLiteDatabase db, String phoneNumber, ContactMatcher matcher) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- mDbHelper.buildPhoneLookupAndRawContactQuery(qb, phoneNumber);
- Cursor c = qb.query(db, CONTACT_ID_COLUMNS, RawContactsColumns.AGGREGATION_NEEDED + "=0",
- null, null, null, null, String.valueOf(SECONDARY_HIT_LIMIT));
- try {
- while (c.moveToNext()) {
- long contactId = c.getLong(COL_CONTACT_ID);
- matcher.updateScoreWithPhoneNumberMatch(contactId);
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * Finds exact email matches and updates their match scores.
- */
- private void lookupEmailMatches(SQLiteDatabase db, String address, ContactMatcher matcher) {
- long mimetypeId = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
- Cursor c = db.query(EmailLookupQuery.TABLE, EmailLookupQuery.COLUMNS,
- DataColumns.MIMETYPE_ID + "=" + mimetypeId
- + " AND " + Email.DATA + "=?"
- + " AND " + RawContactsColumns.AGGREGATION_NEEDED + "=0",
- new String[] {address}, null, null, null, String.valueOf(SECONDARY_HIT_LIMIT));
- try {
- while (c.moveToNext()) {
- long contactId = c.getLong(EmailLookupQuery.CONTACT_ID);
- matcher.updateScoreWithEmailMatch(contactId);
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * Finds exact nickname matches in the name lookup table and updates their match scores.
- */
- private void lookupNicknameMatches(SQLiteDatabase db, String nickname, ContactMatcher matcher) {
- String normalized = NameNormalizer.normalize(nickname);
- Cursor c = db.query(true, Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS, CONTACT_ID_COLUMNS,
- NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NICKNAME + " AND "
- + NameLookupColumns.NORMALIZED_NAME + "='" + normalized + "' AND "
- + RawContactsColumns.AGGREGATION_NEEDED + "=0",
- null, null, null, null, null);
- try {
- while (c.moveToNext()) {
- long contactId = c.getLong(COL_CONTACT_ID);
- matcher.updateScoreWithNicknameMatch(contactId);
- }
- } finally {
- c.close();
- }
- }
-
private interface RawContactsQuery {
- String[] COLUMNS = new String[] {
- RawContactsColumns.CONCRETE_ID,
- RawContactsColumns.DISPLAY_NAME,
- RawContactsColumns.DISPLAY_NAME_SOURCE,
- RawContacts.ACCOUNT_TYPE,
- RawContacts.ACCOUNT_NAME,
- RawContacts.SOURCE_ID,
- RawContacts.CUSTOM_RINGTONE,
- RawContacts.SEND_TO_VOICEMAIL,
- RawContacts.LAST_TIME_CONTACTED,
- RawContacts.TIMES_CONTACTED,
- RawContacts.STARRED,
- RawContacts.IS_RESTRICTED,
- DataColumns.CONCRETE_ID,
- DataColumns.CONCRETE_MIMETYPE_ID,
- Data.IS_SUPER_PRIMARY,
- };
+ String SQL_FORMAT =
+ "SELECT "
+ + RawContactsColumns.CONCRETE_ID + ","
+ + RawContactsColumns.DISPLAY_NAME + ","
+ + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
+ + RawContacts.ACCOUNT_TYPE + ","
+ + RawContacts.ACCOUNT_NAME + ","
+ + RawContacts.SOURCE_ID + ","
+ + RawContacts.CUSTOM_RINGTONE + ","
+ + RawContacts.SEND_TO_VOICEMAIL + ","
+ + RawContacts.LAST_TIME_CONTACTED + ","
+ + RawContacts.TIMES_CONTACTED + ","
+ + RawContacts.STARRED + ","
+ + RawContacts.IS_RESTRICTED + ","
+ + RawContacts.NAME_VERIFIED + ","
+ + DataColumns.CONCRETE_ID + ","
+ + DataColumns.CONCRETE_MIMETYPE_ID + ","
+ + Data.IS_SUPER_PRIMARY +
+ " FROM " + Tables.RAW_CONTACTS +
+ " LEFT OUTER JOIN " + Tables.DATA +
+ " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
+ + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
+ + " AND " + Photo.PHOTO + " NOT NULL)"
+ + " OR (" + DataColumns.MIMETYPE_ID + "=%d"
+ + " AND " + Phone.NUMBER + " NOT NULL)))";
+
+ String SQL_FORMAT_BY_RAW_CONTACT_ID = SQL_FORMAT +
+ " WHERE " + RawContactsColumns.CONCRETE_ID + "=?";
+
+ String SQL_FORMAT_BY_CONTACT_ID = SQL_FORMAT +
+ " WHERE " + RawContacts.CONTACT_ID + "=?"
+ + " AND " + RawContacts.DELETED + "=0";
int RAW_CONTACT_ID = 0;
int DISPLAY_NAME = 1;
@@ -1150,27 +1027,71 @@
int TIMES_CONTACTED = 9;
int STARRED = 10;
int IS_RESTRICTED = 11;
- int DATA_ID = 12;
- int MIMETYPE_ID = 13;
- int IS_SUPER_PRIMARY = 14;
+ int NAME_VERIFIED = 12;
+ int DATA_ID = 13;
+ int MIMETYPE_ID = 14;
+ int IS_SUPER_PRIMARY = 15;
+ }
+
+ private interface ContactReplaceSqlStatement {
+ String UPDATE_SQL =
+ "UPDATE " + Tables.CONTACTS +
+ " SET "
+ + Contacts.NAME_RAW_CONTACT_ID + "=?, "
+ + Contacts.PHOTO_ID + "=?, "
+ + Contacts.SEND_TO_VOICEMAIL + "=?, "
+ + Contacts.CUSTOM_RINGTONE + "=?, "
+ + Contacts.LAST_TIME_CONTACTED + "=?, "
+ + Contacts.TIMES_CONTACTED + "=?, "
+ + Contacts.STARRED + "=?, "
+ + Contacts.HAS_PHONE_NUMBER + "=?, "
+ + ContactsColumns.SINGLE_IS_RESTRICTED + "=?, "
+ + Contacts.LOOKUP_KEY + "=? " +
+ " WHERE " + Contacts._ID + "=?";
+
+ String INSERT_SQL =
+ "INSERT INTO " + Tables.CONTACTS + " ("
+ + Contacts.NAME_RAW_CONTACT_ID + ", "
+ + Contacts.PHOTO_ID + ", "
+ + Contacts.SEND_TO_VOICEMAIL + ", "
+ + Contacts.CUSTOM_RINGTONE + ", "
+ + Contacts.LAST_TIME_CONTACTED + ", "
+ + Contacts.TIMES_CONTACTED + ", "
+ + Contacts.STARRED + ", "
+ + Contacts.HAS_PHONE_NUMBER + ", "
+ + ContactsColumns.SINGLE_IS_RESTRICTED + ", "
+ + Contacts.LOOKUP_KEY + ", "
+ + Contacts.IN_VISIBLE_GROUP + ") " +
+ " VALUES (?,?,?,?,?,?,?,?,?,?,0)";
+
+ int NAME_RAW_CONTACT_ID = 1;
+ int PHOTO_ID = 2;
+ int SEND_TO_VOICEMAIL = 3;
+ int CUSTOM_RINGTONE = 4;
+ int LAST_TIME_CONTACTED = 5;
+ int TIMES_CONTACTED = 6;
+ int STARRED = 7;
+ int HAS_PHONE_NUMBER = 8;
+ int SINGLE_IS_RESTRICTED = 9;
+ int LOOKUP_KEY = 10;
+ int CONTACT_ID = 11;
}
/**
* Computes aggregate-level data for the specified aggregate contact ID.
*/
- private void computeAggregateData(SQLiteDatabase db, long contactId, ContentValues values) {
- computeAggregateData(db, RawContacts.CONTACT_ID + "=" + contactId
- + " AND " + RawContacts.DELETED + "=0", values);
+ private void computeAggregateData(SQLiteDatabase db, long contactId,
+ SQLiteStatement statement) {
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ computeAggregateData(db, mRawContactsQueryByContactId, mSelectionArgs1, statement);
}
/**
* Computes aggregate-level data from constituent raw contacts.
*/
- private void computeAggregateData(final SQLiteDatabase db, String selection,
- final ContentValues values) {
+ private void computeAggregateData(final SQLiteDatabase db, String sql, String[] sqlArgs,
+ SQLiteStatement statement) {
long currentRawContactId = -1;
- int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
- String bestDisplayName = null;
long bestPhotoId = -1;
boolean foundSuperPrimaryPhoto = false;
String photoAccount = null;
@@ -1179,25 +1100,14 @@
String contactCustomRingtone = null;
long contactLastTimeContacted = 0;
int contactTimesContacted = 0;
- boolean contactStarred = false;
+ int contactStarred = 0;
int singleIsRestricted = 1;
int hasPhoneNumber = 0;
- StringBuilder lookupKey = new StringBuilder();
- long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
- long phoneMimeType = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+ mDisplayNameCandidate.clear();
- String isPhotoSql = "(" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
- + Photo.PHOTO + " NOT NULL)";
- String isPhoneSql = "(" + DataColumns.MIMETYPE_ID + "=" + phoneMimeType + " AND "
- + Phone.NUMBER + " NOT NULL)";
-
- String tables = Tables.RAW_CONTACTS + " LEFT OUTER JOIN " + Tables.DATA + " ON("
- + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
- + " AND (" + isPhotoSql + " OR " + isPhoneSql + "))";
-
- final Cursor c = db.query(tables, RawContactsQuery.COLUMNS, selection, null, null, null,
- null);
+ mSb.setLength(0); // Lookup key
+ Cursor c = db.rawQuery(sql, sqlArgs);
try {
while (c.moveToNext()) {
long rawContactId = c.getLong(RawContactsQuery.RAW_CONTACT_ID);
@@ -1208,20 +1118,10 @@
// Display name
String displayName = c.getString(RawContactsQuery.DISPLAY_NAME);
int displayNameSource = c.getInt(RawContactsQuery.DISPLAY_NAME_SOURCE);
- if (!TextUtils.isEmpty(displayName)) {
- if (bestDisplayName == null) {
- bestDisplayName = displayName;
- bestDisplayNameSource = displayNameSource;
- } else if (bestDisplayNameSource != displayNameSource) {
- if (bestDisplayNameSource < displayNameSource) {
- bestDisplayName = displayName;
- bestDisplayNameSource = displayNameSource;
- }
- } else if (NameNormalizer.compareComplexity(displayName,
- bestDisplayName) > 0) {
- bestDisplayName = displayName;
- }
- }
+ int nameVerified = c.getInt(RawContactsQuery.NAME_VERIFIED);
+ processDisplayNameCanditate(rawContactId, displayName, displayNameSource,
+ nameVerified != 0);
+
// Contact options
if (!c.isNull(RawContactsQuery.SEND_TO_VOICEMAIL)) {
@@ -1247,7 +1147,9 @@
contactTimesContacted = timesContacted;
}
- contactStarred |= (c.getInt(RawContactsQuery.STARRED) != 0);
+ if (c.getInt(RawContactsQuery.STARRED) != 0) {
+ contactStarred = 1;
+ }
// Single restricted
if (totalRowCount > 1) {
@@ -1262,7 +1164,7 @@
}
}
- ContactLookupKey.appendToLookupKey(lookupKey,
+ ContactLookupKey.appendToLookupKey(mSb,
c.getString(RawContactsQuery.ACCOUNT_TYPE),
c.getString(RawContactsQuery.ACCOUNT_NAME),
c.getString(RawContactsQuery.SOURCE_ID),
@@ -1273,7 +1175,7 @@
long dataId = c.getLong(RawContactsQuery.DATA_ID);
int mimetypeId = c.getInt(RawContactsQuery.MIMETYPE_ID);
boolean superprimary = c.getInt(RawContactsQuery.IS_SUPER_PRIMARY) != 0;
- if (mimetypeId == photoMimeType) {
+ if (mimetypeId == mMimeTypeIdPhoto) {
// For now, just choose the first photo in a list sorted by account name.
String account = c.getString(RawContactsQuery.ACCOUNT_NAME);
@@ -1285,7 +1187,7 @@
bestPhotoId = dataId;
foundSuperPrimaryPhoto |= superprimary;
}
- } else if (mimetypeId == phoneMimeType) {
+ } else if (mimetypeId == mMimeTypeIdPhone) {
hasPhoneNumber = 1;
}
}
@@ -1295,28 +1197,68 @@
c.close();
}
- values.clear();
-
- // If don't have anything to base the display name on, let's just leave what was in
- // that field hoping that there was something there before and it is still valid.
- if (bestDisplayName != null) {
- values.put(Contacts.DISPLAY_NAME, bestDisplayName);
- }
+ statement.bindLong(ContactReplaceSqlStatement.NAME_RAW_CONTACT_ID,
+ mDisplayNameCandidate.rawContactId);
if (bestPhotoId != -1) {
- values.put(Contacts.PHOTO_ID, bestPhotoId);
+ statement.bindLong(ContactReplaceSqlStatement.PHOTO_ID, bestPhotoId);
} else {
- values.putNull(Contacts.PHOTO_ID);
+ statement.bindNull(ContactReplaceSqlStatement.PHOTO_ID);
}
- values.put(Contacts.SEND_TO_VOICEMAIL, totalRowCount == contactSendToVoicemail);
- values.put(Contacts.CUSTOM_RINGTONE, contactCustomRingtone);
- values.put(Contacts.LAST_TIME_CONTACTED, contactLastTimeContacted);
- values.put(Contacts.TIMES_CONTACTED, contactTimesContacted);
- values.put(Contacts.STARRED, contactStarred);
- values.put(Contacts.HAS_PHONE_NUMBER, hasPhoneNumber);
- values.put(ContactsColumns.SINGLE_IS_RESTRICTED, singleIsRestricted);
- values.put(Contacts.LOOKUP_KEY, Uri.encode(lookupKey.toString()));
+ statement.bindLong(ContactReplaceSqlStatement.SEND_TO_VOICEMAIL,
+ totalRowCount == contactSendToVoicemail ? 1 : 0);
+ DatabaseUtils.bindObjectToProgram(statement, ContactReplaceSqlStatement.CUSTOM_RINGTONE,
+ contactCustomRingtone);
+ statement.bindLong(ContactReplaceSqlStatement.LAST_TIME_CONTACTED,
+ contactLastTimeContacted);
+ statement.bindLong(ContactReplaceSqlStatement.TIMES_CONTACTED,
+ contactTimesContacted);
+ statement.bindLong(ContactReplaceSqlStatement.STARRED,
+ contactStarred);
+ statement.bindLong(ContactReplaceSqlStatement.HAS_PHONE_NUMBER,
+ hasPhoneNumber);
+ statement.bindLong(ContactReplaceSqlStatement.SINGLE_IS_RESTRICTED,
+ singleIsRestricted);
+ statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
+ Uri.encode(mSb.toString()));
+ }
+
+ /**
+ * Uses the supplied values to determine if they represent a "better" display name
+ * for the aggregate contact currently evaluated. If so, it updates
+ * {@link #mDisplayNameCandidate} with the new values.
+ */
+ private void processDisplayNameCanditate(long rawContactId, String displayName,
+ int displayNameSource, boolean verified) {
+
+ boolean replace = false;
+ if (mDisplayNameCandidate.rawContactId == -1) {
+ // No previous values available
+ replace = true;
+ } else if (!TextUtils.isEmpty(displayName)) {
+ if (!mDisplayNameCandidate.verified && verified) {
+ // A verified name is better than any other name
+ replace = true;
+ } else if (mDisplayNameCandidate.verified == verified) {
+ if (mDisplayNameCandidate.displayNameSource < displayNameSource) {
+ // New values come from an superior source, e.g. structured name vs phone number
+ replace = true;
+ } else if (mDisplayNameCandidate.displayNameSource == displayNameSource
+ && NameNormalizer.compareComplexity(displayName,
+ mDisplayNameCandidate.displayName) > 0) {
+ // New name is more complex than the previously found one
+ replace = true;
+ }
+ }
+ }
+
+ if (replace) {
+ mDisplayNameCandidate.rawContactId = rawContactId;
+ mDisplayNameCandidate.displayName = displayName;
+ mDisplayNameCandidate.displayNameSource = displayNameSource;
+ mDisplayNameCandidate.verified = verified;
+ }
}
private interface PhotoIdQuery {
@@ -1348,8 +1290,9 @@
+ " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
+ Photo.PHOTO + " NOT NULL))";
+ mSelectionArgs1[0] = String.valueOf(contactId);
final Cursor c = db.query(tables, PhotoIdQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+ RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
long dataId = c.getLong(PhotoIdQuery.DATA_ID);
@@ -1386,57 +1329,52 @@
private interface DisplayNameQuery {
String[] COLUMNS = new String[] {
+ RawContacts._ID,
RawContactsColumns.DISPLAY_NAME,
RawContactsColumns.DISPLAY_NAME_SOURCE,
+ RawContacts.NAME_VERIFIED,
};
- int DISPLAY_NAME = 0;
- int DISPLAY_NAME_SOURCE = 1;
+ int _ID = 0;
+ int DISPLAY_NAME = 1;
+ int DISPLAY_NAME_SOURCE = 2;
+ int NAME_VERIFIED = 3;
}
- public void updateDisplayName(SQLiteDatabase db, long rawContactId) {
-
+ public void updateDisplayNameForRawContact(SQLiteDatabase db, long rawContactId) {
long contactId = mDbHelper.getContactId(rawContactId);
if (contactId == 0) {
return;
}
- int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
- String bestDisplayName = null;
+ updateDisplayNameForContact(db, contactId);
+ }
+ public void updateDisplayNameForContact(SQLiteDatabase db, long contactId) {
+ mDisplayNameCandidate.clear();
+ mSelectionArgs1[0] = String.valueOf(contactId);
final Cursor c = db.query(Tables.RAW_CONTACTS, DisplayNameQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+ RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
+ long rawContactId = c.getLong(DisplayNameQuery._ID);
String displayName = c.getString(DisplayNameQuery.DISPLAY_NAME);
int displayNameSource = c.getInt(DisplayNameQuery.DISPLAY_NAME_SOURCE);
- if (!TextUtils.isEmpty(displayName)) {
- if (bestDisplayName == null) {
- bestDisplayName = displayName;
- bestDisplayNameSource = displayNameSource;
- } else if (bestDisplayNameSource != displayNameSource) {
- if (bestDisplayNameSource < displayNameSource) {
- bestDisplayName = displayName;
- bestDisplayNameSource = displayNameSource;
- }
- } else if (NameNormalizer.compareComplexity(displayName,
- bestDisplayName) > 0) {
- bestDisplayName = displayName;
- }
- }
+ int nameVerified = c.getInt(DisplayNameQuery.NAME_VERIFIED);
+
+ processDisplayNameCanditate(rawContactId, displayName, displayNameSource,
+ nameVerified != 0);
}
} finally {
c.close();
}
- if (bestDisplayName == null) {
- mDisplayNameUpdate.bindNull(1);
- } else {
- mDisplayNameUpdate.bindString(1, bestDisplayName);
+ if (mDisplayNameCandidate.rawContactId != -1) {
+ mDisplayNameUpdate.bindLong(1, mDisplayNameCandidate.rawContactId);
+ mDisplayNameUpdate.bindLong(2, contactId);
+ mDisplayNameUpdate.execute();
}
- mDisplayNameUpdate.bindLong(2, contactId);
- mDisplayNameUpdate.execute();
}
/**
@@ -1476,12 +1414,13 @@
return;
}
- StringBuilder lookupKey = new StringBuilder();
+ mSb.setLength(0);
+ mSelectionArgs1[0] = String.valueOf(contactId);
final Cursor c = db.query(Tables.RAW_CONTACTS, LookupKeyQuery.COLUMNS,
- RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+ RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
- ContactLookupKey.appendToLookupKey(lookupKey,
+ ContactLookupKey.appendToLookupKey(mSb,
c.getString(LookupKeyQuery.ACCOUNT_TYPE),
c.getString(LookupKeyQuery.ACCOUNT_NAME),
c.getString(LookupKeyQuery.SOURCE_ID),
@@ -1491,10 +1430,10 @@
c.close();
}
- if (lookupKey.length() == 0) {
+ if (mSb.length() == 0) {
mLookupKeyUpdate.bindNull(1);
} else {
- mLookupKeyUpdate.bindString(1, lookupKey.toString());
+ mLookupKeyUpdate.bindString(1, mSb.toString());
}
mLookupKeyUpdate.bindLong(2, contactId);
mLookupKeyUpdate.execute();
@@ -1526,6 +1465,14 @@
filter);
}
+ private interface ContactIdQuery {
+ String[] COLUMNS = new String[] {
+ Contacts._ID
+ };
+
+ int _ID = 0;
+ }
+
/**
* Loads contacts with specified IDs and returns them in the order of IDs in the
* supplied list.
@@ -1610,6 +1557,16 @@
return new ReorderingCursorWrapper(cursor, positionMap);
}
+ private interface RawContactIdQuery {
+ String TABLE = Tables.RAW_CONTACTS;
+
+ String[] COLUMNS = new String[] {
+ RawContacts._ID
+ };
+
+ int _ID = 0;
+ }
+
/**
* Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
* descending order of match score.
@@ -1622,12 +1579,12 @@
// Don't aggregate a contact with itself
matcher.keepOut(contactId);
- final Cursor c = db.query(Tables.RAW_CONTACTS, CONTACT_ID_COLUMN,
+ final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
try {
while (c.moveToNext()) {
- long rawContactId = c.getLong(0);
- updateMatchScoresBasedOnDataMatches(db, rawContactId, MODE_SUGGESTIONS, candidates,
+ long rawContactId = c.getLong(RawContactIdQuery._ID);
+ updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
matcher);
}
} finally {
@@ -1636,4 +1593,17 @@
return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
}
+
+ /**
+ * Computes scores for contacts that have matching data rows.
+ */
+ private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+ long rawContactId, MatchCandidateList candidates, ContactMatcher matcher) {
+
+ updateMatchScoresBasedOnNameMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnEmailMatches(db, rawContactId, matcher);
+ updateMatchScoresBasedOnPhoneMatches(db, rawContactId, matcher);
+ loadNameMatchCandidates(db, rawContactId, candidates, false);
+ lookupApproximateNameMatches(db, candidates, matcher);
+ }
}
diff --git a/src/com/android/providers/contacts/ContactLocaleUtils.java b/src/com/android/providers/contacts/ContactLocaleUtils.java
new file mode 100644
index 0000000..18d43cf
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactLocaleUtils.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010 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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import android.provider.ContactsContract.FullNameStyle;
+import com.android.internal.util.HanziToPinyin;
+import com.android.internal.util.HanziToPinyin.Token;
+
+/**
+ * This utility class provides customized sort key and name lookup key according the locale.
+ */
+public class ContactLocaleUtils {
+
+ /**
+ * This class is the default implementation.
+ * <p>
+ * It should be the base class for other locales' implementation.
+ */
+ public static class ContactLocaleUtilsBase {
+ public String getSortKey(String displayName) {
+ return displayName;
+ }
+ public Iterator<String> getNameLookupKeys(String name) {
+ return null;
+ }
+ }
+
+ private static class ChineseContactUtils extends ContactLocaleUtilsBase {
+ @Override
+ public String getSortKey(String displayName) {
+ ArrayList<Token> tokens = HanziToPinyin.getInstance().get(displayName);
+ if (tokens != null && tokens.size() > 0) {
+ StringBuilder sb = new StringBuilder();
+ for (Token token : tokens) {
+ // Put Chinese character's pinyin, then proceed with the
+ // character itself.
+ if (Token.PINYIN == token.type) {
+ if (sb.length() > 0) {
+ sb.append(' ');
+ }
+ sb.append(token.target);
+ sb.append(' ');
+ sb.append(token.source);
+ } else {
+ if (sb.length() > 0) {
+ sb.append(' ');
+ }
+ sb.append(token.source);
+ }
+ }
+ return sb.toString();
+ }
+ return super.getSortKey(displayName);
+ }
+
+ @Override
+ public Iterator<String> getNameLookupKeys(String name) {
+ // TODO : Reduce the object allocation.
+ HashSet<String> keys = new HashSet<String>();
+ ArrayList<Token> tokens = HanziToPinyin.getInstance().get(name);
+ final int tokenCount = tokens.size();
+ final StringBuilder keyPinyin = new StringBuilder();
+ final StringBuilder keyInitial = new StringBuilder();
+ // There is no space among the Chinese Characters, the variant name
+ // lookup key wouldn't work for Chinese. The keyOrignal is used to
+ // build the lookup keys for itself.
+ final StringBuilder keyOrignal = new StringBuilder();
+ for (int i = tokenCount - 1; i >= 0; i--) {
+ final Token token = tokens.get(i);
+ if (Token.PINYIN == token.type) {
+ keyPinyin.insert(0, token.target);
+ keyInitial.insert(0, token.target.charAt(0));
+ } else if (Token.LATIN == token.type) {
+ // Avoid adding space at the end of String.
+ if (keyPinyin.length() > 0) {
+ keyPinyin.insert(0, ' ');
+ }
+ if (keyOrignal.length() > 0) {
+ keyOrignal.insert(0, ' ');
+ }
+ keyPinyin.insert(0, token.source);
+ keyInitial.insert(0, token.source.charAt(0));
+ }
+ keyOrignal.insert(0, token.source);
+ keys.add(keyOrignal.toString());
+ keys.add(keyPinyin.toString());
+ keys.add(keyInitial.toString());
+ }
+ return keys.iterator();
+ }
+ }
+
+ private static HashMap<Integer, ContactLocaleUtilsBase> mUtils =
+ new HashMap<Integer, ContactLocaleUtilsBase>();
+
+ private static ContactLocaleUtilsBase mBase = new ContactLocaleUtilsBase();
+ public static String getSortKey(String displayName, int nameStyle) {
+ return get(Integer.valueOf(nameStyle)).getSortKey(displayName);
+ }
+
+ public static Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+ return get(Integer.valueOf(nameStyle)).getNameLookupKeys(name);
+ }
+
+ private synchronized static ContactLocaleUtilsBase get(Integer nameStyle) {
+ ContactLocaleUtilsBase utils = mUtils.get(nameStyle);
+ if (utils == null) {
+ if (nameStyle.intValue() == FullNameStyle.CHINESE) {
+ utils = new ChineseContactUtils();
+ mUtils.put(nameStyle, utils);
+ }
+ }
+ return (utils == null) ? mBase: utils;
+ }
+}
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 154f62f..5ae6c8a 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -25,6 +25,7 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
+import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
@@ -41,17 +42,23 @@
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.Settings;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.SocialContract.Activities;
import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
import android.util.Log;
import java.util.HashMap;
+import java.util.Locale;
/**
* Database helper for contacts. Designed as a singleton to make sure that all
@@ -61,11 +68,14 @@
/* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "ContactsDatabaseHelper";
- private static final int DATABASE_VERSION = 105;
+ private static final int DATABASE_VERSION = 206;
private static final String DATABASE_NAME = "contacts2.db";
private static final String DATABASE_PRESENCE = "presence_db";
+ /** size of the compiled-sql statement cache mainatained by {@link SQLiteDatabase} */
+ private static final int MAX_CACHE_SIZE_FOR_CONTACTS_DB = 250;
+
public interface Tables {
public static final String CONTACTS = "contacts";
public static final String RAW_CONTACTS = "raw_contacts";
@@ -95,25 +105,6 @@
+ "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
- public static final String DATA_JOIN_RAW_CONTACTS_GROUPS = "data "
- + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"
- + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID
- + ")";
-
- public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS = "data "
- + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
- + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
- + "LEFT OUTER JOIN packages ON (data.package_id = packages._id)";
-
- public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
- + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
- + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
- + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
- + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
- public static final String RAW_CONTACTS_JOIN_CONTACTS = "raw_contacts "
- + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
// NOTE: This requires late binding of GroupMembership MIME-type
public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = "raw_contacts "
+ "LEFT OUTER JOIN settings ON ("
@@ -138,21 +129,6 @@
+ "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
- public static final String DATA_INNER_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
- + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
- + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
- + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
- public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS_GROUPS =
- "data "
- + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
- + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
- + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
- + "LEFT OUTER JOIN groups "
- + " ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
- + " AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "
- + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS = "data "
+ "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
+ "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
@@ -164,13 +140,6 @@
public static final String GROUPS_JOIN_PACKAGES = "groups "
+ "LEFT OUTER JOIN packages ON (groups.package_id = packages._id)";
- public static final String GROUPS_JOIN_PACKAGES_DATA_RAW_CONTACTS_CONTACTS = "groups "
- + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id) "
- + "LEFT OUTER JOIN data "
- + " ON (groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "
- + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
- + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
public static final String ACTIVITIES = "activities";
public static final String ACTIVITIES_JOIN_MIMETYPES = "activities "
@@ -181,7 +150,7 @@
+ "LEFT OUTER JOIN packages ON (activities.package_id = packages._id) "
+ "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id) "
+ "LEFT OUTER JOIN raw_contacts ON (activities.author_contact_id = " +
- "raw_contacts._id) "
+ "raw_contacts._id) "
+ "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup "
@@ -251,8 +220,6 @@
public static final String LAST_STATUS_UPDATE_ID = "status_update_id";
public static final String CONCRETE_ID = Tables.CONTACTS + "." + BaseColumns._ID;
- public static final String CONCRETE_DISPLAY_NAME = Tables.CONTACTS + "."
- + Contacts.DISPLAY_NAME;
public static final String CONCRETE_TIMES_CONTACTED = Tables.CONTACTS + "."
+ Contacts.TIMES_CONTACTED;
@@ -263,6 +230,8 @@
+ Contacts.CUSTOM_RINGTONE;
public static final String CONCRETE_SEND_TO_VOICEMAIL = Tables.CONTACTS + "."
+ Contacts.SEND_TO_VOICEMAIL;
+ public static final String CONCRETE_LOOKUP_KEY = Tables.CONTACTS + "."
+ + Contacts.LOOKUP_KEY;
}
public interface RawContactsColumns {
@@ -293,22 +262,17 @@
public static final String CONCRETE_IS_RESTRICTED =
Tables.RAW_CONTACTS + "." + RawContacts.IS_RESTRICTED;
- public static final String DISPLAY_NAME = "display_name";
- public static final String DISPLAY_NAME_SOURCE = "display_name_source";
+ public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY;
+ public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE;
public static final String AGGREGATION_NEEDED = "aggregation_needed";
- }
+ public static final String CONTACT_IN_VISIBLE_GROUP = "contact_in_visible_group";
- /**
- * Types of data used to produce the display name for a contact. Listed in the order
- * of increasing priority.
- */
- public interface DisplayNameSources {
- int UNDEFINED = 0;
- int EMAIL = 10;
- int PHONE = 20;
- int ORGANIZATION = 30;
- int NICKNAME = 35;
- int STRUCTURED_NAME = 40;
+ public static final String CONCRETE_DISPLAY_NAME =
+ Tables.RAW_CONTACTS + "." + DISPLAY_NAME;
+ public static final String CONCRETE_CONTACT_ID =
+ Tables.RAW_CONTACTS + "." + RawContacts.CONTACT_ID;
+ public static final String CONCRETE_NAME_VERIFIED =
+ Tables.RAW_CONTACTS + "." + RawContacts.NAME_VERIFIED;
}
public interface DataColumns {
@@ -376,6 +340,7 @@
public static final String DATA_ID = "data_id";
public static final String RAW_CONTACT_ID = "raw_contact_id";
public static final String NORMALIZED_NUMBER = "normalized_number";
+ public static final String MIN_MATCH = "min_match";
}
public interface NameLookupColumns {
@@ -392,9 +357,10 @@
public static final int NICKNAME = 3;
public static final int EMAIL_BASED_NICKNAME = 4;
public static final int ORGANIZATION = 5;
+ public static final int NAME_SHORTHAND = 6;
// This is the highest name lookup type code plus one
- public static final int TYPE_COUNT = 6;
+ public static final int TYPE_COUNT = 7;
public static boolean isBasedOnStructuredName(int nameLookupType) {
return nameLookupType == NameLookupType.NAME_EXACT
@@ -494,14 +460,15 @@
/** Compiled statements for updating {@link Contacts#IN_VISIBLE_GROUP}. */
- private SQLiteStatement mVisibleUpdate;
private SQLiteStatement mVisibleSpecificUpdate;
+ private SQLiteStatement mVisibleUpdateRawContacts;
+ private SQLiteStatement mVisibleSpecificUpdateRawContacts;
private boolean mReopenDatabase = false;
private static ContactsDatabaseHelper sSingleton = null;
- private boolean mUseStrictPhoneNumberComparation;
+ private boolean mUseStrictPhoneNumberComparison;
/**
* List of package names with access to {@link RawContacts#IS_RESTRICTED} data.
@@ -526,7 +493,7 @@
mContext = context;
mSyncState = new SyncStateContentProviderHelper();
- mUseStrictPhoneNumberComparation =
+ mUseStrictPhoneNumberComparison =
resources.getBoolean(
com.android.internal.R.bool.config_use_strict_phone_number_comparation);
int resourceId = resources.getIdentifier("unrestricted_packages", "array",
@@ -562,13 +529,33 @@
+ " FROM " + Tables.ACTIVITIES_JOIN_MIMETYPES + " WHERE " + Tables.ACTIVITIES + "."
+ Activities._ID + "=?");
- // Compile statements for updating visibility
- final String visibleUpdate = "UPDATE " + Tables.CONTACTS + " SET "
- + Contacts.IN_VISIBLE_GROUP + "=(" + Clauses.CONTACT_IS_VISIBLE + ")";
+ // Change visibility of a specific contact
+ mVisibleSpecificUpdate = db.compileStatement(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.IN_VISIBLE_GROUP + "=(" + Clauses.CONTACT_IS_VISIBLE + ")" +
+ " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
- mVisibleUpdate = db.compileStatement(visibleUpdate);
- mVisibleSpecificUpdate = db.compileStatement(visibleUpdate + " WHERE "
- + ContactsColumns.CONCRETE_ID + "=?");
+ // Return visibility of the aggregate contact joined with the raw contact
+ String contactVisibility =
+ "SELECT " + Contacts.IN_VISIBLE_GROUP +
+ " FROM " + Tables.CONTACTS +
+ " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID;
+
+ // Set visibility of raw contacts to the visibility of corresponding aggregate contacts
+ mVisibleUpdateRawContacts = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=("
+ + contactVisibility + ")" +
+ " WHERE " + RawContacts.DELETED + "=0" +
+ " AND " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "!=("
+ + contactVisibility + ")=1");
+
+ // Set visibility of a raw contact to the visibility of corresponding aggregate contact
+ mVisibleSpecificUpdateRawContacts = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=("
+ + contactVisibility + ")" +
+ " WHERE " + RawContacts.DELETED + "=0 AND " + RawContacts.CONTACT_ID + "=?");
db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+
@@ -645,7 +632,7 @@
// One row per group of contacts corresponding to the same person
db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Contacts.DISPLAY_NAME + " TEXT," +
+ Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
Contacts.PHOTO_ID + " INTEGER REFERENCES data(_id)," +
Contacts.CUSTOM_RINGTONE + " TEXT," +
Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
@@ -660,8 +647,7 @@
");");
db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
- Contacts.IN_VISIBLE_GROUP + "," +
- Contacts.DISPLAY_NAME + " COLLATE LOCALIZED" +
+ Contacts.IN_VISIBLE_GROUP +
");");
db.execSQL("CREATE INDEX contacts_has_phone_index ON " + Tables.CONTACTS + " (" +
@@ -672,6 +658,10 @@
ContactsColumns.SINGLE_IS_RESTRICTED +
");");
+ db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
+ Contacts.NAME_RAW_CONTACT_ID +
+ ");");
+
// Contacts table
db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -691,24 +681,22 @@
RawContacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.LAST_TIME_CONTACTED + " INTEGER," +
RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
- RawContactsColumns.DISPLAY_NAME + " TEXT," +
- RawContactsColumns.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
+ RawContacts.DISPLAY_NAME_PRIMARY + " TEXT," +
+ RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT," +
+ RawContacts.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
DisplayNameSources.UNDEFINED + "," +
+ RawContacts.PHONETIC_NAME + " TEXT," +
+ RawContacts.PHONETIC_NAME_STYLE + " TEXT," +
+ RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE LOCALIZED," +
+ RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE LOCALIZED," +
+ RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0," +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.SYNC1 + " TEXT, " +
RawContacts.SYNC2 + " TEXT, " +
RawContacts.SYNC3 + " TEXT, " +
RawContacts.SYNC4 + " TEXT " +
");");
- db.execSQL("CREATE TRIGGER raw_contacts_times_contacted UPDATE OF " +
- RawContacts.LAST_TIME_CONTACTED + " ON " + Tables.RAW_CONTACTS + " " +
- "BEGIN " +
- "UPDATE " + Tables.RAW_CONTACTS + " SET "
- + RawContacts.TIMES_CONTACTED + " = " + "" +
- "(new." + RawContacts.TIMES_CONTACTED + " + 1)"
- + " WHERE _id = new._id;" +
- "END");
-
db.execSQL("CREATE INDEX raw_contacts_contact_id_index ON " + Tables.RAW_CONTACTS + " (" +
RawContacts.CONTACT_ID +
");");
@@ -724,6 +712,16 @@
// RawContactsColumns.AGGREGATION_NEEDED +
// ");");
+ db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ RawContacts.SORT_KEY_PRIMARY +
+ ");");
+
+ db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ RawContacts.SORT_KEY_ALTERNATIVE +
+ ");");
+
// Package name mapping table
db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -784,7 +782,8 @@
+ " INTEGER PRIMARY KEY REFERENCES data(_id) NOT NULL," +
PhoneLookupColumns.RAW_CONTACT_ID
+ " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
- PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL" +
+ PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
+ PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" +
");");
db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
@@ -793,6 +792,12 @@
PhoneLookupColumns.DATA_ID +
");");
+ db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
+ PhoneLookupColumns.MIN_MATCH + "," +
+ PhoneLookupColumns.RAW_CONTACT_ID + "," +
+ PhoneLookupColumns.DATA_ID +
+ ");");
+
// Private name/nickname table used for lookup
db.execSQL("CREATE TABLE " + Tables.NAME_LOOKUP + " (" +
NameLookupColumns.DATA_ID
@@ -975,14 +980,6 @@
db.execSQL("DROP TRIGGER IF EXISTS contacts_times_contacted;");
- db.execSQL("CREATE TRIGGER contacts_times_contacted UPDATE OF " +
- Contacts.LAST_TIME_CONTACTED + " ON " + Tables.CONTACTS + " " +
- "BEGIN " +
- "UPDATE " + Tables.CONTACTS + " SET "
- + Contacts.TIMES_CONTACTED + " = " + "" +
- "(new." + Contacts.TIMES_CONTACTED + " + 1)"
- + " WHERE _id = new._id;" +
- "END");
/*
* Triggers that update {@link RawContacts#VERSION} when the contact is
@@ -991,7 +988,7 @@
*/
db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;");
db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted "
- + " BEFORE UPDATE ON " + Tables.RAW_CONTACTS
+ + " AFTER UPDATE ON " + Tables.RAW_CONTACTS
+ " BEGIN "
+ " UPDATE " + Tables.RAW_CONTACTS
+ " SET "
@@ -1001,7 +998,7 @@
+ " END");
db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_updated;");
- db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated BEFORE UPDATE ON " + Tables.DATA
+ db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA
+ " BEGIN "
+ " UPDATE " + Tables.DATA
+ " SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 "
@@ -1028,7 +1025,7 @@
db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_updated1;");
db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 "
- + " BEFORE UPDATE ON " + Tables.GROUPS
+ + " AFTER UPDATE ON " + Tables.GROUPS
+ " BEGIN "
+ " UPDATE " + Tables.GROUPS
+ " SET "
@@ -1075,6 +1072,7 @@
RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
+ RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
+ RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
+ + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS " + RawContacts.NAME_VERIFIED + ","
+ RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
+ RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
+ RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ","
@@ -1094,17 +1092,34 @@
+ ContactsColumns.CONCRETE_STARRED
+ " AS " + RawContacts.STARRED;
+ String contactNameColumns =
+ "name_raw_contact." + RawContacts.DISPLAY_NAME_SOURCE
+ + " AS " + Contacts.DISPLAY_NAME_SOURCE + ", "
+ + "name_raw_contact." + RawContacts.DISPLAY_NAME_PRIMARY
+ + " AS " + Contacts.DISPLAY_NAME_PRIMARY + ", "
+ + "name_raw_contact." + RawContacts.DISPLAY_NAME_ALTERNATIVE
+ + " AS " + Contacts.DISPLAY_NAME_ALTERNATIVE + ", "
+ + "name_raw_contact." + RawContacts.PHONETIC_NAME
+ + " AS " + Contacts.PHONETIC_NAME + ", "
+ + "name_raw_contact." + RawContacts.PHONETIC_NAME_STYLE
+ + " AS " + Contacts.PHONETIC_NAME_STYLE + ", "
+ + "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY
+ + " AS " + Contacts.SORT_KEY_PRIMARY + ", "
+ + "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE
+ + " AS " + Contacts.SORT_KEY_ALTERNATIVE + ", "
+ + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+ + " AS " + Contacts.IN_VISIBLE_GROUP;
+
String dataSelect = "SELECT "
+ DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
+ Data.RAW_CONTACT_ID + ", "
- + RawContacts.CONTACT_ID + ", "
+ + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
+ syncColumns + ", "
+ dataColumns + ", "
+ contactOptionColumns + ", "
- + ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME + ", "
+ + contactNameColumns + ", "
+ Contacts.LOOKUP_KEY + ", "
+ Contacts.PHOTO_ID + ", "
- + Contacts.IN_VISIBLE_GROUP + ", "
+ ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
+ Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
+ " FROM " + Tables.DATA
@@ -1113,7 +1128,9 @@
+ " JOIN " + Tables.RAW_CONTACTS + " ON ("
+ DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
+ " JOIN " + Tables.CONTACTS + " ON ("
- + RawContacts.CONTACT_ID + "=" + Tables.CONTACTS + "." + Contacts._ID + ")"
+ + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
+ + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
+ + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
+ " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
+ DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
+ " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
@@ -1123,7 +1140,7 @@
db.execSQL("CREATE VIEW " + Views.DATA_ALL + " AS " + dataSelect);
db.execSQL("CREATE VIEW " + Views.DATA_RESTRICTED + " AS " + dataSelect + " WHERE "
- + RawContacts.IS_RESTRICTED + "=0");
+ + RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0");
String rawContactOptionColumns =
RawContacts.CUSTOM_RINGTONE + ","
@@ -1137,6 +1154,13 @@
+ RawContacts.CONTACT_ID + ", "
+ RawContacts.AGGREGATION_MODE + ", "
+ RawContacts.DELETED + ", "
+ + RawContacts.DISPLAY_NAME_SOURCE + ", "
+ + RawContacts.DISPLAY_NAME_PRIMARY + ", "
+ + RawContacts.DISPLAY_NAME_ALTERNATIVE + ", "
+ + RawContacts.PHONETIC_NAME + ", "
+ + RawContacts.PHONETIC_NAME_STYLE + ", "
+ + RawContacts.SORT_KEY_PRIMARY + ", "
+ + RawContacts.SORT_KEY_ALTERNATIVE + ", "
+ rawContactOptionColumns + ", "
+ syncColumns
+ " FROM " + Tables.RAW_CONTACTS;
@@ -1148,13 +1172,10 @@
String contactsColumns =
ContactsColumns.CONCRETE_CUSTOM_RINGTONE
+ " AS " + Contacts.CUSTOM_RINGTONE + ", "
- + ContactsColumns.CONCRETE_DISPLAY_NAME
- + " AS " + Contacts.DISPLAY_NAME + ", "
- + Contacts.IN_VISIBLE_GROUP + ", "
+ + contactNameColumns + ", "
+ Contacts.HAS_PHONE_NUMBER + ", "
+ Contacts.LOOKUP_KEY + ", "
+ Contacts.PHOTO_ID + ", "
- + Contacts.IN_VISIBLE_GROUP + ", "
+ ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
+ " AS " + Contacts.LAST_TIME_CONTACTED + ", "
+ ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
@@ -1168,16 +1189,13 @@
String contactsSelect = "SELECT "
+ ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
+ contactsColumns
- + " FROM " + Tables.CONTACTS;
-
- String restrictedContactsSelect = "SELECT "
- + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
- + contactsColumns
+ " FROM " + Tables.CONTACTS
- + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0";
+ + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
+ + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
db.execSQL("CREATE VIEW " + Views.CONTACTS_ALL + " AS " + contactsSelect);
- db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + restrictedContactsSelect);
+ db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + contactsSelect
+ + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0");
}
private static void createGroupsView(SQLiteDatabase db) {
@@ -1220,6 +1238,7 @@
+ RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
+ RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
+ RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
+ + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS " + RawContacts.NAME_VERIFIED + ","
+ PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
+ RawContacts.CONTACT_ID + ", "
+ RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ", "
@@ -1304,8 +1323,10 @@
Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion);
+ boolean upgradeViewsAndTriggers = false;
+
if (oldVersion == 99) {
- createContactEntitiesView(db);
+ upgradeViewsAndTriggers = true;
oldVersion++;
}
@@ -1317,37 +1338,451 @@
updateIndexStats(db, Tables.MIMETYPES,
"mimetypes_mimetype_index", "50 1 1");
- createContactsViews(db);
+ upgradeViewsAndTriggers = true;
oldVersion++;
}
if (oldVersion == 101) {
- createContactsTriggers(db);
+ upgradeViewsAndTriggers = true;
oldVersion++;
}
if (oldVersion == 102) {
- LegacyApiSupport.createViews(db);
+ upgradeViewsAndTriggers = true;
oldVersion++;
}
if (oldVersion == 103) {
- createContactEntitiesView(db);
+ upgradeViewsAndTriggers = true;
oldVersion++;
}
- if (oldVersion == 104) {
- LegacyApiSupport.createViews(db);
+ if (oldVersion == 104 || oldVersion == 201) {
LegacyApiSupport.createSettingsTable(db);
+ upgradeViewsAndTriggers = true;
oldVersion++;
}
+ if (oldVersion == 105) {
+ upgradeToVersion202(db);
+ oldVersion = 202;
+ }
+
+ if (oldVersion == 202) {
+ upgradeToVersion203(db);
+ upgradeViewsAndTriggers = true;
+ oldVersion++;
+ }
+
+ if (oldVersion == 203) {
+ upgradeViewsAndTriggers = true;
+ oldVersion++;
+ }
+
+ if (oldVersion == 204) {
+ upgradeToVersion205(db);
+ upgradeViewsAndTriggers = true;
+ oldVersion++;
+ }
+
+ if (oldVersion == 205) {
+ upgrateToVersion206(db);
+ upgradeViewsAndTriggers = true;
+ oldVersion++;
+ }
+
+ if (upgradeViewsAndTriggers) {
+ createContactsViews(db);
+ createGroupsView(db);
+ createContactEntitiesView(db);
+ createContactsTriggers(db);
+ LegacyApiSupport.createViews(db);
+ }
+
if (oldVersion != newVersion) {
throw new IllegalStateException(
"error upgrading the database to version " + newVersion);
}
}
+ private void upgradeToVersion202(SQLiteDatabase db) {
+ db.execSQL(
+ "ALTER TABLE " + Tables.PHONE_LOOKUP +
+ " ADD " + PhoneLookupColumns.MIN_MATCH + " TEXT;");
+
+ db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
+ PhoneLookupColumns.MIN_MATCH + "," +
+ PhoneLookupColumns.RAW_CONTACT_ID + "," +
+ PhoneLookupColumns.DATA_ID +
+ ");");
+
+ updateIndexStats(db, Tables.PHONE_LOOKUP,
+ "phone_lookup_min_match_index", "10000 2 2 1");
+
+ SQLiteStatement update = db.compileStatement(
+ "UPDATE " + Tables.PHONE_LOOKUP +
+ " SET " + PhoneLookupColumns.MIN_MATCH + "=?" +
+ " WHERE " + PhoneLookupColumns.DATA_ID + "=?");
+
+ // Populate the new column
+ Cursor c = db.query(Tables.PHONE_LOOKUP + " JOIN " + Tables.DATA +
+ " ON (" + PhoneLookupColumns.DATA_ID + "=" + DataColumns.CONCRETE_ID + ")",
+ new String[]{Data._ID, Phone.NUMBER}, null, null, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ long dataId = c.getLong(0);
+ String number = c.getString(1);
+ if (!TextUtils.isEmpty(number)) {
+ update.bindString(1, PhoneNumberUtils.toCallerIDMinMatch(number));
+ update.bindLong(2, dataId);
+ update.execute();
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ private void upgradeToVersion203(SQLiteDatabase db) {
+ db.execSQL(
+ "ALTER TABLE " + Tables.CONTACTS +
+ " ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)");
+ db.execSQL(
+ "ALTER TABLE " + Tables.RAW_CONTACTS +
+ " ADD " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
+ + " INTEGER NOT NULL DEFAULT 0");
+
+ // For each Contact, find the RawContact that contributed the display name
+ db.execSQL(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
+ " SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
+ " AND " + RawContactsColumns.CONCRETE_DISPLAY_NAME + "=" +
+ Tables.CONTACTS + "." + Contacts.DISPLAY_NAME +
+ " ORDER BY " + RawContacts._ID +
+ " LIMIT 1)"
+ );
+
+ db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
+ Contacts.NAME_RAW_CONTACT_ID +
+ ");");
+
+ // If for some unknown reason we missed some names, let's make sure there are
+ // no contacts without a name, picking a raw contact "at random".
+ db.execSQL(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
+ " SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
+ " ORDER BY " + RawContacts._ID +
+ " LIMIT 1)" +
+ " WHERE " + Contacts.NAME_RAW_CONTACT_ID + " IS NULL"
+ );
+
+ // Wipe out DISPLAY_NAME on the Contacts table as it is no longer in use.
+ db.execSQL(
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.DISPLAY_NAME + "=NULL"
+ );
+
+ // Copy the IN_VISIBLE_GROUP flag down to all raw contacts to allow
+ // indexing on (display_name, in_visible_group)
+ db.execSQL(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
+ "SELECT " + Contacts.IN_VISIBLE_GROUP +
+ " FROM " + Tables.CONTACTS +
+ " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" +
+ " WHERE " + RawContacts.CONTACT_ID + " NOT NULL"
+ );
+
+ db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
+ ");");
+
+ db.execSQL("DROP INDEX contacts_visible_index");
+ db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
+ Contacts.IN_VISIBLE_GROUP +
+ ");");
+ }
+
+ private void upgradeToVersion205(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+ + " ADD " + RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT;");
+ db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+ + " ADD " + RawContacts.PHONETIC_NAME + " TEXT;");
+ db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+ + " ADD " + RawContacts.PHONETIC_NAME_STYLE + " INTEGER;");
+ db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+ + " ADD " + RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE LOCALIZED;");
+ db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+ + " ADD " + RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE LOCALIZED;");
+
+ final Locale locale = Locale.getDefault();
+
+ NameSplitter splitter = new NameSplitter(
+ mContext.getString(com.android.internal.R.string.common_name_prefixes),
+ mContext.getString(com.android.internal.R.string.common_last_name_prefixes),
+ mContext.getString(com.android.internal.R.string.common_name_suffixes),
+ mContext.getString(com.android.internal.R.string.common_name_conjunctions),
+ locale);
+
+ SQLiteStatement rawContactUpdate = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " +
+ RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
+ RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
+ RawContacts.PHONETIC_NAME + "=?," +
+ RawContacts.PHONETIC_NAME_STYLE + "=?," +
+ RawContacts.SORT_KEY_PRIMARY + "=?," +
+ RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
+ " WHERE " + RawContacts._ID + "=?");
+
+ upgradeStructuredNamesToVersion205(db, rawContactUpdate, splitter);
+ upgradeOrganizationsToVersion205(db, rawContactUpdate, splitter);
+
+ db.execSQL("DROP INDEX raw_contact_sort_key1_index");
+ db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ RawContacts.SORT_KEY_PRIMARY +
+ ");");
+
+ db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
+ RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ RawContacts.SORT_KEY_ALTERNATIVE +
+ ");");
+ }
+
+ private interface StructName205Query {
+ String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
+
+ String COLUMNS[] = {
+ DataColumns.CONCRETE_ID,
+ Data.RAW_CONTACT_ID,
+ RawContacts.DISPLAY_NAME_SOURCE,
+ RawContacts.DISPLAY_NAME_PRIMARY,
+ StructuredName.PREFIX,
+ StructuredName.GIVEN_NAME,
+ StructuredName.MIDDLE_NAME,
+ StructuredName.FAMILY_NAME,
+ StructuredName.SUFFIX,
+ StructuredName.PHONETIC_FAMILY_NAME,
+ StructuredName.PHONETIC_MIDDLE_NAME,
+ StructuredName.PHONETIC_GIVEN_NAME,
+ };
+
+ int ID = 0;
+ int RAW_CONTACT_ID = 1;
+ int DISPLAY_NAME_SOURCE = 2;
+ int DISPLAY_NAME = 3;
+ int PREFIX = 4;
+ int GIVEN_NAME = 5;
+ int MIDDLE_NAME = 6;
+ int FAMILY_NAME = 7;
+ int SUFFIX = 8;
+ int PHONETIC_FAMILY_NAME = 9;
+ int PHONETIC_MIDDLE_NAME = 10;
+ int PHONETIC_GIVEN_NAME = 11;
+ }
+
+ private void upgradeStructuredNamesToVersion205(SQLiteDatabase db,
+ SQLiteStatement rawContactUpdate, NameSplitter splitter) {
+
+ // Process structured names to detect the style of the full name and phonetic name
+
+ long mMimeType;
+ try {
+ mMimeType = DatabaseUtils.longForQuery(db,
+ "SELECT " + MimetypesColumns._ID +
+ " FROM " + Tables.MIMETYPES +
+ " WHERE " + MimetypesColumns.MIMETYPE
+ + "='" + StructuredName.CONTENT_ITEM_TYPE + "'", null);
+ } catch (SQLiteDoneException e) {
+ // No structured names in the database
+ return;
+ }
+
+ SQLiteStatement structuredNameUpdate = db.compileStatement(
+ "UPDATE " + Tables.DATA +
+ " SET " +
+ StructuredName.FULL_NAME_STYLE + "=?," +
+ StructuredName.DISPLAY_NAME + "=?," +
+ StructuredName.PHONETIC_NAME_STYLE + "=?" +
+ " WHERE " + Data._ID + "=?");
+
+ NameSplitter.Name name = new NameSplitter.Name();
+ StringBuilder sb = new StringBuilder();
+ Cursor cursor = db.query(StructName205Query.TABLE,
+ StructName205Query.COLUMNS,
+ DataColumns.MIMETYPE_ID + "=" + mMimeType, null, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ long dataId = cursor.getLong(StructName205Query.ID);
+ long rawContactId = cursor.getLong(StructName205Query.RAW_CONTACT_ID);
+ int displayNameSource = cursor.getInt(StructName205Query.DISPLAY_NAME_SOURCE);
+ String displayName = cursor.getString(StructName205Query.DISPLAY_NAME);
+
+ name.clear();
+ name.prefix = cursor.getString(StructName205Query.PREFIX);
+ name.givenNames = cursor.getString(StructName205Query.GIVEN_NAME);
+ name.middleName = cursor.getString(StructName205Query.MIDDLE_NAME);
+ name.familyName = cursor.getString(StructName205Query.FAMILY_NAME);
+ name.suffix = cursor.getString(StructName205Query.SUFFIX);
+ name.phoneticFamilyName = cursor.getString(StructName205Query.PHONETIC_FAMILY_NAME);
+ name.phoneticMiddleName = cursor.getString(StructName205Query.PHONETIC_MIDDLE_NAME);
+ name.phoneticGivenName = cursor.getString(StructName205Query.PHONETIC_GIVEN_NAME);
+
+ upgradeNameToVersion205(dataId, rawContactId, displayNameSource, displayName, name,
+ structuredNameUpdate, rawContactUpdate, splitter, sb);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void upgradeNameToVersion205(long dataId, long rawContactId, int displayNameSource,
+ String currentDisplayName, NameSplitter.Name name,
+ SQLiteStatement structuredNameUpdate, SQLiteStatement rawContactUpdate,
+ NameSplitter splitter, StringBuilder sb) {
+
+ splitter.guessNameStyle(name);
+ name.fullNameStyle = splitter.getAdjustedFullNameStyle(name.fullNameStyle);
+ String displayName = splitter.join(name, true);
+
+ structuredNameUpdate.bindLong(1, name.fullNameStyle);
+ DatabaseUtils.bindObjectToProgram(structuredNameUpdate, 2, displayName);
+ structuredNameUpdate.bindLong(3, name.phoneticNameStyle);
+ structuredNameUpdate.bindLong(4, dataId);
+ structuredNameUpdate.execute();
+
+ if (displayNameSource == DisplayNameSources.STRUCTURED_NAME) {
+ String displayNameAlternative = splitter.join(name, false);
+ String phoneticName = splitter.joinPhoneticName(name);
+ String sortKey = null;
+ String sortKeyAlternative = null;
+
+ if (phoneticName != null) {
+ sortKey = sortKeyAlternative = phoneticName;
+ } else if (name.fullNameStyle == FullNameStyle.CHINESE) {
+ sortKey = sortKeyAlternative = splitter.convertHanziToPinyin(displayName);
+ }
+
+ if (sortKey == null) {
+ sortKey = displayName;
+ sortKeyAlternative = displayNameAlternative;
+ }
+
+ updateRawContact205(rawContactUpdate, rawContactId, displayName,
+ displayNameAlternative, name.phoneticNameStyle, phoneticName, sortKey,
+ sortKeyAlternative);
+ }
+ }
+
+ private interface Organization205Query {
+ String TABLE = Tables.DATA_JOIN_RAW_CONTACTS;
+
+ String COLUMNS[] = {
+ DataColumns.CONCRETE_ID,
+ Data.RAW_CONTACT_ID,
+ Organization.COMPANY,
+ Organization.PHONETIC_NAME,
+ };
+
+ int ID = 0;
+ int RAW_CONTACT_ID = 1;
+ int COMPANY = 2;
+ int PHONETIC_NAME = 3;
+ }
+
+ private void upgradeOrganizationsToVersion205(SQLiteDatabase db,
+ SQLiteStatement rawContactUpdate, NameSplitter splitter) {
+
+ final long mMimeType;
+ try {
+ mMimeType = DatabaseUtils.longForQuery(db,
+ "SELECT " + MimetypesColumns._ID +
+ " FROM " + Tables.MIMETYPES +
+ " WHERE " + MimetypesColumns.MIMETYPE
+ + "='" + Organization.CONTENT_ITEM_TYPE + "'", null);
+ } catch (SQLiteDoneException e) {
+ // No organizations in the database
+ return;
+ }
+
+ SQLiteStatement organizationUpdate = db.compileStatement(
+ "UPDATE " + Tables.DATA +
+ " SET " +
+ Organization.PHONETIC_NAME_STYLE + "=?" +
+ " WHERE " + Data._ID + "=?");
+
+ Cursor cursor = db.query(Organization205Query.TABLE, Organization205Query.COLUMNS,
+ DataColumns.MIMETYPE_ID + "=" + mMimeType + " AND "
+ + RawContacts.DISPLAY_NAME_SOURCE + "=" + DisplayNameSources.ORGANIZATION,
+ null, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ long dataId = cursor.getLong(Organization205Query.ID);
+ long rawContactId = cursor.getLong(Organization205Query.RAW_CONTACT_ID);
+ String company = cursor.getString(Organization205Query.COMPANY);
+ String phoneticName = cursor.getString(Organization205Query.PHONETIC_NAME);
+
+ int phoneticNameStyle = splitter.guessPhoneticNameStyle(phoneticName);
+
+ organizationUpdate.bindLong(1, phoneticNameStyle);
+ organizationUpdate.bindLong(2, dataId);
+ organizationUpdate.execute();
+
+ String sortKey = null;
+ if (phoneticName == null && company != null) {
+ int nameStyle = splitter.guessFullNameStyle(company);
+ nameStyle = splitter.getAdjustedFullNameStyle(nameStyle);
+ if (nameStyle == FullNameStyle.CHINESE) {
+ sortKey = splitter.convertHanziToPinyin(company);
+ }
+ }
+
+ if (sortKey == null) {
+ sortKey = company;
+ }
+
+ updateRawContact205(rawContactUpdate, rawContactId, company,
+ company, phoneticNameStyle, phoneticName, sortKey, sortKey);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void updateRawContact205(SQLiteStatement rawContactUpdate, long rawContactId,
+ String displayName, String displayNameAlternative, int phoneticNameStyle,
+ String phoneticName, String sortKeyPrimary, String sortKeyAlternative) {
+ bindString(rawContactUpdate, 1, displayName);
+ bindString(rawContactUpdate, 2, displayNameAlternative);
+ bindString(rawContactUpdate, 3, phoneticName);
+ rawContactUpdate.bindLong(4, phoneticNameStyle);
+ bindString(rawContactUpdate, 5, sortKeyPrimary);
+ bindString(rawContactUpdate, 6, sortKeyAlternative);
+ rawContactUpdate.bindLong(7, rawContactId);
+ rawContactUpdate.execute();
+ }
+
+ private void upgrateToVersion206(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+ + " ADD " + RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0;");
+ }
+
+ private void bindString(SQLiteStatement stmt, int index, String value) {
+ if (value == null) {
+ stmt.bindNull(index);
+ } else {
+ stmt.bindString(index, value);
+ }
+ }
+
/**
* Adds index stats into the SQLite database to force it to always use the lookup indexes.
*/
@@ -1376,6 +1811,8 @@
updateIndexStats(db, Tables.PHONE_LOOKUP,
"phone_lookup_index", "10000 2 2 1");
+ updateIndexStats(db, Tables.PHONE_LOOKUP,
+ "phone_lookup_min_match_index", "10000 2 2 1");
updateIndexStats(db, Tables.DATA,
"data_mimetype_data1_index", "60000 5000 2");
@@ -1400,7 +1837,8 @@
* the table. The following integer(s) are the expected number of records selected with the
* index. There should be one integer per indexed column.
*/
- private void updateIndexStats(SQLiteDatabase db, String table, String index, String stats) {
+ private void updateIndexStats(SQLiteDatabase db, String table, String index,
+ String stats) {
db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl='" + table + "' AND idx='" + index + "';");
db.execSQL("INSERT INTO sqlite_stat1 (tbl,idx,stat)"
+ " VALUES ('" + table + "','" + index + "','" + stats + "');");
@@ -1435,8 +1873,6 @@
db.execSQL("DELETE FROM " + Tables.CALLS + ";");
// Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
-
- db.execSQL("VACUUM;");
}
/**
@@ -1510,6 +1946,10 @@
public long getMimeTypeId(String mimetype) {
// Make sure compiled statements are ready by opening database
getReadableDatabase();
+ return getMimeTypeIdNoDbCheck(mimetype);
+ }
+
+ private long getMimeTypeIdNoDbCheck(String mimetype) {
return getCachedId(mMimetypeQuery, mMimetypeInsert, mimetype, mMimetypeCache);
}
@@ -1551,19 +1991,67 @@
* Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
*/
public void updateAllVisible() {
+ SQLiteDatabase db = getWritableDatabase();
final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
- mVisibleUpdate.bindLong(1, groupMembershipMimetypeId);
- mVisibleUpdate.execute();
+ String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)};
+
+ // There are a couple questions that can be asked regarding the
+ // following two update statements:
+ //
+ // Q: Why do we run these two queries separately? They seem like they could be combined.
+ // A: This is a result of painstaking experimentation. Turns out that the most
+ // important optimization is to make sure we never update a value to its current value.
+ // Changing 0 to 0 is unexpectedly expensive - SQLite actually writes the unchanged
+ // rows back to disk. The other consideration is that the CONTACT_IS_VISIBLE condition
+ // is very complex and executing it twice in the same statement ("if contact_visible !=
+ // CONTACT_IS_VISIBLE change it to CONTACT_IS_VISIBLE") is more expensive than running
+ // two update statements.
+ //
+ // Q: How come we are using db.update instead of compiled statements?
+ // A: This is a limitation of the compiled statement API. It does not return the
+ // number of rows changed. As you will see later in this method we really need
+ // to know how many rows have been changed.
+
+ // First update contacts that are currently marked as invisible, but need to be visible
+ ContentValues values = new ContentValues();
+ values.put(Contacts.IN_VISIBLE_GROUP, 1);
+ int countMadeVisible = db.update(Tables.CONTACTS, values,
+ Contacts.IN_VISIBLE_GROUP + "=0" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1",
+ selectionArgs);
+
+ // Next update contacts that are currently marked as visible, but need to be invisible
+ values.put(Contacts.IN_VISIBLE_GROUP, 0);
+ int countMadeInvisible = db.update(Tables.CONTACTS, values,
+ Contacts.IN_VISIBLE_GROUP + "=1" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=0",
+ selectionArgs);
+
+ if (countMadeVisible != 0 || countMadeInvisible != 0) {
+ // TODO break out the fields (contact_in_visible_group, sort_key, sort_key_alt) into
+ // a separate table.
+ // Rationale: The following statement will take a very long time on
+ // a large database even though we are only changing one field from 0 to 1 or from
+ // 1 to 0. The reason for the slowness is that SQLite will need to write the whole
+ // page even when only one bit on it changes. Changing the visibility of a
+ // significant number of contacts will likely read and write almost the entire
+ // raw_contacts table. So, the solution is to break out into a separate table
+ // the changing field along with the sort keys used for index-based sorting.
+ // That table will occupy a smaller number of pages, so rewriting it would
+ // not be as expensive.
+ mVisibleUpdateRawContacts.execute();
+ }
}
/**
* Update {@link Contacts#IN_VISIBLE_GROUP} for a specific contact.
*/
- public void updateContactVisible(long aggId) {
+ public void updateContactVisible(long contactId) {
final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
mVisibleSpecificUpdate.bindLong(1, groupMembershipMimetypeId);
- mVisibleSpecificUpdate.bindLong(2, aggId);
+ mVisibleSpecificUpdate.bindLong(2, contactId);
mVisibleSpecificUpdate.execute();
+
+ mVisibleSpecificUpdateRawContacts.bindLong(1, contactId);
+ mVisibleSpecificUpdateRawContacts.execute();
}
/**
@@ -1592,25 +2080,25 @@
}
public void buildPhoneLookupAndRawContactQuery(SQLiteQueryBuilder qb, String number) {
- String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+ String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
qb.setTables(Tables.DATA_JOIN_RAW_CONTACTS +
" JOIN " + Tables.PHONE_LOOKUP
+ " ON(" + DataColumns.CONCRETE_ID + "=" + PhoneLookupColumns.DATA_ID + ")");
StringBuilder sb = new StringBuilder();
- sb.append(PhoneLookupColumns.NORMALIZED_NUMBER + " GLOB '");
- sb.append(normalizedNumber);
- sb.append("*' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
+ sb.append(PhoneLookupColumns.MIN_MATCH + "='");
+ sb.append(minMatch);
+ sb.append("' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
DatabaseUtils.appendEscapedSQLString(sb, number);
- sb.append(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
+ sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
qb.appendWhere(sb.toString());
}
public void buildPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number) {
- String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+ String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
StringBuilder sb = new StringBuilder();
- appendPhoneLookupTables(sb, normalizedNumber, true);
+ appendPhoneLookupTables(sb, minMatch, true);
qb.setTables(sb.toString());
sb = new StringBuilder();
@@ -1620,33 +2108,37 @@
public String buildPhoneLookupAsNestedQuery(String number) {
StringBuilder sb = new StringBuilder();
- final String normalizedNumber = PhoneNumberUtils.toCallerIDMinMatch(number);
+ final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
sb.append("(SELECT DISTINCT raw_contact_id" + " FROM ");
- appendPhoneLookupTables(sb, normalizedNumber, false);
+ appendPhoneLookupTables(sb, minMatch, false);
sb.append(" WHERE ");
appendPhoneLookupSelection(sb, number);
sb.append(")");
return sb.toString();
}
- private void appendPhoneLookupTables(StringBuilder sb, final String normalizedNumber,
+ private void appendPhoneLookupTables(StringBuilder sb, final String minMatch,
boolean joinContacts) {
sb.append(Tables.RAW_CONTACTS);
if (joinContacts) {
- sb.append(" JOIN " + getContactView() + " contacts"
- + " ON (contacts._id = raw_contacts.contact_id)");
+ sb.append(" JOIN " + getContactView() + " contacts_view"
+ + " ON (contacts_view._id = raw_contacts.contact_id)");
}
sb.append(", (SELECT data_id FROM phone_lookup "
- + "WHERE (phone_lookup.normalized_number GLOB '");
- sb.append(normalizedNumber);
- sb.append("*')) AS lookup, " + Tables.DATA);
+ + "WHERE (" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.MIN_MATCH + " = '");
+ sb.append(minMatch);
+ sb.append("')) AS lookup, " + Tables.DATA);
}
private void appendPhoneLookupSelection(StringBuilder sb, String number) {
sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id"
+ " AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
DatabaseUtils.appendEscapedSQLString(sb, number);
- sb.append(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
+ sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
+ }
+
+ public String getUseStrictPhoneNumberComparisonParameter() {
+ return mUseStrictPhoneNumberComparison ? "1" : "0";
}
/**
diff --git a/src/com/android/providers/contacts/ContactsProvider.java b/src/com/android/providers/contacts/ContactsProvider.java
deleted file mode 100644
index 6a3e6dd..0000000
--- a/src/com/android/providers/contacts/ContactsProvider.java
+++ /dev/null
@@ -1,4642 +0,0 @@
-/*
- * Copyright (C) 2006 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 android.app.SearchManager;
-import android.content.AbstractSyncableContentProvider;
-import android.content.AbstractTableMerger;
-import android.content.ContentProvider;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.UriMatcher;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.CursorJoiner;
-import android.database.DatabaseUtils;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteContentHelper;
-import android.database.sqlite.SQLiteCursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteDoneException;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.database.sqlite.SQLiteStatement;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Debug;
-import android.os.MemoryFile;
-import android.os.ParcelFileDescriptor;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.provider.Contacts;
-import android.provider.Contacts.ContactMethods;
-import android.provider.Contacts.Extensions;
-import android.provider.Contacts.GroupMembership;
-import android.provider.Contacts.Groups;
-import android.provider.Contacts.GroupsColumns;
-import android.provider.Contacts.Intents;
-import android.provider.Contacts.Organizations;
-import android.provider.Contacts.People;
-import android.provider.Contacts.PeopleColumns;
-import android.provider.Contacts.Phones;
-import android.provider.Contacts.Photos;
-import android.provider.Contacts.Presence;
-import android.provider.Contacts.PresenceColumns;
-import android.provider.LiveFolders;
-import android.provider.SyncConstValue;
-import android.provider.Calendar;
-import android.telephony.PhoneNumberUtils;
-import android.text.TextUtils;
-import android.util.Config;
-import android.util.Log;
-import android.accounts.Account;
-
-import com.android.internal.database.ArrayListCursor;
-import com.google.android.collect.Maps;
-import com.google.android.collect.Sets;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-public class ContactsProvider extends AbstractSyncableContentProvider {
- private static final String STREQUENT_ORDER_BY = "times_contacted DESC, display_name ASC";
- private static final String STREQUENT_LIMIT =
- "(SELECT COUNT(*) FROM people WHERE starred = 1) + 25";
-
- private static final String PEOPLE_PHONES_JOIN =
- "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
- + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id)";
-
- private static final String PEOPLE_PHONES_PHOTOS_JOIN =
- "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
- + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id) "
- + "LEFT OUTER JOIN photos ON (photos." + Photos.PERSON_ID + "=people._id)";
-
- private static final String PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN =
- "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
- + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id) "
- + "LEFT OUTER JOIN photos ON (photos." + Photos.PERSON_ID + "=people._id) "
- + "LEFT OUTER JOIN organizations ON (organizations._id=people.primary_organization)";
-
- private static final String GTALK_PROTOCOL_STRING =
- ContactMethods.encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK);
-
- private static final String[] ID_TYPE_PROJECTION = new String[]{"_id", "type"};
-
- private static final String[] sIsPrimaryProjectionWithoutKind =
- new String[]{"isprimary", "person", "_id"};
- private static final String[] sIsPrimaryProjectionWithKind =
- new String[]{"isprimary", "person", "_id", "kind"};
-
- private static final String WHERE_ID = "_id=?";
-
- private static final String sGroupsJoinString;
-
- private static final String PREFS_NAME_OWNER = "owner-info";
- private static final String PREF_OWNER_ID = "owner-id";
-
- /** this is suitable for use by insert/update/delete/query and may be passed
- * as a method call parameter. Only insert/update/delete/query should call .clear() on it */
- private final ContentValues mValues = new ContentValues();
-
- /** this is suitable for local use in methods and should never be passed as a parameter to
- * other methods (other than the DB layer) */
- private final ContentValues mValuesLocal = new ContentValues();
-
- private Account[] mAccounts = new Account[0];
- private final Object mAccountsLock = new Object();
-
- private DatabaseUtils.InsertHelper mDeletedPeopleInserter;
- private DatabaseUtils.InsertHelper mPeopleInserter;
- private int mIndexPeopleSyncId;
- private int mIndexPeopleSyncTime;
- private int mIndexPeopleSyncVersion;
- private int mIndexPeopleSyncDirty;
- private int mIndexPeopleSyncAccountName;
- private int mIndexPeopleSyncAccountType;
- private int mIndexPeopleName;
- private int mIndexPeoplePhoneticName;
- private int mIndexPeopleNotes;
- private DatabaseUtils.InsertHelper mGroupsInserter;
- private DatabaseUtils.InsertHelper mPhotosInserter;
- private int mIndexPhotosPersonId;
- private int mIndexPhotosSyncId;
- private int mIndexPhotosSyncTime;
- private int mIndexPhotosSyncVersion;
- private int mIndexPhotosSyncDirty;
- private int mIndexPhotosSyncAccountName;
- private int mIndexPhotosSyncAccountType;
- private int mIndexPhotosExistsOnServer;
- private int mIndexPhotosSyncError;
- private DatabaseUtils.InsertHelper mContactMethodsInserter;
- private int mIndexContactMethodsPersonId;
- private int mIndexContactMethodsLabel;
- private int mIndexContactMethodsKind;
- private int mIndexContactMethodsType;
- private int mIndexContactMethodsData;
- private int mIndexContactMethodsAuxData;
- private int mIndexContactMethodsIsPrimary;
- private DatabaseUtils.InsertHelper mOrganizationsInserter;
- private int mIndexOrganizationsPersonId;
- private int mIndexOrganizationsLabel;
- private int mIndexOrganizationsType;
- private int mIndexOrganizationsCompany;
- private int mIndexOrganizationsTitle;
- private int mIndexOrganizationsIsPrimary;
- private DatabaseUtils.InsertHelper mExtensionsInserter;
- private int mIndexExtensionsPersonId;
- private int mIndexExtensionsName;
- private int mIndexExtensionsValue;
- private DatabaseUtils.InsertHelper mGroupMembershipInserter;
- private int mIndexGroupMembershipPersonId;
- private int mIndexGroupMembershipGroupSyncAccountName;
- private int mIndexGroupMembershipGroupSyncAccountType;
- private int mIndexGroupMembershipGroupSyncId;
- private DatabaseUtils.InsertHelper mCallsInserter;
- private DatabaseUtils.InsertHelper mPhonesInserter;
- private int mIndexPhonesPersonId;
- private int mIndexPhonesLabel;
- private int mIndexPhonesType;
- private int mIndexPhonesNumber;
- private int mIndexPhonesNumberKey;
- private int mIndexPhonesIsPrimary;
- private boolean mUseStrictPhoneNumberComparation;
-
- public ContactsProvider() {
- super(DATABASE_NAME, DATABASE_VERSION, Contacts.CONTENT_URI);
- mUseStrictPhoneNumberComparation =
- getContext().getResources().getBoolean(
- com.android.internal.R.bool.config_use_strict_phone_number_comparation);
- }
-
- @Override
- protected void onDatabaseOpened(SQLiteDatabase db) {
- maybeCreatePresenceTable(db);
-
- // Mark all the tables as syncable
- db.markTableSyncable(sPeopleTable, sDeletedPeopleTable);
- db.markTableSyncable(sPhonesTable, Phones.PERSON_ID, sPeopleTable);
- db.markTableSyncable(sContactMethodsTable, ContactMethods.PERSON_ID, sPeopleTable);
- db.markTableSyncable(sOrganizationsTable, Organizations.PERSON_ID, sPeopleTable);
- db.markTableSyncable(sGroupmembershipTable, GroupMembership.PERSON_ID, sPeopleTable);
- db.markTableSyncable(sExtensionsTable, Extensions.PERSON_ID, sPeopleTable);
- db.markTableSyncable(sGroupsTable, sDeletedGroupsTable);
-
- mDeletedPeopleInserter = new DatabaseUtils.InsertHelper(db, sDeletedPeopleTable);
- mPeopleInserter = new DatabaseUtils.InsertHelper(db, sPeopleTable);
- mIndexPeopleSyncId = mPeopleInserter.getColumnIndex(People._SYNC_ID);
- mIndexPeopleSyncTime = mPeopleInserter.getColumnIndex(People._SYNC_TIME);
- mIndexPeopleSyncVersion = mPeopleInserter.getColumnIndex(People._SYNC_VERSION);
- mIndexPeopleSyncDirty = mPeopleInserter.getColumnIndex(People._SYNC_DIRTY);
- mIndexPeopleSyncAccountName = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT);
- mIndexPeopleSyncAccountType = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT_TYPE);
- mIndexPeopleName = mPeopleInserter.getColumnIndex(People.NAME);
- mIndexPeoplePhoneticName = mPeopleInserter.getColumnIndex(People.PHONETIC_NAME);
- mIndexPeopleNotes = mPeopleInserter.getColumnIndex(People.NOTES);
-
- mGroupsInserter = new DatabaseUtils.InsertHelper(db, sGroupsTable);
-
- mPhotosInserter = new DatabaseUtils.InsertHelper(db, sPhotosTable);
- mIndexPhotosPersonId = mPhotosInserter.getColumnIndex(Photos.PERSON_ID);
- mIndexPhotosSyncId = mPhotosInserter.getColumnIndex(Photos._SYNC_ID);
- mIndexPhotosSyncTime = mPhotosInserter.getColumnIndex(Photos._SYNC_TIME);
- mIndexPhotosSyncVersion = mPhotosInserter.getColumnIndex(Photos._SYNC_VERSION);
- mIndexPhotosSyncDirty = mPhotosInserter.getColumnIndex(Photos._SYNC_DIRTY);
- mIndexPhotosSyncAccountName = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT);
- mIndexPhotosSyncAccountType = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT_TYPE);
- mIndexPhotosSyncError = mPhotosInserter.getColumnIndex(Photos.SYNC_ERROR);
- mIndexPhotosExistsOnServer = mPhotosInserter.getColumnIndex(Photos.EXISTS_ON_SERVER);
-
- mContactMethodsInserter = new DatabaseUtils.InsertHelper(db, sContactMethodsTable);
- mIndexContactMethodsPersonId =
- mContactMethodsInserter.getColumnIndex(ContactMethods.PERSON_ID);
- mIndexContactMethodsLabel = mContactMethodsInserter.getColumnIndex(ContactMethods.LABEL);
- mIndexContactMethodsKind = mContactMethodsInserter.getColumnIndex(ContactMethods.KIND);
- mIndexContactMethodsType = mContactMethodsInserter.getColumnIndex(ContactMethods.TYPE);
- mIndexContactMethodsData = mContactMethodsInserter.getColumnIndex(ContactMethods.DATA);
- mIndexContactMethodsAuxData =
- mContactMethodsInserter.getColumnIndex(ContactMethods.AUX_DATA);
- mIndexContactMethodsIsPrimary =
- mContactMethodsInserter.getColumnIndex(ContactMethods.ISPRIMARY);
-
- mOrganizationsInserter = new DatabaseUtils.InsertHelper(db, sOrganizationsTable);
- mIndexOrganizationsPersonId =
- mOrganizationsInserter.getColumnIndex(Organizations.PERSON_ID);
- mIndexOrganizationsLabel = mOrganizationsInserter.getColumnIndex(Organizations.LABEL);
- mIndexOrganizationsType = mOrganizationsInserter.getColumnIndex(Organizations.TYPE);
- mIndexOrganizationsCompany = mOrganizationsInserter.getColumnIndex(Organizations.COMPANY);
- mIndexOrganizationsTitle = mOrganizationsInserter.getColumnIndex(Organizations.TITLE);
- mIndexOrganizationsIsPrimary =
- mOrganizationsInserter.getColumnIndex(Organizations.ISPRIMARY);
-
- mExtensionsInserter = new DatabaseUtils.InsertHelper(db, sExtensionsTable);
- mIndexExtensionsPersonId = mExtensionsInserter.getColumnIndex(Extensions.PERSON_ID);
- mIndexExtensionsName = mExtensionsInserter.getColumnIndex(Extensions.NAME);
- mIndexExtensionsValue = mExtensionsInserter.getColumnIndex(Extensions.VALUE);
-
- mGroupMembershipInserter = new DatabaseUtils.InsertHelper(db, sGroupmembershipTable);
- mIndexGroupMembershipPersonId =
- mGroupMembershipInserter.getColumnIndex(GroupMembership.PERSON_ID);
- mIndexGroupMembershipGroupSyncAccountName =
- mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT);
- mIndexGroupMembershipGroupSyncAccountType =
- mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
- mIndexGroupMembershipGroupSyncId =
- mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ID);
-
- mCallsInserter = new DatabaseUtils.InsertHelper(db, sCallsTable);
-
- mPhonesInserter = new DatabaseUtils.InsertHelper(db, sPhonesTable);
- mIndexPhonesPersonId = mPhonesInserter.getColumnIndex(Phones.PERSON_ID);
- mIndexPhonesLabel = mPhonesInserter.getColumnIndex(Phones.LABEL);
- mIndexPhonesType = mPhonesInserter.getColumnIndex(Phones.TYPE);
- mIndexPhonesNumber = mPhonesInserter.getColumnIndex(Phones.NUMBER);
- mIndexPhonesNumberKey = mPhonesInserter.getColumnIndex(Phones.NUMBER_KEY);
- mIndexPhonesIsPrimary = mPhonesInserter.getColumnIndex(Phones.ISPRIMARY);
- }
-
- @Override
- protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
- boolean upgradeWasLossless = true;
- if (oldVersion < 71) {
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to " +
- newVersion + ", which will destroy all old data");
- dropTables(db);
- bootstrapDatabase(db);
- return false; // this was lossy
- }
- if (oldVersion == 71) {
- Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
- newVersion + ", which will preserve existing data");
-
- db.delete("_sync_state", null, null);
- mValuesLocal.clear();
- mValuesLocal.putNull(Photos._SYNC_VERSION);
- mValuesLocal.putNull(Photos._SYNC_TIME);
- db.update(sPhotosTable, mValuesLocal, null, null);
- ContentResolver.requestSync(null /* account */, Contacts.AUTHORITY, new Bundle());
- oldVersion = 72;
- }
- if (oldVersion == 72) {
- Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
- newVersion + ", which will preserve existing data");
-
- // use new token format from 73
- db.execSQL("delete from peopleLookup");
- try {
- DatabaseUtils.longForQuery(db,
- "SELECT _TOKENIZE('peopleLookup', _id, name, ' ') from people;",
- null);
- } catch (SQLiteDoneException ex) {
- // it is ok to throw this,
- // it just means you don't have data in people table
- }
- oldVersion = 73;
- }
- // There was a bug for a while in the upgrade logic where going from 72 to 74 would skip
- // the step from 73 to 74, so 74 to 75 just tries the same steps, and gracefully handles
- // errors in case the device was started freshly at 74.
- if (oldVersion == 73 || oldVersion == 74) {
- Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
- newVersion + ", which will preserve existing data");
-
- try {
- db.execSQL("ALTER TABLE calls ADD name TEXT;");
- db.execSQL("ALTER TABLE calls ADD numbertype INTEGER;");
- db.execSQL("ALTER TABLE calls ADD numberlabel TEXT;");
- } catch (SQLiteException sqle) {
- // Maybe the table was altered already... Shouldn't be an issue.
- }
- oldVersion = 75;
- }
- // There were some indices added in version 76
- if (oldVersion == 75) {
- Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
- newVersion + ", which will preserve existing data");
-
- // add the new indices
- db.execSQL("CREATE INDEX IF NOT EXISTS groupsSyncDirtyIndex"
- + " ON groups (" + Groups._SYNC_DIRTY + ");");
- db.execSQL("CREATE INDEX IF NOT EXISTS photosSyncDirtyIndex"
- + " ON photos (" + Photos._SYNC_DIRTY + ");");
- db.execSQL("CREATE INDEX IF NOT EXISTS peopleSyncDirtyIndex"
- + " ON people (" + People._SYNC_DIRTY + ");");
- oldVersion = 76;
- }
-
- if (oldVersion == 76 || oldVersion == 77) {
- db.execSQL("DELETE FROM people");
- db.execSQL("DELETE FROM groups");
- db.execSQL("DELETE FROM photos");
- db.execSQL("DELETE FROM _deleted_people");
- db.execSQL("DELETE FROM _deleted_groups");
- upgradeWasLossless = false;
- oldVersion = 78;
- }
-
- if (oldVersion == 78) {
- db.execSQL("UPDATE photos SET _sync_dirty=0 where _sync_dirty is null;");
- oldVersion = 79;
- }
-
- if (oldVersion == 79) {
- try {
- db.execSQL("ALTER TABLE people ADD phonetic_name TEXT COLLATE LOCALIZED;");
- } catch (SQLiteException sqle) {
- // Maybe the table was altered already... Shouldn't be an issue.
- }
- oldVersion = 80;
- }
-
- if (oldVersion == 80) {
-
- // We will not do this upgrade work, because we are deprecating this content provider
- oldVersion = 81;
- }
-
- if (oldVersion == 81) {
- Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " +
- newVersion + ", which will preserve existing data");
- // 81 adds the token_index column
- db.execSQL("DELETE FROM peopleLookup");
- db.execSQL("ALTER TABLE peopleLookup ADD token_index INTEGER;");
- String[] tokenize = {"_TOKENIZE('peopleLookup', _id, name, ' ', 1)"};
- Cursor cursor = db.query("people", tokenize, null, null, null, null, null);
- int rows = cursor.getCount();
- cursor.close();
- Log.i(TAG, "Processed " + rows + " contacts.");
- oldVersion = 82;
- }
-
- if (oldVersion == 82) {
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to " +
- newVersion + ", which will preserve all old data");
-
- try {
- db.execSQL("ALTER TABLE people ADD COLUMN _sync_account_type TEXT;");
- } catch (SQLException e) {
- // in some upgrade paths, these column might already exists
- }
- try {
- db.execSQL("ALTER TABLE _deleted_people ADD COLUMN _sync_account_type TEXT;");
- } catch (SQLException e) {
- // in some upgrade paths, these column might already exists
- }
- try {
- db.execSQL("ALTER TABLE groups ADD COLUMN _sync_account_type TEXT;");
- } catch (SQLException e) {
- // in some upgrade paths, these column might already exists
- }
- try {
- db.execSQL("ALTER TABLE _deleted_groups ADD COLUMN _sync_account_type TEXT;");
- } catch (SQLException e) {
- // in some upgrade paths, these column might already exists
- }
- try {
- db.execSQL("ALTER TABLE settings ADD COLUMN _sync_account_type TEXT;");
- } catch (SQLException e) {
- // in some upgrade paths, these column might already exists
- }
- try {
- db.execSQL("ALTER TABLE photos ADD COLUMN _sync_account_type TEXT;");
- } catch (SQLException e) {
- // in some upgrade paths, these column might already exists
- }
- try {
- db.execSQL("ALTER TABLE groupmembership ADD COLUMN group_sync_account_type TEXT;");
- } catch (SQLException e) {
- // in some upgrade paths, these column might already exists
- }
- db.execSQL("UPDATE people"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- db.execSQL("UPDATE _deleted_people"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- db.execSQL("UPDATE groups"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- db.execSQL("UPDATE _deleted_groups"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- db.execSQL("UPDATE settings"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- db.execSQL("UPDATE photos"
- + " SET _sync_account_type='com.google'"
- + " WHERE _sync_account IS NOT NULL");
- db.execSQL("UPDATE groupmembership"
- + " SET group_sync_account_type='com.google'"
- + " WHERE group_sync_account IS NOT NULL");
-
- db.execSQL("CREATE INDEX groupTempIndex ON groups ("
- + Groups.NAME + "," + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + ","
- + Groups._SYNC_ACCOUNT_TYPE + ");");
-
- db.execSQL("DROP INDEX groupmembershipIndex3");
- db.execSQL("CREATE INDEX groupmembershipIndex3 ON groupmembership "
- + "(group_sync_account_type, group_sync_account, group_sync_id);");
-
- // Trigger to move an account_people row to _deleted_account_people when it is deleted
- db.execSQL("DROP TRIGGER groups_to_deleted");
- db.execSQL("CREATE TRIGGER groups_to_deleted DELETE ON groups " +
- "WHEN old._sync_id is not null " +
- "BEGIN " +
- "INSERT INTO _deleted_groups " +
- "(_sync_id, _sync_account, _sync_account_type, _sync_version) " +
- "VALUES (old._sync_id, old._sync_account, old._sync_account_type," +
- "old._sync_version);" +
- "END");
-
- oldVersion++;
- }
-
- return upgradeWasLossless;
- }
-
- protected void dropTables(SQLiteDatabase db) {
- db.execSQL("DROP TABLE IF EXISTS people");
- db.execSQL("DROP TABLE IF EXISTS peopleLookup");
- db.execSQL("DROP TABLE IF EXISTS _deleted_people");
- db.execSQL("DROP TABLE IF EXISTS phones");
- db.execSQL("DROP TABLE IF EXISTS contact_methods");
- db.execSQL("DROP TABLE IF EXISTS calls");
- db.execSQL("DROP TABLE IF EXISTS organizations");
- db.execSQL("DROP TABLE IF EXISTS voice_dialer_timestamp");
- db.execSQL("DROP TABLE IF EXISTS groups");
- db.execSQL("DROP TABLE IF EXISTS _deleted_groups");
- db.execSQL("DROP TABLE IF EXISTS groupmembership");
- db.execSQL("DROP TABLE IF EXISTS photos");
- db.execSQL("DROP TABLE IF EXISTS extensions");
- db.execSQL("DROP TABLE IF EXISTS settings");
- }
-
- @Override
- protected void bootstrapDatabase(SQLiteDatabase db) {
- super.bootstrapDatabase(db);
- db.execSQL("CREATE TABLE people (" +
- People._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- People._SYNC_ACCOUNT + " TEXT," + // From the sync source
- People._SYNC_ACCOUNT_TYPE + " TEXT," + // From the sync source
- People._SYNC_ID + " TEXT," + // From the sync source
- People._SYNC_TIME + " TEXT," + // From the sync source
- People._SYNC_VERSION + " TEXT," + // From the sync source
- People._SYNC_LOCAL_ID + " INTEGER," + // Used while syncing, not persistent
- People._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
- // if syncable, non-zero if the record
- // has local, unsynced, changes
- People._SYNC_MARK + " INTEGER," + // Used to filter out new rows
-
- People.NAME + " TEXT COLLATE LOCALIZED," +
- People.NOTES + " TEXT COLLATE LOCALIZED," +
- People.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
- People.LAST_TIME_CONTACTED + " INTEGER," +
- People.STARRED + " INTEGER NOT NULL DEFAULT 0," +
- People.PRIMARY_PHONE_ID + " INTEGER REFERENCES phones(_id)," +
- People.PRIMARY_ORGANIZATION_ID + " INTEGER REFERENCES organizations(_id)," +
- People.PRIMARY_EMAIL_ID + " INTEGER REFERENCES contact_methods(_id)," +
- People.PHOTO_VERSION + " TEXT," +
- People.CUSTOM_RINGTONE + " TEXT," +
- People.SEND_TO_VOICEMAIL + " INTEGER," +
- People.PHONETIC_NAME + " TEXT COLLATE LOCALIZED" +
- ");");
-
- db.execSQL("CREATE INDEX peopleNameIndex ON people (" + People.NAME + ");");
- db.execSQL("CREATE INDEX peopleSyncDirtyIndex ON people (" + People._SYNC_DIRTY + ");");
- db.execSQL("CREATE INDEX peopleSyncIdIndex ON people (" + People._SYNC_ID + ");");
-
- db.execSQL("CREATE TRIGGER people_timesContacted UPDATE OF last_time_contacted ON people " +
- "BEGIN " +
- "UPDATE people SET "
- + People.TIMES_CONTACTED + " = (new." + People.TIMES_CONTACTED + " + 1)"
- + " WHERE _id = new._id;" +
- "END");
-
- // table of all the groups that exist for an account
- db.execSQL("CREATE TABLE groups (" +
- Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Groups._SYNC_ACCOUNT + " TEXT," + // From the sync source
- Groups._SYNC_ACCOUNT_TYPE + " TEXT," + // From the sync source
- Groups._SYNC_ID + " TEXT," + // From the sync source
- Groups._SYNC_TIME + " TEXT," + // From the sync source
- Groups._SYNC_VERSION + " TEXT," + // From the sync source
- Groups._SYNC_LOCAL_ID + " INTEGER," + // Used while syncing, not persistent
- Groups._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
- // if syncable, non-zero if the record
- // has local, unsynced, changes
- Groups._SYNC_MARK + " INTEGER," + // Used to filter out new rows
-
- Groups.NAME + " TEXT NOT NULL," +
- Groups.NOTES + " TEXT," +
- Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 0," +
- Groups.SYSTEM_ID + " TEXT," +
- "UNIQUE(" +
- Groups.NAME + "," + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + "," +
- Groups._SYNC_ACCOUNT_TYPE + ")" +
- ");");
-
- db.execSQL("CREATE INDEX groupsSyncDirtyIndex ON groups (" + Groups._SYNC_DIRTY + ");");
-
- if (!isTemporary()) {
- // Add the system groups, since we always need them.
- db.execSQL("INSERT INTO groups (" + Groups.NAME + ", " + Groups.SYSTEM_ID + ") VALUES "
- + "('" + Groups.GROUP_MY_CONTACTS + "', '" + Groups.GROUP_MY_CONTACTS + "')");
- }
-
- db.execSQL("CREATE TABLE peopleLookup (" +
- "token TEXT," +
- "source INTEGER REFERENCES people(_id)," +
- "token_index INTEGER"+
- ");");
- db.execSQL("CREATE INDEX peopleLookupIndex ON peopleLookup (" +
- "token," +
- "source" +
- ");");
-
- db.execSQL("CREATE TABLE photos ("
- + Photos._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
- + Photos.EXISTS_ON_SERVER + " INTEGER NOT NULL DEFAULT 0,"
- + Photos.PERSON_ID + " INTEGER REFERENCES people(_id), "
- + Photos.LOCAL_VERSION + " TEXT,"
- + Photos.DATA + " BLOB,"
- + Photos.SYNC_ERROR + " TEXT,"
- + Photos._SYNC_ACCOUNT + " TEXT,"
- + Photos._SYNC_ACCOUNT_TYPE + " TEXT,"
- + Photos._SYNC_ID + " TEXT,"
- + Photos._SYNC_TIME + " TEXT,"
- + Photos._SYNC_VERSION + " TEXT,"
- + Photos._SYNC_LOCAL_ID + " INTEGER,"
- + Photos._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0,"
- + Photos._SYNC_MARK + " INTEGER,"
- + "UNIQUE(" + Photos.PERSON_ID + ") "
- + ")");
-
- db.execSQL("CREATE INDEX photosSyncDirtyIndex ON photos (" + Photos._SYNC_DIRTY + ");");
- db.execSQL("CREATE INDEX photoPersonIndex ON photos (person);");
-
- // Delete the photo row when the people row is deleted
- db.execSQL(""
- + " CREATE TRIGGER peopleDeleteAndPhotos DELETE ON people "
- + " BEGIN"
- + " DELETE FROM photos WHERE person=OLD._id;"
- + " END");
-
- db.execSQL("CREATE TABLE _deleted_people (" +
- "_sync_version TEXT," + // From the sync source
- "_sync_id TEXT," +
- (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
- "_sync_account TEXT," +
- "_sync_account_type TEXT," +
- "_sync_mark INTEGER)"); // Used to filter out new rows
-
- db.execSQL("CREATE TABLE _deleted_groups (" +
- "_sync_version TEXT," + // From the sync source
- "_sync_id TEXT," +
- (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
- "_sync_account TEXT," +
- "_sync_account_type TEXT," +
- "_sync_mark INTEGER)"); // Used to filter out new rows
-
- db.execSQL("CREATE TABLE phones (" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "person INTEGER REFERENCES people(_id)," +
- "type INTEGER NOT NULL," + // kind specific (home, work, etc)
- "number TEXT," +
- "number_key TEXT," +
- "label TEXT," +
- "isprimary INTEGER NOT NULL DEFAULT 0" +
- ");");
- db.execSQL("CREATE INDEX phonesIndex1 ON phones (person);");
- db.execSQL("CREATE INDEX phonesIndex2 ON phones (number_key);");
-
- db.execSQL("CREATE TABLE contact_methods (" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "person INTEGER REFERENCES people(_id)," +
- "kind INTEGER NOT NULL," + // the kind of contact method
- "data TEXT," +
- "aux_data TEXT," +
- "type INTEGER NOT NULL," + // kind specific (home, work, etc)
- "label TEXT," +
- "isprimary INTEGER NOT NULL DEFAULT 0" +
- ");");
- db.execSQL("CREATE INDEX contactMethodsPeopleIndex "
- + "ON contact_methods (person);");
-
- // The table for recent calls is here so we can do table joins
- // on people, phones, and calls all in one place.
- db.execSQL("CREATE TABLE calls (" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "number TEXT," +
- "date INTEGER," +
- "duration INTEGER," +
- "type INTEGER," +
- "new INTEGER," +
- "name TEXT," +
- "numbertype INTEGER," +
- "numberlabel TEXT" +
- ");");
-
- // Various settings for the contacts sync adapter. The _sync_account column may
- // be null, but it must not be the empty string.
- db.execSQL("CREATE TABLE settings (" +
- "_id INTEGER PRIMARY KEY," +
- "_sync_account TEXT," +
- "_sync_account_type TEXT," +
- "key STRING NOT NULL," +
- "value STRING " +
- ");");
-
- // The table for the organizations of a person.
- db.execSQL("CREATE TABLE organizations (" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "company TEXT," +
- "title TEXT," +
- "isprimary INTEGER NOT NULL DEFAULT 0," +
- "type INTEGER NOT NULL," + // kind specific (home, work, etc)
- "label TEXT," +
- "person INTEGER REFERENCES people(_id)" +
- ");");
- db.execSQL("CREATE INDEX organizationsIndex1 ON organizations (person);");
-
- // The table for the extensions of a person.
- db.execSQL("CREATE TABLE extensions (" +
- "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "name TEXT NOT NULL," +
- "value TEXT NOT NULL," +
- "person INTEGER REFERENCES people(_id)," +
- "UNIQUE(person, name)" +
- ");");
- db.execSQL("CREATE INDEX extensionsIndex1 ON extensions (person, name);");
-
- // The table for the groups of a person.
- db.execSQL("CREATE TABLE groupmembership (" +
- "_id INTEGER PRIMARY KEY," +
- "person INTEGER REFERENCES people(_id)," +
- "group_id INTEGER REFERENCES groups(_id)," +
- "group_sync_account STRING," +
- "group_sync_account_type STRING," +
- "group_sync_id STRING" +
- ");");
- db.execSQL("CREATE INDEX groupmembershipIndex1 ON groupmembership (person, group_id);");
- db.execSQL("CREATE INDEX groupmembershipIndex2 ON groupmembership (group_id, person);");
- db.execSQL("CREATE INDEX groupmembershipIndex3 ON groupmembership "
- + "(group_sync_account, group_sync_account_type, group_sync_id);");
-
- // Trigger to completely remove a contacts data when they're deleted
- db.execSQL("CREATE TRIGGER contact_cleanup DELETE ON people " +
- "BEGIN " +
- "DELETE FROM peopleLookup WHERE source = old._id;" +
- "DELETE FROM phones WHERE person = old._id;" +
- "DELETE FROM contact_methods WHERE person = old._id;" +
- "DELETE FROM organizations WHERE person = old._id;" +
- "DELETE FROM groupmembership WHERE person = old._id;" +
- "DELETE FROM extensions WHERE person = old._id;" +
- "END");
-
- // Trigger to disassociate the groupmembership from the groups when an
- // groups entry is deleted
- db.execSQL("CREATE TRIGGER groups_cleanup DELETE ON groups " +
- "BEGIN " +
- "UPDATE groupmembership SET group_id = null WHERE group_id = old._id;" +
- "END");
-
- // Trigger to move an account_people row to _deleted_account_people when it is deleted
- db.execSQL("CREATE TRIGGER groups_to_deleted DELETE ON groups " +
- "WHEN old._sync_id is not null " +
- "BEGIN " +
- "INSERT INTO _deleted_groups " +
- "(_sync_id, _sync_account, _sync_account_type, _sync_version) " +
- "VALUES (old._sync_id, old._sync_account, old._sync_account_type, " +
- "old._sync_version);" +
- "END");
-
- // Triggers to keep the peopleLookup table up to date
- db.execSQL("CREATE TRIGGER peopleLookup_update UPDATE OF name ON people " +
- "BEGIN " +
- "DELETE FROM peopleLookup WHERE source = new._id;" +
- "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
- "END");
- db.execSQL("CREATE TRIGGER peopleLookup_insert AFTER INSERT ON people " +
- "BEGIN " +
- "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" +
- "END");
-
- // Triggers to set the _sync_dirty flag when a phone is changed,
- // inserted or deleted
- db.execSQL("CREATE TRIGGER phones_update UPDATE ON phones " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
- "END");
- db.execSQL("CREATE TRIGGER phones_insert INSERT ON phones " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person;" +
- "END");
- db.execSQL("CREATE TRIGGER phones_delete DELETE ON phones " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
- "END");
-
- // Triggers to set the _sync_dirty flag when a contact_method is
- // changed, inserted or deleted
- db.execSQL("CREATE TRIGGER contact_methods_update UPDATE ON contact_methods " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
- "END");
- db.execSQL("CREATE TRIGGER contact_methods_insert INSERT ON contact_methods " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person;" +
- "END");
- db.execSQL("CREATE TRIGGER contact_methods_delete DELETE ON contact_methods " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
- "END");
-
- // Triggers for when an organization is changed, inserted or deleted
- db.execSQL("CREATE TRIGGER organizations_update AFTER UPDATE ON organizations " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
- "END");
- db.execSQL("CREATE TRIGGER organizations_insert INSERT ON organizations " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
- "END");
- db.execSQL("CREATE TRIGGER organizations_delete DELETE ON organizations " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
- "END");
-
- // Triggers for when an groupmembership is changed, inserted or deleted
- db.execSQL("CREATE TRIGGER groupmembership_update AFTER UPDATE ON groupmembership " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
- "END");
- db.execSQL("CREATE TRIGGER groupmembership_insert INSERT ON groupmembership " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
- "END");
- db.execSQL("CREATE TRIGGER groupmembership_delete DELETE ON groupmembership " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
- "END");
-
- // Triggers for when an extension is changed, inserted or deleted
- db.execSQL("CREATE TRIGGER extensions_update AFTER UPDATE ON extensions " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " +
- "END");
- db.execSQL("CREATE TRIGGER extensions_insert INSERT ON extensions " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " +
- "END");
- db.execSQL("CREATE TRIGGER extensions_delete DELETE ON extensions " +
- "BEGIN " +
- "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" +
- "END");
-
- createTypeLabelTrigger(db, sPhonesTable, "INSERT");
- createTypeLabelTrigger(db, sPhonesTable, "UPDATE");
- createTypeLabelTrigger(db, sOrganizationsTable, "INSERT");
- createTypeLabelTrigger(db, sOrganizationsTable, "UPDATE");
- createTypeLabelTrigger(db, sContactMethodsTable, "INSERT");
- createTypeLabelTrigger(db, sContactMethodsTable, "UPDATE");
-
- // Temporary table that holds a time stamp of the last time data the voice
- // dialer is interested in has changed so the grammar won't need to be
- // recompiled when unused data is changed.
- db.execSQL("CREATE TABLE voice_dialer_timestamp (" +
- "_id INTEGER PRIMARY KEY," +
- "timestamp INTEGER" +
- ");");
- db.execSQL("INSERT INTO voice_dialer_timestamp (_id, timestamp) VALUES " +
- "(1, strftime('%s', 'now'));");
- db.execSQL("CREATE TRIGGER timestamp_trigger1 AFTER UPDATE ON phones " +
- "BEGIN " +
- "UPDATE voice_dialer_timestamp SET timestamp=strftime('%s', 'now') "+
- "WHERE _id=1;" +
- "END");
- db.execSQL("CREATE TRIGGER timestamp_trigger2 AFTER UPDATE OF name ON people " +
- "BEGIN " +
- "UPDATE voice_dialer_timestamp SET timestamp=strftime('%s', 'now') " +
- "WHERE _id=1;" +
- "END");
- }
-
- private void createTypeLabelTrigger(SQLiteDatabase db, String table, String operation) {
- final String name = table + "_" + operation + "_typeAndLabel";
- db.execSQL("CREATE TRIGGER " + name + " AFTER " + operation + " ON " + table
- + " WHEN (NEW.type != 0 AND NEW.label IS NOT NULL) OR "
- + " (NEW.type = 0 AND NEW.label IS NULL)"
- + " BEGIN "
- + " SELECT RAISE (ABORT, 'exactly one of type or label must be set'); "
- + " END");
- }
-
- private void maybeCreatePresenceTable(SQLiteDatabase db) {
- // Load the presence table from the presence_db. Just create the table
- // if we are
- String cpDbName;
- if (!isTemporary()) {
- db.execSQL("ATTACH DATABASE ':memory:' AS presence_db;");
- cpDbName = "presence_db.";
- } else {
- cpDbName = "";
- }
- db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + "presence ("+
- Presence._ID + " INTEGER PRIMARY KEY," +
- Presence.PERSON_ID + " INTEGER REFERENCES people(_id)," +
- Presence.IM_PROTOCOL + " TEXT," +
- Presence.IM_HANDLE + " TEXT," +
- Presence.IM_ACCOUNT + " TEXT," +
- Presence.PRESENCE_STATUS + " INTEGER," +
- Presence.PRESENCE_CUSTOM_STATUS + " TEXT," +
- "UNIQUE(" + Presence.IM_PROTOCOL + ", " + Presence.IM_HANDLE + ", "
- + Presence.IM_ACCOUNT + ")" +
- ");");
-
- db.execSQL("CREATE INDEX IF NOT EXISTS " + cpDbName + "presenceIndex ON presence ("
- + Presence.PERSON_ID + ");");
- }
-
- @SuppressWarnings("deprecation")
- private String buildPeopleLookupWhereClause(String filterParam) {
- StringBuilder filter = new StringBuilder(
- "people._id IN (SELECT source FROM peopleLookup WHERE token GLOB ");
- // NOTE: Query parameters won't work here since the SQL compiler
- // needs to parse the actual string to know that it can use the
- // index to do a prefix scan.
- DatabaseUtils.appendEscapedSQLString(filter,
- DatabaseUtils.getHexCollationKey(filterParam) + "*");
- filter.append(')');
- return filter.toString();
- }
-
- @Override
- public Cursor queryInternal(Uri url, String[] projectionIn,
- String selection, String[] selectionArgs, String sort) {
-
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- Uri notificationUri = Contacts.CONTENT_URI;
- String limit = getLimit(url);
- StringBuilder whereClause;
- String groupBy = null;
-
- // Generate the body of the query
- int match = sURIMatcher.match(url);
-
- if (Config.LOGV) Log.v(TAG, "ContactsProvider.query: url=" + url + ", match is " + match);
-
- switch (match) {
- case DELETED_GROUPS:
- if (!isTemporary()) {
- throw new UnsupportedOperationException();
- }
-
- qb.setTables(sDeletedGroupsTable);
- break;
-
- case GROUPS_ID:
- qb.appendWhere("_id=");
- qb.appendWhere(url.getPathSegments().get(1));
- // fall through
- case GROUPS:
- qb.setTables(sGroupsTable);
- qb.setProjectionMap(sGroupsProjectionMap);
- break;
-
- case SETTINGS:
- qb.setTables(sSettingsTable);
- break;
-
- case PEOPLE_GROUPMEMBERSHIP_ID:
- qb.appendWhere("groupmembership._id=");
- qb.appendWhere(url.getPathSegments().get(3));
- qb.appendWhere(" AND ");
- // fall through
- case PEOPLE_GROUPMEMBERSHIP:
- qb.appendWhere(sGroupsJoinString + " AND ");
- qb.appendWhere("person=" + url.getPathSegments().get(1));
- qb.setTables("groups, groupmembership");
- qb.setProjectionMap(sGroupMembershipProjectionMap);
- break;
-
- case GROUPMEMBERSHIP_ID:
- qb.appendWhere("groupmembership._id=");
- qb.appendWhere(url.getPathSegments().get(1));
- qb.appendWhere(" AND ");
- // fall through
- case GROUPMEMBERSHIP:
- qb.setTables("groups, groupmembership");
- qb.setProjectionMap(sGroupMembershipProjectionMap);
- qb.appendWhere(sGroupsJoinString);
- break;
-
- case GROUPMEMBERSHIP_RAW:
- qb.setTables("groupmembership");
- break;
-
- case GROUP_NAME_MEMBERS_FILTER:
- if (url.getPathSegments().size() > 5) {
- qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
- qb.appendWhere(" AND ");
- }
- // fall through
- case GROUP_NAME_MEMBERS:
- qb.setTables(PEOPLE_PHONES_JOIN);
- qb.setProjectionMap(sPeopleProjectionMap);
- qb.appendWhere(buildGroupNameMatchWhereClause(url.getPathSegments().get(2)));
- break;
-
- case GROUP_SYSTEM_ID_MEMBERS_FILTER:
- if (url.getPathSegments().size() > 5) {
- qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
- qb.appendWhere(" AND ");
- }
- // fall through
- case GROUP_SYSTEM_ID_MEMBERS:
- qb.setTables(PEOPLE_PHONES_JOIN);
- qb.setProjectionMap(sPeopleProjectionMap);
- qb.appendWhere(buildGroupSystemIdMatchWhereClause(url.getPathSegments().get(2)));
- break;
-
- case PEOPLE:
- qb.setTables(PEOPLE_PHONES_JOIN);
- qb.setProjectionMap(sPeopleProjectionMap);
- break;
- case PEOPLE_RAW:
- qb.setTables(sPeopleTable);
- break;
-
- case PEOPLE_OWNER:
- return queryOwner(projectionIn);
-
- case PEOPLE_WITH_PHONES_FILTER:
-
- qb.appendWhere("number IS NOT NULL AND ");
-
- // Fall through.
-
- case PEOPLE_FILTER: {
- qb.setTables(PEOPLE_PHONES_JOIN);
- qb.setProjectionMap(sPeopleProjectionMap);
- if (url.getPathSegments().size() > 2) {
- qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
- }
- break;
- }
-
- case PEOPLE_WITH_EMAIL_OR_IM_FILTER:
- String email = url.getPathSegments().get(2);
- whereClause = new StringBuilder();
-
- // Match any E-mail or IM contact methods where data exactly
- // matches the provided string.
- whereClause.append(ContactMethods.DATA);
- whereClause.append("=");
- DatabaseUtils.appendEscapedSQLString(whereClause, email);
- whereClause.append(" AND (kind = " + Contacts.KIND_EMAIL +
- " OR kind = " + Contacts.KIND_IM + ")");
- qb.appendWhere(whereClause.toString());
-
- qb.setTables("people INNER JOIN contact_methods on (people._id = contact_methods.person)");
- qb.setProjectionMap(sPeopleWithEmailOrImProjectionMap);
-
- // Prevent returning the same person for multiple matches
- groupBy = "contact_methods.person";
-
- qb.setDistinct(true);
- break;
-
- case PHOTOS_ID:
- qb.appendWhere("_id="+url.getPathSegments().get(1));
- // Fall through.
- case PHOTOS:
- qb.setTables(sPhotosTable);
- qb.setProjectionMap(sPhotosProjectionMap);
- break;
-
- case PEOPLE_PHOTO:
- qb.appendWhere("person="+url.getPathSegments().get(1));
- qb.setTables(sPhotosTable);
- qb.setProjectionMap(sPhotosProjectionMap);
- break;
-
- case SEARCH_SUGGESTIONS: {
- // Force the default sort order, since the SearchManage doesn't ask for things
- // sorted, though they should be
- if (sort != null && !People.DEFAULT_SORT_ORDER.equals(sort)) {
- throw new IllegalArgumentException("Sort ordering not allowed for this URI");
- }
- sort = SearchManager.SUGGEST_COLUMN_TEXT_1 + " COLLATE LOCALIZED ASC";
-
- // This will either setup the query builder so we can run the proper query below
- // and return null, or it will return a cursor with the results already in it.
- Cursor c = handleSearchSuggestionsQuery(url, qb);
- if (c != null) {
- return c;
- }
- break;
- }
- case SEARCH_SHORTCUT: {
- qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN);
- qb.setProjectionMap(sSearchSuggestionsProjectionMap);
- qb.appendWhere(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID + "=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- }
- case PEOPLE_STREQUENT: {
- // Build the first query for starred
- qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
- qb.setProjectionMap(sStrequentStarredProjectionMap);
- final String starredQuery = qb.buildQuery(projectionIn, "starred = 1",
- null, null, null, null,
- null /* limit */);
-
- // Build the second query for frequent
- qb = new SQLiteQueryBuilder();
- qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
- qb.setProjectionMap(sPeopleWithPhotoProjectionMap);
- final String frequentQuery = qb.buildQuery(projectionIn,
- "times_contacted > 0 AND starred = 0", null, null, null, null, null);
-
- // Put them together
- final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
- STREQUENT_ORDER_BY, STREQUENT_LIMIT);
- final SQLiteDatabase db = getDatabase();
- Cursor c = db.rawQueryWithFactory(null, query, null, sPeopleTable);
- if ((c != null) && !isTemporary()) {
- c.setNotificationUri(getContext().getContentResolver(), notificationUri);
- }
- return c;
- }
- case PEOPLE_STREQUENT_FILTER: {
- // Build the first query for starred
- qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
- qb.setProjectionMap(sStrequentStarredProjectionMap);
- if (url.getPathSegments().size() > 3) {
- qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
- }
- final String starredQuery = qb.buildQuery(projectionIn, "starred = 1",
- null, null, null, null,
- null /* limit */);
-
- // Build the second query for frequent
- qb = new SQLiteQueryBuilder();
- qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN);
- qb.setProjectionMap(sPeopleWithPhotoProjectionMap);
- if (url.getPathSegments().size() > 3) {
- qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
- }
- final String frequentQuery = qb.buildQuery(projectionIn,
- "times_contacted > 0 AND starred = 0", null, null, null, null, null);
-
- // Put them together
- final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
- STREQUENT_ORDER_BY, null);
- final SQLiteDatabase db = getDatabase();
- Cursor c = db.rawQueryWithFactory(null, query, null, sPeopleTable);
- if ((c != null) && !isTemporary()) {
- c.setNotificationUri(getContext().getContentResolver(), notificationUri);
- }
- return c;
- }
- case DELETED_PEOPLE:
- if (isTemporary()) {
- qb.setTables("_deleted_people");
- break;
- }
- throw new UnsupportedOperationException();
- case PEOPLE_ID:
- qb.setTables("people LEFT OUTER JOIN phones ON people.primary_phone=phones._id "
- + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID
- + "=people._id)");
- qb.setProjectionMap(sPeopleProjectionMap);
- qb.appendWhere("people._id=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case PEOPLE_PHONES:
- qb.setTables("phones, people");
- qb.setProjectionMap(sPhonesProjectionMap);
- qb.appendWhere("people._id = phones.person AND person=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case PEOPLE_PHONES_ID:
- qb.setTables("phones, people");
- qb.setProjectionMap(sPhonesProjectionMap);
- qb.appendWhere("people._id = phones.person AND person=");
- qb.appendWhere(url.getPathSegments().get(1));
- qb.appendWhere(" AND phones._id=");
- qb.appendWhere(url.getPathSegments().get(3));
- break;
-
- case PEOPLE_PHONES_WITH_PRESENCE:
- qb.appendWhere("people._id=?");
- selectionArgs = appendSelectionArg(selectionArgs, url.getPathSegments().get(1));
- // Fall through.
-
- case PHONES_WITH_PRESENCE:
- qb.setTables("phones JOIN people ON (phones.person = people._id)"
- + " LEFT OUTER JOIN presence ON (presence.person = people._id)");
- qb.setProjectionMap(sPhonesWithPresenceProjectionMap);
- break;
-
- case PEOPLE_CONTACTMETHODS:
- qb.setTables("contact_methods, people");
- qb.setProjectionMap(sContactMethodsProjectionMap);
- qb.appendWhere("people._id = contact_methods.person AND person=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case PEOPLE_CONTACTMETHODS_ID:
- qb.setTables("contact_methods, people");
- qb.setProjectionMap(sContactMethodsProjectionMap);
- qb.appendWhere("people._id = contact_methods.person AND person=");
- qb.appendWhere(url.getPathSegments().get(1));
- qb.appendWhere(" AND contact_methods._id=");
- qb.appendWhere(url.getPathSegments().get(3));
- break;
- case PEOPLE_ORGANIZATIONS:
- qb.setTables("organizations, people");
- qb.setProjectionMap(sOrganizationsProjectionMap);
- qb.appendWhere("people._id = organizations.person AND person=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case PEOPLE_ORGANIZATIONS_ID:
- qb.setTables("organizations, people");
- qb.setProjectionMap(sOrganizationsProjectionMap);
- qb.appendWhere("people._id = organizations.person AND person=");
- qb.appendWhere(url.getPathSegments().get(1));
- qb.appendWhere(" AND organizations._id=");
- qb.appendWhere(url.getPathSegments().get(3));
- break;
- case PHONES:
- qb.setTables("phones, people");
- qb.appendWhere("people._id = phones.person");
- qb.setProjectionMap(sPhonesProjectionMap);
- break;
- case PHONES_ID:
- qb.setTables("phones, people");
- qb.appendWhere("people._id = phones.person AND phones._id="
- + url.getPathSegments().get(1));
- qb.setProjectionMap(sPhonesProjectionMap);
- break;
- case ORGANIZATIONS:
- qb.setTables("organizations, people");
- qb.appendWhere("people._id = organizations.person");
- qb.setProjectionMap(sOrganizationsProjectionMap);
- break;
- case ORGANIZATIONS_ID:
- qb.setTables("organizations, people");
- qb.appendWhere("people._id = organizations.person AND organizations._id="
- + url.getPathSegments().get(1));
- qb.setProjectionMap(sOrganizationsProjectionMap);
- break;
- case PHONES_MOBILE_FILTER_NAME:
- qb.appendWhere("type=" + Contacts.PhonesColumns.TYPE_MOBILE + " AND ");
-
- // Fall through.
-
- case PHONES_FILTER_NAME:
- qb.setTables("phones JOIN people ON (people._id = phones.person)");
- qb.setProjectionMap(sPhonesProjectionMap);
- if (url.getPathSegments().size() > 2) {
- qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment()));
- }
- break;
-
- case PHONES_FILTER: {
- String phoneNumber = url.getPathSegments().get(2);
- String indexable = PhoneNumberUtils.toCallerIDMinMatch(phoneNumber);
- StringBuilder subQuery = new StringBuilder();
- if (TextUtils.isEmpty(sort)) {
- // Default the sort order to something reasonable so we get consistent
- // results when callers don't request an ordering
- sort = People.DEFAULT_SORT_ORDER;
- }
-
- subQuery.append("people, (SELECT * FROM phones WHERE (phones.number_key GLOB '");
- subQuery.append(indexable);
- subQuery.append("*')) AS phones");
- qb.setTables(subQuery.toString());
- qb.appendWhere("phones.person=people._id AND PHONE_NUMBERS_EQUAL(phones.number, ");
- qb.appendWhereEscapeString(phoneNumber);
- qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
- qb.setProjectionMap(sPhonesProjectionMap);
- break;
- }
- case CONTACTMETHODS:
- qb.setTables("contact_methods, people");
- qb.setProjectionMap(sContactMethodsProjectionMap);
- qb.appendWhere("people._id = contact_methods.person");
- break;
- case CONTACTMETHODS_ID:
- qb.setTables("contact_methods LEFT OUTER JOIN people ON contact_methods.person = people._id");
- qb.setProjectionMap(sContactMethodsProjectionMap);
- qb.appendWhere("contact_methods._id=");
- qb.appendWhere(url.getPathSegments().get(1));
- break;
- case CONTACTMETHODS_EMAIL_FILTER:
- String pattern = url.getPathSegments().get(2);
- whereClause = new StringBuilder();
-
- // TODO This is going to be REALLY slow. Come up with
- // something faster.
- whereClause.append(ContactMethods.KIND);
- whereClause.append('=');
- whereClause.append('\'');
- whereClause.append(Contacts.KIND_EMAIL);
- whereClause.append("' AND (UPPER(");
- whereClause.append(ContactMethods.NAME);
- whereClause.append(") GLOB ");
- DatabaseUtils.appendEscapedSQLString(whereClause, pattern + "*");
- whereClause.append(" OR UPPER(");
- whereClause.append(ContactMethods.NAME);
- whereClause.append(") GLOB ");
- DatabaseUtils.appendEscapedSQLString(whereClause, "* " + pattern + "*");
- whereClause.append(") AND ");
- qb.appendWhere(whereClause.toString());
-
- // Fall through.
-
- case CONTACTMETHODS_EMAIL:
- qb.setTables("contact_methods INNER JOIN people on (contact_methods.person = people._id)");
- qb.setProjectionMap(sEmailSearchProjectionMap);
- qb.appendWhere("kind = " + Contacts.KIND_EMAIL);
- qb.setDistinct(true);
- break;
-
- case PEOPLE_CONTACTMETHODS_WITH_PRESENCE:
- qb.appendWhere("people._id=?");
- selectionArgs = appendSelectionArg(selectionArgs, url.getPathSegments().get(1));
- // Fall through.
-
- case CONTACTMETHODS_WITH_PRESENCE:
- qb.setTables("contact_methods JOIN people ON (contact_methods.person = people._id)"
- + " LEFT OUTER JOIN presence ON "
- // Match gtalk presence items
- + "((kind=" + Contacts.KIND_EMAIL +
- " AND im_protocol='"
- + ContactMethods.encodePredefinedImProtocol(
- ContactMethods.PROTOCOL_GOOGLE_TALK)
- + "' AND data=im_handle)"
- + " OR "
- // Match IM presence items
- + "(kind=" + Contacts.KIND_IM
- + " AND data=im_handle AND aux_data=im_protocol))");
- qb.setProjectionMap(sContactMethodsWithPresenceProjectionMap);
- break;
-
- case CALLS:
- qb.setTables("calls");
- qb.setProjectionMap(sCallsProjectionMap);
- notificationUri = CallLog.CONTENT_URI;
- break;
- case CALLS_ID:
- qb.setTables("calls");
- qb.setProjectionMap(sCallsProjectionMap);
- qb.appendWhere("calls._id=");
- qb.appendWhere(url.getPathSegments().get(1));
- notificationUri = CallLog.CONTENT_URI;
- break;
- case CALLS_FILTER: {
- qb.setTables("calls");
- qb.setProjectionMap(sCallsProjectionMap);
-
- String phoneNumber = url.getPathSegments().get(2);
- qb.appendWhere("PHONE_NUMBERS_EQUAL(number, ");
- qb.appendWhereEscapeString(phoneNumber);
- qb.appendWhere(mUseStrictPhoneNumberComparation ? ", 1)" : ", 0)");
- notificationUri = CallLog.CONTENT_URI;
- break;
- }
-
- case PRESENCE:
- qb.setTables("presence LEFT OUTER JOIN people on (presence." + Presence.PERSON_ID
- + "= people._id)");
- qb.setProjectionMap(sPresenceProjectionMap);
- break;
- case PRESENCE_ID:
- qb.setTables("presence LEFT OUTER JOIN people on (presence." + Presence.PERSON_ID
- + "= people._id)");
- qb.appendWhere("presence._id=");
- qb.appendWhere(url.getLastPathSegment());
- break;
- case VOICE_DIALER_TIMESTAMP:
- qb.setTables("voice_dialer_timestamp");
- qb.appendWhere("_id=1");
- break;
-
- case PEOPLE_EXTENSIONS_ID:
- qb.appendWhere("extensions._id=" + url.getPathSegments().get(3) + " AND ");
- // fall through
- case PEOPLE_EXTENSIONS:
- qb.appendWhere("person=" + url.getPathSegments().get(1));
- qb.setTables(sExtensionsTable);
- qb.setProjectionMap(sExtensionsProjectionMap);
- break;
-
- case EXTENSIONS_ID:
- qb.appendWhere("extensions._id=" + url.getPathSegments().get(1));
- // fall through
- case EXTENSIONS:
- qb.setTables(sExtensionsTable);
- qb.setProjectionMap(sExtensionsProjectionMap);
- break;
-
- case LIVE_FOLDERS_PEOPLE:
- qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
- qb.setProjectionMap(sLiveFoldersProjectionMap);
- break;
-
- case LIVE_FOLDERS_PEOPLE_WITH_PHONES:
- qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
- qb.setProjectionMap(sLiveFoldersProjectionMap);
- qb.appendWhere(People.PRIMARY_PHONE_ID + " IS NOT NULL");
- break;
-
- case LIVE_FOLDERS_PEOPLE_FAVORITES:
- qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
- qb.setProjectionMap(sLiveFoldersProjectionMap);
- qb.appendWhere(People.STARRED + " <> 0");
- break;
-
- case LIVE_FOLDERS_PEOPLE_GROUP_NAME:
- qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)");
- qb.setProjectionMap(sLiveFoldersProjectionMap);
- qb.appendWhere(buildGroupNameMatchWhereClause(url.getLastPathSegment()));
- break;
-
- default:
- throw new IllegalArgumentException("Unknown URL " + url);
- }
-
- // run the query
- final SQLiteDatabase db = getDatabase();
- Cursor c = qb.query(db, projectionIn, selection, selectionArgs,
- groupBy, null, sort, limit);
- if ((c != null) && !isTemporary()) {
- c.setNotificationUri(getContext().getContentResolver(), notificationUri);
- }
- return c;
- }
-
- /**
- * Gets the value of the "limit" URI query parameter.
- *
- * @return A string containing a non-negative integer, or <code>null</code> if
- * the parameter is not set, or is set to an invalid value.
- */
- private String getLimit(Uri url) {
- String limit = url.getQueryParameter("limit");
- if (limit == null) {
- return null;
- }
- // make sure that the limit is a non-negative integer
- try {
- int l = Integer.parseInt(limit);
- if (l < 0) {
- Log.w(TAG, "Invalid limit parameter: " + limit);
- return null;
- }
- return String.valueOf(l);
- } catch (NumberFormatException ex) {
- Log.w(TAG, "Invalid limit parameter: " + limit);
- return null;
- }
- }
-
- /**
- * Build a WHERE clause that restricts the query to match people that are a member of
- * a particular system group. The projection map of the query must include {@link People#_ID}.
- *
- * @param groupSystemId The system group id (e.g {@link Groups#GROUP_MY_CONTACTS})
- * @return The where clause.
- */
- private CharSequence buildGroupSystemIdMatchWhereClause(String groupSystemId) {
- return "people._id IN (SELECT person FROM groupmembership JOIN groups " +
- "ON (group_id=groups._id OR " +
- "(group_sync_id = groups._sync_id AND " +
- "group_sync_account = groups._sync_account)) "+
- "WHERE " + Groups.SYSTEM_ID + "="
- + DatabaseUtils.sqlEscapeString(groupSystemId) + ")";
- }
-
- /**
- * Build a WHERE clause that restricts the query to match people that are a member of
- * a group with a particular name. The projection map of the query must include
- * {@link People#_ID}.
- *
- * @param groupName The name of the group
- * @return The where clause.
- */
- private CharSequence buildGroupNameMatchWhereClause(String groupName) {
- return "people._id IN (SELECT person FROM groupmembership JOIN groups " +
- "ON (group_id=groups._id OR " +
- "(group_sync_id = groups._sync_id AND " +
- "group_sync_account = groups._sync_account AND " +
- "group_sync_account_type = groups._sync_account_type)) "+
- "WHERE " + Groups.NAME + "="
- + DatabaseUtils.sqlEscapeString(groupName) + ")";
- }
-
- private Cursor queryOwner(String[] projection) {
- // Check the permissions
- getContext().enforceCallingPermission("android.permission.READ_OWNER_DATA",
- "No permission to access owner info");
-
- // Read the owner id
- SharedPreferences prefs = getContext().getSharedPreferences(PREFS_NAME_OWNER,
- Context.MODE_PRIVATE);
- long ownerId = prefs.getLong(PREF_OWNER_ID, 0);
-
- // Run the query
- return queryInternal(ContentUris.withAppendedId(People.CONTENT_URI, ownerId), projection,
- null, null, null);
- }
-
- /**
- * Append a string to a selection args array
- *
- * @param selectionArgs the old arg
- * @param newArg the new arg to append
- * @return a new string array with all of the args
- */
- private String[] appendSelectionArg(String[] selectionArgs, String newArg) {
- if (selectionArgs == null || selectionArgs.length == 0) {
- return new String[] { newArg };
- } else {
- int length = selectionArgs.length;
- String[] newArgs = new String[length + 1];
- System.arraycopy(selectionArgs, 0, newArgs, 0, length);
- newArgs[length] = newArg;
- return newArgs;
- }
- }
-
- /**
- * Either sets up the query builder so we can run the proper query against the database
- * and returns null, or returns a cursor with the results already in it.
- *
- * @param url the URL passed for the suggestion
- * @param qb the query builder to use if a query needs to be run on the database
- * @return null with qb configured for a query, a cursor with the results already in it.
- */
- private Cursor handleSearchSuggestionsQuery(Uri url, SQLiteQueryBuilder qb) {
- qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN);
- qb.setProjectionMap(sSearchSuggestionsProjectionMap);
- if (url.getPathSegments().size() > 1) {
- // A search term was entered, use it to filter
-
- // only match within 'my contacts'
- // TODO: match the 'display group' instead of hard coding 'my contacts'
- // once that information is factored out of the shared prefs of the contacts
- // app into this content provider.
- qb.appendWhere(buildGroupSystemIdMatchWhereClause(Groups.GROUP_MY_CONTACTS));
- qb.appendWhere(" AND ");
-
- // match the query
- final String searchClause = url.getLastPathSegment();
- if (!TextUtils.isDigitsOnly(searchClause)) {
- qb.appendWhere(buildPeopleLookupWhereClause(searchClause));
- } else {
- final String[] columnNames = new String[] {
- "_id",
- SearchManager.SUGGEST_COLUMN_TEXT_1,
- SearchManager.SUGGEST_COLUMN_TEXT_2,
- SearchManager.SUGGEST_COLUMN_ICON_1,
- SearchManager.SUGGEST_COLUMN_INTENT_DATA,
- SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
- SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
- };
-
- Resources r = getContext().getResources();
- String s;
- int i;
-
- ArrayList<Object> dialNumber = new ArrayList<Object>();
- dialNumber.add(0); // _id
- s = r.getString(com.android.internal.R.string.dial_number_using, searchClause);
- i = s.indexOf('\n');
- if (i < 0) {
- dialNumber.add(s);
- dialNumber.add("");
- } else {
- dialNumber.add(s.substring(0, i));
- dialNumber.add(s.substring(i + 1));
- }
- dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact));
- dialNumber.add("tel:" + searchClause);
- dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
- dialNumber.add(null);
-
- ArrayList<Object> createContact = new ArrayList<Object>();
- createContact.add(1); // _id
- s = r.getString(com.android.internal.R.string.create_contact_using, searchClause);
- i = s.indexOf('\n');
- if (i < 0) {
- createContact.add(s);
- createContact.add("");
- } else {
- createContact.add(s.substring(0, i));
- createContact.add(s.substring(i + 1));
- }
- createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact));
- createContact.add("tel:" + searchClause);
- createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
- createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
-
- ArrayList<ArrayList> rows = new ArrayList<ArrayList>();
- rows.add(dialNumber);
- rows.add(createContact);
-
- ArrayListCursor cursor = new ArrayListCursor(columnNames, rows);
- return cursor;
- }
- }
- return null;
- }
-
- @Override
- public String getType(Uri url) {
- int match = sURIMatcher.match(url);
- switch (match) {
- case EXTENSIONS:
- case PEOPLE_EXTENSIONS:
- return Extensions.CONTENT_TYPE;
- case EXTENSIONS_ID:
- case PEOPLE_EXTENSIONS_ID:
- return Extensions.CONTENT_ITEM_TYPE;
- case PEOPLE:
- return "vnd.android.cursor.dir/person";
- case PEOPLE_ID:
- return "vnd.android.cursor.item/person";
- case PEOPLE_PHONES:
- return "vnd.android.cursor.dir/phone";
- case PEOPLE_PHONES_ID:
- return "vnd.android.cursor.item/phone";
- case PEOPLE_CONTACTMETHODS:
- return "vnd.android.cursor.dir/contact-methods";
- case PEOPLE_CONTACTMETHODS_ID:
- return getContactMethodType(url);
- case PHONES:
- return "vnd.android.cursor.dir/phone";
- case PHONES_ID:
- return "vnd.android.cursor.item/phone";
- case PHONES_FILTER:
- case PHONES_FILTER_NAME:
- case PHONES_MOBILE_FILTER_NAME:
- return "vnd.android.cursor.dir/phone";
- case PHOTOS_ID:
- return "vnd.android.cursor.item/photo";
- case PHOTOS:
- return "vnd.android.cursor.dir/photo";
- case PEOPLE_PHOTO:
- return "vnd.android.cursor.item/photo";
- case PEOPLE_PHOTO_DATA:
- return "image/png";
- case CONTACTMETHODS:
- return "vnd.android.cursor.dir/contact-methods";
- case CONTACTMETHODS_ID:
- return getContactMethodType(url);
- case CONTACTMETHODS_EMAIL:
- case CONTACTMETHODS_EMAIL_FILTER:
- return "vnd.android.cursor.dir/email";
- case CALLS:
- return "vnd.android.cursor.dir/calls";
- case CALLS_ID:
- return "vnd.android.cursor.item/calls";
- case ORGANIZATIONS:
- return "vnd.android.cursor.dir/organizations";
- case ORGANIZATIONS_ID:
- return "vnd.android.cursor.item/organization";
- case CALLS_FILTER:
- return "vnd.android.cursor.dir/calls";
- case SEARCH_SUGGESTIONS:
- return SearchManager.SUGGEST_MIME_TYPE;
- case SEARCH_SHORTCUT:
- return SearchManager.SHORTCUT_MIME_TYPE;
- default:
- throw new IllegalArgumentException("Unknown URL");
- }
- }
-
- private String getContactMethodType(Uri url)
- {
- String mime = null;
-
- Cursor c = query(url, new String[] {ContactMethods.KIND}, null, null, null);
- if (c != null) {
- try {
- if (c.moveToFirst()) {
- int kind = c.getInt(0);
- switch (kind) {
- case Contacts.KIND_EMAIL:
- mime = "vnd.android.cursor.item/email";
- break;
-
- case Contacts.KIND_IM:
- mime = "vnd.android.cursor.item/jabber-im";
- break;
-
- case Contacts.KIND_POSTAL:
- mime = "vnd.android.cursor.item/postal-address";
- break;
- }
- }
- } finally {
- c.close();
- }
- }
- return mime;
- }
-
- private ContentValues queryAndroidStarredGroupId(Account account) {
- String whereString;
- String[] whereArgs;
- if (account != null) {
- whereString = "_sync_account=? AND _sync_account_type=? AND name=?";
- whereArgs = new String[]{account.name, account.type, Groups.GROUP_ANDROID_STARRED};
- } else {
- whereString = "_sync_account is null AND name=?";
- whereArgs = new String[]{Groups.GROUP_ANDROID_STARRED};
- }
- Cursor cursor = getDatabase().query(sGroupsTable,
- new String[]{Groups._ID, Groups._SYNC_ID, Groups._SYNC_ACCOUNT,
- Groups._SYNC_ACCOUNT_TYPE},
- whereString, whereArgs, null, null, null);
- try {
- if (cursor.moveToNext()) {
- ContentValues result = new ContentValues();
- result.put(Groups._ID, cursor.getLong(0));
- result.put(Groups._SYNC_ID, cursor.getString(1));
- result.put(Groups._SYNC_ACCOUNT, cursor.getString(2));
- result.put(Groups._SYNC_ACCOUNT_TYPE, cursor.getString(3));
- return result;
- }
- return null;
- } finally {
- cursor.close();
- }
- }
-
- @Override
- public Uri insertInternal(Uri url, ContentValues initialValues) {
- Uri resultUri = null;
- long rowID;
-
- final SQLiteDatabase db = getDatabase();
- int match = sURIMatcher.match(url);
- switch (match) {
- case PEOPLE_GROUPMEMBERSHIP:
- case GROUPMEMBERSHIP: {
- mValues.clear();
- mValues.putAll(initialValues);
- if (match == PEOPLE_GROUPMEMBERSHIP) {
- mValues.put(GroupMembership.PERSON_ID,
- Long.valueOf(url.getPathSegments().get(1)));
- }
- resultUri = insertIntoGroupmembership(mValues);
- }
- break;
-
- case PEOPLE_OWNER:
- return insertOwner(initialValues);
-
- case PEOPLE_EXTENSIONS:
- case EXTENSIONS: {
- ContentValues newMap = new ContentValues(initialValues);
- if (match == PEOPLE_EXTENSIONS) {
- newMap.put(Extensions.PERSON_ID,
- Long.valueOf(url.getPathSegments().get(1)));
- }
- rowID = mExtensionsInserter.insert(newMap);
- if (rowID > 0) {
- resultUri = ContentUris.withAppendedId(Extensions.CONTENT_URI, rowID);
- }
- }
- break;
-
- case PHOTOS: {
- if (!isTemporary()) {
- throw new UnsupportedOperationException();
- }
- rowID = mPhotosInserter.insert(initialValues);
- if (rowID > 0) {
- resultUri = ContentUris.withAppendedId(Photos.CONTENT_URI, rowID);
- }
- }
- break;
-
- case GROUPS: {
- ContentValues newMap = new ContentValues(initialValues);
- ensureSyncAccountIsSet(newMap);
- newMap.put(Groups._SYNC_DIRTY, 1);
- // Insert into the groups table
- rowID = mGroupsInserter.insert(newMap);
- if (rowID > 0) {
- resultUri = ContentUris.withAppendedId(Groups.CONTENT_URI, rowID);
- if (!isTemporary() && newMap.containsKey(Groups.SHOULD_SYNC)) {
- final String accountName = newMap.getAsString(Groups._SYNC_ACCOUNT);
- final String accountType = newMap.getAsString(Groups._SYNC_ACCOUNT_TYPE);
- if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
- final ContentResolver cr = getContext().getContentResolver();
- final Account account = new Account(accountName, accountType);
- onLocalChangesForAccount(cr, account, false);
- }
- }
- }
- }
- break;
-
- case PEOPLE_RAW:
- case PEOPLE: {
- mValues.clear();
- mValues.putAll(initialValues);
- ensureSyncAccountIsSet(mValues);
- mValues.put(People._SYNC_DIRTY, 1);
- // Insert into the people table
- rowID = mPeopleInserter.insert(mValues);
- if (rowID > 0) {
- resultUri = ContentUris.withAppendedId(People.CONTENT_URI, rowID);
- if (!isTemporary()) {
- String accountName = mValues.getAsString(People._SYNC_ACCOUNT);
- String accountType = mValues.getAsString(People._SYNC_ACCOUNT_TYPE);
- Account account = null;
- if (accountName != null || accountType != null) {
- account = new Account(accountName, accountType);
- }
- Long starredValue = mValues.getAsLong(People.STARRED);
- final String syncId = mValues.getAsString(People._SYNC_ID);
- boolean isStarred = starredValue != null && starredValue != 0;
- fixupGroupMembershipAfterPeopleUpdate(account, rowID, isStarred);
- // create a photo row for this person
- mDb.delete(sPhotosTable, "person=" + rowID, null);
- mValues.clear();
- mValues.put(Photos.PERSON_ID, rowID);
- mValues.put(Photos._SYNC_ACCOUNT, accountName);
- mValues.put(Photos._SYNC_ACCOUNT_TYPE, accountType);
- mValues.put(Photos._SYNC_ID, syncId);
- mValues.put(Photos._SYNC_DIRTY, 0);
- mPhotosInserter.insert(mValues);
- }
- }
- }
- break;
-
- case DELETED_PEOPLE: {
- if (isTemporary()) {
- // Insert into the people table
- rowID = db.insert("_deleted_people", "_sync_id", initialValues);
- if (rowID > 0) {
- resultUri = Uri.parse("content://contacts/_deleted_people/" + rowID);
- }
- } else {
- throw new UnsupportedOperationException();
- }
- }
- break;
-
- case DELETED_GROUPS: {
- if (isTemporary()) {
- rowID = db.insert(sDeletedGroupsTable, Groups._SYNC_ID,
- initialValues);
- if (rowID > 0) {
- resultUri =ContentUris.withAppendedId(
- Groups.DELETED_CONTENT_URI, rowID);
- }
- } else {
- throw new UnsupportedOperationException();
- }
- }
- break;
-
- case PEOPLE_PHONES:
- case PHONES: {
- mValues.clear();
- mValues.putAll(initialValues);
- if (match == PEOPLE_PHONES) {
- mValues.put(Contacts.Phones.PERSON_ID,
- Long.valueOf(url.getPathSegments().get(1)));
- }
- String number = mValues.getAsString(Contacts.Phones.NUMBER);
- if (number != null) {
- mValues.put("number_key", PhoneNumberUtils.getStrippedReversed(number));
- }
-
- rowID = insertAndFixupPrimary(Contacts.KIND_PHONE, mValues);
- resultUri = ContentUris.withAppendedId(Phones.CONTENT_URI, rowID);
- }
- break;
-
- case CONTACTMETHODS:
- case PEOPLE_CONTACTMETHODS: {
- mValues.clear();
- mValues.putAll(initialValues);
- if (match == PEOPLE_CONTACTMETHODS) {
- mValues.put("person", url.getPathSegments().get(1));
- }
- Integer kind = mValues.getAsInteger(ContactMethods.KIND);
- if (kind == null) {
- throw new IllegalArgumentException("you must specify the ContactMethods.KIND");
- }
- rowID = insertAndFixupPrimary(kind, mValues);
- if (rowID > 0) {
- resultUri = ContentUris.withAppendedId(ContactMethods.CONTENT_URI, rowID);
- }
- }
- break;
-
- case CALLS: {
- rowID = mCallsInserter.insert(initialValues);
- if (rowID > 0) {
- resultUri = Uri.parse("content://call_log/calls/" + rowID);
- }
- }
- break;
-
- case PRESENCE: {
- final String handle = initialValues.getAsString(Presence.IM_HANDLE);
- final String protocol = initialValues.getAsString(Presence.IM_PROTOCOL);
- if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) {
- throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required");
- }
-
- // Look for the contact for this presence update
- StringBuilder query = new StringBuilder("SELECT ");
- query.append(ContactMethods.PERSON_ID);
- query.append(" FROM contact_methods WHERE (kind=");
- query.append(Contacts.KIND_IM);
- query.append(" AND ");
- query.append(ContactMethods.DATA);
- query.append("=? AND ");
- query.append(ContactMethods.AUX_DATA);
- query.append("=?)");
-
- String[] selectionArgs;
- if (GTALK_PROTOCOL_STRING.equals(protocol)) {
- // For gtalk accounts we usually don't have an explicit IM
- // entry, so also look for the email address as well
- query.append(" OR (");
- query.append("kind=");
- query.append(Contacts.KIND_EMAIL);
- query.append(" AND ");
- query.append(ContactMethods.DATA);
- query.append("=?)");
- selectionArgs = new String[] { handle, protocol, handle };
- } else {
- selectionArgs = new String[] { handle, protocol };
- }
-
- Cursor c = db.rawQueryWithFactory(null, query.toString(), selectionArgs, null);
-
- long personId = 0;
- try {
- if (c.moveToFirst()) {
- personId = c.getLong(0);
- } else {
- // No contact found, return a null URI
- return null;
- }
- } finally {
- c.close();
- }
-
- mValues.clear();
- mValues.putAll(initialValues);
- mValues.put(Presence.PERSON_ID, personId);
-
- // Insert the presence update
- rowID = db.replace("presence", null, mValues);
- if (rowID > 0) {
- resultUri = Uri.parse("content://contacts/presence/" + rowID);
- }
- }
- break;
-
- case PEOPLE_ORGANIZATIONS:
- case ORGANIZATIONS: {
- ContentValues newMap = new ContentValues(initialValues);
- if (match == PEOPLE_ORGANIZATIONS) {
- newMap.put(Contacts.Phones.PERSON_ID,
- Long.valueOf(url.getPathSegments().get(1)));
- }
- rowID = insertAndFixupPrimary(Contacts.KIND_ORGANIZATION, newMap);
- if (rowID > 0) {
- resultUri = Uri.parse("content://contacts/organizations/" + rowID);
- }
- }
- break;
- default:
- throw new UnsupportedOperationException("Cannot insert into URL: " + url);
- }
-
- return resultUri;
- }
-
- @Override
- protected void onAccountsChanged(Account[] accountsArray) {
- super.onAccountsChanged(accountsArray);
- synchronized (mAccountsLock) {
- mAccounts = new Account[accountsArray.length];
- System.arraycopy(accountsArray, 0, mAccounts, 0, mAccounts.length);
- }
- }
-
- private void ensureSyncAccountIsSet(ContentValues values) {
- synchronized (mAccountsLock) {
- final String accountName = values.getAsString(SyncConstValue._SYNC_ACCOUNT);
- final String accountType = values.getAsString(SyncConstValue._SYNC_ACCOUNT_TYPE);
- Account account = null;
- if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
- account = new Account(accountName, accountType);
- }
- if (account == null && mAccounts.length > 0) {
- // TODO(fredq) change this to pick the account that is syncable for contacts
- values.put(SyncConstValue._SYNC_ACCOUNT, mAccounts[0].name);
- values.put(SyncConstValue._SYNC_ACCOUNT_TYPE, mAccounts[0].type);
- }
- }
- }
-
- private Uri insertOwner(ContentValues values) {
- // Check the permissions
- getContext().enforceCallingPermission("android.permission.WRITE_OWNER_DATA",
- "No permission to set owner info");
-
- // Insert the owner info
- Uri uri = insertInternal(People.CONTENT_URI, values);
-
- // Record which person is the owner
- long id = ContentUris.parseId(uri);
- SharedPreferences.Editor prefs = getContext().getSharedPreferences(PREFS_NAME_OWNER,
- Context.MODE_PRIVATE).edit();
- prefs.putLong(PREF_OWNER_ID, id);
- prefs.commit();
- return uri;
- }
-
- private Uri insertIntoGroupmembership(ContentValues values) {
- String groupSyncAccountName = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT);
- String groupSyncAccountType = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
- String groupSyncId = values.getAsString(GroupMembership.GROUP_SYNC_ID);
- final Long personId = values.getAsLong(GroupMembership.PERSON_ID);
- if (!values.containsKey(GroupMembership.GROUP_ID)) {
- if (TextUtils.isEmpty(groupSyncAccountName) || TextUtils.isEmpty(groupSyncAccountType)
- || TextUtils.isEmpty(groupSyncId)) {
- throw new IllegalArgumentException(
- "insertIntoGroupmembership: no GROUP_ID wasn't specified and non-empty "
- + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT and GROUP_SYNC_ACCOUNT_TYPE fields "
- + "weren't specifid, "
- + values);
- }
- if (0 != DatabaseUtils.longForQuery(getDatabase(), ""
- + "SELECT COUNT(*) "
- + "FROM groupmembership "
- + "WHERE group_sync_id=? "
- + " AND group_sync_account=? "
- + " AND group_sync_account_type=? "
- + " AND person=?",
- new String[]{groupSyncId, groupSyncAccountName, groupSyncAccountType,
- String.valueOf(personId)})) {
- final String errorMessage =
- "insertIntoGroupmembership: a row with this server key already exists, "
- + values;
- if (Config.LOGD) Log.d(TAG, errorMessage);
- return null;
- }
- } else {
- long groupId = values.getAsLong(GroupMembership.GROUP_ID);
- if (!TextUtils.isEmpty(groupSyncAccountName) || !TextUtils.isEmpty(groupSyncAccountType)
- || !TextUtils.isEmpty(groupSyncId)) {
- throw new IllegalArgumentException(
- "insertIntoGroupmembership: GROUP_ID was specified but "
- + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT and GROUP_SYNC_ACCOUNT_TYPE fields "
- + "were also specifid, "
- + values);
- }
- if (0 != DatabaseUtils.longForQuery(getDatabase(),
- "SELECT COUNT(*) FROM groupmembership where group_id=? AND person=?",
- new String[]{String.valueOf(groupId), String.valueOf(personId)})) {
- final String errorMessage =
- "insertIntoGroupmembership: a row with this local key already exists, "
- + values;
- if (Config.LOGD) Log.d(TAG, errorMessage);
- return null;
- }
- }
-
- long rowId = mGroupMembershipInserter.insert(values);
- if (rowId <= 0) {
- final String errorMessage = "insertIntoGroupmembership: the insert failed, values are "
- + values;
- if (Config.LOGD) Log.d(TAG, errorMessage);
- return null;
- }
-
- // set the STARRED column in the people row if this group is the GROUP_ANDROID_STARRED
- if (!isTemporary() && queryGroupMembershipContainsStarred(personId)) {
- fixupPeopleStarred(personId, true);
- }
-
- return ContentUris.withAppendedId(GroupMembership.CONTENT_URI, rowId);
- }
-
- private void fixupGroupMembershipAfterPeopleUpdate(Account account, long personId,
- boolean makeStarred) {
- ContentValues starredGroupInfo = queryAndroidStarredGroupId(account);
- if (makeStarred) {
- if (starredGroupInfo == null) {
- // we need to add the starred group
- mValuesLocal.clear();
- mValuesLocal.put(Groups.NAME, Groups.GROUP_ANDROID_STARRED);
- mValuesLocal.put(Groups._SYNC_DIRTY, 1);
- mValuesLocal.put(Groups._SYNC_ACCOUNT, account == null ? null : account.name);
- mValuesLocal.put(Groups._SYNC_ACCOUNT_TYPE, account == null ? null : account.type);
- long groupId = mGroupsInserter.insert(mValuesLocal);
- starredGroupInfo = new ContentValues();
- starredGroupInfo.put(Groups._ID, groupId);
- starredGroupInfo.put(Groups._SYNC_ACCOUNT,
- mValuesLocal.getAsString(Groups._SYNC_ACCOUNT));
- starredGroupInfo.put(Groups._SYNC_ACCOUNT_TYPE,
- mValuesLocal.getAsString(Groups._SYNC_ACCOUNT_TYPE));
- // don't put the _SYNC_ID in here since we don't know it yet
- }
-
- final Long groupId = starredGroupInfo.getAsLong(Groups._ID);
- final String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
- final String syncAccountName = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
- final String syncAccountType = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT_TYPE);
-
- // check that either groupId is set or the syncId/Account is set
- final boolean hasSyncId = !TextUtils.isEmpty(syncId);
- final boolean hasGroupId = groupId != null;
- if (!hasGroupId && !hasSyncId) {
- throw new IllegalStateException("at least one of the groupId or "
- + "the syncId must be set, " + starredGroupInfo);
- }
-
- // now add this person to the group
- mValuesLocal.clear();
- mValuesLocal.put(GroupMembership.PERSON_ID, personId);
- mValuesLocal.put(GroupMembership.GROUP_ID, groupId);
- mValuesLocal.put(GroupMembership.GROUP_SYNC_ID, syncId);
- mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT, syncAccountName);
- mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, syncAccountType);
- mGroupMembershipInserter.insert(mValuesLocal);
- } else {
- if (starredGroupInfo != null) {
- // delete the groupmembership rows for this person that match the starred group id
- String syncAccountName = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT);
- String syncAccountType = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT_TYPE);
- String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID);
- if (!TextUtils.isEmpty(syncId)) {
- mDb.delete(sGroupmembershipTable,
- "person=? AND group_sync_id=? AND group_sync_account=?"
- + " AND group_sync_account_type=?",
- new String[]{String.valueOf(personId), syncId,
- syncAccountName, syncAccountType});
- } else {
- mDb.delete(sGroupmembershipTable, "person=? AND group_id=?",
- new String[]{
- Long.toString(personId),
- Long.toString(starredGroupInfo.getAsLong(Groups._ID))});
- }
- }
- }
- }
-
- private int fixupPeopleStarred(long personId, boolean inStarredGroup) {
- mValuesLocal.clear();
- mValuesLocal.put(People.STARRED, inStarredGroup ? 1 : 0);
- return getDatabase().update(sPeopleTable, mValuesLocal, WHERE_ID,
- new String[]{String.valueOf(personId)});
- }
-
- private String kindToTable(int kind) {
- switch (kind) {
- case Contacts.KIND_EMAIL: return sContactMethodsTable;
- case Contacts.KIND_POSTAL: return sContactMethodsTable;
- case Contacts.KIND_IM: return sContactMethodsTable;
- case Contacts.KIND_PHONE: return sPhonesTable;
- case Contacts.KIND_ORGANIZATION: return sOrganizationsTable;
- default: throw new IllegalArgumentException("unknown kind, " + kind);
- }
- }
-
- private DatabaseUtils.InsertHelper kindToInserter(int kind) {
- switch (kind) {
- case Contacts.KIND_EMAIL: return mContactMethodsInserter;
- case Contacts.KIND_POSTAL: return mContactMethodsInserter;
- case Contacts.KIND_IM: return mContactMethodsInserter;
- case Contacts.KIND_PHONE: return mPhonesInserter;
- case Contacts.KIND_ORGANIZATION: return mOrganizationsInserter;
- default: throw new IllegalArgumentException("unknown kind, " + kind);
- }
- }
-
- private long insertAndFixupPrimary(int kind, ContentValues values) {
- final String table = kindToTable(kind);
- boolean isPrimary = false;
- Long personId = null;
-
- if (!isTemporary()) {
- // when you add a item, if isPrimary or if there is no primary,
- // make this it, set the isPrimary flag, and clear other primary flags
- isPrimary = values.containsKey("isprimary")
- && (values.getAsInteger("isprimary") != 0);
- personId = values.getAsLong("person");
- if (!isPrimary) {
- // make it primary anyway if this person doesn't have any rows of this type yet
- StringBuilder sb = new StringBuilder("person=" + personId);
- if (sContactMethodsTable.equals(table)) {
- sb.append(" AND kind=");
- sb.append(kind);
- }
- final boolean isFirstRowOfType = DatabaseUtils.longForQuery(getDatabase(),
- "SELECT count(*) FROM " + table + " where " + sb.toString(), null) == 0;
- isPrimary = isFirstRowOfType;
- }
-
- values.put("isprimary", isPrimary ? 1 : 0);
- }
-
- // do the actual insert
- long newRowId = kindToInserter(kind).insert(values);
-
- if (newRowId <= 0) {
- throw new RuntimeException("error while inserting into " + table + ", " + values);
- }
-
- if (!isTemporary()) {
- // If this row was made the primary then clear the other isprimary flags and update
- // corresponding people row, if necessary.
- if (isPrimary) {
- clearOtherIsPrimary(kind, personId, newRowId);
- if (kind == Contacts.KIND_PHONE) {
- updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newRowId);
- } else if (kind == Contacts.KIND_EMAIL) {
- updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newRowId);
- } else if (kind == Contacts.KIND_ORGANIZATION) {
- updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newRowId);
- }
- }
- }
-
- return newRowId;
- }
-
- @Override
- public int deleteInternal(Uri url, String userWhere, String[] whereArgs) {
- String tableToChange;
- String changedItemId;
-
- final int matchedUriId = sURIMatcher.match(url);
- switch (matchedUriId) {
- case GROUPMEMBERSHIP_ID:
- return deleteFromGroupMembership(Long.parseLong(url.getPathSegments().get(1)),
- userWhere, whereArgs);
- case GROUPS:
- return deleteFromGroups(userWhere, whereArgs);
- case GROUPS_ID:
- changedItemId = url.getPathSegments().get(1);
- return deleteFromGroups(addIdToWhereClause(changedItemId, userWhere), whereArgs);
- case EXTENSIONS:
- tableToChange = sExtensionsTable;
- changedItemId = null;
- break;
- case EXTENSIONS_ID:
- tableToChange = sExtensionsTable;
- changedItemId = url.getPathSegments().get(1);
- break;
- case PEOPLE_RAW:
- case PEOPLE:
- return deleteFromPeople(null, userWhere, whereArgs);
- case PEOPLE_ID:
- return deleteFromPeople(url.getPathSegments().get(1), userWhere, whereArgs);
- case PEOPLE_PHONES_ID:
- tableToChange = sPhonesTable;
- changedItemId = url.getPathSegments().get(3);
- break;
- case PEOPLE_CONTACTMETHODS_ID:
- tableToChange = sContactMethodsTable;
- changedItemId = url.getPathSegments().get(3);
- break;
- case PHONES_ID:
- tableToChange = sPhonesTable;
- changedItemId = url.getPathSegments().get(1);
- break;
- case ORGANIZATIONS_ID:
- tableToChange = sOrganizationsTable;
- changedItemId = url.getPathSegments().get(1);
- break;
- case CONTACTMETHODS_ID:
- tableToChange = sContactMethodsTable;
- changedItemId = url.getPathSegments().get(1);
- break;
- case PRESENCE:
- tableToChange = "presence";
- changedItemId = null;
- break;
- case CALLS:
- tableToChange = "calls";
- changedItemId = null;
- break;
- default:
- throw new UnsupportedOperationException("Cannot delete that URL: " + url);
- }
-
- String where = addIdToWhereClause(changedItemId, userWhere);
- IsPrimaryInfo oldPrimaryInfo = null;
- switch (matchedUriId) {
- case PEOPLE_PHONES_ID:
- case PHONES_ID:
- case ORGANIZATIONS_ID:
- oldPrimaryInfo = lookupIsPrimaryInfo(tableToChange,
- sIsPrimaryProjectionWithoutKind, where, whereArgs);
- break;
-
- case PEOPLE_CONTACTMETHODS_ID:
- case CONTACTMETHODS_ID:
- oldPrimaryInfo = lookupIsPrimaryInfo(tableToChange,
- sIsPrimaryProjectionWithKind, where, whereArgs);
- break;
- }
-
- final SQLiteDatabase db = getDatabase();
- int count = db.delete(tableToChange, where, whereArgs);
- if (count > 0) {
- if (oldPrimaryInfo != null && oldPrimaryInfo.isPrimary) {
- fixupPrimaryAfterDelete(oldPrimaryInfo.kind,
- oldPrimaryInfo.id, oldPrimaryInfo.person);
- }
- }
-
- return count;
- }
-
- @Override
- public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
- int match = sURIMatcher.match(uri);
- switch (match) {
- case PEOPLE_PHOTO_DATA:
- if (!"r".equals(mode)) {
- throw new FileNotFoundException("Mode " + mode + " not supported.");
- }
- String person = uri.getPathSegments().get(1);
- String sql = "SELECT " + Photos.DATA + " FROM " + sPhotosTable
- + " WHERE " + Photos.PERSON_ID + "=?";
- String[] selectionArgs = { person };
- return SQLiteContentHelper.getBlobColumnAsAssetFile(getDatabase(), sql,
- selectionArgs);
- default:
- throw new FileNotFoundException("No file at: " + uri);
- }
- }
-
- @Override
- public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
- int match = sURIMatcher.match(uri);
- switch (match) {
- default:
- throw new UnsupportedOperationException(uri.toString());
- }
- }
-
- private int deleteFromGroupMembership(long rowId, String where, String[] whereArgs) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables("groups, groupmembership");
- qb.setProjectionMap(sGroupMembershipProjectionMap);
- qb.appendWhere(sGroupsJoinString);
- qb.appendWhere(" AND groupmembership._id=" + rowId);
- Cursor cursor = qb.query(getDatabase(), null, where, whereArgs, null, null, null);
- try {
- final int indexPersonId = cursor.getColumnIndexOrThrow(GroupMembership.PERSON_ID);
- final int indexName = cursor.getColumnIndexOrThrow(GroupMembership.NAME);
- while (cursor.moveToNext()) {
- if (Groups.GROUP_ANDROID_STARRED.equals(cursor.getString(indexName))) {
- fixupPeopleStarred(cursor.getLong(indexPersonId), false);
- }
- }
- } finally {
- cursor.close();
- }
-
- return mDb.delete(sGroupmembershipTable,
- addIdToWhereClause(String.valueOf(rowId), where),
- whereArgs);
- }
-
- private int deleteFromPeople(String rowId, String where, String[] whereArgs) {
- final SQLiteDatabase db = getDatabase();
- where = addIdToWhereClause(rowId, where);
- Cursor cursor = db.query(sPeopleTable, null, where, whereArgs, null, null, null);
- try {
- final int idxSyncId = cursor.getColumnIndexOrThrow(People._SYNC_ID);
- final int idxSyncAccountName = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
- final int idxSyncAccountType = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT_TYPE);
- final int idxSyncVersion = cursor.getColumnIndexOrThrow(People._SYNC_VERSION);
- final int dstIdxSyncId = mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ID);
- final int dstIdxSyncAccountName =
- mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT);
- final int dstIdxSyncAccountType =
- mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT_TYPE);
- final int dstIdxSyncVersion =
- mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_VERSION);
- while (cursor.moveToNext()) {
- final String syncId = cursor.getString(idxSyncId);
- if (TextUtils.isEmpty(syncId)) continue;
- // insert into deleted table
- mDeletedPeopleInserter.prepareForInsert();
- mDeletedPeopleInserter.bind(dstIdxSyncId, syncId);
- mDeletedPeopleInserter.bind(dstIdxSyncAccountName,
- cursor.getString(idxSyncAccountName));
- mDeletedPeopleInserter.bind(dstIdxSyncAccountType,
- cursor.getString(idxSyncAccountType));
- mDeletedPeopleInserter.bind(dstIdxSyncVersion, cursor.getString(idxSyncVersion));
- mDeletedPeopleInserter.execute();
- }
- } finally {
- cursor.close();
- }
-
- // perform the actual delete
- return db.delete(sPeopleTable, where, whereArgs);
- }
-
- private int deleteFromGroups(String where, String[] whereArgs) {
- HashSet<Account> modifiedAccounts = Sets.newHashSet();
- Cursor cursor = getDatabase().query(sGroupsTable, null, where, whereArgs,
- null, null, null);
- try {
- final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME);
- final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT);
- final int indexSyncAccountType =
- cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE);
- final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
- final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
- final int indexShouldSync = cursor.getColumnIndexOrThrow(Groups.SHOULD_SYNC);
- while (cursor.moveToNext()) {
- String oldName = cursor.getString(indexName);
- String syncAccountName = cursor.getString(indexSyncAccount);
- String syncAccountType = cursor.getString(indexSyncAccountType);
- String syncId = cursor.getString(indexSyncId);
- boolean shouldSync = cursor.getLong(indexShouldSync) != 0;
- long id = cursor.getLong(indexId);
- fixupPeopleStarredOnGroupRename(oldName, null, id);
- if (!TextUtils.isEmpty(syncAccountName) && !TextUtils.isEmpty(syncId)) {
- fixupPeopleStarredOnGroupRename(oldName, null,
- new Account(syncAccountName, syncAccountType), syncId);
- }
- if (!TextUtils.isEmpty(syncAccountName) && shouldSync) {
- modifiedAccounts.add(new Account(syncAccountName, syncAccountType));
- }
- }
- } finally {
- cursor.close();
- }
-
- int numRows = mDb.delete(sGroupsTable, where, whereArgs);
- if (numRows > 0) {
- if (!isTemporary()) {
- final ContentResolver cr = getContext().getContentResolver();
- for (Account account : modifiedAccounts) {
- onLocalChangesForAccount(cr, account, true);
- }
- }
- }
- return numRows;
- }
-
- /**
- * Called when local changes are made, so subclasses have
- * an opportunity to react as they see fit.
- *
- * @param resolver the content resolver to use
- * @param account the account the changes are tied to
- */
- protected void onLocalChangesForAccount(final ContentResolver resolver, Account account,
- boolean groupsModified) {
- // Do nothing
- }
-
- private void fixupPrimaryAfterDelete(int kind, Long itemId, Long personId) {
- final String table = kindToTable(kind);
- // when you delete an item with isPrimary,
- // select a new one as isPrimary and clear the primary if no more items
- Long newPrimaryId = findNewPrimary(kind, personId, itemId);
-
- // we found a new primary, set its isprimary flag
- if (newPrimaryId != null) {
- mValuesLocal.clear();
- mValuesLocal.put("isprimary", 1);
- if (getDatabase().update(table, mValuesLocal, "_id=" + newPrimaryId, null) != 1) {
- throw new RuntimeException("error updating " + table + ", _id "
- + newPrimaryId + ", values " + mValuesLocal);
- }
- }
-
- // if this kind's primary status should be reflected in the people row, update it
- if (kind == Contacts.KIND_PHONE) {
- updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newPrimaryId);
- } else if (kind == Contacts.KIND_EMAIL) {
- updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newPrimaryId);
- } else if (kind == Contacts.KIND_ORGANIZATION) {
- updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newPrimaryId);
- }
- }
-
- @Override
- public int updateInternal(Uri url, ContentValues values, String userWhere, String[] whereArgs) {
- final SQLiteDatabase db = getDatabase();
- String tableToChange;
- String changedItemId;
- final int matchedUriId = sURIMatcher.match(url);
- switch (matchedUriId) {
- case GROUPS_ID:
- changedItemId = url.getPathSegments().get(1);
- return updateGroups(values,
- addIdToWhereClause(changedItemId, userWhere), whereArgs);
-
- case PEOPLE_EXTENSIONS_ID:
- tableToChange = sExtensionsTable;
- changedItemId = url.getPathSegments().get(3);
- break;
-
- case EXTENSIONS_ID:
- tableToChange = sExtensionsTable;
- changedItemId = url.getPathSegments().get(1);
- break;
-
- case PEOPLE_UPDATE_CONTACT_TIME:
- if (values.size() != 1 || !values.containsKey(People.LAST_TIME_CONTACTED)) {
- throw new IllegalArgumentException(
- "You may only use " + url + " to update People.LAST_TIME_CONTACTED");
- }
- tableToChange = sPeopleTable;
- changedItemId = url.getPathSegments().get(1);
- break;
-
- case PEOPLE_ID:
- mValues.clear();
- mValues.putAll(values);
- mValues.put(Photos._SYNC_DIRTY, 1);
- values = mValues;
- tableToChange = sPeopleTable;
- changedItemId = url.getPathSegments().get(1);
- break;
-
- case PEOPLE_PHONES_ID:
- tableToChange = sPhonesTable;
- changedItemId = url.getPathSegments().get(3);
- break;
-
- case PEOPLE_CONTACTMETHODS_ID:
- tableToChange = sContactMethodsTable;
- changedItemId = url.getPathSegments().get(3);
- break;
-
- case PHONES_ID:
- tableToChange = sPhonesTable;
- changedItemId = url.getPathSegments().get(1);
- break;
-
- case PEOPLE_PHOTO:
- case PHOTOS_ID:
- mValues.clear();
- mValues.putAll(values);
-
- // The _SYNC_DIRTY flag should only be set if the data was modified and if
- // it isn't already provided.
- if (!mValues.containsKey(Photos._SYNC_DIRTY) && mValues.containsKey(Photos.DATA)) {
- mValues.put(Photos._SYNC_DIRTY, 1);
- }
- StringBuilder where;
- if (matchedUriId == PEOPLE_PHOTO) {
- where = new StringBuilder("_id=" + url.getPathSegments().get(1));
- } else {
- where = new StringBuilder("person=" + url.getPathSegments().get(1));
- }
- if (!TextUtils.isEmpty(userWhere)) {
- where.append(" AND (");
- where.append(userWhere);
- where.append(')');
- }
- return db.update(sPhotosTable, mValues, where.toString(), whereArgs);
-
- case ORGANIZATIONS_ID:
- tableToChange = sOrganizationsTable;
- changedItemId = url.getPathSegments().get(1);
- break;
-
- case CONTACTMETHODS_ID:
- tableToChange = sContactMethodsTable;
- changedItemId = url.getPathSegments().get(1);
- break;
-
- case SETTINGS:
- if (whereArgs != null) {
- throw new IllegalArgumentException(
- "you aren't allowed to specify where args when updating settings");
- }
- if (userWhere != null) {
- throw new IllegalArgumentException(
- "you aren't allowed to specify a where string when updating settings");
- }
- return updateSettings(values);
-
- case CALLS:
- tableToChange = "calls";
- changedItemId = null;
- break;
-
- case CALLS_ID:
- tableToChange = "calls";
- changedItemId = url.getPathSegments().get(1);
- break;
-
- default:
- throw new UnsupportedOperationException("Cannot update URL: " + url);
- }
-
- String where = addIdToWhereClause(changedItemId, userWhere);
- int numRowsUpdated = db.update(tableToChange, values, where, whereArgs);
-
- if (numRowsUpdated > 0 && changedItemId != null) {
- long itemId = Long.parseLong(changedItemId);
- switch (matchedUriId) {
- case ORGANIZATIONS_ID:
- fixupPrimaryAfterUpdate(
- Contacts.KIND_ORGANIZATION, null, itemId,
- values.getAsInteger(Organizations.ISPRIMARY));
- break;
-
- case PHONES_ID:
- case PEOPLE_PHONES_ID:
- fixupPrimaryAfterUpdate(
- Contacts.KIND_PHONE, matchedUriId == PEOPLE_PHONES_ID
- ? Long.parseLong(url.getPathSegments().get(1))
- : null, itemId,
- values.getAsInteger(Phones.ISPRIMARY));
- break;
-
- case CONTACTMETHODS_ID:
- case PEOPLE_CONTACTMETHODS_ID:
- IsPrimaryInfo isPrimaryInfo = lookupIsPrimaryInfo(sContactMethodsTable,
- sIsPrimaryProjectionWithKind, where, whereArgs);
- fixupPrimaryAfterUpdate(
- isPrimaryInfo.kind, isPrimaryInfo.person, itemId,
- values.getAsInteger(ContactMethods.ISPRIMARY));
- break;
-
- case PEOPLE_ID:
- boolean hasStarred = values.containsKey(People.STARRED);
- boolean hasPrimaryPhone = values.containsKey(People.PRIMARY_PHONE_ID);
- boolean hasPrimaryOrganization =
- values.containsKey(People.PRIMARY_ORGANIZATION_ID);
- boolean hasPrimaryEmail = values.containsKey(People.PRIMARY_EMAIL_ID);
- if (hasStarred || hasPrimaryPhone || hasPrimaryOrganization
- || hasPrimaryEmail) {
- Cursor c = mDb.query(sPeopleTable, null,
- where, whereArgs, null, null, null);
- try {
- int indexAccountName = c.getColumnIndexOrThrow(People._SYNC_ACCOUNT);
- int indexAccountType =
- c.getColumnIndexOrThrow(People._SYNC_ACCOUNT_TYPE);
- int indexId = c.getColumnIndexOrThrow(People._ID);
- Long starredValue = values.getAsLong(People.STARRED);
- Long primaryPhone = values.getAsLong(People.PRIMARY_PHONE_ID);
- Long primaryOrganization =
- values.getAsLong(People.PRIMARY_ORGANIZATION_ID);
- Long primaryEmail = values.getAsLong(People.PRIMARY_EMAIL_ID);
- while (c.moveToNext()) {
- final long personId = c.getLong(indexId);
- if (hasStarred) {
- final String accountName = c.getString(indexAccountName);
- final String accountType = c.getString(indexAccountType);
- final Account account =
- (accountName != null || accountType != null)
- ? new Account(accountName, accountType)
- : null;
- fixupGroupMembershipAfterPeopleUpdate(account,
- personId, starredValue != null && starredValue != 0);
- }
-
- if (hasPrimaryPhone) {
- if (primaryPhone == null) {
- throw new IllegalArgumentException(
- "the value of PRIMARY_PHONE_ID must not be null");
- }
- setIsPrimary(Contacts.KIND_PHONE, personId, primaryPhone);
- }
- if (hasPrimaryOrganization) {
- if (primaryOrganization == null) {
- throw new IllegalArgumentException(
- "the value of PRIMARY_ORGANIZATION_ID must "
- + "not be null");
- }
- setIsPrimary(Contacts.KIND_ORGANIZATION, personId,
- primaryOrganization);
- }
- if (hasPrimaryEmail) {
- if (primaryEmail == null) {
- throw new IllegalArgumentException(
- "the value of PRIMARY_EMAIL_ID must not be null");
- }
- setIsPrimary(Contacts.KIND_EMAIL, personId, primaryEmail);
- }
- }
- } finally {
- c.close();
- }
- }
- break;
- }
- }
-
- return numRowsUpdated;
- }
-
- private int updateSettings(ContentValues values) {
- final SQLiteDatabase db = getDatabase();
- final String accountName = values.getAsString(Contacts.Settings._SYNC_ACCOUNT);
- final String accountType = values.getAsString(Contacts.Settings._SYNC_ACCOUNT_TYPE);
- final String key = values.getAsString(Contacts.Settings.KEY);
- if (key == null) {
- throw new IllegalArgumentException("you must specify the key when updating settings");
- }
- Account account = null;
- if (accountName != null || accountType != null) {
- account = new Account(accountName, accountType);
- }
- if (account == null) {
- db.delete(sSettingsTable, "_sync_account IS NULL AND key=?", new String[]{key});
- } else {
- db.delete(sSettingsTable, "_sync_account=? AND _sync_account_type=? AND key=?",
- new String[]{account.name, account.type, key});
- }
- long rowId = db.insert(sSettingsTable, Contacts.Settings.KEY, values);
- if (rowId < 0) {
- throw new SQLException("error updating settings with " + values);
- }
- return 1;
- }
-
- private int updateGroups(ContentValues values, String where, String[] whereArgs) {
- for (Map.Entry<String, Object> entry : values.valueSet()) {
- final String column = entry.getKey();
- if (!Groups.NAME.equals(column) && !Groups.NOTES.equals(column)
- && !Groups.SYSTEM_ID.equals(column) && !Groups.SHOULD_SYNC.equals(column)) {
- throw new IllegalArgumentException(
- "you are not allowed to change column " + column);
- }
- }
-
- Set<Account> modifiedAccounts = Sets.newHashSet();
- final SQLiteDatabase db = getDatabase();
- if (values.containsKey(Groups.NAME) || values.containsKey(Groups.SHOULD_SYNC)) {
- String newName = values.getAsString(Groups.NAME);
- Cursor cursor = db.query(sGroupsTable, null, where, whereArgs, null, null, null);
- try {
- final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME);
- final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT);
- final int indexSyncAccountType =
- cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE);
- final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID);
- final int indexId = cursor.getColumnIndexOrThrow(Groups._ID);
- while (cursor.moveToNext()) {
- String accountName = cursor.getString(indexSyncAccount);
- String accountType = cursor.getString(indexSyncAccountType);
- if (values.containsKey(Groups.NAME)) {
- String oldName = cursor.getString(indexName);
- String syncId = cursor.getString(indexSyncId);
- long id = cursor.getLong(indexId);
- fixupPeopleStarredOnGroupRename(oldName, newName, id);
- if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(syncId)) {
- fixupPeopleStarredOnGroupRename(oldName, newName,
- new Account(accountName, accountType), syncId);
- }
- }
- if (!TextUtils.isEmpty(accountName) && values.containsKey(Groups.SHOULD_SYNC)) {
- modifiedAccounts.add(new Account(accountName, accountType));
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- int numRows = db.update(sGroupsTable, values, where, whereArgs);
- if (numRows > 0) {
- if (!isTemporary()) {
- final ContentResolver cr = getContext().getContentResolver();
- for (Account account : modifiedAccounts) {
- onLocalChangesForAccount(cr, account, true);
- }
- }
- }
- return numRows;
- }
-
- void fixupPeopleStarredOnGroupRename(String oldName, String newName,
- String where, String[] whereArgs) {
- if (TextUtils.equals(oldName, newName)) return;
-
- int starredValue;
- if (Groups.GROUP_ANDROID_STARRED.equals(newName)) {
- starredValue = 1;
- } else if (Groups.GROUP_ANDROID_STARRED.equals(oldName)) {
- starredValue = 0;
- } else {
- return;
- }
-
- getDatabase().execSQL("UPDATE people SET starred=" + starredValue + " WHERE _id in ("
- + "SELECT person "
- + "FROM groups, groupmembership "
- + "WHERE " + where + " AND " + sGroupsJoinString + ")",
- whereArgs);
- }
-
- void fixupPeopleStarredOnGroupRename(String oldName, String newName,
- Account syncAccount, String syncId) {
- fixupPeopleStarredOnGroupRename(oldName, newName,
- "_sync_account=? AND _sync_account_type=? AND _sync_id=?",
- new String[]{syncAccount.name, syncAccount.type, syncId});
- }
-
- void fixupPeopleStarredOnGroupRename(String oldName, String newName, long groupId) {
- fixupPeopleStarredOnGroupRename(oldName, newName, "group_id=?",
- new String[]{String.valueOf(groupId)});
- }
-
- private void fixupPrimaryAfterUpdate(int kind, Long personId, Long changedItemId,
- Integer isPrimaryValue) {
- final String table = kindToTable(kind);
-
- // - when you update isPrimary to true,
- // make the changed item the primary, clear others
- // - when you update isPrimary to false,
- // select a new one as isPrimary, clear the primary if no more phones
- if (isPrimaryValue != null) {
- if (personId == null) {
- personId = lookupPerson(table, changedItemId);
- }
-
- boolean isPrimary = isPrimaryValue != 0;
- Long newPrimary = changedItemId;
- if (!isPrimary) {
- newPrimary = findNewPrimary(kind, personId, changedItemId);
- }
- clearOtherIsPrimary(kind, personId, changedItemId);
-
- if (kind == Contacts.KIND_PHONE) {
- updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newPrimary);
- } else if (kind == Contacts.KIND_EMAIL) {
- updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newPrimary);
- } else if (kind == Contacts.KIND_ORGANIZATION) {
- updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newPrimary);
- }
- }
- }
-
- /**
- * Queries table to find the value of the person column for the row with _id. There must
- * be exactly one row that matches this id.
- * @param table the table to query
- * @param id the id of the row to query
- * @return the value of the person column for the specified row, returned as a String.
- */
- private long lookupPerson(String table, long id) {
- return DatabaseUtils.longForQuery(
- getDatabase(),
- "SELECT person FROM " + table + " where _id=" + id,
- null);
- }
-
- /**
- * Used to pass around information about a row that has the isprimary column.
- */
- private class IsPrimaryInfo {
- boolean isPrimary;
- Long person;
- Long id;
- Integer kind;
- }
-
- /**
- * Queries the table to determine the state of the row's isprimary column and the kind.
- * The where and whereArgs must be sufficient to match either 0 or 1 row.
- * @param table the table of rows to consider, supports "phones" and "contact_methods"
- * @param projection the projection to use to get the columns that pertain to table
- * @param where used in conjunction with the whereArgs to identify the row
- * @param where used in conjunction with the where string to identify the row
- * @return the IsPrimaryInfo about the matched row, or null if no row was matched
- */
- private IsPrimaryInfo lookupIsPrimaryInfo(String table, String[] projection, String where,
- String[] whereArgs) {
- Cursor cursor = getDatabase().query(table, projection, where, whereArgs, null, null, null);
- try {
- if (!(cursor.getCount() <= 1)) {
- throw new IllegalArgumentException("expected only zero or one rows, got "
- + DatabaseUtils.dumpCursorToString(cursor));
- }
- if (!cursor.moveToFirst()) return null;
- IsPrimaryInfo info = new IsPrimaryInfo();
- info.isPrimary = cursor.getInt(0) != 0;
- info.person = cursor.getLong(1);
- info.id = cursor.getLong(2);
- if (projection == sIsPrimaryProjectionWithKind) {
- info.kind = cursor.getInt(3);
- } else {
- if (sPhonesTable.equals(table)) {
- info.kind = Contacts.KIND_PHONE;
- } else if (sOrganizationsTable.equals(table)) {
- info.kind = Contacts.KIND_ORGANIZATION;
- } else {
- throw new IllegalArgumentException("unexpected table, " + table);
- }
- }
- return info;
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Returns the rank of the table-specific type, used when deciding which row
- * should be primary when none are primary. The lower the rank the better the type.
- * @param table supports "phones", "contact_methods" and "organizations"
- * @param type the table-specific type from the TYPE column
- * @return the rank of the table-specific type, the lower the better
- */
- private int getRankOfType(String table, int type) {
- if (table.equals(sPhonesTable)) {
- switch (type) {
- case Contacts.Phones.TYPE_MOBILE: return 0;
- case Contacts.Phones.TYPE_WORK: return 1;
- case Contacts.Phones.TYPE_HOME: return 2;
- case Contacts.Phones.TYPE_PAGER: return 3;
- case Contacts.Phones.TYPE_CUSTOM: return 4;
- case Contacts.Phones.TYPE_OTHER: return 5;
- case Contacts.Phones.TYPE_FAX_WORK: return 6;
- case Contacts.Phones.TYPE_FAX_HOME: return 7;
- default: return 1000;
- }
- }
-
- if (table.equals(sContactMethodsTable)) {
- switch (type) {
- case Contacts.ContactMethods.TYPE_HOME: return 0;
- case Contacts.ContactMethods.TYPE_WORK: return 1;
- case Contacts.ContactMethods.TYPE_CUSTOM: return 2;
- case Contacts.ContactMethods.TYPE_OTHER: return 3;
- default: return 1000;
- }
- }
-
- if (table.equals(sOrganizationsTable)) {
- switch (type) {
- case Organizations.TYPE_WORK: return 0;
- case Organizations.TYPE_CUSTOM: return 1;
- case Organizations.TYPE_OTHER: return 2;
- default: return 1000;
- }
- }
-
- throw new IllegalArgumentException("unexpected table, " + table);
- }
-
- /**
- * Determines which of the rows in table for the personId should be picked as the primary
- * row based on the rank of the row's type.
- * @param kind the kind of contact
- * @param personId used to limit the rows to those pertaining to this person
- * @param itemId optional, a row to ignore
- * @return the _id of the row that should be the new primary. Is null if there are no
- * matching rows.
- */
- private Long findNewPrimary(int kind, Long personId, Long itemId) {
- final String table = kindToTable(kind);
- if (personId == null) throw new IllegalArgumentException("personId must not be null");
- StringBuilder sb = new StringBuilder();
- sb.append("person=");
- sb.append(personId);
- if (itemId != null) {
- sb.append(" and _id!=");
- sb.append(itemId);
- }
- if (sContactMethodsTable.equals(table)) {
- sb.append(" and ");
- sb.append(ContactMethods.KIND);
- sb.append("=");
- sb.append(kind);
- }
-
- Cursor cursor = getDatabase().query(table, ID_TYPE_PROJECTION, sb.toString(),
- null, null, null, null);
- try {
- Long newPrimaryId = null;
- int bestRank = -1;
- while (cursor.moveToNext()) {
- final int rank = getRankOfType(table, cursor.getInt(1));
- if (bestRank == -1 || rank < bestRank) {
- newPrimaryId = cursor.getLong(0);
- bestRank = rank;
- }
- }
- return newPrimaryId;
- } finally {
- cursor.close();
- }
- }
-
- private void setIsPrimary(int kind, long personId, long itemId) {
- final String table = kindToTable(kind);
- StringBuilder sb = new StringBuilder();
- sb.append("person=");
- sb.append(personId);
-
- if (sContactMethodsTable.equals(table)) {
- sb.append(" and ");
- sb.append(ContactMethods.KIND);
- sb.append("=");
- sb.append(kind);
- }
-
- final String where = sb.toString();
- getDatabase().execSQL(
- "UPDATE " + table + " SET isprimary=(_id=" + itemId + ") WHERE " + where);
- }
-
- /**
- * Clears the isprimary flag for all rows other than the itemId.
- * @param kind the kind of item
- * @param personId used to limit the updates to rows pertaining to this person
- * @param itemId which row to leave untouched
- */
- private void clearOtherIsPrimary(int kind, Long personId, Long itemId) {
- final String table = kindToTable(kind);
- if (personId == null) throw new IllegalArgumentException("personId must not be null");
- StringBuilder sb = new StringBuilder();
- sb.append("person=");
- sb.append(personId);
- if (itemId != null) {
- sb.append(" and _id!=");
- sb.append(itemId);
- }
- if (sContactMethodsTable.equals(table)) {
- sb.append(" and ");
- sb.append(ContactMethods.KIND);
- sb.append("=");
- sb.append(kind);
- }
-
- mValuesLocal.clear();
- mValuesLocal.put("isprimary", 0);
- getDatabase().update(table, mValuesLocal, sb.toString(), null);
- }
-
- /**
- * Set the specified primary column for the person. This is used to make the people
- * row reflect the isprimary flag in the people or contactmethods tables, which is
- * authoritative.
- * @param personId the person to modify
- * @param column the name of the primary column (phone or email)
- * @param primaryId the new value to write into the primary column
- */
- private void updatePeoplePrimary(Long personId, String column, Long primaryId) {
- mValuesLocal.clear();
- mValuesLocal.put(column, primaryId);
- getDatabase().update(sPeopleTable, mValuesLocal, "_id=" + personId, null);
- }
-
- private static String addIdToWhereClause(String id, String where) {
- if (id != null) {
- StringBuilder whereSb = new StringBuilder("_id=");
- whereSb.append(id);
- if (!TextUtils.isEmpty(where)) {
- whereSb.append(" AND (");
- whereSb.append(where);
- whereSb.append(')');
- }
- return whereSb.toString();
- } else {
- return where;
- }
- }
-
- private boolean queryGroupMembershipContainsStarred(long personId) {
- // TODO: Part 1 of 2 part hack to work around a bug in reusing SQLiteStatements
- SQLiteStatement mGroupsMembershipQuery = null;
-
- if (mGroupsMembershipQuery == null) {
- String query =
- "SELECT COUNT(*) FROM groups, groupmembership WHERE "
- + sGroupsJoinString + " AND person=? AND groups.name=?";
- mGroupsMembershipQuery = getDatabase().compileStatement(query);
- }
- long result = DatabaseUtils.longForQuery(mGroupsMembershipQuery,
- new String[]{String.valueOf(personId), Groups.GROUP_ANDROID_STARRED});
-
- // TODO: Part 2 of 2 part hack to work around a bug in reusing SQLiteStatements
- mGroupsMembershipQuery.close();
-
- return result != 0;
- }
-
- @Override
- public boolean changeRequiresLocalSync(Uri uri) {
- final int match = sURIMatcher.match(uri);
- switch (match) {
- // Changes to these URIs cannot cause syncable data to be changed, so don't
- // bother trying to sync them.
- case CALLS:
- case CALLS_FILTER:
- case CALLS_ID:
- case PRESENCE:
- case PRESENCE_ID:
- case PEOPLE_UPDATE_CONTACT_TIME:
- return false;
-
- default:
- return true;
- }
- }
-
- @Override
- protected Iterable<? extends AbstractTableMerger> getMergers() {
- ArrayList<AbstractTableMerger> list = new ArrayList<AbstractTableMerger> ();
- list.add(new PersonMerger());
- list.add(new GroupMerger());
- list.add(new PhotoMerger());
- return list;
- }
-
- protected static String sPeopleTable = "people";
- protected static Uri sPeopleRawURL = Uri.parse("content://contacts/people/raw/");
- protected static String sDeletedPeopleTable = "_deleted_people";
- protected static Uri sDeletedPeopleURL = Uri.parse("content://contacts/deleted_people/");
- protected static String sGroupsTable = "groups";
- protected static String sSettingsTable = "settings";
- protected static Uri sGroupsURL = Uri.parse("content://contacts/groups/");
- protected static String sDeletedGroupsTable = "_deleted_groups";
- protected static Uri sDeletedGroupsURL =
- Uri.parse("content://contacts/deleted_groups/");
- protected static String sPhonesTable = "phones";
- protected static String sOrganizationsTable = "organizations";
- protected static String sContactMethodsTable = "contact_methods";
- protected static String sGroupmembershipTable = "groupmembership";
- protected static String sPhotosTable = "photos";
- protected static Uri sPhotosURL = Uri.parse("content://contacts/photos/");
- protected static String sExtensionsTable = "extensions";
- protected static String sCallsTable = "calls";
-
- protected class PersonMerger extends AbstractTableMerger
- {
- private ContentValues mValues = new ContentValues();
- Map<String, SQLiteCursor> mCursorMap = Maps.newHashMap();
- public PersonMerger()
- {
- super(getDatabase(),
- sPeopleTable, sPeopleRawURL, sDeletedPeopleTable, sDeletedPeopleURL);
- }
-
- @Override
- protected void notifyChanges() {
- // notify that a change has occurred.
- getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
- null /* observer */, false /* do not sync to network */);
- }
-
- @Override
- public void insertRow(ContentProvider diffs, Cursor diffsCursor) {
- final SQLiteDatabase db = getDatabase();
-
- Long localPrimaryPhoneId = null;
- Long localPrimaryEmailId = null;
- Long localPrimaryOrganizationId = null;
-
- // Copy the person
- mPeopleInserter.prepareForInsert();
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People._SYNC_ID, mPeopleInserter, mIndexPeopleSyncId);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People._SYNC_TIME, mPeopleInserter, mIndexPeopleSyncTime);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People._SYNC_VERSION, mPeopleInserter, mIndexPeopleSyncVersion);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People._SYNC_DIRTY, mPeopleInserter, mIndexPeopleSyncDirty);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People._SYNC_ACCOUNT, mPeopleInserter, mIndexPeopleSyncAccountName);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People._SYNC_ACCOUNT_TYPE, mPeopleInserter, mIndexPeopleSyncAccountType);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People.NAME, mPeopleInserter, mIndexPeopleName);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People.PHONETIC_NAME, mPeopleInserter, mIndexPeoplePhoneticName);
- DatabaseUtils.cursorStringToInsertHelper(diffsCursor,
- People.NOTES, mPeopleInserter, mIndexPeopleNotes);
- long localPersonID = mPeopleInserter.execute();
-
- Cursor c;
- final SQLiteDatabase diffsDb = ((ContactsProvider) diffs).getDatabase();
- long diffsPersonID = diffsCursor.getLong(diffsCursor.getColumnIndexOrThrow(People._ID));
-
- // Copy the Photo info
- c = doSubQuery(diffsDb, sPhotosTable, null, diffsPersonID, null);
- try {
- if (c.moveToNext()) {
- mDb.delete(sPhotosTable, "person=" + localPersonID, null);
- mPhotosInserter.prepareForInsert();
- DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ID,
- mPhotosInserter, mIndexPhotosSyncId);
- DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_TIME,
- mPhotosInserter, mIndexPhotosSyncTime);
- DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_VERSION,
- mPhotosInserter, mIndexPhotosSyncVersion);
- DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ACCOUNT,
- mPhotosInserter, mIndexPhotosSyncAccountName);
- DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ACCOUNT_TYPE,
- mPhotosInserter, mIndexPhotosSyncAccountType);
- DatabaseUtils.cursorStringToInsertHelper(c, Photos.EXISTS_ON_SERVER,
- mPhotosInserter, mIndexPhotosExistsOnServer);
- mPhotosInserter.bind(mIndexPhotosSyncError, (String)null);
- mPhotosInserter.bind(mIndexPhotosSyncDirty, 0);
- mPhotosInserter.bind(mIndexPhotosPersonId, localPersonID);
- mPhotosInserter.execute();
- }
- } finally {
- c.deactivate();
- }
-
- // Copy all phones
- c = doSubQuery(diffsDb, sPhonesTable, null, diffsPersonID, sPhonesTable + "._id");
- if (c != null) {
- Long newPrimaryId = null;
- int bestRank = -1;
- final int labelIndex = c.getColumnIndexOrThrow(Phones.LABEL);
- final int typeIndex = c.getColumnIndexOrThrow(Phones.TYPE);
- final int numberIndex = c.getColumnIndexOrThrow(Phones.NUMBER);
- final int keyIndex = c.getColumnIndexOrThrow(Phones.NUMBER_KEY);
- final int primaryIndex = c.getColumnIndexOrThrow(Phones.ISPRIMARY);
- while(c.moveToNext()) {
- final int type = c.getInt(typeIndex);
- final int isPrimaryValue = c.getInt(primaryIndex);
- mPhonesInserter.prepareForInsert();
- mPhonesInserter.bind(mIndexPhonesPersonId, localPersonID);
- mPhonesInserter.bind(mIndexPhonesLabel, c.getString(labelIndex));
- mPhonesInserter.bind(mIndexPhonesType, type);
- mPhonesInserter.bind(mIndexPhonesNumber, c.getString(numberIndex));
- mPhonesInserter.bind(mIndexPhonesNumberKey, c.getString(keyIndex));
- mPhonesInserter.bind(mIndexPhonesIsPrimary, isPrimaryValue);
- long rowId = mPhonesInserter.execute();
-
- if (isPrimaryValue != 0) {
- if (localPrimaryPhoneId != null) {
- throw new IllegalArgumentException(
- "more than one phone was marked as primary, "
- + DatabaseUtils.dumpCursorToString(c));
- }
- localPrimaryPhoneId = rowId;
- }
-
- if (localPrimaryPhoneId == null) {
- final int rank = getRankOfType(sPhonesTable, type);
- if (bestRank == -1 || rank < bestRank) {
- newPrimaryId = rowId;
- bestRank = rank;
- }
- }
- }
- c.deactivate();
-
- if (localPrimaryPhoneId == null) {
- localPrimaryPhoneId = newPrimaryId;
- }
- }
-
- // Copy all contact_methods
- c = doSubQuery(diffsDb, sContactMethodsTable, null, diffsPersonID,
- sContactMethodsTable + "._id");
- if (c != null) {
- Long newPrimaryId = null;
- int bestRank = -1;
- final int labelIndex = c.getColumnIndexOrThrow(ContactMethods.LABEL);
- final int kindIndex = c.getColumnIndexOrThrow(ContactMethods.KIND);
- final int typeIndex = c.getColumnIndexOrThrow(ContactMethods.TYPE);
- final int dataIndex = c.getColumnIndexOrThrow(ContactMethods.DATA);
- final int auxDataIndex = c.getColumnIndexOrThrow(ContactMethods.AUX_DATA);
- final int primaryIndex = c.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
- while(c.moveToNext()) {
- final int type = c.getInt(typeIndex);
- final int kind = c.getInt(kindIndex);
- final int isPrimaryValue = c.getInt(primaryIndex);
- mContactMethodsInserter.prepareForInsert();
- mContactMethodsInserter.bind(mIndexContactMethodsPersonId, localPersonID);
- mContactMethodsInserter.bind(mIndexContactMethodsLabel,
- c.getString(labelIndex));
- mContactMethodsInserter.bind(mIndexContactMethodsKind, kind);
- mContactMethodsInserter.bind(mIndexContactMethodsType, type);
- mContactMethodsInserter.bind(mIndexContactMethodsData, c.getString(dataIndex));
- mContactMethodsInserter.bind(mIndexContactMethodsAuxData,
- c.getString(auxDataIndex));
- mContactMethodsInserter.bind(mIndexContactMethodsIsPrimary, isPrimaryValue);
- long rowId = mContactMethodsInserter.execute();
- if ((kind == Contacts.KIND_EMAIL) && (isPrimaryValue != 0)) {
- if (localPrimaryEmailId != null) {
- throw new IllegalArgumentException(
- "more than one email was marked as primary, "
- + DatabaseUtils.dumpCursorToString(c));
- }
- localPrimaryEmailId = rowId;
- }
-
- if (localPrimaryEmailId == null) {
- final int rank = getRankOfType(sContactMethodsTable, type);
- if (bestRank == -1 || rank < bestRank) {
- newPrimaryId = rowId;
- bestRank = rank;
- }
- }
- }
- c.deactivate();
-
- if (localPrimaryEmailId == null) {
- localPrimaryEmailId = newPrimaryId;
- }
- }
-
- // Copy all organizations
- c = doSubQuery(diffsDb, sOrganizationsTable, null, diffsPersonID,
- sOrganizationsTable + "._id");
- try {
- Long newPrimaryId = null;
- int bestRank = -1;
- final int labelIndex = c.getColumnIndexOrThrow(Organizations.LABEL);
- final int typeIndex = c.getColumnIndexOrThrow(Organizations.TYPE);
- final int companyIndex = c.getColumnIndexOrThrow(Organizations.COMPANY);
- final int titleIndex = c.getColumnIndexOrThrow(Organizations.TITLE);
- final int primaryIndex = c.getColumnIndexOrThrow(Organizations.ISPRIMARY);
- while(c.moveToNext()) {
- final int type = c.getInt(typeIndex);
- final int isPrimaryValue = c.getInt(primaryIndex);
- mOrganizationsInserter.prepareForInsert();
- mOrganizationsInserter.bind(mIndexOrganizationsPersonId, localPersonID);
- mOrganizationsInserter.bind(mIndexOrganizationsLabel, c.getString(labelIndex));
- mOrganizationsInserter.bind(mIndexOrganizationsType, type);
- mOrganizationsInserter.bind(mIndexOrganizationsCompany,
- c.getString(companyIndex));
- mOrganizationsInserter.bind(mIndexOrganizationsTitle, c.getString(titleIndex));
- mOrganizationsInserter.bind(mIndexOrganizationsIsPrimary, isPrimaryValue);
- long rowId = mOrganizationsInserter.execute();
- if (isPrimaryValue != 0) {
- if (localPrimaryOrganizationId != null) {
- throw new IllegalArgumentException(
- "more than one organization was marked as primary, "
- + DatabaseUtils.dumpCursorToString(c));
- }
- localPrimaryOrganizationId = rowId;
- }
-
- if (localPrimaryOrganizationId == null) {
- final int rank = getRankOfType(sOrganizationsTable, type);
- if (bestRank == -1 || rank < bestRank) {
- newPrimaryId = rowId;
- bestRank = rank;
- }
- }
- }
-
- if (localPrimaryOrganizationId == null) {
- localPrimaryOrganizationId = newPrimaryId;
- }
- } finally {
- c.deactivate();
- }
-
- // Copy all groupmembership rows
- c = doSubQuery(diffsDb, sGroupmembershipTable, null, diffsPersonID,
- sGroupmembershipTable + "._id");
- try {
- final int accountNameIndex =
- c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT);
- final int accountTypeIndex =
- c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
- final int idIndex = c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ID);
- while(c.moveToNext()) {
- mGroupMembershipInserter.prepareForInsert();
- mGroupMembershipInserter.bind(mIndexGroupMembershipPersonId, localPersonID);
- mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccountName,
- c.getString(accountNameIndex));
- mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccountType,
- c.getString(accountTypeIndex));
- mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncId,
- c.getString(idIndex));
- mGroupMembershipInserter.execute();
- }
- } finally {
- c.deactivate();
- }
-
- // Copy all extensions rows
- c = doSubQuery(diffsDb, sExtensionsTable, null, diffsPersonID,
- sExtensionsTable + "._id");
- try {
- final int nameIndex = c.getColumnIndexOrThrow(Extensions.NAME);
- final int valueIndex = c.getColumnIndexOrThrow(Extensions.VALUE);
- while(c.moveToNext()) {
- mExtensionsInserter.prepareForInsert();
- mExtensionsInserter.bind(mIndexExtensionsPersonId, localPersonID);
- mExtensionsInserter.bind(mIndexExtensionsName, c.getString(nameIndex));
- mExtensionsInserter.bind(mIndexExtensionsValue, c.getString(valueIndex));
- mExtensionsInserter.execute();
- }
- } finally {
- c.deactivate();
- }
-
- // Update the _SYNC_DIRTY flag of the person. We have to do this
- // after inserting since the updated of the phones, contact
- // methods and organizations will fire a sql trigger that will
- // cause this flag to be set.
- mValues.clear();
- mValues.put(People._SYNC_DIRTY, 0);
- mValues.put(People.PRIMARY_PHONE_ID, localPrimaryPhoneId);
- mValues.put(People.PRIMARY_EMAIL_ID, localPrimaryEmailId);
- mValues.put(People.PRIMARY_ORGANIZATION_ID, localPrimaryOrganizationId);
- final boolean isStarred = queryGroupMembershipContainsStarred(localPersonID);
- mValues.put(People.STARRED, isStarred ? 1 : 0);
- db.update(mTable, mValues, People._ID + '=' + localPersonID, null);
- }
-
- @Override
- public void updateRow(long localPersonID, ContentProvider diffs, Cursor diffsCursor) {
- updateOrResolveRow(localPersonID, null, diffs, diffsCursor, false);
- }
-
- @Override
- public void resolveRow(long localPersonID, String syncID,
- ContentProvider diffs, Cursor diffsCursor) {
- updateOrResolveRow(localPersonID, syncID, diffs, diffsCursor, true);
- }
-
- protected void updateOrResolveRow(long localPersonID, String syncID,
- ContentProvider diffs, Cursor diffsCursor, boolean conflicts) {
- final SQLiteDatabase db = getDatabase();
- // The local version of localPersonId's record has changed. This
- // person also has a changed record in the diffs. Merge the changes
- // in the following way:
- // - if any fields in the people table changed use the server's
- // version
- // - for phones, emails, addresses, compute the join of all unique
- // subrecords. If any of the subrecords has changes in both
- // places then choose the server version of the subrecord
- //
- // Limitation: deletes of phones, emails, or addresses are ignored
- // when the record has changed on both the client and the server
-
- long diffsPersonID = diffsCursor.getLong(diffsCursor.getColumnIndexOrThrow("_id"));
-
- // Join the server phones, organizations, and contact_methods with the local ones.
- // - Add locally any that exist only on the server.
- // - If the row conflicts, delete locally any that exist only on the client.
- // - If the row doesn't conflict, ignore any that exist only on the client.
- // - Update any that exist in both places.
-
- Map<Integer, Long> primaryLocal = new HashMap<Integer, Long>();
- Map<Integer, Long> primaryDiffs = new HashMap<Integer, Long>();
-
- Cursor cRemote;
- Cursor cLocal;
-
- // Phones
- cRemote = null;
- cLocal = null;
- final SQLiteDatabase diffsDb = ((ContactsProvider) diffs).getDatabase();
- try {
- cLocal = doSubQuery(db, sPhonesTable, null, localPersonID, sPhonesKeyOrderBy);
- cRemote = doSubQuery(diffsDb, sPhonesTable,
- null, diffsPersonID, sPhonesKeyOrderBy);
-
- final int idColLocal = cLocal.getColumnIndexOrThrow(Phones._ID);
- final int isPrimaryColLocal = cLocal.getColumnIndexOrThrow(Phones.ISPRIMARY);
- final int isPrimaryColRemote = cRemote.getColumnIndexOrThrow(Phones.ISPRIMARY);
-
- CursorJoiner joiner =
- new CursorJoiner(cLocal, sPhonesKeyColumns, cRemote, sPhonesKeyColumns);
- for (CursorJoiner.Result joinResult : joiner) {
- switch(joinResult) {
- case LEFT:
- if (!conflicts) {
- db.delete(sPhonesTable,
- Phones._ID + "=" + cLocal.getLong(idColLocal), null);
- } else {
- if (cLocal.getLong(isPrimaryColLocal) != 0) {
- savePrimaryId(primaryLocal, Contacts.KIND_PHONE,
- cLocal.getLong(idColLocal));
- }
- }
- break;
-
- case RIGHT:
- case BOTH:
- mValues.clear();
- DatabaseUtils.cursorIntToContentValues(
- cRemote, Phones.TYPE, mValues);
- DatabaseUtils.cursorStringToContentValues(
- cRemote, Phones.LABEL, mValues);
- DatabaseUtils.cursorStringToContentValues(
- cRemote, Phones.NUMBER, mValues);
- DatabaseUtils.cursorStringToContentValues(
- cRemote, Phones.NUMBER_KEY, mValues);
- DatabaseUtils.cursorIntToContentValues(
- cRemote, Phones.ISPRIMARY, mValues);
-
- long localId;
- if (joinResult == CursorJoiner.Result.RIGHT) {
- mValues.put(Phones.PERSON_ID, localPersonID);
- localId = mPhonesInserter.insert(mValues);
- } else {
- localId = cLocal.getLong(idColLocal);
- db.update(sPhonesTable, mValues, "_id =" + localId, null);
- }
- if (cRemote.getLong(isPrimaryColRemote) != 0) {
- savePrimaryId(primaryDiffs, Contacts.KIND_PHONE, localId);
- }
- break;
- }
- }
- } finally {
- if (cRemote != null) cRemote.deactivate();
- if (cLocal != null) cLocal.deactivate();
- }
-
- // Contact methods
- cRemote = null;
- cLocal = null;
- try {
- cLocal = doSubQuery(db,
- sContactMethodsTable, null, localPersonID, sContactMethodsKeyOrderBy);
- cRemote = doSubQuery(diffsDb,
- sContactMethodsTable, null, diffsPersonID, sContactMethodsKeyOrderBy);
-
- final int idColLocal = cLocal.getColumnIndexOrThrow(ContactMethods._ID);
- final int kindColLocal = cLocal.getColumnIndexOrThrow(ContactMethods.KIND);
- final int kindColRemote = cRemote.getColumnIndexOrThrow(ContactMethods.KIND);
- final int isPrimaryColLocal =
- cLocal.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
- final int isPrimaryColRemote =
- cRemote.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
-
- CursorJoiner joiner = new CursorJoiner(
- cLocal, sContactMethodsKeyColumns, cRemote, sContactMethodsKeyColumns);
- for (CursorJoiner.Result joinResult : joiner) {
- switch(joinResult) {
- case LEFT:
- if (!conflicts) {
- db.delete(sContactMethodsTable, ContactMethods._ID + "="
- + cLocal.getLong(idColLocal), null);
- } else {
- if (cLocal.getLong(isPrimaryColLocal) != 0) {
- savePrimaryId(primaryLocal, cLocal.getInt(kindColLocal),
- cLocal.getLong(idColLocal));
- }
- }
- break;
-
- case RIGHT:
- case BOTH:
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(cRemote,
- ContactMethods.LABEL, mValues);
- DatabaseUtils.cursorIntToContentValues(cRemote,
- ContactMethods.TYPE, mValues);
- DatabaseUtils.cursorIntToContentValues(cRemote,
- ContactMethods.KIND, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- ContactMethods.DATA, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- ContactMethods.AUX_DATA, mValues);
- DatabaseUtils.cursorIntToContentValues(cRemote,
- ContactMethods.ISPRIMARY, mValues);
-
- long localId;
- if (joinResult == CursorJoiner.Result.RIGHT) {
- mValues.put(ContactMethods.PERSON_ID, localPersonID);
- localId = mContactMethodsInserter.insert(mValues);
- } else {
- localId = cLocal.getLong(idColLocal);
- db.update(sContactMethodsTable, mValues, "_id =" + localId, null);
- }
- if (cRemote.getLong(isPrimaryColRemote) != 0) {
- savePrimaryId(primaryDiffs, cRemote.getInt(kindColRemote), localId);
- }
- break;
- }
- }
- } finally {
- if (cRemote != null) cRemote.deactivate();
- if (cLocal != null) cLocal.deactivate();
- }
-
- // Organizations
- cRemote = null;
- cLocal = null;
- try {
- cLocal = doSubQuery(db,
- sOrganizationsTable, null, localPersonID, sOrganizationsKeyOrderBy);
- cRemote = doSubQuery(diffsDb,
- sOrganizationsTable, null, diffsPersonID, sOrganizationsKeyOrderBy);
-
- final int idColLocal = cLocal.getColumnIndexOrThrow(Organizations._ID);
- final int isPrimaryColLocal =
- cLocal.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
- final int isPrimaryColRemote =
- cRemote.getColumnIndexOrThrow(ContactMethods.ISPRIMARY);
- CursorJoiner joiner = new CursorJoiner(
- cLocal, sOrganizationsKeyColumns, cRemote, sOrganizationsKeyColumns);
- for (CursorJoiner.Result joinResult : joiner) {
- switch(joinResult) {
- case LEFT:
- if (!conflicts) {
- db.delete(sOrganizationsTable,
- Phones._ID + "=" + cLocal.getLong(idColLocal), null);
- } else {
- if (cLocal.getLong(isPrimaryColLocal) != 0) {
- savePrimaryId(primaryLocal, Contacts.KIND_ORGANIZATION,
- cLocal.getLong(idColLocal));
- }
- }
- break;
-
- case RIGHT:
- case BOTH:
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(cRemote,
- Organizations.LABEL, mValues);
- DatabaseUtils.cursorIntToContentValues(cRemote,
- Organizations.TYPE, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- Organizations.COMPANY, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- Organizations.TITLE, mValues);
- DatabaseUtils.cursorIntToContentValues(cRemote,
- Organizations.ISPRIMARY, mValues);
- long localId;
- if (joinResult == CursorJoiner.Result.RIGHT) {
- mValues.put(Organizations.PERSON_ID, localPersonID);
- localId = mOrganizationsInserter.insert(mValues);
- } else {
- localId = cLocal.getLong(idColLocal);
- db.update(sOrganizationsTable, mValues,
- "_id =" + localId, null /* whereArgs */);
- }
- if (cRemote.getLong(isPrimaryColRemote) != 0) {
- savePrimaryId(primaryDiffs, Contacts.KIND_ORGANIZATION, localId);
- }
- break;
- }
- }
- } finally {
- if (cRemote != null) cRemote.deactivate();
- if (cLocal != null) cLocal.deactivate();
- }
-
- // Groupmembership
- cRemote = null;
- cLocal = null;
- try {
- cLocal = doSubQuery(db,
- sGroupmembershipTable, null, localPersonID, sGroupmembershipKeyOrderBy);
- cRemote = doSubQuery(diffsDb,
- sGroupmembershipTable, null, diffsPersonID, sGroupmembershipKeyOrderBy);
-
- final int idColLocal = cLocal.getColumnIndexOrThrow(GroupMembership._ID);
- CursorJoiner joiner = new CursorJoiner(
- cLocal, sGroupmembershipKeyColumns, cRemote, sGroupmembershipKeyColumns);
- for (CursorJoiner.Result joinResult : joiner) {
- switch(joinResult) {
- case LEFT:
- if (!conflicts) {
- db.delete(sGroupmembershipTable,
- Phones._ID + "=" + cLocal.getLong(idColLocal), null);
- }
- break;
-
- case RIGHT:
- case BOTH:
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(cRemote,
- GroupMembership.GROUP_SYNC_ACCOUNT, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- GroupMembership.GROUP_SYNC_ID, mValues);
- if (joinResult == CursorJoiner.Result.RIGHT) {
- mValues.put(GroupMembership.PERSON_ID, localPersonID);
- mGroupMembershipInserter.insert(mValues);
- } else {
- db.update(sGroupmembershipTable, mValues,
- "_id =" + cLocal.getLong(idColLocal), null /* whereArgs */);
- }
- break;
- }
- }
- } finally {
- if (cRemote != null) cRemote.deactivate();
- if (cLocal != null) cLocal.deactivate();
- }
-
- // Extensions
- cRemote = null;
- cLocal = null;
- try {
- cLocal = doSubQuery(db,
- sExtensionsTable, null, localPersonID, Extensions.NAME);
- cRemote = doSubQuery(diffsDb,
- sExtensionsTable, null, diffsPersonID, Extensions.NAME);
-
- final int idColLocal = cLocal.getColumnIndexOrThrow(Extensions._ID);
- CursorJoiner joiner = new CursorJoiner(
- cLocal, sExtensionsKeyColumns, cRemote, sExtensionsKeyColumns);
- for (CursorJoiner.Result joinResult : joiner) {
- switch(joinResult) {
- case LEFT:
- if (!conflicts) {
- db.delete(sExtensionsTable,
- Phones._ID + "=" + cLocal.getLong(idColLocal), null);
- }
- break;
-
- case RIGHT:
- case BOTH:
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(cRemote,
- Extensions.NAME, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- Extensions.VALUE, mValues);
- if (joinResult == CursorJoiner.Result.RIGHT) {
- mValues.put(Extensions.PERSON_ID, localPersonID);
- mExtensionsInserter.insert(mValues);
- } else {
- db.update(sExtensionsTable, mValues,
- "_id =" + cLocal.getLong(idColLocal), null /* whereArgs */);
- }
- break;
- }
- }
- } finally {
- if (cRemote != null) cRemote.deactivate();
- if (cLocal != null) cLocal.deactivate();
- }
-
- // Copy the Photo's server id and account so that the merger will find it
- cRemote = doSubQuery(diffsDb, sPhotosTable, null, diffsPersonID, null);
- try {
- if(cRemote.moveToNext()) {
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(cRemote, Photos._SYNC_ID, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- Photos._SYNC_ACCOUNT, mValues);
- DatabaseUtils.cursorStringToContentValues(cRemote,
- Photos._SYNC_ACCOUNT_TYPE, mValues);
- db.update(sPhotosTable, mValues, Photos.PERSON_ID + '=' + localPersonID, null);
- }
- } finally {
- cRemote.deactivate();
- }
-
- // make sure there is exactly one primary set for each of these types
- Long primaryPhoneId = setSinglePrimary(
- primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_PHONE);
-
- Long primaryEmailId = setSinglePrimary(
- primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_EMAIL);
-
- Long primaryOrganizationId = setSinglePrimary(
- primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_ORGANIZATION);
-
- setSinglePrimary(primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_IM);
-
- setSinglePrimary(primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_POSTAL);
-
- // Update the person
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ID, mValues);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_TIME, mValues);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_VERSION, mValues);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ACCOUNT, mValues);
- DatabaseUtils.cursorStringToContentValues(diffsCursor,
- People._SYNC_ACCOUNT_TYPE, mValues);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NAME, mValues);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, People.PHONETIC_NAME, mValues);
- DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NOTES, mValues);
- mValues.put(People.PRIMARY_PHONE_ID, primaryPhoneId);
- mValues.put(People.PRIMARY_EMAIL_ID, primaryEmailId);
- mValues.put(People.PRIMARY_ORGANIZATION_ID, primaryOrganizationId);
- final boolean isStarred = queryGroupMembershipContainsStarred(localPersonID);
- mValues.put(People.STARRED, isStarred ? 1 : 0);
- mValues.put(People._SYNC_DIRTY, conflicts ? 1 : 0);
- db.update(mTable, mValues, People._ID + '=' + localPersonID, null);
- }
-
- private void savePrimaryId(Map<Integer, Long> primaryDiffs, Integer kind, long localId) {
- if (primaryDiffs.containsKey(kind)) {
- throw new IllegalArgumentException("more than one of kind "
- + kind + " was marked as primary");
- }
- primaryDiffs.put(kind, localId);
- }
-
- private Long setSinglePrimary(
- Map<Integer, Long> diffsMap,
- Map<Integer, Long> localMap,
- long localPersonID, int kind) {
- Long primaryId = diffsMap.containsKey(kind) ? diffsMap.get(kind) : null;
- if (primaryId == null) {
- primaryId = localMap.containsKey(kind) ? localMap.get(kind) : null;
- }
- if (primaryId == null) {
- primaryId = findNewPrimary(kind, localPersonID, null);
- }
- clearOtherIsPrimary(kind, localPersonID, primaryId);
- return primaryId;
- }
-
- /**
- * Returns a cursor on the specified table that selects rows where
- * the "person" column is equal to the personId parameter. The cursor
- * is also saved and may be returned in future calls where db and table
- * parameter are the same. In that case the projection and orderBy parameters
- * are ignored, so one must take care to not change those parameters across
- * multiple calls to the same db/table.
- * <p>
- * Since the cursor may be saced by this call, the caller must be sure to not
- * close the cursor, though they still must deactivate it when they are done
- * with it.
- */
- private Cursor doSubQuery(SQLiteDatabase db, String table, String[] projection,
- long personId, String orderBy) {
- final String[] selectArgs = new String[]{Long.toString(personId)};
- final String key = (db == getDatabase() ? "local_" : "remote_") + table;
- SQLiteCursor cursor = mCursorMap.get(key);
-
- // don't use the cached cursor if it is from a different DB
- if (cursor != null && cursor.getDatabase() != db) {
- cursor.close();
- cursor = null;
- }
-
- // If we can't find a cached cursor then create a new one and add it to the cache.
- // Otherwise just change the selection arguments and requery it.
- if (cursor == null) {
- cursor = (SQLiteCursor)db.query(table, projection, "person=?", selectArgs,
- null, null, orderBy);
- mCursorMap.put(key, cursor);
- } else {
- cursor.setSelectionArguments(selectArgs);
- cursor.requery();
- }
- return cursor;
- }
- }
-
- protected class GroupMerger extends AbstractTableMerger {
- private ContentValues mValues = new ContentValues();
-
- private static final String UNSYNCED_GROUP_BY_NAME_WHERE_CLAUSE =
- Groups._SYNC_ID + " is null AND "
- + Groups._SYNC_ACCOUNT + " is null AND "
- + Groups.NAME + "=?";
-
- private static final String UNSYNCED_GROUP_BY_SYSTEM_ID_WHERE_CLAUSE =
- Groups._SYNC_ID + " is null AND "
- + Groups._SYNC_ACCOUNT + " is null AND "
- + Groups.SYSTEM_ID + "=?";
-
- public GroupMerger()
- {
- super(getDatabase(), sGroupsTable, sGroupsURL, sDeletedGroupsTable, sDeletedGroupsURL);
- }
-
- @Override
- protected void notifyChanges() {
- // notify that a change has occurred.
- getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
- null /* observer */, false /* do not sync to network */);
- }
-
- @Override
- public void insertRow(ContentProvider diffs, Cursor cursor) {
- // if an unsynced group with this name already exists then update it, otherwise
- // insert a new group
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT_TYPE, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues);
- mValues.put(Groups._SYNC_DIRTY, 0);
-
- final String systemId = mValues.getAsString(Groups.SYSTEM_ID);
- boolean rowUpdated = false;
- if (TextUtils.isEmpty(systemId)) {
- rowUpdated = getDatabase().update(mTable, mValues,
- UNSYNCED_GROUP_BY_NAME_WHERE_CLAUSE,
- new String[]{mValues.getAsString(Groups.NAME)}) > 0;
- } else {
- rowUpdated = getDatabase().update(mTable, mValues,
- UNSYNCED_GROUP_BY_SYSTEM_ID_WHERE_CLAUSE,
- new String[]{systemId}) > 0;
- }
- if (!rowUpdated) {
- mGroupsInserter.insert(mValues);
- } else {
- // We may have just synced the metadata for a groups we previously marked for
- // syncing.
- final ContentResolver cr = getContext().getContentResolver();
- final String accountName = mValues.getAsString(Groups._SYNC_ACCOUNT);
- final String accountType = mValues.getAsString(Groups._SYNC_ACCOUNT_TYPE);
- onLocalChangesForAccount(cr, new Account(accountName, accountType), false);
- }
-
- String oldName = null;
- String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
- String accountName = cursor.getString(
- cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
- String accountType = cursor.getString(
- cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
- String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
- // this must come after the insert, otherwise the join won't work
- fixupPeopleStarredOnGroupRename(oldName, newName, new Account(accountName, accountType),
- syncId);
- }
-
- @Override
- public void updateRow(long localId, ContentProvider diffs, Cursor diffsCursor) {
- updateOrResolveRow(localId, null, diffs, diffsCursor, false);
- }
-
- @Override
- public void resolveRow(long localId, String syncID,
- ContentProvider diffs, Cursor diffsCursor) {
- updateOrResolveRow(localId, syncID, diffs, diffsCursor, true);
- }
-
- protected void updateOrResolveRow(long localRowId, String syncID,
- ContentProvider diffs, Cursor cursor, boolean conflicts) {
- final SQLiteDatabase db = getDatabase();
-
- String oldName = DatabaseUtils.stringForQuery(db,
- "select name from groups where _id=" + localRowId, null);
- String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME));
- String accountName = cursor.getString(
- cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
- String accountType = cursor.getString(
- cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
- String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID));
- // this can come before or after the delete
- fixupPeopleStarredOnGroupRename(oldName, newName,
- new Account(accountName, accountType), syncId);
-
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT_TYPE, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues);
- mValues.put(Groups._SYNC_DIRTY, 0);
- db.update(mTable, mValues, Groups._ID + '=' + localRowId, null);
- }
-
- @Override
- public void deleteRow(Cursor cursor) {
- // we have to read this row from the DB since the projection that is used
- // by cursor doesn't necessarily contain the columns we need
- Cursor c = getDatabase().query(sGroupsTable, null,
- "_id=" + cursor.getLong(cursor.getColumnIndexOrThrow(Groups._ID)),
- null, null, null, null);
- try {
- c.moveToNext();
- String oldName = c.getString(c.getColumnIndexOrThrow(Groups.NAME));
- String newName = null;
- String accountName = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT));
- String accountType = c.getString(
- c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT_TYPE));
- String syncId = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ID));
- String systemId = c.getString(c.getColumnIndexOrThrow(Groups.SYSTEM_ID));
- if (!TextUtils.isEmpty(systemId)) {
- // We don't support deleting of system groups, but due to a server bug they
- // occasionally get sent. Ignore the delete.
- Log.w(TAG, "ignoring a delete for a system group: " +
- DatabaseUtils.dumpCurrentRowToString(c));
- cursor.moveToNext();
- return;
- }
-
- // this must come before the delete, since the join won't work once this row is gone
- fixupPeopleStarredOnGroupRename(oldName, newName,
- new Account(accountName, accountType), syncId);
- } finally {
- c.close();
- }
-
- cursor.deleteRow();
- }
- }
-
- protected class PhotoMerger extends AbstractTableMerger {
- private ContentValues mValues = new ContentValues();
-
- public PhotoMerger() {
- super(getDatabase(), sPhotosTable, sPhotosURL, null, null);
- }
-
- @Override
- protected void notifyChanges() {
- // notify that a change has occurred.
- getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI,
- null /* observer */, false /* do not sync to network */);
- }
-
- @Override
- public void insertRow(ContentProvider diffs, Cursor cursor) {
- // This photo may correspond to a contact that is in the delete table. If so then
- // ignore this insert.
- String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Photos._SYNC_ID));
- boolean contactIsDeleted = DatabaseUtils.longForQuery(getDatabase(),
- "select count(*) from _deleted_people where _sync_id=?",
- new String[]{syncId}) > 0;
- if (contactIsDeleted) {
- return;
- }
-
- throw new UnsupportedOperationException(
- "the photo row is inserted by PersonMerger.insertRow");
- }
-
- @Override
- public void updateRow(long localId, ContentProvider diffs, Cursor diffsCursor) {
- updateOrResolveRow(localId, null, diffs, diffsCursor, false);
- }
-
- @Override
- public void resolveRow(long localId, String syncID,
- ContentProvider diffs, Cursor diffsCursor) {
- updateOrResolveRow(localId, syncID, diffs, diffsCursor, true);
- }
-
- protected void updateOrResolveRow(long localRowId, String syncID,
- ContentProvider diffs, Cursor cursor, boolean conflicts) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "PhotoMerger.updateOrResolveRow: localRowId " + localRowId
- + ", syncId " + syncID + ", conflicts " + conflicts
- + ", server row " + DatabaseUtils.dumpCurrentRowToString(cursor));
- }
- mValues.clear();
- DatabaseUtils.cursorStringToContentValues(cursor, Photos._SYNC_TIME, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Photos._SYNC_VERSION, mValues);
- DatabaseUtils.cursorStringToContentValues(cursor, Photos.EXISTS_ON_SERVER, mValues);
- // reset the error field to allow the phone to attempt to redownload the photo.
- mValues.put(Photos.SYNC_ERROR, (String)null);
-
- // If the photo didn't change locally and the server doesn't have a photo for this
- // contact then delete the local photo.
- long syncDirty = DatabaseUtils.longForQuery(getDatabase(),
- "SELECT _sync_dirty FROM photos WHERE _id=" + localRowId
- + " UNION SELECT 0 AS _sync_dirty ORDER BY _sync_dirty DESC LIMIT 1",
- null);
- if (syncDirty == 0) {
- if (mValues.getAsInteger(Photos.EXISTS_ON_SERVER) == 0) {
- mValues.put(Photos.DATA, (String)null);
- mValues.put(Photos.LOCAL_VERSION, mValues.getAsString(Photos.LOCAL_VERSION));
- }
- // if it does exist on the server then we will attempt to download it later
- }
- // if it does conflict then we will send the client version of the photo to
- // the server later. That will trigger a new sync of the photo data which will
- // cause this method to be called again, at which time the row will no longer
- // conflict. We will then download the photo we just sent to the server and
- // set the LOCAL_VERSION to match the data we just downloaded.
-
- getDatabase().update(mTable, mValues, Photos._ID + '=' + localRowId, null);
- }
-
- @Override
- public void deleteRow(Cursor cursor) {
- // this row is never deleted explicitly, instead it is deleted by a trigger on
- // the people table
- cursor.moveToNext();
- }
- }
-
- private static final String TAG = "ContactsProvider";
-
- /* package private */ static final String DATABASE_NAME = "contacts.db";
- /* package private */ static final int DATABASE_VERSION = 83;
-
- protected static final String CONTACTS_AUTHORITY = "contacts";
- protected static final String CALL_LOG_AUTHORITY = "call_log";
-
- private static final int PEOPLE_BASE = 0;
- private static final int PEOPLE = PEOPLE_BASE;
- private static final int PEOPLE_FILTER = PEOPLE_BASE + 1;
- private static final int PEOPLE_ID = PEOPLE_BASE + 2;
- private static final int PEOPLE_PHONES = PEOPLE_BASE + 3;
- private static final int PEOPLE_PHONES_ID = PEOPLE_BASE + 4;
- private static final int PEOPLE_CONTACTMETHODS = PEOPLE_BASE + 5;
- private static final int PEOPLE_CONTACTMETHODS_ID = PEOPLE_BASE + 6;
- private static final int PEOPLE_RAW = PEOPLE_BASE + 7;
- private static final int PEOPLE_WITH_PHONES_FILTER = PEOPLE_BASE + 8;
- private static final int PEOPLE_STREQUENT = PEOPLE_BASE + 9;
- private static final int PEOPLE_STREQUENT_FILTER = PEOPLE_BASE + 10;
- private static final int PEOPLE_ORGANIZATIONS = PEOPLE_BASE + 11;
- private static final int PEOPLE_ORGANIZATIONS_ID = PEOPLE_BASE + 12;
- private static final int PEOPLE_GROUPMEMBERSHIP = PEOPLE_BASE + 13;
- private static final int PEOPLE_GROUPMEMBERSHIP_ID = PEOPLE_BASE + 14;
- private static final int PEOPLE_PHOTO = PEOPLE_BASE + 15;
- private static final int PEOPLE_EXTENSIONS = PEOPLE_BASE + 16;
- private static final int PEOPLE_EXTENSIONS_ID = PEOPLE_BASE + 17;
- private static final int PEOPLE_CONTACTMETHODS_WITH_PRESENCE = PEOPLE_BASE + 18;
- private static final int PEOPLE_OWNER = PEOPLE_BASE + 19;
- private static final int PEOPLE_UPDATE_CONTACT_TIME = PEOPLE_BASE + 20;
- private static final int PEOPLE_PHONES_WITH_PRESENCE = PEOPLE_BASE + 21;
- private static final int PEOPLE_WITH_EMAIL_OR_IM_FILTER = PEOPLE_BASE + 22;
- private static final int PEOPLE_PHOTO_DATA = PEOPLE_BASE + 23;
-
- private static final int DELETED_BASE = 1000;
- private static final int DELETED_PEOPLE = DELETED_BASE;
- private static final int DELETED_GROUPS = DELETED_BASE + 1;
-
- private static final int PHONES_BASE = 2000;
- private static final int PHONES = PHONES_BASE;
- private static final int PHONES_ID = PHONES_BASE + 1;
- private static final int PHONES_FILTER = PHONES_BASE + 2;
- private static final int PHONES_FILTER_NAME = PHONES_BASE + 3;
- private static final int PHONES_MOBILE_FILTER_NAME = PHONES_BASE + 4;
- private static final int PHONES_WITH_PRESENCE = PHONES_BASE + 5;
-
- private static final int CONTACTMETHODS_BASE = 3000;
- private static final int CONTACTMETHODS = CONTACTMETHODS_BASE;
- private static final int CONTACTMETHODS_ID = CONTACTMETHODS_BASE + 1;
- private static final int CONTACTMETHODS_EMAIL = CONTACTMETHODS_BASE + 2;
- private static final int CONTACTMETHODS_EMAIL_FILTER = CONTACTMETHODS_BASE + 3;
- private static final int CONTACTMETHODS_WITH_PRESENCE = CONTACTMETHODS_BASE + 4;
-
- private static final int CALLS_BASE = 4000;
- private static final int CALLS = CALLS_BASE;
- private static final int CALLS_ID = CALLS_BASE + 1;
- private static final int CALLS_FILTER = CALLS_BASE + 2;
-
- private static final int PRESENCE_BASE = 5000;
- private static final int PRESENCE = PRESENCE_BASE;
- private static final int PRESENCE_ID = PRESENCE_BASE + 1;
-
- private static final int ORGANIZATIONS_BASE = 6000;
- private static final int ORGANIZATIONS = ORGANIZATIONS_BASE;
- private static final int ORGANIZATIONS_ID = ORGANIZATIONS_BASE + 1;
-
- private static final int VOICE_DIALER_TIMESTAMP = 7000;
- private static final int SEARCH_SUGGESTIONS = 7001;
- private static final int SEARCH_SHORTCUT = 7002;
-
- private static final int GROUPS_BASE = 8000;
- private static final int GROUPS = GROUPS_BASE;
- private static final int GROUPS_ID = GROUPS_BASE + 2;
- private static final int GROUP_NAME_MEMBERS = GROUPS_BASE + 3;
- private static final int GROUP_NAME_MEMBERS_FILTER = GROUPS_BASE + 4;
- private static final int GROUP_SYSTEM_ID_MEMBERS = GROUPS_BASE + 5;
- private static final int GROUP_SYSTEM_ID_MEMBERS_FILTER = GROUPS_BASE + 6;
-
- private static final int GROUPMEMBERSHIP_BASE = 9000;
- private static final int GROUPMEMBERSHIP = GROUPMEMBERSHIP_BASE;
- private static final int GROUPMEMBERSHIP_ID = GROUPMEMBERSHIP_BASE + 2;
- private static final int GROUPMEMBERSHIP_RAW = GROUPMEMBERSHIP_BASE + 3;
-
- private static final int PHOTOS_BASE = 10000;
- private static final int PHOTOS = PHOTOS_BASE;
- private static final int PHOTOS_ID = PHOTOS_BASE + 1;
-
- private static final int EXTENSIONS_BASE = 11000;
- private static final int EXTENSIONS = EXTENSIONS_BASE;
- private static final int EXTENSIONS_ID = EXTENSIONS_BASE + 2;
-
- private static final int SETTINGS = 12000;
-
- private static final int LIVE_FOLDERS_BASE = 13000;
- private static final int LIVE_FOLDERS_PEOPLE = LIVE_FOLDERS_BASE + 1;
- private static final int LIVE_FOLDERS_PEOPLE_GROUP_NAME = LIVE_FOLDERS_BASE + 2;
- private static final int LIVE_FOLDERS_PEOPLE_WITH_PHONES = LIVE_FOLDERS_BASE + 3;
- private static final int LIVE_FOLDERS_PEOPLE_FAVORITES = LIVE_FOLDERS_BASE + 4;
-
- private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
-
- private static final HashMap<String, String> sGroupsProjectionMap;
- private static final HashMap<String, String> sPeopleProjectionMap;
- private static final HashMap<String, String> sPeopleWithPhotoProjectionMap;
- private static final HashMap<String, String> sPeopleWithEmailOrImProjectionMap;
- /** Used to force items to the top of a times_contacted list */
- private static final HashMap<String, String> sStrequentStarredProjectionMap;
- private static final HashMap<String, String> sCallsProjectionMap;
- private static final HashMap<String, String> sPhonesProjectionMap;
- private static final HashMap<String, String> sPhonesWithPresenceProjectionMap;
- private static final HashMap<String, String> sContactMethodsProjectionMap;
- private static final HashMap<String, String> sContactMethodsWithPresenceProjectionMap;
- private static final HashMap<String, String> sPresenceProjectionMap;
- private static final HashMap<String, String> sEmailSearchProjectionMap;
- private static final HashMap<String, String> sOrganizationsProjectionMap;
- private static final HashMap<String, String> sSearchSuggestionsProjectionMap;
- private static final HashMap<String, String> sGroupMembershipProjectionMap;
- private static final HashMap<String, String> sPhotosProjectionMap;
- private static final HashMap<String, String> sExtensionsProjectionMap;
- private static final HashMap<String, String> sLiveFoldersProjectionMap;
-
- private static final String sPhonesKeyOrderBy;
- private static final String sContactMethodsKeyOrderBy;
- private static final String sOrganizationsKeyOrderBy;
- private static final String sGroupmembershipKeyOrderBy;
-
- private static final String DISPLAY_NAME_SQL
- = "(CASE WHEN (name IS NOT NULL AND name != '') "
- + "THEN name "
- + "ELSE "
- + "(CASE WHEN primary_organization is NOT NULL THEN "
- + "(SELECT company FROM organizations WHERE "
- + "organizations._id = primary_organization) "
- + "ELSE "
- + "(CASE WHEN primary_phone IS NOT NULL THEN "
- +"(SELECT number FROM phones WHERE phones._id = primary_phone) "
- + "ELSE "
- + "(CASE WHEN primary_email IS NOT NULL THEN "
- + "(SELECT data FROM contact_methods WHERE "
- + "contact_methods._id = primary_email) "
- + "ELSE "
- + "null "
- + "END) "
- + "END) "
- + "END) "
- + "END) ";
-
- private static final String PHONETICALLY_SORTABLE_STRING_SQL =
- "GET_PHONETICALLY_SORTABLE_STRING("
- + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') "
- + "THEN phonetic_name "
- + "ELSE "
- + "(CASE WHEN (name is NOT NULL AND name != '')"
- + "THEN name "
- + "ELSE "
- + "(CASE WHEN primary_email IS NOT NULL THEN "
- + "(SELECT data FROM contact_methods WHERE "
- + "contact_methods._id = primary_email) "
- + "ELSE "
- + "(CASE WHEN primary_phone IS NOT NULL THEN "
- + "(SELECT number FROM phones WHERE phones._id = primary_phone) "
- + "ELSE "
- + "null "
- + "END) "
- + "END) "
- + "END) "
- + "END"
- + ")";
-
- private static final String PRIMARY_ORGANIZATION_WHEN_SQL
- = " WHEN primary_organization is NOT NULL THEN "
- + "(SELECT company FROM organizations WHERE organizations._id = primary_organization)";
-
- private static final String PRIMARY_PHONE_WHEN_SQL
- = " WHEN primary_phone IS NOT NULL THEN "
- + "(SELECT number FROM phones WHERE phones._id = primary_phone)";
-
- private static final String PRIMARY_EMAIL_WHEN_SQL
- = " WHEN primary_email IS NOT NULL THEN "
- + "(SELECT data FROM contact_methods WHERE contact_methods._id = primary_email)";
-
- // The outer CASE is for figuring out what info DISPLAY_NAME_SQL returned.
- // We then pick the next piece of info, to avoid the two lines in the search
- // suggestion being identical.
- private static final String SUGGEST_DESCRIPTION_SQL
- = "(CASE"
- // DISPLAY_NAME_SQL returns name, try org, phone, email
- + " WHEN (name IS NOT NULL AND name != '') THEN "
- + "(CASE"
- + PRIMARY_ORGANIZATION_WHEN_SQL
- + PRIMARY_PHONE_WHEN_SQL
- + PRIMARY_EMAIL_WHEN_SQL
- + " ELSE null END)"
- // DISPLAY_NAME_SQL returns org, try phone, email
- + " WHEN primary_organization is NOT NULL THEN "
- + "(CASE"
- + PRIMARY_PHONE_WHEN_SQL
- + PRIMARY_EMAIL_WHEN_SQL
- + " ELSE null END)"
- // DISPLAY_NAME_SQL returns phone, try email
- + " WHEN primary_phone IS NOT NULL THEN "
- + "(CASE"
- + PRIMARY_EMAIL_WHEN_SQL
- + " ELSE null END)"
- // DISPLAY_NAME_SQL returns email or NULL, return NULL
- + " ELSE null END)";
-
- private static final String PRESENCE_ICON_SQL
- = "(CASE"
- + buildPresenceStatusWhen(People.OFFLINE)
- + buildPresenceStatusWhen(People.INVISIBLE)
- + buildPresenceStatusWhen(People.AWAY)
- + buildPresenceStatusWhen(People.IDLE)
- + buildPresenceStatusWhen(People.DO_NOT_DISTURB)
- + buildPresenceStatusWhen(People.AVAILABLE)
- + " ELSE null END)";
-
- private static String buildPresenceStatusWhen(int status) {
- return " WHEN " + Presence.PRESENCE_STATUS + " = " + status
- + " THEN " + Presence.getPresenceIconResourceId(status);
- }
-
- private static final String[] sPhonesKeyColumns;
- private static final String[] sContactMethodsKeyColumns;
- private static final String[] sOrganizationsKeyColumns;
- private static final String[] sGroupmembershipKeyColumns;
- private static final String[] sExtensionsKeyColumns;
-
- static private String buildOrderBy(String table, String... columns) {
- StringBuilder sb = null;
- for (String column : columns) {
- if (sb == null) {
- sb = new StringBuilder();
- } else {
- sb.append(", ");
- }
- sb.append(table);
- sb.append('.');
- sb.append(column);
- }
- return (sb == null) ? "" : sb.toString();
- }
-
- static {
- // Contacts URI matching table
- UriMatcher matcher = sURIMatcher;
- matcher.addURI(CONTACTS_AUTHORITY, "extensions", EXTENSIONS);
- matcher.addURI(CONTACTS_AUTHORITY, "extensions/#", EXTENSIONS_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "groups", GROUPS);
- matcher.addURI(CONTACTS_AUTHORITY, "groups/#", GROUPS_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "groups/name/*/members", GROUP_NAME_MEMBERS);
- matcher.addURI(CONTACTS_AUTHORITY, "groups/name/*/members/filter/*",
- GROUP_NAME_MEMBERS_FILTER);
- matcher.addURI(CONTACTS_AUTHORITY, "groups/system_id/*/members", GROUP_SYSTEM_ID_MEMBERS);
- matcher.addURI(CONTACTS_AUTHORITY, "groups/system_id/*/members/filter/*",
- GROUP_SYSTEM_ID_MEMBERS_FILTER);
- matcher.addURI(CONTACTS_AUTHORITY, "groupmembership", GROUPMEMBERSHIP);
- matcher.addURI(CONTACTS_AUTHORITY, "groupmembership/#", GROUPMEMBERSHIP_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "groupmembershipraw", GROUPMEMBERSHIP_RAW);
- matcher.addURI(CONTACTS_AUTHORITY, "people", PEOPLE);
- matcher.addURI(CONTACTS_AUTHORITY, "people/strequent", PEOPLE_STREQUENT);
- matcher.addURI(CONTACTS_AUTHORITY, "people/strequent/filter/*", PEOPLE_STREQUENT_FILTER);
- matcher.addURI(CONTACTS_AUTHORITY, "people/filter/*", PEOPLE_FILTER);
- matcher.addURI(CONTACTS_AUTHORITY, "people/with_phones_filter/*",
- PEOPLE_WITH_PHONES_FILTER);
- matcher.addURI(CONTACTS_AUTHORITY, "people/with_email_or_im_filter/*",
- PEOPLE_WITH_EMAIL_OR_IM_FILTER);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#", PEOPLE_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/extensions", PEOPLE_EXTENSIONS);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/extensions/#", PEOPLE_EXTENSIONS_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones", PEOPLE_PHONES);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones_with_presence",
- PEOPLE_PHONES_WITH_PRESENCE);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/photo", PEOPLE_PHOTO);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/photo/data", PEOPLE_PHOTO_DATA);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones/#", PEOPLE_PHONES_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods", PEOPLE_CONTACTMETHODS);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods_with_presence",
- PEOPLE_CONTACTMETHODS_WITH_PRESENCE);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/organizations", PEOPLE_ORGANIZATIONS);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/organizations/#", PEOPLE_ORGANIZATIONS_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/groupmembership", PEOPLE_GROUPMEMBERSHIP);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/groupmembership/#", PEOPLE_GROUPMEMBERSHIP_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "people/raw", PEOPLE_RAW);
- matcher.addURI(CONTACTS_AUTHORITY, "people/owner", PEOPLE_OWNER);
- matcher.addURI(CONTACTS_AUTHORITY, "people/#/update_contact_time",
- PEOPLE_UPDATE_CONTACT_TIME);
- matcher.addURI(CONTACTS_AUTHORITY, "deleted_people", DELETED_PEOPLE);
- matcher.addURI(CONTACTS_AUTHORITY, "deleted_groups", DELETED_GROUPS);
- matcher.addURI(CONTACTS_AUTHORITY, "phones", PHONES);
- matcher.addURI(CONTACTS_AUTHORITY, "phones_with_presence", PHONES_WITH_PRESENCE);
- matcher.addURI(CONTACTS_AUTHORITY, "phones/filter/*", PHONES_FILTER);
- matcher.addURI(CONTACTS_AUTHORITY, "phones/filter_name/*", PHONES_FILTER_NAME);
- matcher.addURI(CONTACTS_AUTHORITY, "phones/mobile_filter_name/*",
- PHONES_MOBILE_FILTER_NAME);
- matcher.addURI(CONTACTS_AUTHORITY, "phones/#", PHONES_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "photos", PHOTOS);
- matcher.addURI(CONTACTS_AUTHORITY, "photos/#", PHOTOS_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "contact_methods", CONTACTMETHODS);
- matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/email", CONTACTMETHODS_EMAIL);
- matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/email/*", CONTACTMETHODS_EMAIL_FILTER);
- matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/#", CONTACTMETHODS_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/with_presence",
- CONTACTMETHODS_WITH_PRESENCE);
- matcher.addURI(CONTACTS_AUTHORITY, "presence", PRESENCE);
- matcher.addURI(CONTACTS_AUTHORITY, "presence/#", PRESENCE_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "organizations", ORGANIZATIONS);
- matcher.addURI(CONTACTS_AUTHORITY, "organizations/#", ORGANIZATIONS_ID);
- matcher.addURI(CONTACTS_AUTHORITY, "voice_dialer_timestamp", VOICE_DIALER_TIMESTAMP);
- matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
- SEARCH_SUGGESTIONS);
- matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
- SEARCH_SUGGESTIONS);
- matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
- SEARCH_SHORTCUT);
- matcher.addURI(CONTACTS_AUTHORITY, "settings", SETTINGS);
-
- matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people", LIVE_FOLDERS_PEOPLE);
- matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people/*",
- LIVE_FOLDERS_PEOPLE_GROUP_NAME);
- matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people_with_phones",
- LIVE_FOLDERS_PEOPLE_WITH_PHONES);
- matcher.addURI(CONTACTS_AUTHORITY, "live_folders/favorites",
- LIVE_FOLDERS_PEOPLE_FAVORITES);
-
- // Call log URI matching table
- matcher.addURI(CALL_LOG_AUTHORITY, "calls", CALLS);
- matcher.addURI(CALL_LOG_AUTHORITY, "calls/filter/*", CALLS_FILTER);
- matcher.addURI(CALL_LOG_AUTHORITY, "calls/#", CALLS_ID);
-
- HashMap<String, String> map;
-
- // Create the common people columns
- HashMap<String, String> peopleColumns = new HashMap<String, String>();
- peopleColumns.put(PeopleColumns.NAME, People.NAME);
- peopleColumns.put(PeopleColumns.NOTES, People.NOTES);
- peopleColumns.put(PeopleColumns.TIMES_CONTACTED, People.TIMES_CONTACTED);
- peopleColumns.put(PeopleColumns.LAST_TIME_CONTACTED, People.LAST_TIME_CONTACTED);
- peopleColumns.put(PeopleColumns.STARRED, People.STARRED);
- peopleColumns.put(PeopleColumns.CUSTOM_RINGTONE, People.CUSTOM_RINGTONE);
- peopleColumns.put(PeopleColumns.SEND_TO_VOICEMAIL, People.SEND_TO_VOICEMAIL);
- peopleColumns.put(PeopleColumns.PHONETIC_NAME, People.PHONETIC_NAME);
- peopleColumns.put(PeopleColumns.DISPLAY_NAME,
- DISPLAY_NAME_SQL + " AS " + People.DISPLAY_NAME);
- peopleColumns.put(PeopleColumns.SORT_STRING,
- PHONETICALLY_SORTABLE_STRING_SQL + " AS " + People.SORT_STRING);
-
- // Create the common groups columns
- HashMap<String, String> groupsColumns = new HashMap<String, String>();
- groupsColumns.put(GroupsColumns.NAME, Groups.NAME);
- groupsColumns.put(GroupsColumns.NOTES, Groups.NOTES);
- groupsColumns.put(GroupsColumns.SYSTEM_ID, Groups.SYSTEM_ID);
- groupsColumns.put(GroupsColumns.SHOULD_SYNC, Groups.SHOULD_SYNC);
-
- // Create the common presence columns
- HashMap<String, String> presenceColumns = new HashMap<String, String>();
- presenceColumns.put(PresenceColumns.IM_PROTOCOL, PresenceColumns.IM_PROTOCOL);
- presenceColumns.put(PresenceColumns.IM_HANDLE, PresenceColumns.IM_HANDLE);
- presenceColumns.put(PresenceColumns.IM_ACCOUNT, PresenceColumns.IM_ACCOUNT);
- presenceColumns.put(PresenceColumns.PRESENCE_STATUS, PresenceColumns.PRESENCE_STATUS);
- presenceColumns.put(PresenceColumns.PRESENCE_CUSTOM_STATUS,
- PresenceColumns.PRESENCE_CUSTOM_STATUS);
-
- // Create the common sync columns
- HashMap<String, String> syncColumns = new HashMap<String, String>();
- syncColumns.put(SyncConstValue._SYNC_ID, SyncConstValue._SYNC_ID);
- syncColumns.put(SyncConstValue._SYNC_TIME, SyncConstValue._SYNC_TIME);
- syncColumns.put(SyncConstValue._SYNC_VERSION, SyncConstValue._SYNC_VERSION);
- syncColumns.put(SyncConstValue._SYNC_LOCAL_ID, SyncConstValue._SYNC_LOCAL_ID);
- syncColumns.put(SyncConstValue._SYNC_DIRTY, SyncConstValue._SYNC_DIRTY);
- syncColumns.put(SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT);
- syncColumns.put(SyncConstValue._SYNC_ACCOUNT_TYPE, SyncConstValue._SYNC_ACCOUNT_TYPE);
-
- // Phones columns
- HashMap<String, String> phonesColumns = new HashMap<String, String>();
- phonesColumns.put(Phones.NUMBER, Phones.NUMBER);
- phonesColumns.put(Phones.NUMBER_KEY, Phones.NUMBER_KEY);
- phonesColumns.put(Phones.TYPE, Phones.TYPE);
- phonesColumns.put(Phones.LABEL, Phones.LABEL);
-
- // People projection map
- map = new HashMap<String, String>();
- map.put(People._ID, "people._id AS " + People._ID);
- peopleColumns.put(People.PRIMARY_PHONE_ID, People.PRIMARY_PHONE_ID);
- peopleColumns.put(People.PRIMARY_EMAIL_ID, People.PRIMARY_EMAIL_ID);
- peopleColumns.put(People.PRIMARY_ORGANIZATION_ID, People.PRIMARY_ORGANIZATION_ID);
- map.putAll(peopleColumns);
- map.putAll(phonesColumns);
- map.putAll(syncColumns);
- map.putAll(presenceColumns);
- sPeopleProjectionMap = map;
-
- // People with photo projection map
- map = new HashMap<String, String>(sPeopleProjectionMap);
- map.put("photo_data", "photos.data AS photo_data");
- sPeopleWithPhotoProjectionMap = map;
-
- // People with E-mail or IM projection map
- map = new HashMap<String, String>();
- map.put(People._ID, "people._id AS " + People._ID);
- map.put(ContactMethods.DATA,
- "contact_methods." + ContactMethods.DATA + " AS " + ContactMethods.DATA);
- map.put(ContactMethods.KIND,
- "contact_methods." + ContactMethods.KIND + " AS " + ContactMethods.KIND);
- map.putAll(peopleColumns);
- sPeopleWithEmailOrImProjectionMap = map;
-
- // Groups projection map
- map = new HashMap<String, String>();
- map.put(Groups._ID, Groups._ID);
- map.putAll(groupsColumns);
- map.putAll(syncColumns);
- sGroupsProjectionMap = map;
-
- // Group Membership projection map
- map = new HashMap<String, String>();
- map.put(GroupMembership._ID, "groupmembership._id AS " + GroupMembership._ID);
- map.put(GroupMembership.PERSON_ID, GroupMembership.PERSON_ID);
- map.put(GroupMembership.GROUP_ID, "groups._id AS " + GroupMembership.GROUP_ID);
- map.put(GroupMembership.GROUP_SYNC_ACCOUNT, GroupMembership.GROUP_SYNC_ACCOUNT);
- map.put(GroupMembership.GROUP_SYNC_ACCOUNT_TYPE, GroupMembership.GROUP_SYNC_ACCOUNT_TYPE);
- map.put(GroupMembership.GROUP_SYNC_ID, GroupMembership.GROUP_SYNC_ID);
- map.putAll(groupsColumns);
- sGroupMembershipProjectionMap = map;
-
- // Use this when you need to force items to the top of a times_contacted list
- map = new HashMap<String, String>(sPeopleProjectionMap);
- map.put(People.TIMES_CONTACTED, Long.MAX_VALUE + " AS " + People.TIMES_CONTACTED);
- map.put("photo_data", "photos.data AS photo_data");
- sStrequentStarredProjectionMap = map;
-
- // Calls projection map
- map = new HashMap<String, String>();
- map.put(Calls._ID, Calls._ID);
- map.put(Calls.NUMBER, Calls.NUMBER);
- map.put(Calls.DATE, Calls.DATE);
- map.put(Calls.DURATION, Calls.DURATION);
- map.put(Calls.TYPE, Calls.TYPE);
- map.put(Calls.NEW, Calls.NEW);
- map.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
- map.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
- map.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
- sCallsProjectionMap = map;
-
- // Phones projection map
- map = new HashMap<String, String>();
- map.put(Phones._ID, "phones._id AS " + Phones._ID);
- map.putAll(phonesColumns);
- map.put(Phones.PERSON_ID, "phones.person AS " + Phones.PERSON_ID);
- map.put(Phones.ISPRIMARY, Phones.ISPRIMARY);
- map.putAll(peopleColumns);
- sPhonesProjectionMap = map;
-
- // Phones with presence projection map
- map = new HashMap<String, String>(sPhonesProjectionMap);
- map.putAll(presenceColumns);
- sPhonesWithPresenceProjectionMap = map;
-
- // Organizations projection map
- map = new HashMap<String, String>();
- map.put(Organizations._ID, "organizations._id AS " + Organizations._ID);
- map.put(Organizations.LABEL, Organizations.LABEL);
- map.put(Organizations.TYPE, Organizations.TYPE);
- map.put(Organizations.PERSON_ID, Organizations.PERSON_ID);
- map.put(Organizations.COMPANY, Organizations.COMPANY);
- map.put(Organizations.TITLE, Organizations.TITLE);
- map.put(Organizations.ISPRIMARY, Organizations.ISPRIMARY);
- sOrganizationsProjectionMap = map;
-
- // Extensions projection map
- map = new HashMap<String, String>();
- map.put(Extensions._ID, Extensions._ID);
- map.put(Extensions.NAME, Extensions.NAME);
- map.put(Extensions.VALUE, Extensions.VALUE);
- map.put(Extensions.PERSON_ID, Extensions.PERSON_ID);
- sExtensionsProjectionMap = map;
-
- // Contact methods projection map
- map = new HashMap<String, String>();
- map.put(ContactMethods._ID, "contact_methods._id AS " + ContactMethods._ID);
- map.put(ContactMethods.KIND, ContactMethods.KIND);
- map.put(ContactMethods.TYPE, ContactMethods.TYPE);
- map.put(ContactMethods.LABEL, ContactMethods.LABEL);
- map.put(ContactMethods.DATA, ContactMethods.DATA);
- map.put(ContactMethods.AUX_DATA, ContactMethods.AUX_DATA);
- map.put(ContactMethods.PERSON_ID, "contact_methods.person AS " + ContactMethods.PERSON_ID);
- map.put(ContactMethods.ISPRIMARY, ContactMethods.ISPRIMARY);
- map.putAll(peopleColumns);
- sContactMethodsProjectionMap = map;
-
- // Contact methods with presence projection map
- map = new HashMap<String, String>(sContactMethodsProjectionMap);
- map.putAll(presenceColumns);
- sContactMethodsWithPresenceProjectionMap = map;
-
- // Email search projection map
- map = new HashMap<String, String>();
- map.put(ContactMethods.NAME, ContactMethods.NAME);
- map.put(ContactMethods.DATA, ContactMethods.DATA);
- map.put(ContactMethods._ID, "contact_methods._id AS " + ContactMethods._ID);
- sEmailSearchProjectionMap = map;
-
- // Presence projection map
- map = new HashMap<String, String>();
- map.put(Presence._ID, "presence._id AS " + Presence._ID);
- map.putAll(presenceColumns);
- map.putAll(peopleColumns);
- sPresenceProjectionMap = map;
-
- // Search suggestions projection map
- map = new HashMap<String, String>();
- map.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
- DISPLAY_NAME_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
- map.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
- SUGGEST_DESCRIPTION_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2);
- map.put(SearchManager.SUGGEST_COLUMN_ICON_1,
- "(CASE WHEN " + Photos.DATA + " IS NOT NULL"
- + " THEN '" + People.CONTENT_URI + "/' || people._id ||"
- + " '/" + Photos.CONTENT_DIRECTORY + "/data'"
- + " ELSE " + com.android.internal.R.drawable.ic_contact_picture
- + " END) AS " + SearchManager.SUGGEST_COLUMN_ICON_1);
- map.put(SearchManager.SUGGEST_COLUMN_ICON_2,
- PRESENCE_ICON_SQL + " AS " + SearchManager.SUGGEST_COLUMN_ICON_2);
- map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
- "people._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
- map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
- "people._id AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
- map.put(People._ID, "people._id AS " + People._ID);
- sSearchSuggestionsProjectionMap = map;
-
- // Photos projection map
- map = new HashMap<String, String>();
- map.put(Photos._ID, Photos._ID);
- map.put(Photos.LOCAL_VERSION, Photos.LOCAL_VERSION);
- map.put(Photos.EXISTS_ON_SERVER, Photos.EXISTS_ON_SERVER);
- map.put(Photos.SYNC_ERROR, Photos.SYNC_ERROR);
- map.put(Photos.PERSON_ID, Photos.PERSON_ID);
- map.put(Photos.DATA, Photos.DATA);
- map.put(Photos.DOWNLOAD_REQUIRED, ""
- + "(exists_on_server!=0 "
- + " AND sync_error IS NULL "
- + " AND (local_version IS NULL OR _sync_version != local_version)) "
- + "AS " + Photos.DOWNLOAD_REQUIRED);
- map.putAll(syncColumns);
- sPhotosProjectionMap = map;
-
- // Live folder projection
- map = new HashMap<String, String>();
- map.put(LiveFolders._ID, "people._id AS " + LiveFolders._ID);
- map.put(LiveFolders.NAME, DISPLAY_NAME_SQL + " AS " + LiveFolders.NAME);
- // TODO: Put contact photo back when we have a way to display a default icon
- // for contacts without a photo
- // map.put(LiveFolders.ICON_BITMAP, Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
- sLiveFoldersProjectionMap = map;
-
- // Order by statements
- sPhonesKeyOrderBy = buildOrderBy(sPhonesTable, Phones.NUMBER);
- sContactMethodsKeyOrderBy = buildOrderBy(sContactMethodsTable,
- ContactMethods.DATA, ContactMethods.KIND);
- sOrganizationsKeyOrderBy = buildOrderBy(sOrganizationsTable, Organizations.COMPANY);
- sGroupmembershipKeyOrderBy =
- buildOrderBy(sGroupmembershipTable, GroupMembership.GROUP_SYNC_ACCOUNT_TYPE,
- GroupMembership.GROUP_SYNC_ACCOUNT);
-
- sPhonesKeyColumns = new String[]{Phones.NUMBER};
- sContactMethodsKeyColumns = new String[]{ContactMethods.DATA, ContactMethods.KIND};
- sOrganizationsKeyColumns = new String[]{Organizations.COMPANY};
- sGroupmembershipKeyColumns = new String[]{GroupMembership.GROUP_SYNC_ACCOUNT,
- GroupMembership.GROUP_SYNC_ACCOUNT_TYPE};
- sExtensionsKeyColumns = new String[]{Extensions.NAME};
-
- String groupJoinByLocalId = "groups._id=groupmembership.group_id";
- String groupJoinByServerId = "("
- + "groups._sync_account=groupmembership.group_sync_account"
- + " AND "
- + "groups._sync_account_type=groupmembership.group_sync_account_type"
- + " AND "
- + "groups._sync_id=groupmembership.group_sync_id"
- + ")";
- sGroupsJoinString = "(" + groupJoinByLocalId + " OR " + groupJoinByServerId + ")";
- }
-}
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 57e1e5d..c95d452 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -24,7 +24,6 @@
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.DisplayNameSources;
import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
@@ -51,8 +50,6 @@
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
-import android.content.Entity;
-import android.content.EntityIterator;
import android.content.IContentService;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
@@ -60,11 +57,12 @@
import android.content.UriMatcher;
import android.content.SharedPreferences.Editor;
import android.content.res.AssetFileDescriptor;
+import android.content.res.Configuration;
+import android.database.CharArrayBuffer;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteContentHelper;
-import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.database.sqlite.SQLiteStatement;
@@ -83,8 +81,11 @@
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.PhoneticNameStyle;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.Settings;
import android.provider.ContactsContract.StatusUpdates;
@@ -110,9 +111,11 @@
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
+import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -156,6 +159,16 @@
"(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
+ Contacts.STARRED + "=1) + 25";
+ /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
+ "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
+ " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
+ " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?";
+
+ /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
+ "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
+ " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
+ " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?";
+
private static final int CONTACTS = 1000;
private static final int CONTACTS_ID = 1001;
private static final int CONTACTS_LOOKUP = 1002;
@@ -214,20 +227,6 @@
private static final int RAW_CONTACT_ENTITIES = 15001;
- private interface ContactsQuery {
- public static final String TABLE = Tables.RAW_CONTACTS;
-
- public static final String[] PROJECTION = new String[] {
- RawContactsColumns.CONCRETE_ID,
- RawContacts.ACCOUNT_NAME,
- RawContacts.ACCOUNT_TYPE,
- };
-
- public static final int RAW_CONTACT_ID = 0;
- public static final int ACCOUNT_NAME = 1;
- public static final int ACCOUNT_TYPE = 2;
- }
-
private interface DataContactsQuery {
public static final String TABLE = "data "
+ "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
@@ -244,22 +243,6 @@
public static final int CONTACT_ID = 2;
}
- private interface DisplayNameQuery {
- public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
-
- public static final String[] COLUMNS = new String[] {
- MimetypesColumns.MIMETYPE,
- Data.IS_PRIMARY,
- Data.DATA1,
- Organization.TITLE,
- };
-
- public static final int MIMETYPE = 0;
- public static final int IS_PRIMARY = 1;
- public static final int DATA = 2;
- public static final int TITLE = 3;
- }
-
private interface DataDeleteQuery {
public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
@@ -295,39 +278,18 @@
}
- private interface NicknameLookupQuery {
- String TABLE = Tables.NICKNAME_LOOKUP;
-
- String[] COLUMNS = new String[] {
- NicknameLookupColumns.CLUSTER
- };
-
- int CLUSTER = 0;
- }
-
private interface RawContactsQuery {
String TABLE = Tables.RAW_CONTACTS;
String[] COLUMNS = new String[] {
- ContactsContract.RawContacts.DELETED
+ RawContacts.DELETED,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.ACCOUNT_NAME,
};
int DELETED = 0;
- }
-
- private static final HashMap<String, Integer> sDisplayNameSources;
- static {
- sDisplayNameSources = new HashMap<String, Integer>();
- sDisplayNameSources.put(StructuredName.CONTENT_ITEM_TYPE,
- DisplayNameSources.STRUCTURED_NAME);
- sDisplayNameSources.put(Nickname.CONTENT_ITEM_TYPE,
- DisplayNameSources.NICKNAME);
- sDisplayNameSources.put(Organization.CONTENT_ITEM_TYPE,
- DisplayNameSources.ORGANIZATION);
- sDisplayNameSources.put(Phone.CONTENT_ITEM_TYPE,
- DisplayNameSources.PHONE);
- sDisplayNameSources.put(Email.CONTENT_ITEM_TYPE,
- DisplayNameSources.EMAIL);
+ int ACCOUNT_TYPE = 1;
+ int ACCOUNT_NAME = 2;
}
public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
@@ -347,6 +309,18 @@
+ " FROM " + Tables.GROUPS
+ " WHERE " + Groups.TITLE + "=?)))";
+ /** Sql for updating DIRTY flag on multiple raw contacts */
+ private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.DIRTY + "=1" +
+ " WHERE " + RawContacts._ID + " IN (";
+
+ /** Sql for updating VERSION on multiple raw contacts */
+ private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
+ " WHERE " + RawContacts._ID + " IN (";
+
/** Contains just BaseColumns._COUNT */
private static final HashMap<String, String> sCountProjectionMap;
/** Contains just the contacts columns */
@@ -379,16 +353,18 @@
/** Contains Live Folders columns */
private static final HashMap<String, String> sLiveFoldersProjectionMap;
+ // where clause to update the status_updates table
+ private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
+ StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
+ " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
+ " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
+
/** Precompiled sql statement for setting a data record to the primary. */
private SQLiteStatement mSetPrimaryStatement;
/** Precompiled sql statement for setting a data record to the super primary. */
private SQLiteStatement mSetSuperPrimaryStatement;
- /** Precompiled sql statement for incrementing times contacted for a contact */
- private SQLiteStatement mContactsLastTimeContactedUpdate;
/** Precompiled sql statement for updating a contact display name */
private SQLiteStatement mRawContactDisplayNameUpdate;
- /** Precompiled sql statement for marking a raw contact as dirty */
- private SQLiteStatement mRawContactDirtyUpdate;
/** Precompiled sql statement for updating an aggregated status update */
private SQLiteStatement mLastStatusUpdate;
private SQLiteStatement mNameLookupInsert;
@@ -398,10 +374,19 @@
private SQLiteStatement mStatusUpdateReplace;
private SQLiteStatement mStatusAttributionUpdate;
private SQLiteStatement mStatusUpdateDelete;
+ private SQLiteStatement mResetNameVerifiedForOtherRawContacts;
private long mMimeTypeIdEmail;
private long mMimeTypeIdIm;
+ private long mMimeTypeIdStructuredName;
+ private long mMimeTypeIdOrganization;
+ private long mMimeTypeIdNickname;
+ private long mMimeTypeIdPhone;
private StringBuilder mSb = new StringBuilder();
+ private String[] mSelectionArgs1 = new String[1];
+ private String[] mSelectionArgs2 = new String[2];
+ private String[] mSelectionArgs3 = new String[3];
+ private Account mAccount;
static {
// Contacts URI matching table
@@ -467,7 +452,7 @@
SEARCH_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
SEARCH_SUGGESTIONS);
- matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
+ matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
SEARCH_SHORTCUT);
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts",
@@ -486,7 +471,14 @@
sContactsProjectionMap = new HashMap<String, String>();
sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
- sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
+ sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY);
+ sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.DISPLAY_NAME_ALTERNATIVE);
+ sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
+ sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
+ sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
+ sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
+ sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
@@ -522,7 +514,7 @@
sContactsVCardProjectionMap = Maps.newHashMap();
sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME
+ " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME);
- sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "0 AS " + OpenableColumns.SIZE);
+ sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE);
sRawContactsProjectionMap = new HashMap<String, String>();
sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
@@ -533,6 +525,22 @@
sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
+ sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY,
+ RawContacts.DISPLAY_NAME_PRIMARY);
+ sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE,
+ RawContacts.DISPLAY_NAME_ALTERNATIVE);
+ sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE,
+ RawContacts.DISPLAY_NAME_SOURCE);
+ sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME,
+ RawContacts.PHONETIC_NAME);
+ sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE,
+ RawContacts.PHONETIC_NAME_STYLE);
+ sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED,
+ RawContacts.NAME_VERIFIED);
+ sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY,
+ RawContacts.SORT_KEY_PRIMARY);
+ sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE,
+ RawContacts.SORT_KEY_ALTERNATIVE);
sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
RawContacts.LAST_TIME_CONTACTED);
@@ -578,8 +586,16 @@
sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
+ sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
+ sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.DISPLAY_NAME_ALTERNATIVE);
+ sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
+ sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
+ sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
+ sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
+ sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
@@ -604,6 +620,7 @@
columns.put(RawContacts.SYNC2, RawContacts.SYNC2);
columns.put(RawContacts.SYNC3, RawContacts.SYNC3);
columns.put(RawContacts.SYNC4, RawContacts.SYNC4);
+ columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
columns.put(Data.MIMETYPE, Data.MIMETYPE);
columns.put(Data.DATA1, Data.DATA1);
@@ -692,6 +709,14 @@
sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
+ sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.DISPLAY_NAME_ALTERNATIVE);
+ sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
+ sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
+ sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
+ sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
+ sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE,
+ Contacts.SORT_KEY_ALTERNATIVE);
sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
@@ -732,28 +757,37 @@
sPhoneLookupProjectionMap = new HashMap<String, String>();
sPhoneLookupProjectionMap.put(PhoneLookup._ID,
- ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID);
+ "contacts_view." + Contacts._ID
+ + " AS " + PhoneLookup._ID);
sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
- Contacts.LOOKUP_KEY + " AS " + PhoneLookup.LOOKUP_KEY);
+ "contacts_view." + Contacts.LOOKUP_KEY
+ + " AS " + PhoneLookup.LOOKUP_KEY);
sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
- ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME);
+ "contacts_view." + Contacts.DISPLAY_NAME
+ + " AS " + PhoneLookup.DISPLAY_NAME);
sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
- ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
+ "contacts_view." + Contacts.LAST_TIME_CONTACTED
+ " AS " + PhoneLookup.LAST_TIME_CONTACTED);
sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
- ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED);
+ "contacts_view." + Contacts.TIMES_CONTACTED
+ + " AS " + PhoneLookup.TIMES_CONTACTED);
sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
- ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED);
+ "contacts_view." + Contacts.STARRED
+ + " AS " + PhoneLookup.STARRED);
sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
- Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
+ "contacts_view." + Contacts.IN_VISIBLE_GROUP
+ + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
- Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID);
+ "contacts_view." + Contacts.PHOTO_ID
+ + " AS " + PhoneLookup.PHOTO_ID);
sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
- ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE);
+ "contacts_view." + Contacts.CUSTOM_RINGTONE
+ + " AS " + PhoneLookup.CUSTOM_RINGTONE);
sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
- Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
+ "contacts_view." + Contacts.HAS_PHONE_NUMBER
+ + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
- ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
+ "contacts_view." + Contacts.SEND_TO_VOICEMAIL
+ " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
@@ -875,6 +909,7 @@
protected final String mMimetype;
protected long mMimetypeId;
+ @SuppressWarnings("all")
public DataRowHandler(String mimetype) {
mMimetype = mimetype;
@@ -933,7 +968,8 @@
}
if (values.size() > 0) {
- mDb.update(Tables.DATA, values, Data._ID + " = " + dataId, null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
}
if (!callerIsSyncAdapter) {
@@ -945,8 +981,10 @@
long dataId = c.getLong(DataDeleteQuery._ID);
long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
- int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
- db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
if (count != 0 && primary) {
fixPrimary(db, rawContactId);
}
@@ -954,16 +992,15 @@
}
private void fixPrimary(SQLiteDatabase db, long rawContactId) {
- long newPrimaryId = findNewPrimaryDataId(db, rawContactId);
- if (newPrimaryId != -1) {
- setIsPrimary(rawContactId, newPrimaryId, getMimeTypeId());
- }
- }
-
- protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) {
+ long mimeTypeId = getMimeTypeId();
long primaryId = -1;
int primaryType = -1;
- Cursor c = queryData(db, rawContactId);
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(DataDeleteQuery.TABLE,
+ DataDeleteQuery.CONCRETE_COLUMNS,
+ Data.RAW_CONTACT_ID + "=?" +
+ " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
+ mSelectionArgs1, null, null, null);
try {
while (c.moveToNext()) {
long dataId = c.getLong(DataDeleteQuery._ID);
@@ -976,7 +1013,9 @@
} finally {
c.close();
}
- return primaryId;
+ if (primaryId != -1) {
+ setIsPrimary(rawContactId, primaryId, mimeTypeId);
+ }
}
/**
@@ -987,50 +1026,10 @@
return 0;
}
- protected Cursor queryData(SQLiteDatabase db, long rawContactId) {
- return db.query(DataDeleteQuery.TABLE, DataDeleteQuery.CONCRETE_COLUMNS,
- Data.RAW_CONTACT_ID + "=" + rawContactId +
- " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
- null, null, null, null);
- }
-
protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
- String bestDisplayName = null;
- int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
-
- Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS,
- Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null);
- try {
- while (c.moveToNext()) {
- String mimeType = c.getString(DisplayNameQuery.MIMETYPE);
-
- // Display name is at DATA1 in all type. This is ensured in the constructor.
- String name = c.getString(DisplayNameQuery.DATA);
- if (TextUtils.isEmpty(name)
- && Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
- name = c.getString(DisplayNameQuery.TITLE);
- }
- boolean primary = StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)
- || (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);
-
- if (name != null) {
- Integer source = sDisplayNameSources.get(mimeType);
- if (source != null
- && (source > bestDisplayNameSource
- || (source == bestDisplayNameSource && primary))) {
- bestDisplayNameSource = source;
- bestDisplayName = name;
- }
- }
- }
-
- } finally {
- c.close();
- }
-
- setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource);
if (!isNewRawContact(rawContactId)) {
- mContactAggregator.updateDisplayName(db, rawContactId);
+ updateRawContactDisplayName(db, rawContactId);
+ mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
}
}
@@ -1045,8 +1044,9 @@
public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
ContentValues update) {
final ContentValues values = new ContentValues();
- final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=" + dataId,
- null, null, null, null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
+ mSelectionArgs1, null, null, null);
try {
if (cursor.moveToFirst()) {
for (int i = 0; i < cursor.getColumnCount(); i++) {
@@ -1152,8 +1152,22 @@
// if there are non-null values, we know for a fact that some values are present.
NameSplitter.Name name = new NameSplitter.Name();
name.fromValues(augmented);
- final String joined = mSplitter.join(name);
+ // As the name could be changed, let's guess the name style again.
+ name.fullNameStyle = FullNameStyle.UNDEFINED;
+ mSplitter.guessNameStyle(name);
+
+ final String joined = mSplitter.join(name, true);
update.put(StructuredName.DISPLAY_NAME, joined);
+
+ update.put(StructuredName.FULL_NAME_STYLE, name.fullNameStyle);
+ update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle);
+ } else if (touchedUnstruct && touchedStruct){
+ if (TextUtils.isEmpty(update.getAsString(StructuredName.FULL_NAME_STYLE))) {
+ update.put(StructuredName.FULL_NAME_STYLE, mSplitter.guessFullNameStyle(unstruct));
+ }
+ if (TextUtils.isEmpty(update.getAsString(StructuredName.PHONETIC_NAME_STYLE))) {
+ update.put(StructuredName.PHONETIC_NAME_STYLE, mSplitter.guessPhoneticNameStyle(unstruct));
+ }
}
}
}
@@ -1491,9 +1505,13 @@
phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
+ phoneValues.put(PhoneLookupColumns.MIN_MATCH,
+ PhoneNumberUtils.toCallerIDMinMatch(number));
+
db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
} else {
- db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=" + dataId, null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1);
}
}
@@ -1573,7 +1591,8 @@
if (containsGroupSourceId) {
final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
- final long groupId = getOrMakeGroup(db, rawContactId, sourceId);
+ final long groupId = getOrMakeGroup(db, rawContactId, sourceId,
+ mInsertedRawContacts.get(rawContactId));
values.remove(GroupMembership.GROUP_SOURCE_ID);
values.put(GroupMembership.GROUP_ROW_ID, groupId);
}
@@ -1622,45 +1641,56 @@
}
}
+ /**
+ * An entry in group id cache. It maps the combination of (account type, account name
+ * and source id) to group row id.
+ */
+ public class GroupIdCacheEntry {
+ String accountType;
+ String accountName;
+ String sourceId;
+ long groupId;
+ }
private HashMap<String, DataRowHandler> mDataRowHandlers;
- private final ContactAggregationScheduler mAggregationScheduler;
private ContactsDatabaseHelper mDbHelper;
private NameSplitter mNameSplitter;
private NameLookupBuilder mNameLookupBuilder;
- private HashMap<String, SoftReference<String[]>> mNicknameClusterCache =
- new HashMap<String, SoftReference<String[]>>();
+
+ // We will use this much memory (in bits) to optimize the nickname cluster lookup
+ private static final int NICKNAME_BLOOM_FILTER_SIZE = 0x1FFF; // =long[128]
+ private BitSet mNicknameBloomFilter;
+
+ private HashMap<String, SoftReference<String[]>> mNicknameClusterCache = Maps.newHashMap();
+
private PostalSplitter mPostalSplitter;
+ // We don't need a soft cache for groups - the assumption is that there will only
+ // be a small number of contact groups. The cache is keyed off source id. The value
+ // is a list of groups with this group id.
+ private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
+
private ContactAggregator mContactAggregator;
private LegacyApiSupport mLegacyApiSupport;
private GlobalSearchSupport mGlobalSearchSupport;
private ContentValues mValues = new ContentValues();
+ private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128);
+ private NameSplitter.Name mName = new NameSplitter.Name();
private volatile CountDownLatch mAccessLatch;
- private boolean mImportMode;
- private HashSet<Long> mInsertedRawContacts = Sets.newHashSet();
+ private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap();
private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
+ private HashSet<Long> mDirtyRawContacts = Sets.newHashSet();
private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();
private boolean mVisibleTouched = false;
private boolean mSyncToNetwork;
- public ContactsProvider2() {
- this(new ContactAggregationScheduler());
- }
-
- /**
- * Constructor for testing.
- */
- /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
- mAggregationScheduler = scheduler;
- }
-
+ private Locale mCurrentLocale;
@Override
public boolean onCreate() {
super.onCreate();
@@ -1677,7 +1707,7 @@
mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
mGlobalSearchSupport = new GlobalSearchSupport(this);
mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
- mContactAggregator = new ContactAggregator(this, mDbHelper, mAggregationScheduler);
+ mContactAggregator = new ContactAggregator(this, mDbHelper);
mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
final SQLiteDatabase db = mDbHelper.getReadableDatabase();
@@ -1700,23 +1730,21 @@
" FROM " + Tables.RAW_CONTACTS +
" WHERE " + RawContacts._ID + "=?))");
- mContactsLastTimeContactedUpdate = db.compileStatement(
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.LAST_TIME_CONTACTED + "=? " +
- "WHERE " + Contacts._ID + "=?");
-
mRawContactDisplayNameUpdate = db.compileStatement(
"UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContactsColumns.DISPLAY_NAME + "=?,"
- + RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" +
+ " SET " +
+ RawContacts.DISPLAY_NAME_SOURCE + "=?," +
+ RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
+ RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
+ RawContacts.PHONETIC_NAME + "=?," +
+ RawContacts.PHONETIC_NAME_STYLE + "=?," +
+ RawContacts.SORT_KEY_PRIMARY + "=?," +
+ RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
" WHERE " + RawContacts._ID + "=?");
- mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
- + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?");
-
mLastStatusUpdate = db.compileStatement(
- "UPDATE " + Tables.CONTACTS
- + " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
"(SELECT " + DataColumns.CONCRETE_ID +
" FROM " + Tables.STATUS_UPDATES +
" JOIN " + Tables.DATA +
@@ -1728,18 +1756,10 @@
" WHERE " + RawContacts.CONTACT_ID + "=?" +
" ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
+ StatusUpdates.STATUS +
- " LIMIT 1)"
- + " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
+ " LIMIT 1)" +
+ " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
- final Locale locale = Locale.getDefault();
- mNameSplitter = new NameSplitter(
- context.getString(com.android.internal.R.string.common_name_prefixes),
- context.getString(com.android.internal.R.string.common_last_name_prefixes),
- context.getString(com.android.internal.R.string.common_name_suffixes),
- context.getString(com.android.internal.R.string.common_name_conjunctions),
- locale);
- mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
- mPostalSplitter = new PostalSplitter(locale);
+ initByLocale(context);
mNameLookupInsert = db.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
+ NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
@@ -1785,6 +1805,17 @@
"DELETE FROM " + Tables.STATUS_UPDATES +
" WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
+ // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0
+ // on all other raw contacts in the same aggregate
+ mResetNameVerifiedForOtherRawContacts = db.compileStatement(
+ "UPDATE " + Tables.RAW_CONTACTS +
+ " SET " + RawContacts.NAME_VERIFIED + "=0" +
+ " WHERE " + RawContacts.CONTACT_ID + "=(" +
+ "SELECT " + RawContacts.CONTACT_ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts._ID + "=?)" +
+ " AND " + RawContacts._ID + "!=?");
+
mDataRowHandlers = new HashMap<String, DataRowHandler>();
mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
@@ -1810,9 +1841,36 @@
mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
+ mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
+ mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
+ mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+ preloadNicknameBloomFilter();
return (db != null);
}
+ private void initByLocale(Context context) {
+ mCurrentLocale = getLocale();
+ if (mCurrentLocale == null) {
+ return;
+ }
+ mNameSplitter = new NameSplitter(
+ context.getString(com.android.internal.R.string.common_name_prefixes),
+ context.getString(com.android.internal.R.string.common_last_name_prefixes),
+ context.getString(com.android.internal.R.string.common_name_suffixes),
+ context.getString(com.android.internal.R.string.common_name_conjunctions),
+ mCurrentLocale);
+ mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
+ mPostalSplitter = new PostalSplitter(mCurrentLocale);
+ }
+
+ @Override
+ public void onConfigurationChanged (Configuration newConfig) {
+ if (newConfig != null && mCurrentLocale != null
+ && mCurrentLocale.equals(newConfig.locale)) {
+ initByLocale(getContext());
+ }
+ }
protected void verifyAccounts() {
AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
@@ -1824,14 +1882,15 @@
return ContactsDatabaseHelper.getInstance(context);
}
- /* package */ ContactAggregationScheduler getContactAggregationScheduler() {
- return mAggregationScheduler;
- }
-
/* package */ NameSplitter getNameSplitter() {
return mNameSplitter;
}
+ /* Visible for testing */
+ protected Locale getLocale() {
+ return Locale.getDefault();
+ }
+
protected boolean isLegacyContactImportNeeded() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION;
@@ -1852,6 +1911,7 @@
@Override
public void run() {
if (importLegacyContacts()) {
+ // TODO aggregate all newly added raw contacts
/*
* When the import process is done, we can unlock the provider and
@@ -1859,7 +1919,6 @@
*/
mAccessLatch.countDown();
mAccessLatch = null;
- scheduleContactAggregation();
}
}
};
@@ -1884,7 +1943,6 @@
/* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
boolean aggregatorEnabled = mContactAggregator.isEnabled();
mContactAggregator.setEnabled(false);
- mImportMode = true;
try {
importer.importContacts();
mContactAggregator.setEnabled(aggregatorEnabled);
@@ -1892,20 +1950,9 @@
} catch (Throwable e) {
Log.e(TAG, "Legacy contact import failed", e);
return false;
- } finally {
- mImportMode = false;
}
}
- @Override
- protected void finalize() throws Throwable {
- if (mContactAggregator != null) {
- mContactAggregator.quit();
- }
-
- super.finalize();
- }
-
/**
* Wipes all data from the contacts database.
*/
@@ -1974,10 +2021,12 @@
mInsertedRawContacts.clear();
mUpdatedRawContacts.clear();
mUpdatedSyncStates.clear();
+ mDirtyRawContacts.clear();
}
@Override
protected void beforeTransactionCommit() {
+
if (VERBOSE_LOGGING) {
Log.v(TAG, "beforeTransactionCommit");
}
@@ -1994,15 +2043,26 @@
if (VERBOSE_LOGGING) {
Log.v(TAG, "flushTransactionChanges");
}
- for (long rawContactId : mInsertedRawContacts) {
- mContactAggregator.insertContact(mDb, rawContactId);
+
+ for (long rawContactId : mInsertedRawContacts.keySet()) {
+ updateRawContactDisplayName(mDb, rawContactId);
+ mContactAggregator.onRawContactInsert(mDb, rawContactId);
}
- String ids;
+ if (!mDirtyRawContacts.isEmpty()) {
+ mSb.setLength(0);
+ mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
+ appendIds(mSb, mDirtyRawContacts);
+ mSb.append(")");
+ mDb.execSQL(mSb.toString());
+ }
+
if (!mUpdatedRawContacts.isEmpty()) {
- ids = buildIdsString(mUpdatedRawContacts);
- mDb.execSQL("UPDATE raw_contacts SET version = version + 1 WHERE _id in " + ids,
- new Object[]{});
+ mSb.setLength(0);
+ mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
+ appendIds(mSb, mUpdatedRawContacts);
+ mSb.append(")");
+ mDb.execSQL(mSb.toString());
}
for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
@@ -2013,19 +2073,16 @@
clearTransactionalChanges();
}
- private String buildIdsString(HashSet<Long> ids) {
- StringBuilder idsBuilder = null;
+ /**
+ * Appends comma separated ids.
+ * @param ids Should not be empty
+ */
+ private void appendIds(StringBuilder sb, HashSet<Long> ids) {
for (long id : ids) {
- if (idsBuilder == null) {
- idsBuilder = new StringBuilder();
- idsBuilder.append("(");
- } else {
- idsBuilder.append(",");
- }
- idsBuilder.append(id);
+ sb.append(id).append(',');
}
- idsBuilder.append(")");
- return idsBuilder.toString();
+
+ sb.setLength(sb.length() - 1); // Yank the last comma
}
@Override
@@ -2039,12 +2096,8 @@
syncToNetwork);
}
- protected void scheduleContactAggregation() {
- mContactAggregator.schedule();
- }
-
private boolean isNewRawContact(long rawContactId) {
- return mInsertedRawContacts.contains(rawContactId);
+ return mInsertedRawContacts.containsKey(rawContactId);
}
private DataRowHandler getDataRowHandler(final String mimeType) {
@@ -2059,7 +2112,7 @@
@Override
protected Uri insertInTransaction(Uri uri, ContentValues values) {
if (VERBOSE_LOGGING) {
- Log.v(TAG, "insertInTransaction: " + uri);
+ Log.v(TAG, "insertInTransaction: " + uri + " " + values);
}
final boolean callerIsSyncAdapter =
@@ -2079,8 +2132,7 @@
}
case RAW_CONTACTS: {
- final Account account = readAccountFromQueryParams(uri);
- id = insertRawContact(values, account);
+ id = insertRawContact(uri, values);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
@@ -2099,8 +2151,7 @@
}
case GROUPS: {
- final Account account = readAccountFromQueryParams(uri);
- id = insertGroup(uri, values, account, callerIsSyncAdapter);
+ id = insertGroup(uri, values, callerIsSyncAdapter);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
@@ -2129,28 +2180,68 @@
}
/**
- * If account is non-null then store it in the values. If the account is already
- * specified in the values then it must be consistent with the account, if it is non-null.
- * @param values the ContentValues to read from and update
- * @param account the explicitly provided Account
- * @return false if the accounts are inconsistent
+ * If account is non-null then store it in the values. If the account is
+ * already specified in the values then it must be consistent with the
+ * account, if it is non-null.
+ *
+ * @param uri Current {@link Uri} being operated on.
+ * @param values {@link ContentValues} to read and possibly update.
+ * @throws IllegalArgumentException when only one of
+ * {@link RawContacts#ACCOUNT_NAME} or
+ * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
+ * other undefined.
+ * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
+ * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
+ * the given {@link Uri} and {@link ContentValues}.
*/
- private boolean resolveAccount(ContentValues values, Account account) {
- // If either is specified then both must be specified.
- final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
- final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
- if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
- final Account valuesAccount = new Account(accountName, accountType);
- if (account != null && !valuesAccount.equals(account)) {
- return false;
+ private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
+ String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+ String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+ final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+
+ String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+ String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+ final boolean partialValues = TextUtils.isEmpty(valueAccountName)
+ ^ TextUtils.isEmpty(valueAccountType);
+
+ if (partialUri || partialValues) {
+ // Throw when either account is incomplete
+ throw new IllegalArgumentException("Must specify both or neither of"
+ + " ACCOUNT_NAME and ACCOUNT_TYPE");
+ }
+
+ // Accounts are valid by only checking one parameter, since we've
+ // already ruled out partial accounts.
+ final boolean validUri = !TextUtils.isEmpty(accountName);
+ final boolean validValues = !TextUtils.isEmpty(valueAccountName);
+
+ if (validValues && validUri) {
+ // Check that accounts match when both present
+ final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
+ && TextUtils.equals(accountType, valueAccountType);
+ if (!accountMatch) {
+ throw new IllegalArgumentException("When both specified, "
+ + " ACCOUNT_NAME and ACCOUNT_TYPE must match");
}
- account = valuesAccount;
+ } else if (validUri) {
+ // Fill values from Uri when not present
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ } else if (validValues) {
+ accountName = valueAccountName;
+ accountType = valueAccountType;
+ } else {
+ return null;
}
- if (account != null) {
- values.put(RawContacts.ACCOUNT_NAME, account.name);
- values.put(RawContacts.ACCOUNT_TYPE, account.type);
+
+ // Use cached Account object when matches, otherwise create
+ if (mAccount == null
+ || !mAccount.name.equals(accountName)
+ || !mAccount.type.equals(accountType)) {
+ mAccount = new Account(accountName, accountType);
}
- return true;
+
+ return mAccount;
}
/**
@@ -2166,29 +2257,28 @@
/**
* Inserts an item in the contacts table
*
- * @param values the values for the new row
- * @param account the account this contact should be associated with. may be null.
+ * @param uri the values for the new row
+ * @param values the account this contact should be associated with. may be null.
* @return the row ID of the newly created row
*/
- private long insertRawContact(ContentValues values, Account account) {
- ContentValues overriddenValues = new ContentValues(values);
- overriddenValues.putNull(RawContacts.CONTACT_ID);
- if (!resolveAccount(overriddenValues, account)) {
- return -1;
- }
+ private long insertRawContact(Uri uri, ContentValues values) {
+ mValues.clear();
+ mValues.putAll(values);
+ mValues.putNull(RawContacts.CONTACT_ID);
+
+ final Account account = resolveAccount(uri, mValues);
if (values.containsKey(RawContacts.DELETED)
&& values.getAsInteger(RawContacts.DELETED) != 0) {
- overriddenValues.put(RawContacts.AGGREGATION_MODE,
- RawContacts.AGGREGATION_MODE_DISABLED);
+ mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
}
- long rawContactId =
- mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
+ long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues);
mContactAggregator.markNewForAggregation(rawContactId);
// Trigger creation of a Contact based on this RawContact at the end of transaction
- mInsertedRawContacts.add(rawContactId);
+ mInsertedRawContacts.put(rawContactId, account);
+
return rawContactId;
}
@@ -2276,35 +2366,60 @@
* @throws IllegalArgumentException if the contact is not associated with an account
* @throws IllegalStateException if a group needs to be created but the creation failed
*/
- private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
- Account account = null;
- Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
- + rawContactId, null, null, null, null);
- try {
- if (c.moveToNext()) {
- final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
- final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
- if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
- account = new Account(accountName, accountType);
+ private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
+ Account account) {
+
+ if (account == null) {
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
+ RawContacts._ID + "=?", mSelectionArgs1, null, null, null);
+ try {
+ if (c.moveToFirst()) {
+ String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME);
+ String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+ account = new Account(accountName, accountType);
+ }
}
+ } finally {
+ c.close();
}
- } finally {
- c.close();
}
+
if (account == null) {
throw new IllegalArgumentException("if the groupmembership only "
- + "has a sourceid the the contact must be associate with "
+ + "has a sourceid the the contact must be associated with "
+ "an account");
}
+ ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
+ if (entries == null) {
+ entries = new ArrayList<GroupIdCacheEntry>(1);
+ mGroupIdCache.put(sourceId, entries);
+ }
+
+ int count = entries.size();
+ for (int i = 0; i < count; i++) {
+ GroupIdCacheEntry entry = entries.get(i);
+ if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) {
+ return entry.groupId;
+ }
+ }
+
+ GroupIdCacheEntry entry = new GroupIdCacheEntry();
+ entry.accountName = account.name;
+ entry.accountType = account.type;
+ entry.sourceId = sourceId;
+ entries.add(0, entry);
+
// look up the group that contains this sourceId and has the same account name and type
// as the contact refered to by rawContactId
- c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
+ Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
new String[]{sourceId, account.name, account.type}, null, null, null);
try {
- if (c.moveToNext()) {
- return c.getLong(0);
+ if (c.moveToFirst()) {
+ entry.groupId = c.getLong(0);
} else {
ContentValues groupValues = new ContentValues();
groupValues.put(Groups.ACCOUNT_NAME, account.name);
@@ -2315,11 +2430,215 @@
throw new IllegalStateException("unable to create a new group with "
+ "this sourceid: " + groupValues);
}
- return groupId;
+ entry.groupId = groupId;
}
} finally {
c.close();
}
+
+ return entry.groupId;
+ }
+
+ private interface DisplayNameQuery {
+ public static final String RAW_SQL =
+ "SELECT "
+ + DataColumns.MIMETYPE_ID + ","
+ + Data.IS_PRIMARY + ","
+ + Data.DATA1 + ","
+ + Data.DATA2 + ","
+ + Data.DATA3 + ","
+ + Data.DATA4 + ","
+ + Data.DATA5 + ","
+ + Data.DATA6 + ","
+ + Data.DATA7 + ","
+ + Data.DATA8 + ","
+ + Data.DATA9 + ","
+ + Data.DATA10 + ","
+ + Data.DATA11 +
+ " FROM " + Tables.DATA +
+ " WHERE " + Data.RAW_CONTACT_ID + "=?" +
+ " AND (" + Data.DATA1 + " NOT NULL OR " +
+ Organization.TITLE + " NOT NULL)";
+
+ public static final int MIMETYPE = 0;
+ public static final int IS_PRIMARY = 1;
+ public static final int DATA1 = 2;
+ public static final int GIVEN_NAME = 3; // data2
+ public static final int FAMILY_NAME = 4; // data3
+ public static final int PREFIX = 5; // data4
+ public static final int TITLE = 5; // data4
+ public static final int MIDDLE_NAME = 6; // data5
+ public static final int SUFFIX = 7; // data6
+ public static final int PHONETIC_GIVEN_NAME = 8; // data7
+ public static final int PHONETIC_MIDDLE_NAME = 9; // data8
+ public static final int ORGANIZATION_PHONETIC_NAME = 9; // data8
+ public static final int PHONETIC_FAMILY_NAME = 10; // data9
+ public static final int FULL_NAME_STYLE = 11; // data10
+ public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11; // data10
+ public static final int PHONETIC_NAME_STYLE = 12; // data11
+ }
+
+ /**
+ * Updates a raw contact display name based on data rows, e.g. structured name,
+ * organization, email etc.
+ */
+ private void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
+ int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
+ NameSplitter.Name bestName = null;
+ String bestDisplayName = null;
+ String bestPhoneticName = null;
+ int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1);
+ try {
+ while (c.moveToNext()) {
+ int mimeType = c.getInt(DisplayNameQuery.MIMETYPE);
+ int source = getDisplayNameSource(mimeType);
+ if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
+ continue;
+ }
+
+ if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) {
+ continue;
+ }
+
+ if (mimeType == mMimeTypeIdStructuredName) {
+ NameSplitter.Name name;
+ if (bestName != null) {
+ name = new NameSplitter.Name();
+ } else {
+ name = mName;
+ name.clear();
+ }
+ name.prefix = c.getString(DisplayNameQuery.PREFIX);
+ name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME);
+ name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME);
+ name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME);
+ name.suffix = c.getString(DisplayNameQuery.SUFFIX);
+ name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE)
+ ? FullNameStyle.UNDEFINED
+ : c.getInt(DisplayNameQuery.FULL_NAME_STYLE);
+ name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME);
+ name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME);
+ name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME);
+ name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE)
+ ? PhoneticNameStyle.UNDEFINED
+ : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE);
+ if (!name.isEmpty()) {
+ bestDisplayNameSource = source;
+ bestName = name;
+ }
+ } else if (mimeType == mMimeTypeIdOrganization) {
+ mCharArrayBuffer.sizeCopied = 0;
+ c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
+ if (mCharArrayBuffer.sizeCopied != 0) {
+ bestDisplayNameSource = source;
+ bestDisplayName = new String(mCharArrayBuffer.data, 0,
+ mCharArrayBuffer.sizeCopied);
+ bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME);
+ bestPhoneticNameStyle =
+ c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE)
+ ? PhoneticNameStyle.UNDEFINED
+ : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE);
+ } else {
+ c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer);
+ if (mCharArrayBuffer.sizeCopied != 0) {
+ bestDisplayNameSource = source;
+ bestDisplayName = new String(mCharArrayBuffer.data, 0,
+ mCharArrayBuffer.sizeCopied);
+ bestPhoneticName = null;
+ bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+ }
+ }
+ } else {
+ // Display name is at DATA1 in all other types.
+ // This is ensured in the constructor.
+
+ mCharArrayBuffer.sizeCopied = 0;
+ c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
+ if (mCharArrayBuffer.sizeCopied != 0) {
+ bestDisplayNameSource = source;
+ bestDisplayName = new String(mCharArrayBuffer.data, 0,
+ mCharArrayBuffer.sizeCopied);
+ bestPhoneticName = null;
+ bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+ }
+ }
+ }
+
+ } finally {
+ c.close();
+ }
+
+ String displayNamePrimary;
+ String displayNameAlternative;
+ String sortKeyPrimary = null;
+ String sortKeyAlternative = null;
+ int displayNameStyle = FullNameStyle.UNDEFINED;
+
+ if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) {
+ displayNameStyle = bestName.fullNameStyle;
+ if (displayNameStyle == FullNameStyle.CJK
+ || displayNameStyle == FullNameStyle.UNDEFINED) {
+ displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
+ bestName.fullNameStyle = displayNameStyle;
+ }
+
+ displayNamePrimary = mNameSplitter.join(bestName, true);
+ displayNameAlternative = mNameSplitter.join(bestName, false);
+
+ bestPhoneticName = mNameSplitter.joinPhoneticName(bestName);
+ bestPhoneticNameStyle = bestName.phoneticNameStyle;
+ } else {
+ displayNamePrimary = displayNameAlternative = bestDisplayName;
+ }
+
+ if (bestPhoneticName != null) {
+ sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
+ if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
+ bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
+ }
+ } else {
+ if (displayNameStyle == FullNameStyle.UNDEFINED) {
+ displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
+ if (displayNameStyle == FullNameStyle.UNDEFINED
+ || displayNameStyle == FullNameStyle.CJK) {
+ displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle(
+ displayNameStyle, bestPhoneticNameStyle);
+ }
+ displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
+ }
+ if (displayNameStyle == FullNameStyle.CHINESE) {
+ sortKeyPrimary = sortKeyAlternative =
+ ContactLocaleUtils.getSortKey(displayNamePrimary, FullNameStyle.CHINESE);
+ }
+ }
+
+ if (sortKeyPrimary == null) {
+ sortKeyPrimary = displayNamePrimary;
+ sortKeyAlternative = displayNameAlternative;
+ }
+
+ setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary,
+ displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle,
+ sortKeyPrimary, sortKeyAlternative);
+ }
+
+ private int getDisplayNameSource(int mimeTypeId) {
+ if (mimeTypeId == mMimeTypeIdStructuredName) {
+ return DisplayNameSources.STRUCTURED_NAME;
+ } else if (mimeTypeId == mMimeTypeIdEmail) {
+ return DisplayNameSources.EMAIL;
+ } else if (mimeTypeId == mMimeTypeIdPhone) {
+ return DisplayNameSources.PHONE;
+ } else if (mimeTypeId == mMimeTypeIdOrganization) {
+ return DisplayNameSources.ORGANIZATION;
+ } else if (mimeTypeId == mMimeTypeIdNickname) {
+ return DisplayNameSources.NICKNAME;
+ } else {
+ return DisplayNameSources.UNDEFINED;
+ }
}
/**
@@ -2358,8 +2677,9 @@
// Note that the query will return data according to the access restrictions,
// so we don't need to worry about deleting data we don't have permission to read.
- Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=" + dataId, null,
- null);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?",
+ mSelectionArgs1, null);
try {
if (!c.moveToFirst()) {
@@ -2395,27 +2715,26 @@
/**
* Inserts an item in the groups table
*/
- private long insertGroup(Uri uri, ContentValues values, Account account,
- boolean callerIsSyncAdapter) {
- ContentValues overriddenValues = new ContentValues(values);
- if (!resolveAccount(overriddenValues, account)) {
- return -1;
- }
+ private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+ mValues.clear();
+ mValues.putAll(values);
+
+ final Account account = resolveAccount(uri, mValues);
// Replace package with internal mapping
- final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
+ final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
if (packageName != null) {
- overriddenValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
+ mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
}
- overriddenValues.remove(Groups.RES_PACKAGE);
+ mValues.remove(Groups.RES_PACKAGE);
if (!callerIsSyncAdapter) {
- overriddenValues.put(Groups.DIRTY, 1);
+ mValues.put(Groups.DIRTY, 1);
}
- long result = mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
+ long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
- if (overriddenValues.containsKey(Groups.GROUP_VISIBLE)) {
+ if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
mVisibleTouched = true;
}
@@ -2577,32 +2896,32 @@
long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
mStatusUpdateReplace.bindLong(1, dataId);
mStatusUpdateReplace.bindLong(2, timestamp);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 3, status);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 4, resPackage);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 5, iconResource);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 6, labelResource);
+ bindString(mStatusUpdateReplace, 3, status);
+ bindString(mStatusUpdateReplace, 4, resPackage);
+ bindLong(mStatusUpdateReplace, 5, iconResource);
+ bindLong(mStatusUpdateReplace, 6, labelResource);
mStatusUpdateReplace.execute();
} else {
try {
mStatusUpdateInsert.bindLong(1, dataId);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 2, status);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 3, resPackage);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 4, iconResource);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 5, labelResource);
+ bindString(mStatusUpdateInsert, 2, status);
+ bindString(mStatusUpdateInsert, 3, resPackage);
+ bindLong(mStatusUpdateInsert, 4, iconResource);
+ bindLong(mStatusUpdateInsert, 5, labelResource);
mStatusUpdateInsert.executeInsert();
} catch (SQLiteConstraintException e) {
// The row already exists - update it
long timestamp = System.currentTimeMillis();
mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 2, status);
+ bindString(mStatusUpdateAutoTimestamp, 2, status);
mStatusUpdateAutoTimestamp.bindLong(3, dataId);
- DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 4, status);
+ bindString(mStatusUpdateAutoTimestamp, 4, status);
mStatusUpdateAutoTimestamp.execute();
- DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 1, resPackage);
- DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 2, iconResource);
- DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 3, labelResource);
+ bindString(mStatusAttributionUpdate, 1, resPackage);
+ bindLong(mStatusAttributionUpdate, 2, iconResource);
+ bindLong(mStatusAttributionUpdate, 3, labelResource);
mStatusAttributionUpdate.bindLong(4, dataId);
mStatusAttributionUpdate.execute();
}
@@ -2661,12 +2980,15 @@
case RAW_CONTACTS: {
int numDeletes = 0;
- Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
+ Cursor c = mDb.query(Tables.RAW_CONTACTS,
+ new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
try {
while (c.moveToNext()) {
final long rawContactId = c.getLong(0);
- numDeletes += deleteRawContact(rawContactId, callerIsSyncAdapter);
+ long contactId = c.getLong(1);
+ numDeletes += deleteRawContact(rawContactId, contactId,
+ callerIsSyncAdapter);
}
} finally {
c.close();
@@ -2676,7 +2998,8 @@
case RAW_CONTACTS_ID: {
final long rawContactId = ContentUris.parseId(uri);
- return deleteRawContact(rawContactId, callerIsSyncAdapter);
+ return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId),
+ callerIsSyncAdapter);
}
case DATA: {
@@ -2691,7 +3014,8 @@
case POSTALS_ID: {
long dataId = ContentUris.parseId(uri);
mSyncToNetwork |= !callerIsSyncAdapter;
- return deleteData(Data._ID + "=" + dataId, null, callerIsSyncAdapter);
+ mSelectionArgs1[0] = String.valueOf(dataId);
+ return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
}
case GROUPS_ID: {
@@ -2718,7 +3042,7 @@
case SETTINGS: {
mSyncToNetwork |= !callerIsSyncAdapter;
- return deleteSettings(uri, selection, selectionArgs);
+ return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
}
case STATUS_UPDATES: {
@@ -2732,14 +3056,8 @@
}
}
- private static boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) {
- final String flag = uri.getQueryParameter(name);
- return flag == null
- ? defaultValue
- : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
- }
-
public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
+ mGroupIdCache.clear();
final long groupMembershipMimetypeId = mDbHelper
.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
@@ -2781,10 +3099,13 @@
return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
}
- public int deleteRawContact(long rawContactId, boolean callerIsSyncAdapter) {
+ public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
+ mContactAggregator.invalidateAggregationExceptionCache();
if (callerIsSyncAdapter) {
mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
- return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
+ int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
+ mContactAggregator.updateDisplayNameForContact(mDb, contactId);
+ return count;
} else {
mDbHelper.removeContactIfSingleton(rawContactId);
return markRawContactAsDeleted(rawContactId);
@@ -2792,8 +3113,14 @@
}
private int deleteStatusUpdates(String selection, String[] selectionArgs) {
- // TODO delete from both tables: presence and status_updates
- return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
+ // delete from both tables: presence and status_updates
+ // TODO should account type/name be appended to the where clause?
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "deleting data from status_updates for " + selection);
+ }
+ mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
+ selectionArgs);
+ return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
}
private int markRawContactAsDeleted(long rawContactId) {
@@ -2820,7 +3147,7 @@
final int match = sUriMatcher.match(uri);
if (match == SYNCSTATE_ID && selection == null) {
long rowId = ContentUris.parseId(uri);
- Object data = values.get(ContactsContract.SyncStateColumns.DATA);
+ Object data = values.get(ContactsContract.SyncState.DATA);
mUpdatedSyncStates.put(rowId, data);
return 1;
}
@@ -2903,10 +3230,12 @@
case RAW_CONTACTS_ID: {
long rawContactId = ContentUris.parseId(uri);
if (selection != null) {
- count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+ count = updateRawContacts(values, RawContacts._ID + "=?"
+ " AND(" + selection + ")", selectionArgs);
} else {
- count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId, null);
+ mSelectionArgs1[0] = String.valueOf(rawContactId);
+ count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1);
}
break;
}
@@ -2922,7 +3251,8 @@
case GROUPS_ID: {
long groupId = ContentUris.parseId(uri);
- String selectionWithId = (Groups._ID + "=" + groupId + " ")
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
+ String selectionWithId = Groups._ID + "=? "
+ (selection == null ? "" : " AND " + selection);
count = updateGroups(uri, values, selectionWithId, selectionArgs,
callerIsSyncAdapter);
@@ -2938,11 +3268,17 @@
}
case SETTINGS: {
- count = updateSettings(uri, values, selection, selectionArgs);
+ count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
+ selectionArgs);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
+ case STATUS_UPDATES: {
+ count = updateStatusUpdate(uri, values, selection, selectionArgs);
+ break;
+ }
+
default: {
mSyncToNetwork = true;
return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
@@ -2952,9 +3288,68 @@
return count;
}
+ private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ // update status_updates table, if status is provided
+ // TODO should account type/name be appended to the where clause?
+ int updateCount = 0;
+ ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
+ if (settableValues.size() > 0) {
+ updateCount = mDb.update(Tables.STATUS_UPDATES,
+ settableValues,
+ getWhereClauseForStatusUpdatesTable(selection),
+ selectionArgs);
+ }
+
+ // now update the Presence table
+ settableValues = getSettableColumnsForPresenceTable(values);
+ if (settableValues.size() > 0) {
+ updateCount = mDb.update(Tables.PRESENCE, settableValues,
+ selection, selectionArgs);
+ }
+ // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
+ // potentially get updated in this method.
+ return updateCount;
+ }
+
+ /**
+ * Build a where clause to select the rows to be updated in status_updates table.
+ */
+ private String getWhereClauseForStatusUpdatesTable(String selection) {
+ mSb.setLength(0);
+ mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
+ mSb.append(selection);
+ mSb.append(")");
+ return mSb.toString();
+ }
+
+ private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
+ mValues.clear();
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
+ StatusUpdates.STATUS);
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
+ StatusUpdates.STATUS_TIMESTAMP);
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
+ StatusUpdates.STATUS_RES_PACKAGE);
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
+ StatusUpdates.STATUS_LABEL);
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
+ StatusUpdates.STATUS_ICON);
+ return mValues;
+ }
+
+ private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
+ mValues.clear();
+ ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
+ StatusUpdates.PRESENCE);
+ return mValues;
+ }
+
private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
String[] selectionArgs, boolean callerIsSyncAdapter) {
+ mGroupIdCache.clear();
+
ContentValues updatedValues;
if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
updatedValues = mValues;
@@ -2971,9 +3366,8 @@
}
if (updatedValues.containsKey(Groups.SHOULD_SYNC)
&& updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
- final long groupId = ContentUris.parseId(uri);
Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
- Groups.ACCOUNT_TYPE}, Groups._ID + "=" + groupId, null, null,
+ Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
null, null);
String accountName;
String accountType;
@@ -2983,7 +3377,7 @@
accountType = c.getString(1);
if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
Account account = new Account(accountName, accountType);
- ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
+ ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
new Bundle());
break;
}
@@ -3032,12 +3426,16 @@
final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
&& values.getAsInteger(RawContacts.DELETED) == 0);
int previousDeleted = 0;
+ String accountType = null;
+ String accountName = null;
if (requestUndoDelete) {
Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
null, null, null, null);
try {
if (cursor.moveToFirst()) {
previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
+ accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
+ accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
}
} finally {
cursor.close();
@@ -3045,6 +3443,7 @@
values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
}
+
int count = mDb.update(Tables.RAW_CONTACTS, values, selection, null);
if (count != 0) {
if (values.containsKey(RawContacts.STARRED)) {
@@ -3053,9 +3452,20 @@
if (values.containsKey(RawContacts.SOURCE_ID)) {
mContactAggregator.updateLookupKey(mDb, rawContactId);
}
+ if (values.containsKey(RawContacts.NAME_VERIFIED)) {
+
+ // If setting NAME_VERIFIED for this raw contact, reset it for all
+ // other raw contacts in the same aggregate
+ if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
+ mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId);
+ mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId);
+ mResetNameVerifiedForOtherRawContacts.execute();
+ }
+ mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId);
+ }
if (requestUndoDelete && previousDeleted == 1) {
// undo delete, needs aggregation again.
- mInsertedRawContacts.add(rawContactId);
+ mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType));
}
}
return count;
@@ -3164,7 +3574,8 @@
mValues.put(RawContacts.DIRTY, 1);
}
- mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=" + contactId, null);
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
// Copy changeable values to prevent automatically managed fields from
// being explicitly updated by clients.
@@ -3180,13 +3591,14 @@
ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
values, Contacts.STARRED);
- return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=" + contactId, null);
- }
+ int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
- public void updateContactLastContactedTime(long contactId, long lastTimeContacted) {
- mContactsLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
- mContactsLastTimeContactedUpdate.bindLong(2, contactId);
- mContactsLastTimeContactedUpdate.execute();
+ if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
+ !values.containsKey(Contacts.TIMES_CONTACTED)) {
+ mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
+ mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
+ }
+ return rslt;
}
private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
@@ -3204,9 +3616,11 @@
}
if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
+ mSelectionArgs2[0] = String.valueOf(rawContactId1);
+ mSelectionArgs2[1] = String.valueOf(rawContactId2);
db.delete(Tables.AGGREGATION_EXCEPTIONS,
- AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND "
- + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null);
+ AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
+ + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
} else {
ContentValues exceptionValues = new ContentValues(3);
exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
@@ -3216,6 +3630,7 @@
exceptionValues);
}
+ mContactAggregator.invalidateAggregationExceptionCache();
mContactAggregator.markForAggregation(rawContactId1);
mContactAggregator.markForAggregation(rawContactId2);
@@ -3387,7 +3802,8 @@
case CONTACTS_ID: {
long contactId = ContentUris.parseId(uri);
setTablesAndProjectionMapForContacts(qb, uri, projection);
- qb.appendWhere(Contacts._ID + "=" + contactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+ qb.appendWhere(Contacts._ID + "=?");
break;
}
@@ -3400,13 +3816,21 @@
}
String lookupKey = pathSegments.get(2);
if (segmentCount == 4) {
+ // TODO: pull this out into a method and generalize to not require contactId
long contactId = Long.parseLong(pathSegments.get(3));
SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
- lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " +
- Contacts.LOOKUP_KEY + "=");
- lookupQb.appendWhereEscapeString(lookupKey);
- Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder,
+ String[] args;
+ if (selectionArgs == null) {
+ args = new String[2];
+ } else {
+ args = new String[selectionArgs.length + 2];
+ System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
+ }
+ args[0] = String.valueOf(contactId);
+ args[1] = lookupKey;
+ lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
+ Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
groupBy, limit);
if (c.getCount() != 0) {
return c;
@@ -3416,7 +3840,9 @@
}
setTablesAndProjectionMapForContacts(qb, uri, projection);
- qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
+ selectionArgs = insertSelectionArg(selectionArgs,
+ String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+ qb.appendWhere(Contacts._ID + "=?");
break;
}
@@ -3425,7 +3851,9 @@
final String lookupKey = uri.getPathSegments().get(2);
qb.setTables(mDbHelper.getContactView(true /* require restricted */));
qb.setProjectionMap(sContactsVCardProjectionMap);
- qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
+ selectionArgs = insertSelectionArg(selectionArgs,
+ String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+ qb.appendWhere(Contacts._ID + "=?");
break;
}
@@ -3505,14 +3933,16 @@
case CONTACTS_DATA: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+ qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
break;
}
case CONTACTS_PHOTO: {
long contactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+ qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
break;
}
@@ -3525,8 +3955,9 @@
case PHONES_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
- qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
@@ -3536,13 +3967,13 @@
if (uri.getPathSegments().size() > 2) {
String filterParam = uri.getLastPathSegment();
StringBuilder sb = new StringBuilder();
- sb.append("(");
+ sb.append(" AND (");
boolean orNeeded = false;
String normalizedName = NameNormalizer.normalize(filterParam);
if (normalizedName.length() > 0) {
sb.append(Data.RAW_CONTACT_ID + " IN ");
- appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false);
+ appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
orNeeded = true;
}
@@ -3560,7 +3991,7 @@
sb.append("')");
}
sb.append(")");
- qb.appendWhere(" AND " + sb);
+ qb.appendWhere(sb);
}
groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
if (sortOrder == null) {
@@ -3577,8 +4008,9 @@
case EMAILS_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
- qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"
+ + " AND " + Data._ID + "=?");
break;
}
@@ -3586,8 +4018,8 @@
setTablesAndProjectionMapForData(qb, uri, projection, false);
qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
if (uri.getPathSegments().size() > 2) {
- qb.appendWhere(" AND " + Email.DATA + "=");
- qb.appendWhereEscapeString(uri.getLastPathSegment());
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Email.DATA + "=?");
}
break;
}
@@ -3631,7 +4063,7 @@
" FROM " + Tables.DATA +
" WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
" AND " + Data.RAW_CONTACT_ID + " IN ");
- appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false);
+ appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
}
}
sb.append(")");
@@ -3653,9 +4085,10 @@
case POSTALS_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
+ StructuredPostal.CONTENT_ITEM_TYPE + "'");
- qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
@@ -3667,14 +4100,16 @@
case RAW_CONTACTS_ID: {
long rawContactId = ContentUris.parseId(uri);
setTablesAndProjectionMapForRawContacts(qb, uri);
- qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+ qb.appendWhere(" AND " + RawContacts._ID + "=?");
break;
}
case RAW_CONTACTS_DATA: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=" + rawContactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+ qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
break;
}
@@ -3685,7 +4120,8 @@
case DATA_ID: {
setTablesAndProjectionMapForData(qb, uri, projection, false);
- qb.appendWhere(" AND " + Data._ID + "=" + ContentUris.parseId(uri));
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(" AND " + Data._ID + "=?");
break;
}
@@ -3715,10 +4151,10 @@
}
case GROUPS_ID: {
- long groupId = ContentUris.parseId(uri);
qb.setTables(mDbHelper.getGroupView());
qb.setProjectionMap(sGroupsProjectionMap);
- qb.appendWhere(Groups._ID + "=" + groupId);
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(Groups._ID + "=?");
break;
}
@@ -3783,7 +4219,8 @@
case STATUS_UPDATES_ID: {
setTableAndProjectionMapForStatusUpdates(qb, projection);
- qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri));
+ selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
+ qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
break;
}
@@ -3792,8 +4229,8 @@
}
case SEARCH_SHORTCUT: {
- long contactId = ContentUris.parseId(uri);
- return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection);
+ String lookupKey = uri.getLastPathSegment();
+ return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
}
case LIVE_FOLDERS_CONTACTS:
@@ -3828,7 +4265,8 @@
case RAW_CONTACT_ENTITY_ID: {
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
setTablesAndProjectionMapForRawContactsEntities(qb, uri);
- qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
+ qb.appendWhere(" AND " + RawContacts._ID + "=?");
break;
}
@@ -3855,7 +4293,12 @@
return c;
}
- private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
+ /**
+ * Returns the contact Id for the contact identified by the lookupKey. Robust against changes
+ * in the lookup key: if the key has changed, will look up the contact by the name encoded in
+ * the lookup key.
+ */
+ public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
ContactLookupKey key = new ContactLookupKey();
ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
@@ -4043,7 +4486,7 @@
String[] projection) {
StringBuilder sb = new StringBuilder();
boolean excludeRestrictedData = false;
- String requestingPackage = uri.getQueryParameter(
+ String requestingPackage = getQueryParameter(uri,
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
if (requestingPackage != null) {
excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
@@ -4072,7 +4515,7 @@
private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
StringBuilder sb = new StringBuilder();
boolean excludeRestrictedData = false;
- String requestingPackage = uri.getQueryParameter(
+ String requestingPackage = getQueryParameter(uri,
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
if (requestingPackage != null) {
excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
@@ -4088,7 +4531,7 @@
boolean excludeRestrictedData = readBooleanQueryParameter(uri,
Data.FOR_EXPORT_ONLY, false);
- String requestingPackage = uri.getQueryParameter(
+ String requestingPackage = getQueryParameter(uri,
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
if (requestingPackage != null) {
excludeRestrictedData = excludeRestrictedData
@@ -4106,7 +4549,7 @@
boolean excludeRestrictedData = readBooleanQueryParameter(uri,
Data.FOR_EXPORT_ONLY, false);
- String requestingPackage = uri.getQueryParameter(
+ String requestingPackage = getQueryParameter(uri,
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
if (requestingPackage != null) {
excludeRestrictedData = excludeRestrictedData
@@ -4187,9 +4630,20 @@
}
private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
- final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
- final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
- if (!TextUtils.isEmpty(accountName)) {
+ final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+ final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+
+ final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+ if (partialUri) {
+ // Throw when either account is incomplete
+ throw new IllegalArgumentException("Must specify both or neither of"
+ + " ACCOUNT_NAME and ACCOUNT_TYPE");
+ }
+
+ // Accounts are valid by only checking one parameter, since we've
+ // already ruled out partial accounts.
+ final boolean validAccount = !TextUtils.isEmpty(accountName);
+ if (validAccount) {
qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
+ DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ RawContacts.ACCOUNT_TYPE + "="
@@ -4200,9 +4654,20 @@
}
private String appendAccountToSelection(Uri uri, String selection) {
- final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
- final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
- if (!TextUtils.isEmpty(accountName)) {
+ final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
+ final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
+
+ final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+ if (partialUri) {
+ // Throw when either account is incomplete
+ throw new IllegalArgumentException("Must specify both or neither of"
+ + " ACCOUNT_NAME and ACCOUNT_TYPE");
+ }
+
+ // Accounts are valid by only checking one parameter, since we've
+ // already ruled out partial accounts.
+ final boolean validAccount = !TextUtils.isEmpty(accountName);
+ if (validAccount) {
StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
+ DatabaseUtils.sqlEscapeString(accountName) + " AND "
+ RawContacts.ACCOUNT_TYPE + "="
@@ -4224,8 +4689,8 @@
* @return A string containing a non-negative integer, or <code>null</code> if
* the parameter is not set, or is set to an invalid value.
*/
- private String getLimit(Uri url) {
- String limitParam = url.getQueryParameter("limit");
+ private String getLimit(Uri uri) {
+ String limitParam = getQueryParameter(uri, "limit");
if (limitParam == null) {
return null;
}
@@ -4277,7 +4742,7 @@
if (mDbHelper.hasAccessToRestrictedData()) {
return "1";
} else {
- return RawContacts.IS_RESTRICTED + "=0";
+ return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
}
}
@@ -4299,14 +4764,13 @@
throw new FileNotFoundException("Mode " + mode + " not supported.");
}
- long contactId = Long.parseLong(uri.getPathSegments().get(1));
-
String sql =
"SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
" WHERE " + Data._ID + "=" + Contacts.PHOTO_ID
- + " AND " + RawContacts.CONTACT_ID + "=" + contactId;
+ + " AND " + RawContacts.CONTACT_ID + "=?";
SQLiteDatabase db = mDbHelper.getReadableDatabase();
- return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null);
+ return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql,
+ new String[]{uri.getPathSegments().get(1)});
}
case CONTACTS_AS_VCARD: {
@@ -4376,401 +4840,13 @@
composer.terminate();
}
-
- private static Account readAccountFromQueryParams(Uri uri) {
- final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
- final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
- if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
- return null;
- }
- return new Account(name, type);
- }
-
-
- /**
- * An implementation of EntityIterator that joins the contacts and data tables
- * and consumes all the data rows for a contact in order to build the Entity for a contact.
- */
- private static class RawContactsEntityIterator implements EntityIterator {
- private final Cursor mEntityCursor;
- private volatile boolean mIsClosed;
-
- private static final String[] DATA_KEYS = new String[]{
- Data.DATA1,
- Data.DATA2,
- Data.DATA3,
- Data.DATA4,
- Data.DATA5,
- Data.DATA6,
- Data.DATA7,
- Data.DATA8,
- Data.DATA9,
- Data.DATA10,
- Data.DATA11,
- Data.DATA12,
- Data.DATA13,
- Data.DATA14,
- Data.DATA15,
- Data.SYNC1,
- Data.SYNC2,
- Data.SYNC3,
- Data.SYNC4};
-
- public static final String[] PROJECTION = new String[]{
- RawContacts.ACCOUNT_NAME,
- RawContacts.ACCOUNT_TYPE,
- RawContacts.SOURCE_ID,
- RawContacts.VERSION,
- RawContacts.DIRTY,
- RawContacts.Entity.DATA_ID,
- Data.RES_PACKAGE,
- Data.MIMETYPE,
- Data.DATA1,
- Data.DATA2,
- Data.DATA3,
- Data.DATA4,
- Data.DATA5,
- Data.DATA6,
- Data.DATA7,
- Data.DATA8,
- Data.DATA9,
- Data.DATA10,
- Data.DATA11,
- Data.DATA12,
- Data.DATA13,
- Data.DATA14,
- Data.DATA15,
- Data.SYNC1,
- Data.SYNC2,
- Data.SYNC3,
- Data.SYNC4,
- RawContacts._ID,
- Data.IS_PRIMARY,
- Data.IS_SUPER_PRIMARY,
- Data.DATA_VERSION,
- GroupMembership.GROUP_SOURCE_ID,
- RawContacts.SYNC1,
- RawContacts.SYNC2,
- RawContacts.SYNC3,
- RawContacts.SYNC4,
- RawContacts.DELETED,
- RawContacts.CONTACT_ID,
- RawContacts.STARRED,
- RawContacts.IS_RESTRICTED};
-
- private static final int COLUMN_ACCOUNT_NAME = 0;
- private static final int COLUMN_ACCOUNT_TYPE = 1;
- private static final int COLUMN_SOURCE_ID = 2;
- private static final int COLUMN_VERSION = 3;
- private static final int COLUMN_DIRTY = 4;
- private static final int COLUMN_DATA_ID = 5;
- private static final int COLUMN_RES_PACKAGE = 6;
- private static final int COLUMN_MIMETYPE = 7;
- private static final int COLUMN_DATA1 = 8;
- private static final int COLUMN_RAW_CONTACT_ID = 27;
- private static final int COLUMN_IS_PRIMARY = 28;
- private static final int COLUMN_IS_SUPER_PRIMARY = 29;
- private static final int COLUMN_DATA_VERSION = 30;
- private static final int COLUMN_GROUP_SOURCE_ID = 31;
- private static final int COLUMN_SYNC1 = 32;
- private static final int COLUMN_SYNC2 = 33;
- private static final int COLUMN_SYNC3 = 34;
- private static final int COLUMN_SYNC4 = 35;
- private static final int COLUMN_DELETED = 36;
- private static final int COLUMN_CONTACT_ID = 37;
- private static final int COLUMN_STARRED = 38;
- private static final int COLUMN_IS_RESTRICTED = 39;
-
- public RawContactsEntityIterator(ContactsProvider2 provider, Uri entityUri,
- String contactsIdString,
- String selection, String[] selectionArgs, String sortOrder) {
- mIsClosed = false;
- Uri uri;
- if (contactsIdString != null) {
- uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, contactsIdString);
- uri = Uri.withAppendedPath(uri, RawContacts.Entity.CONTENT_DIRECTORY);
- } else {
- uri = ContactsContract.RawContactsEntity.CONTENT_URI;
- }
- final Uri.Builder builder = uri.buildUpon();
- String query = entityUri.getQuery();
- builder.encodedQuery(query);
- mEntityCursor = provider.query(builder.build(),
- PROJECTION, selection, selectionArgs, sortOrder);
- mEntityCursor.moveToFirst();
- }
-
- public void reset() throws RemoteException {
- if (mIsClosed) {
- throw new IllegalStateException("calling reset() when the iterator is closed");
- }
- mEntityCursor.moveToFirst();
- }
-
- public void close() {
- if (mIsClosed) {
- throw new IllegalStateException("closing when already closed");
- }
- mIsClosed = true;
- mEntityCursor.close();
- }
-
- public boolean hasNext() throws RemoteException {
- if (mIsClosed) {
- throw new IllegalStateException("calling hasNext() when the iterator is closed");
- }
-
- return !mEntityCursor.isAfterLast();
- }
-
- public Entity next() throws RemoteException {
- if (mIsClosed) {
- throw new IllegalStateException("calling next() when the iterator is closed");
- }
- if (!hasNext()) {
- throw new IllegalStateException("you may only call next() if hasNext() is true");
- }
-
- final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
-
- final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID);
-
- // we expect the cursor is already at the row we need to read from
- ContentValues contactValues = new ContentValues();
- contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
- contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
- contactValues.put(RawContacts._ID, rawContactId);
- contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
- contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
- contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
- contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1));
- contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2));
- contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3));
- contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4));
- contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED));
- contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID));
- contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED));
- contactValues.put(RawContacts.IS_RESTRICTED, c.getInt(COLUMN_IS_RESTRICTED));
- Entity contact = new Entity(contactValues);
-
- // read data rows until the contact id changes
- do {
- if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) {
- break;
- }
-// if (c.isNull(COLUMN_CONTACT_ID)) {
-// continue;
-// }
- // add the data to to the contact
- ContentValues dataValues = new ContentValues();
- dataValues.put(Data._ID, c.getLong(COLUMN_DATA_ID));
- dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
- dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
- dataValues.put(Data.IS_PRIMARY, c.getLong(COLUMN_IS_PRIMARY));
- dataValues.put(Data.IS_SUPER_PRIMARY, c.getLong(COLUMN_IS_SUPER_PRIMARY));
- dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
- if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
- dataValues.put(GroupMembership.GROUP_SOURCE_ID,
- c.getString(COLUMN_GROUP_SOURCE_ID));
- }
- dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
- for (int i = 0; i < DATA_KEYS.length; i++) {
- final int columnIndex = i + COLUMN_DATA1;
- String key = DATA_KEYS[i];
- if (c.isNull(columnIndex)) {
- // don't put anything
- } else if (c.isLong(columnIndex)) {
- dataValues.put(key, c.getLong(columnIndex));
- } else if (c.isFloat(columnIndex)) {
- dataValues.put(key, c.getFloat(columnIndex));
- } else if (c.isString(columnIndex)) {
- dataValues.put(key, c.getString(columnIndex));
- } else if (c.isBlob(columnIndex)) {
- dataValues.put(key, c.getBlob(columnIndex));
- }
- }
- contact.addSubValue(Data.CONTENT_URI, dataValues);
- } while (mEntityCursor.moveToNext());
-
- return contact;
- }
- }
-
- /**
- * An implementation of EntityIterator that joins the contacts and data tables
- * and consumes all the data rows for a contact in order to build the Entity for a contact.
- */
- private static class GroupsEntityIterator implements EntityIterator {
- private final Cursor mEntityCursor;
- private volatile boolean mIsClosed;
-
- private static final String[] PROJECTION = new String[]{
- Groups._ID,
- Groups.ACCOUNT_NAME,
- Groups.ACCOUNT_TYPE,
- Groups.SOURCE_ID,
- Groups.DIRTY,
- Groups.VERSION,
- Groups.RES_PACKAGE,
- Groups.TITLE,
- Groups.TITLE_RES,
- Groups.GROUP_VISIBLE,
- Groups.SYNC1,
- Groups.SYNC2,
- Groups.SYNC3,
- Groups.SYNC4,
- Groups.SYSTEM_ID,
- Groups.NOTES,
- Groups.DELETED,
- Groups.SHOULD_SYNC};
-
- private static final int COLUMN_ID = 0;
- private static final int COLUMN_ACCOUNT_NAME = 1;
- private static final int COLUMN_ACCOUNT_TYPE = 2;
- private static final int COLUMN_SOURCE_ID = 3;
- private static final int COLUMN_DIRTY = 4;
- private static final int COLUMN_VERSION = 5;
- private static final int COLUMN_RES_PACKAGE = 6;
- private static final int COLUMN_TITLE = 7;
- private static final int COLUMN_TITLE_RES = 8;
- private static final int COLUMN_GROUP_VISIBLE = 9;
- private static final int COLUMN_SYNC1 = 10;
- private static final int COLUMN_SYNC2 = 11;
- private static final int COLUMN_SYNC3 = 12;
- private static final int COLUMN_SYNC4 = 13;
- private static final int COLUMN_SYSTEM_ID = 14;
- private static final int COLUMN_NOTES = 15;
- private static final int COLUMN_DELETED = 16;
- private static final int COLUMN_SHOULD_SYNC = 17;
-
- public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
- String selection, String[] selectionArgs, String sortOrder) {
- mIsClosed = false;
-
- final String updatedSortOrder = (sortOrder == null)
- ? Groups._ID
- : (Groups._ID + "," + sortOrder);
-
- final SQLiteDatabase db = provider.mDbHelper.getReadableDatabase();
- final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables(provider.mDbHelper.getGroupView());
- qb.setProjectionMap(sGroupsProjectionMap);
- if (groupIdString != null) {
- qb.appendWhere(Groups._ID + "=" + groupIdString);
- }
- final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
- final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
- if (!TextUtils.isEmpty(accountName)) {
- qb.appendWhere(Groups.ACCOUNT_NAME + "="
- + DatabaseUtils.sqlEscapeString(accountName) + " AND "
- + Groups.ACCOUNT_TYPE + "="
- + DatabaseUtils.sqlEscapeString(accountType));
- }
- mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
- null, null, updatedSortOrder);
- mEntityCursor.moveToFirst();
- }
-
- public void close() {
- if (mIsClosed) {
- throw new IllegalStateException("closing when already closed");
- }
- mIsClosed = true;
- mEntityCursor.close();
- }
-
- public boolean hasNext() throws RemoteException {
- if (mIsClosed) {
- throw new IllegalStateException("calling hasNext() when the iterator is closed");
- }
-
- return !mEntityCursor.isAfterLast();
- }
-
- public void reset() throws RemoteException {
- if (mIsClosed) {
- throw new IllegalStateException("calling reset() when the iterator is closed");
- }
- mEntityCursor.moveToFirst();
- }
-
- public Entity next() throws RemoteException {
- if (mIsClosed) {
- throw new IllegalStateException("calling next() when the iterator is closed");
- }
- if (!hasNext()) {
- throw new IllegalStateException("you may only call next() if hasNext() is true");
- }
-
- final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
-
- final long groupId = c.getLong(COLUMN_ID);
-
- // we expect the cursor is already at the row we need to read from
- ContentValues groupValues = new ContentValues();
- groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
- groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
- groupValues.put(Groups._ID, groupId);
- groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
- groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
- groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
- groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
- groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
- groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
- groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
- groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1));
- groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2));
- groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3));
- groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4));
- groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID));
- groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED));
- groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES));
- groupValues.put(Groups.SHOULD_SYNC, c.getString(COLUMN_SHOULD_SYNC));
- Entity group = new Entity(groupValues);
-
- mEntityCursor.moveToNext();
-
- return group;
- }
- }
-
- @Override
- public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
- String sortOrder) {
- waitForAccess();
-
- final int match = sUriMatcher.match(uri);
- switch (match) {
- case RAW_CONTACTS:
- case RAW_CONTACTS_ID:
- String contactsIdString = null;
- if (match == RAW_CONTACTS_ID) {
- contactsIdString = uri.getPathSegments().get(1);
- }
-
- return new RawContactsEntityIterator(this, uri, contactsIdString,
- selection, selectionArgs, sortOrder);
- case GROUPS:
- case GROUPS_ID:
- String idString = null;
- if (match == GROUPS_ID) {
- idString = uri.getPathSegments().get(1);
- }
-
- return new GroupsEntityIterator(this, idString,
- uri, selection, selectionArgs, sortOrder);
- default:
- throw new UnsupportedOperationException("Unknown uri: " + uri);
- }
- }
-
@Override
public String getType(Uri uri) {
final int match = sUriMatcher.match(uri);
switch (match) {
case CONTACTS:
- case CONTACTS_LOOKUP:
return Contacts.CONTENT_TYPE;
+ case CONTACTS_LOOKUP:
case CONTACTS_ID:
case CONTACTS_LOOKUP_ID:
return Contacts.CONTENT_ITEM_TYPE;
@@ -4811,14 +4887,17 @@
}
}
- private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) {
- if (displayName != null) {
- mRawContactDisplayNameUpdate.bindString(1, displayName);
- } else {
- mRawContactDisplayNameUpdate.bindNull(1);
- }
- mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource);
- mRawContactDisplayNameUpdate.bindLong(3, rawContactId);
+ private void setDisplayName(long rawContactId, int displayNameSource,
+ String displayNamePrimary, String displayNameAlternative, String phoneticName,
+ int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) {
+ mRawContactDisplayNameUpdate.bindLong(1, displayNameSource);
+ bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary);
+ bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative);
+ bindString(mRawContactDisplayNameUpdate, 4, phoneticName);
+ mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle);
+ bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary);
+ bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative);
+ mRawContactDisplayNameUpdate.bindLong(8, rawContactId);
mRawContactDisplayNameUpdate.execute();
}
@@ -4826,8 +4905,7 @@
* Sets the {@link RawContacts#DIRTY} for the specified raw contact.
*/
private void setRawContactDirty(long rawContactId) {
- mRawContactDirtyUpdate.bindLong(1, rawContactId);
- mRawContactDirtyUpdate.execute();
+ mDirtyRawContacts.add(rawContactId);
}
/*
@@ -4902,12 +4980,70 @@
public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name) {
mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name);
+ if (!TextUtils.isEmpty(name)) {
+ Iterator<String> it = ContactLocaleUtils.getNameLookupKeys(name, FullNameStyle.CHINESE);
+ if (it != null) {
+ while(it.hasNext()) {
+ String key = it.next();
+ mNameLookupBuilder.insertNameLookup(rawContactId, dataId,
+ NameLookupType.NAME_SHORTHAND,
+ mNameLookupBuilder.normalizeName(key));
+ }
+ }
+ }
}
+ private interface NicknameLookupPreloadQuery {
+ String TABLE = Tables.NICKNAME_LOOKUP;
+
+ String[] COLUMNS = new String[] {
+ NicknameLookupColumns.NAME
+ };
+
+ int NAME = 0;
+ }
+
+ /**
+ * Read all known common nicknames from the database and populate a Bloom
+ * filter using the corresponding hash codes. The idea is to eliminate most
+ * of unnecessary database lookups for nicknames. Given a name, we will take
+ * its hash code and see if it is set in the Bloom filter. If not, we will know
+ * that the name is not in the database. If it is, we still need to run a
+ * query.
+ * <p>
+ * Given the size of the filter and the expected size of the nickname table,
+ * we should expect the combination of the Bloom filter and cache will
+ * prevent around 98-99% of unnecessary queries from running.
+ */
+ private void preloadNicknameBloomFilter() {
+ mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1);
+ SQLiteDatabase db = mDbHelper.getReadableDatabase();
+ Cursor cursor = db.query(NicknameLookupPreloadQuery.TABLE,
+ NicknameLookupPreloadQuery.COLUMNS,
+ null, null, null, null, null);
+ try {
+ int count = cursor.getCount();
+ for (int i = 0; i < count; i++) {
+ cursor.moveToNext();
+ String normalizedName = cursor.getString(NicknameLookupPreloadQuery.NAME);
+ int hashCode = normalizedName.hashCode();
+ mNicknameBloomFilter.set(hashCode & NICKNAME_BLOOM_FILTER_SIZE);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+
/**
* Returns nickname cluster IDs or null. Maintains cache.
*/
protected String[] getCommonNicknameClusters(String normalizedName) {
+ int hashCode = normalizedName.hashCode();
+ if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) {
+ return null;
+ }
+
SoftReference<String[]> ref;
String[] clusters = null;
synchronized (mNicknameClusterCache) {
@@ -4930,6 +5066,16 @@
return clusters;
}
+ private interface NicknameLookupQuery {
+ String TABLE = Tables.NICKNAME_LOOKUP;
+
+ String[] COLUMNS = new String[] {
+ NicknameLookupColumns.CLUSTER
+ };
+
+ int CLUSTER = 0;
+ }
+
protected String[] loadNicknameClusters(String normalizedName) {
SQLiteDatabase db = mDbHelper.getReadableDatabase();
String[] clusters = null;
@@ -4973,10 +5119,10 @@
* Inserts a record in the {@link Tables#NAME_LOOKUP} table.
*/
public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
- DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 1, rawContactId);
- DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 2, dataId);
- DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 3, lookupType);
- DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 4, name);
+ mNameLookupInsert.bindLong(1, rawContactId);
+ mNameLookupInsert.bindLong(2, dataId);
+ mNameLookupInsert.bindLong(3, lookupType);
+ bindString(mNameLookupInsert, 4, name);
mNameLookupInsert.executeInsert();
}
@@ -4984,7 +5130,7 @@
* Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
*/
public void deleteNameLookup(long dataId) {
- DatabaseUtils.bindObjectToProgram(mNameLookupDelete, 1, dataId);
+ mNameLookupDelete.bindLong(1, dataId);
mNameLookupDelete.execute();
}
@@ -5001,25 +5147,24 @@
+ NameLookupType.NAME_COLLATION_KEY + ","
+ NameLookupType.EMAIL_BASED_NICKNAME + ","
+ NameLookupType.NICKNAME + ","
+ + NameLookupType.NAME_SHORTHAND + ","
+ NameLookupType.ORGANIZATION + "))");
}
public String getRawContactsByFilterAsNestedQuery(String filterParam) {
StringBuilder sb = new StringBuilder();
- appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
+ appendRawContactsByFilterAsNestedQuery(sb, filterParam);
return sb.toString();
}
- public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
- String limit) {
- appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit,
- true);
+ public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) {
+ appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true);
}
private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
- String limit, boolean allowEmailMatch) {
+ boolean allowEmailMatch) {
sb.append("(" +
- "SELECT DISTINCT " + NameLookupColumns.RAW_CONTACT_ID +
+ "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
" FROM " + Tables.NAME_LOOKUP +
" WHERE " + NameLookupColumns.NORMALIZED_NAME +
" GLOB '");
@@ -5027,16 +5172,12 @@
sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
+ NameLookupType.NAME_COLLATION_KEY + ","
+ NameLookupType.NICKNAME + ","
+ + NameLookupType.NAME_SHORTHAND + ","
+ NameLookupType.ORGANIZATION);
if (allowEmailMatch) {
sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
}
- sb.append(")");
-
- if (limit != null) {
- sb.append(" LIMIT ").append(limit);
- }
- sb.append(")");
+ sb.append("))");
}
/**
@@ -5091,7 +5232,91 @@
} catch (RemoteException e) {
Log.e(TAG, "Could not acquire sync adapter types");
}
-
return false;
}
+
+ /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
+ boolean defaultValue) {
+
+ // Manually parse the query, which is much faster than calling uri.getQueryParameter
+ String query = uri.getEncodedQuery();
+ if (query == null) {
+ return defaultValue;
+ }
+
+ int index = query.indexOf(parameter);
+ if (index == -1) {
+ return defaultValue;
+ }
+
+ index += parameter.length();
+
+ return !matchQueryParameter(query, index, "=0", false)
+ && !matchQueryParameter(query, index, "=false", true);
+ }
+
+ private static boolean matchQueryParameter(String query, int index, String value,
+ boolean ignoreCase) {
+ int length = value.length();
+ return query.regionMatches(ignoreCase, index, value, 0, length)
+ && (query.length() == index + length || query.charAt(index + length) == '&');
+ }
+
+ /**
+ * A fast re-implementation of {@link Uri#getQueryParameter}
+ */
+ /* package */ static String getQueryParameter(Uri uri, String parameter) {
+ String query = uri.getEncodedQuery();
+ if (query == null) {
+ return null;
+ }
+
+ int queryLength = query.length();
+ int parameterLength = parameter.length();
+
+ String value;
+ int index = 0;
+ while (true) {
+ index = query.indexOf(parameter, index);
+ if (index == -1) {
+ return null;
+ }
+
+ index += parameterLength;
+
+ if (queryLength == index) {
+ return null;
+ }
+
+ if (query.charAt(index) == '=') {
+ index++;
+ break;
+ }
+ }
+
+ int ampIndex = query.indexOf('&', index);
+ if (ampIndex == -1) {
+ value = query.substring(index);
+ } else {
+ value = query.substring(index, ampIndex);
+ }
+
+ return Uri.decode(value);
+ }
+
+ private void bindString(SQLiteStatement stmt, int index, String value) {
+ if (value == null) {
+ stmt.bindNull(index);
+ } else {
+ stmt.bindString(index, value);
+ }
+ }
+
+ private void bindLong(SQLiteStatement stmt, int index, Number value) {
+ if (value == null) {
+ stmt.bindNull(index);
+ } else {
+ stmt.bindLong(index, value.longValue());
+ }
+ }
}
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index e939ef7..d890310 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -16,11 +16,11 @@
package com.android.providers.contacts;
-import com.android.internal.database.ArrayListCursor;
+import com.android.common.ArrayListCursor;
import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import android.app.SearchManager;
@@ -32,11 +32,9 @@
import android.provider.Contacts.Intents;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Presence;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
@@ -74,20 +72,11 @@
};
private interface SearchSuggestionQuery {
- public static final String JOIN_RAW_CONTACTS =
- " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) ";
-
- public static final String JOIN_CONTACTS =
- " JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
-
- public static final String JOIN_MIMETYPES =
- " JOIN mimetypes ON (data.mimetype_id = mimetypes._id AND mimetypes.mimetype IN ('"
- + StructuredName.CONTENT_ITEM_TYPE + "','" + Email.CONTENT_ITEM_TYPE + "','"
- + Phone.CONTENT_ITEM_TYPE + "','" + Organization.CONTENT_ITEM_TYPE + "','"
- + GroupMembership.CONTENT_ITEM_TYPE + "')) ";
-
- public static final String TABLE = "data " + JOIN_RAW_CONTACTS + JOIN_MIMETYPES
- + JOIN_CONTACTS;
+ public static final String TABLE = "data "
+ + " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+ + " JOIN contacts ON (raw_contacts.contact_id = contacts._id)"
+ + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON ("
+ + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
public static final String PRESENCE_SQL =
"(SELECT " + StatusUpdates.PRESENCE_STATUS +
@@ -97,36 +86,38 @@
public static final String[] COLUMNS = {
ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID,
- ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME,
+ "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
+ + " AS " + Contacts.DISPLAY_NAME,
PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE,
DataColumns.CONCRETE_ID + " AS data_id",
- MimetypesColumns.MIMETYPE,
+ DataColumns.MIMETYPE_ID,
Data.IS_SUPER_PRIMARY,
- Organization.COMPANY,
- Email.DATA,
- Phone.NUMBER,
+ Data.DATA1,
Contacts.PHOTO_ID,
+ Contacts.LOOKUP_KEY,
};
public static final int CONTACT_ID = 0;
public static final int DISPLAY_NAME = 1;
public static final int PRESENCE_STATUS = 2;
public static final int DATA_ID = 3;
- public static final int MIMETYPE = 4;
+ public static final int MIMETYPE_ID = 4;
public static final int IS_SUPER_PRIMARY = 5;
public static final int ORGANIZATION = 6;
- public static final int EMAIL = 7;
- public static final int PHONE = 8;
- public static final int PHOTO_ID = 9;
+ public static final int EMAIL = 6;
+ public static final int PHONE = 6;
+ public static final int PHOTO_ID = 7;
+ public static final int LOOKUP_KEY = 8;
}
private static class SearchSuggestion {
- String contactId;
+ long contactId;
boolean titleIsName;
String organization;
String email;
String phoneNumber;
Uri photoUri;
+ String lookupKey;
String normalizedName;
int presence = -1;
boolean processed;
@@ -136,7 +127,7 @@
String icon2;
public SearchSuggestion(long contactId) {
- this.contactId = String.valueOf(contactId);
+ this.contactId = contactId;
}
private void process() {
@@ -174,6 +165,11 @@
processed = true;
}
+ /**
+ * Returns key for sorting search suggestions.
+ *
+ * <p>TODO: switch to new sort key
+ */
public String getSortKey() {
if (normalizedName == null) {
process();
@@ -193,8 +189,8 @@
list.add(text2);
list.add(icon1);
list.add(icon2);
- list.add(contactId);
- list.add(contactId);
+ list.add(lookupKey);
+ list.add(lookupKey);
} else {
for (int i = 0; i < projection.length; i++) {
addColumnValue(list, projection[i]);
@@ -215,9 +211,9 @@
} else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) {
list.add(icon2);
} else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) {
- list.add(contactId);
+ list.add(lookupKey);
} else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) {
- list.add(contactId);
+ list.add(lookupKey);
} else {
throw new IllegalArgumentException("Invalid column name: " + column);
}
@@ -225,9 +221,34 @@
}
private final ContactsProvider2 mContactsProvider;
+ private boolean mMimeTypeIdsLoaded;
+ private long mMimeTypeIdEmail;
+ private long mMimeTypeIdStructuredName;
+ private long mMimeTypeIdOrganization;
+ private long mMimeTypeIdPhone;
+ @SuppressWarnings("all")
public GlobalSearchSupport(ContactsProvider2 contactsProvider) {
mContactsProvider = contactsProvider;
+
+ // To ensure the data column position. This is dead code if properly configured.
+ if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
+ || Email.DATA != Data.DATA1) {
+ throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
+ + " data is not in DATA1 column");
+ }
+ }
+
+ private void ensureMimetypeIdsLoaded() {
+ if (!mMimeTypeIdsLoaded) {
+ ContactsDatabaseHelper dbHelper = (ContactsDatabaseHelper)mContactsProvider
+ .getDatabaseHelper();
+ mMimeTypeIdStructuredName = dbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
+ mMimeTypeIdOrganization = dbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
+ mMimeTypeIdPhone = dbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
+ mMimeTypeIdEmail = dbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
+ mMimeTypeIdsLoaded = true;
+ }
}
public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String limit) {
@@ -243,11 +264,30 @@
}
}
- public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, long contactId, String[] projection) {
+ /**
+ * Returns a search suggestions cursor for the contact bearing the provided lookup key. If the
+ * lookup key cannot be found in the database, the contact name is decoded from the lookup key
+ * and used to re-identify the contact. If the contact still cannot be found, an empty cursor
+ * is returned.
+ *
+ * <p>Note that if {@code lookupKey} is not a valid lookup key, an empty cursor is returned
+ * silently. This would occur with old-style shortcuts that were created using the contact id
+ * instead of the lookup key.
+ */
+ public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String lookupKey,
+ String[] projection) {
+ ensureMimetypeIdsLoaded();
+ long contactId;
+ try {
+ contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey);
+ } catch (IllegalArgumentException e) {
+ contactId = -1L;
+ }
StringBuilder sb = new StringBuilder();
sb.append(mContactsProvider.getContactsRestrictions());
- sb.append(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
- return buildCursorForSearchSuggestions(db, sb.toString(), projection);
+ appendMimeTypeFilter(sb);
+ sb.append(" AND " + ContactsColumns.CONCRETE_ID + "=" + contactId);
+ return buildCursorForSearchSuggestions(db, sb.toString(), projection, null);
}
private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
@@ -296,22 +336,46 @@
private Cursor buildCursorForSearchSuggestionsBasedOnName(SQLiteDatabase db,
String searchClause, String limit) {
-
+ ensureMimetypeIdsLoaded();
StringBuilder sb = new StringBuilder();
sb.append(mContactsProvider.getContactsRestrictions());
+ appendMimeTypeFilter(sb);
sb.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN ");
- mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause, limit);
- sb.append(" AND " + Contacts.IN_VISIBLE_GROUP + "=1");
+ mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause);
- return buildCursorForSearchSuggestions(db, sb.toString(), null);
+ /*
+ * Prepending "+" to the IN_VISIBLE_GROUP column disables the index on the
+ * that column. The logic is this: let's say we have 10,000 contacts
+ * of which 500 are visible. The first letter we type narrows this down
+ * to 10,000/26 = 384, which is already less than 500 that we would get
+ * from the IN_VISIBLE_GROUP index. Typing the second letter will narrow
+ * the search down to 10,000/26/26 = 14 contacts. And a lot of people
+ * will have more that 5% of their contacts visible, while the alphabet
+ * will always have 26 letters.
+ */
+ sb.append(" AND " + "+" + Contacts.IN_VISIBLE_GROUP + "=1");
+ String selection = sb.toString();
+
+ return buildCursorForSearchSuggestions(db, selection, null, limit);
}
- private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db, String selection,
- String[] projection) {
+ private void appendMimeTypeFilter(StringBuilder sb) {
+
+ /*
+ * The "+" syntax prevents the mime type index from being used - we just want
+ * to reduce the size of the result set, not actually search by mime types.
+ */
+ sb.append(" AND " + "+" + DataColumns.MIMETYPE_ID + " IN (" + mMimeTypeIdEmail + "," +
+ mMimeTypeIdOrganization + "," + mMimeTypeIdPhone + "," +
+ mMimeTypeIdStructuredName + ")");
+ }
+
+ private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db,
+ String selection, String[] projection, String limit) {
ArrayList<SearchSuggestion> suggestionList = new ArrayList<SearchSuggestion>();
HashMap<Long, SearchSuggestion> suggestionMap = new HashMap<Long, SearchSuggestion>();
- Cursor c = db.query(true, SearchSuggestionQuery.TABLE,
- SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, null);
+ Cursor c = db.query(false, SearchSuggestionQuery.TABLE,
+ SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, limit);
try {
while (c.moveToNext()) {
@@ -330,18 +394,18 @@
suggestion.presence = c.getInt(SearchSuggestionQuery.PRESENCE_STATUS);
}
- String mimetype = c.getString(SearchSuggestionQuery.MIMETYPE);
- if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ long mimetype = c.getLong(SearchSuggestionQuery.MIMETYPE_ID);
+ if (mimetype == mMimeTypeIdStructuredName) {
suggestion.titleIsName = true;
- } else if (Organization.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ } else if (mimetype == mMimeTypeIdOrganization) {
if (isSuperPrimary || suggestion.organization == null) {
suggestion.organization = c.getString(SearchSuggestionQuery.ORGANIZATION);
}
- } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ } else if (mimetype == mMimeTypeIdEmail) {
if (isSuperPrimary || suggestion.email == null) {
suggestion.email = c.getString(SearchSuggestionQuery.EMAIL);
}
- } else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+ } else if (mimetype == mMimeTypeIdPhone) {
if (isSuperPrimary || suggestion.phoneNumber == null) {
suggestion.phoneNumber = c.getString(SearchSuggestionQuery.PHONE);
}
@@ -352,6 +416,8 @@
ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
Photo.CONTENT_DIRECTORY);
}
+
+ suggestion.lookupKey = c.getString(SearchSuggestionQuery.LOOKUP_KEY);
}
} finally {
c.close();
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index 65120f9..898eb45 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -45,6 +45,7 @@
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.Extensions;
import android.provider.Contacts.People;
+import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
@@ -188,6 +189,18 @@
private static final Uri LIVE_FOLDERS_CONTACTS_FAVORITES_URI = Uri.withAppendedPath(
ContactsContract.AUTHORITY_URI, "live_folders/favorites");
+ private static final String CONTACTS_UPDATE_LASTTIMECONTACTED =
+ "UPDATE " + Tables.CONTACTS +
+ " SET " + Contacts.LAST_TIME_CONTACTED + "=? " +
+ "WHERE " + Contacts._ID + "=?";
+ private static final String RAWCONTACTS_UPDATE_LASTTIMECONTACTED =
+ "UPDATE " + Tables.RAW_CONTACTS + " SET "
+ + RawContacts.LAST_TIME_CONTACTED + "=? WHERE "
+ + RawContacts._ID + "=?";
+
+ private String[] mSelectionArgs1 = new String[1];
+ private String[] mSelectionArgs2 = new String[2];
+
public interface LegacyTables {
public static final String PEOPLE = "view_v1_people";
public static final String PEOPLE_JOIN_PRESENCE = "view_v1_people people " + PRESENCE_JOINS;
@@ -335,7 +348,7 @@
SEARCH_SUGGESTIONS);
matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
SEARCH_SUGGESTIONS);
- matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
+ matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
SEARCH_SHORTCUT);
matcher.addURI(authority, "settings", SETTINGS);
@@ -484,8 +497,6 @@
private final NameSplitter mPhoneticNameSplitter;
private final GlobalSearchSupport mGlobalSearchSupport;
- /** Precompiled sql statement for incrementing times contacted for a contact */
- private final SQLiteStatement mLastTimeContactedUpdate;
private final SQLiteStatement mDataMimetypeQuery;
private final SQLiteStatement mDataRawContactIdQuery;
@@ -512,10 +523,6 @@
.getDefault());
SQLiteDatabase db = mDbHelper.getReadableDatabase();
- mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
- + RawContacts.LAST_TIME_CONTACTED + "=? WHERE "
- + RawContacts._ID + "=?");
-
mDataMimetypeQuery = db.compileStatement(
"SELECT " + DataColumns.MIMETYPE_ID +
" FROM " + Tables.DATA +
@@ -1144,14 +1151,20 @@
long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
long contactId = mDbHelper.getContactId(rawContactId);
+ SQLiteDatabase mDb = mDbHelper.getWritableDatabase();
+ mSelectionArgs2[0] = String.valueOf(lastTimeContacted);
if (contactId != 0) {
- mContactsProvider.updateContactLastContactedTime(contactId, lastTimeContacted);
+ mSelectionArgs2[1] = String.valueOf(contactId);
+ mDb.execSQL(CONTACTS_UPDATE_LASTTIMECONTACTED, mSelectionArgs2);
+ // increment times_contacted column
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ mDb.execSQL(ContactsProvider2.UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
}
-
- mLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
- mLastTimeContactedUpdate.bindLong(2, rawContactId);
- mLastTimeContactedUpdate.execute();
-
+ mSelectionArgs2[1] = String.valueOf(rawContactId);
+ mDb.execSQL(RAWCONTACTS_UPDATE_LASTTIMECONTACTED, mSelectionArgs2);
+ // increment times_contacted column
+ mSelectionArgs1[0] = String.valueOf(contactId);
+ mDb.execSQL(ContactsProvider2.UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
return 1;
}
@@ -1531,7 +1544,7 @@
switch (match) {
case PEOPLE:
case PEOPLE_ID:
- count = mContactsProvider.deleteRawContact(id, false);
+ count = mContactsProvider.deleteRawContact(id, mDbHelper.getContactId(id), false);
break;
case PEOPLE_PHOTO:
@@ -1857,8 +1870,8 @@
return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
case SEARCH_SHORTCUT: {
- long contactId = ContentUris.parseId(uri);
- return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection);
+ String lookupKey = uri.getLastPathSegment();
+ return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
}
case LIVE_FOLDERS_PEOPLE:
diff --git a/src/com/android/providers/contacts/LegacyContactImporter.java b/src/com/android/providers/contacts/LegacyContactImporter.java
index 9c17e4a..2f63637 100644
--- a/src/com/android/providers/contacts/LegacyContactImporter.java
+++ b/src/com/android/providers/contacts/LegacyContactImporter.java
@@ -383,7 +383,7 @@
Contacts.SEND_TO_VOICEMAIL + "," +
Contacts.STARRED + "," +
Contacts.TIMES_CONTACTED + "," +
- Contacts.DISPLAY_NAME +
+ Contacts.NAME_RAW_CONTACT_ID +
") VALUES (?,?,?,?,?,?,?)";
int ID = 1;
@@ -392,7 +392,7 @@
int SEND_TO_VOICEMAIL = 4;
int STARRED = 5;
int TIMES_CONTACTED = 6;
- int DISPLAY_NAME = 7;
+ int NAME_RAW_CONTACT_ID = 7;
}
private interface StructuredNameInsert {
@@ -518,8 +518,7 @@
c.getLong(PeopleQuery.STARRED));
insert.bindLong(ContactsInsert.TIMES_CONTACTED,
c.getLong(PeopleQuery.TIMES_CONTACTED));
- bindString(insert, ContactsInsert.DISPLAY_NAME,
- c.getString(PeopleQuery.NAME));
+ insert.bindLong(ContactsInsert.NAME_RAW_CONTACT_ID, id);
insert(insert);
}
@@ -814,12 +813,14 @@
String INSERT_SQL = "INSERT INTO " + Tables.PHONE_LOOKUP + "(" +
PhoneLookupColumns.RAW_CONTACT_ID + "," +
PhoneLookupColumns.DATA_ID + "," +
- PhoneLookupColumns.NORMALIZED_NUMBER +
- ") VALUES (?,?,?)";
+ PhoneLookupColumns.NORMALIZED_NUMBER + "," +
+ PhoneLookupColumns.MIN_MATCH +
+ ") VALUES (?,?,?,?)";
int RAW_CONTACT_ID = 1;
int DATA_ID = 2;
int NORMALIZED_NUMBER = 3;
+ int MIN_MATCH = 4;
}
private interface HasPhoneNumberUpdate {
@@ -868,6 +869,8 @@
phoneLookupInsert.bindLong(PhoneLookupInsert.RAW_CONTACT_ID, id);
phoneLookupInsert.bindLong(PhoneLookupInsert.DATA_ID, dataId);
phoneLookupInsert.bindString(PhoneLookupInsert.NORMALIZED_NUMBER, normalizedNumber);
+ phoneLookupInsert.bindString(PhoneLookupInsert.MIN_MATCH,
+ PhoneNumberUtils.toCallerIDMinMatch(number));
insert(phoneLookupInsert);
if (lastUpdatedContact != id) {
diff --git a/src/com/android/providers/contacts/NameSplitter.java b/src/com/android/providers/contacts/NameSplitter.java
index 873f83b..0b98343 100644
--- a/src/com/android/providers/contacts/NameSplitter.java
+++ b/src/com/android/providers/contacts/NameSplitter.java
@@ -15,10 +15,17 @@
*/
package com.android.providers.contacts;
+import com.android.internal.util.HanziToPinyin;
+import com.android.internal.util.HanziToPinyin.Token;
+
import android.content.ContentValues;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhoneticNameStyle;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.text.TextUtils;
+import java.lang.Character.UnicodeBlock;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.Locale;
import java.util.StringTokenizer;
@@ -42,19 +49,34 @@
public static final int MAX_TOKENS = 10;
+ private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
+ private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase();
+
+ // This includes simplified and traditional Chinese
+ private static final String CHINESE_LANGUAGE = Locale.CHINESE.getLanguage().toLowerCase();
+
private final HashSet<String> mPrefixesSet;
private final HashSet<String> mSuffixesSet;
private final int mMaxSuffixLength;
private final HashSet<String> mLastNamePrefixesSet;
private final HashSet<String> mConjuctions;
private final Locale mLocale;
+ private final String mLanguage;
public static class Name {
- private String prefix;
- private String givenNames;
- private String middleName;
- private String familyName;
- private String suffix;
+ public String prefix;
+ public String givenNames;
+ public String middleName;
+ public String familyName;
+ public String suffix;
+
+ public int fullNameStyle;
+
+ public String phoneticFamilyName;
+ public String phoneticMiddleName;
+ public String phoneticGivenName;
+
+ public int phoneticNameStyle;
public Name() {
}
@@ -94,20 +116,73 @@
middleName = values.getAsString(StructuredName.MIDDLE_NAME);
familyName = values.getAsString(StructuredName.FAMILY_NAME);
suffix = values.getAsString(StructuredName.SUFFIX);
+
+ Integer integer = values.getAsInteger(StructuredName.FULL_NAME_STYLE);
+ fullNameStyle = integer == null ? FullNameStyle.UNDEFINED : integer;
+
+ phoneticFamilyName = values.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+ phoneticMiddleName = values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+ phoneticGivenName = values.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+
+ integer = values.getAsInteger(StructuredName.PHONETIC_NAME_STYLE);
+ phoneticNameStyle = integer == null ? PhoneticNameStyle.UNDEFINED : integer;
}
public void toValues(ContentValues values) {
- values.put(StructuredName.PREFIX, prefix);
- values.put(StructuredName.GIVEN_NAME, givenNames);
- values.put(StructuredName.MIDDLE_NAME, middleName);
- values.put(StructuredName.FAMILY_NAME, familyName);
- values.put(StructuredName.SUFFIX, suffix);
+ putValueIfPresent(values, StructuredName.PREFIX, prefix);
+ putValueIfPresent(values, StructuredName.GIVEN_NAME, givenNames);
+ putValueIfPresent(values, StructuredName.MIDDLE_NAME, middleName);
+ putValueIfPresent(values, StructuredName.FAMILY_NAME, familyName);
+ putValueIfPresent(values, StructuredName.SUFFIX, suffix);
+ values.put(StructuredName.FULL_NAME_STYLE, fullNameStyle);
+ putValueIfPresent(values, StructuredName.PHONETIC_FAMILY_NAME, phoneticFamilyName);
+ putValueIfPresent(values, StructuredName.PHONETIC_MIDDLE_NAME, phoneticMiddleName);
+ putValueIfPresent(values, StructuredName.PHONETIC_GIVEN_NAME, phoneticGivenName);
+ values.put(StructuredName.PHONETIC_NAME_STYLE, phoneticNameStyle);
}
+
+ private void putValueIfPresent(ContentValues values, String name, String value) {
+ if (value != null) {
+ values.put(name, value);
+ }
+ }
+
+ public void clear() {
+ prefix = null;
+ givenNames = null;
+ middleName = null;
+ familyName = null;
+ suffix = null;
+ fullNameStyle = FullNameStyle.UNDEFINED;
+ phoneticFamilyName = null;
+ phoneticMiddleName = null;
+ phoneticGivenName = null;
+ phoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+ }
+
+ public boolean isEmpty() {
+ return TextUtils.isEmpty(givenNames)
+ && TextUtils.isEmpty(middleName)
+ && TextUtils.isEmpty(familyName)
+ && TextUtils.isEmpty(suffix)
+ && TextUtils.isEmpty(phoneticFamilyName)
+ && TextUtils.isEmpty(phoneticMiddleName)
+ && TextUtils.isEmpty(phoneticGivenName);
+ }
+
+ @Override
+ public String toString() {
+ return "[given: " + givenNames + " middle: " + middleName + " family: " + familyName
+ + " ph/given: " + phoneticGivenName + " ph/middle: " + phoneticMiddleName
+ + " ph/family: " + phoneticFamilyName + "]";
+ }
+
}
private static class NameTokenizer extends StringTokenizer {
private final String[] mTokens;
private int mDotBitmask;
+ private int mCommaBitmask;
private int mStartPointer;
private int mEndPointer;
@@ -122,13 +197,15 @@
final String token = nextToken();
if (token.length() > 0) {
final char c = token.charAt(0);
- if (c == ' ' || c == ',') {
+ if (c == ' ') {
continue;
}
}
if (mEndPointer > 0 && token.charAt(0) == '.') {
mDotBitmask |= (1 << (mEndPointer - 1));
+ } else if (mEndPointer > 0 && token.charAt(0) == ',') {
+ mCommaBitmask |= (1 << (mEndPointer - 1));
} else {
mTokens[mEndPointer] = token;
mEndPointer++;
@@ -142,6 +219,13 @@
public boolean hasDot(int index) {
return (mDotBitmask & (1 << index)) != 0;
}
+
+ /**
+ * Returns true if the token is followed by a comma in the original full name.
+ */
+ public boolean hasComma(int index) {
+ return (mCommaBitmask & (1 << index)) != 0;
+ }
}
/**
@@ -150,7 +234,7 @@
* @param commonPrefixes comma-separated list of common prefixes,
* e.g. "Mr, Ms, Mrs"
* @param commonLastNamePrefixes comma-separated list of common last name prefixes,
- * e.g. "d', st, st., von"
+ * e.g. "d', st, st., von"
* @param commonSuffixes comma-separated list of common suffixes,
* e.g. "Jr, M.D., MD, D.D.S."
* @param commonConjunctions comma-separated list of common conjuctions,
@@ -163,7 +247,8 @@
mLastNamePrefixesSet = convertToSet(commonLastNamePrefixes);
mSuffixesSet = convertToSet(commonSuffixes);
mConjuctions = convertToSet(commonConjunctions);
- mLocale = locale;
+ mLocale = locale != null ? locale : Locale.getDefault();
+ mLanguage = mLocale.getLanguage().toLowerCase();
int maxLength = 0;
for (String suffix : mSuffixesSet) {
@@ -225,6 +310,36 @@
return;
}
+ int fullNameStyle = guessFullNameStyle(fullName);
+ if (fullNameStyle == FullNameStyle.CJK) {
+ fullNameStyle = getAdjustedFullNameStyle(fullNameStyle);
+ }
+
+ name.fullNameStyle = fullNameStyle;
+
+ switch (fullNameStyle) {
+ case FullNameStyle.CHINESE:
+ splitChineseName(name, fullName);
+ break;
+
+ case FullNameStyle.JAPANESE:
+ case FullNameStyle.KOREAN:
+ splitJapaneseOrKoreanName(name, fullName);
+ break;
+
+ default:
+ splitWesternName(name, fullName);
+ }
+ }
+
+ /**
+ * Splits a full name composed according to the Western tradition:
+ * <pre>
+ * [prefix] given name(s) [[middle name] family name] [, suffix]
+ * [prefix] family name, given name [middle name] [,suffix]
+ * </pre>
+ */
+ private void splitWesternName(Name name, String fullName) {
NameTokenizer tokens = new NameTokenizer(fullName);
parsePrefix(name, tokens);
@@ -244,6 +359,70 @@
}
/**
+ * Splits a full name composed according to the Chinese tradition:
+ * <pre>
+ * [family name [middle name]] given name
+ * </pre>
+ */
+ private void splitChineseName(Name name, String fullName) {
+ StringTokenizer tokenizer = new StringTokenizer(fullName);
+ while (tokenizer.hasMoreTokens()) {
+ String token = tokenizer.nextToken();
+ if (name.givenNames == null) {
+ name.givenNames = token;
+ } else if (name.familyName == null) {
+ name.familyName = name.givenNames;
+ name.givenNames = token;
+ } else if (name.middleName == null) {
+ name.middleName = name.givenNames;
+ name.givenNames = token;
+ } else {
+ name.middleName = name.middleName + name.givenNames;
+ name.givenNames = token;
+ }
+ }
+
+ // If a single word parse that word up.
+ if (name.givenNames != null && name.familyName == null && name.middleName == null) {
+ int length = fullName.length();
+ if (length == 2) {
+ name.familyName = fullName.substring(0, 1);
+ name.givenNames = fullName.substring(1);
+ } else if (length == 3) {
+ name.familyName = fullName.substring(0, 1);
+ name.middleName = fullName.substring(1, 2);
+ name.givenNames = fullName.substring(2);
+ } else if (length == 4) {
+ name.familyName = fullName.substring(0, 2);
+ name.middleName = fullName.substring(2, 3);
+ name.givenNames = fullName.substring(3);
+ }
+
+ }
+ }
+
+ /**
+ * Splits a full name composed according to the Japanese tradition:
+ * <pre>
+ * [family name] given name(s)
+ * </pre>
+ */
+ private void splitJapaneseOrKoreanName(Name name, String fullName) {
+ StringTokenizer tokenizer = new StringTokenizer(fullName);
+ while (tokenizer.hasMoreTokens()) {
+ String token = tokenizer.nextToken();
+ if (name.givenNames == null) {
+ name.givenNames = token;
+ } else if (name.familyName == null) {
+ name.familyName = name.givenNames;
+ name.givenNames = token;
+ } else {
+ name.givenNames += " " + token;
+ }
+ }
+ }
+
+ /**
* Flattens the given {@link Name} into a single field, usually for storage
* in {@link StructuredName#DISPLAY_NAME}.
*/
@@ -264,6 +443,200 @@
}
/**
+ * Concatenates components of a name according to the rules dictated by the name style.
+ *
+ * @param givenNameFirst is ignored for CJK display name styles
+ */
+ public String join(Name name, boolean givenNameFirst) {
+ switch (name.fullNameStyle) {
+ case FullNameStyle.CHINESE:
+ case FullNameStyle.KOREAN:
+ return join(name.familyName, name.middleName, name.givenNames, name.suffix,
+ false, false, false);
+
+ case FullNameStyle.JAPANESE:
+ return join(name.familyName, name.middleName, name.givenNames, name.suffix,
+ true, false, false);
+
+ default:
+ if (givenNameFirst) {
+ return join(name.givenNames, name.middleName, name.familyName, name.suffix,
+ true, false, true);
+ } else {
+ return join(name.familyName, name.givenNames, name.middleName, name.suffix,
+ true, true, true);
+ }
+ }
+ }
+
+ /**
+ * Concatenates components of the phonetic name following the CJK tradition:
+ * family name + middle name + given name(s).
+ */
+ public String joinPhoneticName(Name name) {
+ return join(name.phoneticFamilyName, name.phoneticMiddleName,
+ name.phoneticGivenName, null, true, false, false);
+ }
+
+ /**
+ * Given a name in Chinese, returns a Pinyin representation.
+ */
+ public String convertHanziToPinyin(String name) {
+
+ // TODO: move this code to HanziToPinyin and optimize
+ ArrayList<Token> tokens = HanziToPinyin.getInstance().get(name);
+ if (tokens != null) {
+ int size = tokens.size();
+ if (size != 0) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < size; i++) {
+ String pinyin = tokens.get(i).target;
+ if (!TextUtils.isEmpty(pinyin)) {
+ if (sb.length() != 0) {
+ sb.append(' ');
+ }
+ sb.append(pinyin);
+ }
+ }
+ return sb.toString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Concatenates parts of a full name inserting spaces and commas as specified.
+ */
+ private String join(String part1, String part2, String part3, String suffix,
+ boolean useSpace, boolean useCommaAfterPart1, boolean useCommaAfterPart3) {
+ boolean hasPart1 = !TextUtils.isEmpty(part1);
+ boolean hasPart2 = !TextUtils.isEmpty(part2);
+ boolean hasPart3 = !TextUtils.isEmpty(part3);
+ boolean hasSuffix = !TextUtils.isEmpty(suffix);
+
+ boolean isSingleWord = true;
+ String singleWord = null;
+ if (hasPart1) {
+ singleWord = part1;
+ }
+
+ if (hasPart2) {
+ if (singleWord != null) {
+ isSingleWord = false;
+ } else {
+ singleWord = part2;
+ }
+ }
+
+ if (hasPart3) {
+ if (singleWord != null) {
+ isSingleWord = false;
+ } else {
+ singleWord = part3;
+ }
+ }
+
+ if (hasSuffix) {
+ if (singleWord != null) {
+ isSingleWord = false;
+ } else {
+ singleWord = normalizedSuffix(suffix);
+ }
+ }
+
+ if (isSingleWord) {
+ return singleWord;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (hasPart1) {
+ sb.append(part1);
+ }
+
+ if (hasPart2) {
+ if (hasPart1) {
+ if (useCommaAfterPart1) {
+ sb.append(',');
+ }
+ if (useSpace) {
+ sb.append(' ');
+ }
+ }
+ sb.append(part2);
+ }
+
+ if (hasPart3) {
+ if (hasPart1 || hasPart2) {
+ if (useSpace) {
+ sb.append(' ');
+ }
+ }
+ sb.append(part3);
+ }
+
+ if (hasSuffix) {
+ if (hasPart1 || hasPart2 || hasPart3) {
+ if (useCommaAfterPart3) {
+ sb.append(',');
+ }
+ if (useSpace) {
+ sb.append(' ');
+ }
+ }
+ sb.append(normalizedSuffix(suffix));
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Puts a dot after the supplied suffix if that is the accepted form of the suffix,
+ * e.g. "Jr." and "Sr.", but not "I", "II" and "III".
+ */
+ private String normalizedSuffix(String suffix) {
+ int length = suffix.length();
+ if (length == 0 || suffix.charAt(length - 1) == '.') {
+ return suffix;
+ }
+
+ String withDot = suffix + '.';
+ if (mSuffixesSet.contains(withDot.toUpperCase())) {
+ return withDot;
+ } else {
+ return suffix;
+ }
+ }
+
+ /**
+ * If the supplied name style is undefined, returns a default based on the language,
+ * otherwise returns the supplied name style itself.
+ *
+ * @param nameStyle See {@link FullNameStyle}.
+ */
+ public int getAdjustedFullNameStyle(int nameStyle) {
+ if (nameStyle == FullNameStyle.UNDEFINED) {
+ if (JAPANESE_LANGUAGE.equals(mLanguage)) {
+ return FullNameStyle.JAPANESE;
+ } else if (KOREAN_LANGUAGE.equals(mLanguage)) {
+ return FullNameStyle.KOREAN;
+ } else if (CHINESE_LANGUAGE.equals(mLanguage)) {
+ return FullNameStyle.CHINESE;
+ } else {
+ return FullNameStyle.WESTERN;
+ }
+ } else if (nameStyle == FullNameStyle.CJK) {
+ if (JAPANESE_LANGUAGE.equals(mLanguage)) {
+ return FullNameStyle.JAPANESE;
+ } else if (KOREAN_LANGUAGE.equals(mLanguage)) {
+ return FullNameStyle.KOREAN;
+ } else {
+ return FullNameStyle.CHINESE;
+ }
+ }
+ return nameStyle;
+ }
+
+ /**
* Parses the first word from the name if it is a prefix.
*/
private void parsePrefix(Name name, NameTokenizer tokens) {
@@ -333,15 +706,36 @@
return;
}
+ // If the first word is followed by a comma, assume that it's the family name
+ if (tokens.hasComma(tokens.mStartPointer)) {
+ name.familyName = tokens.mTokens[tokens.mStartPointer];
+ tokens.mStartPointer++;
+ return;
+ }
+
+ // If the second word is followed by a comma and the first word
+ // is a last name prefix as in "de Sade" and "von Cliburn", treat
+ // the first two words as the family name.
+ if (tokens.mStartPointer + 1 < tokens.mEndPointer
+ && tokens.hasComma(tokens.mStartPointer + 1)
+ && isFamilyNamePrefix(tokens.mTokens[tokens.mStartPointer])) {
+ String familyNamePrefix = tokens.mTokens[tokens.mStartPointer];
+ if (tokens.hasDot(tokens.mStartPointer)) {
+ familyNamePrefix += '.';
+ }
+ name.familyName = familyNamePrefix + " " + tokens.mTokens[tokens.mStartPointer + 1];
+ tokens.mStartPointer += 2;
+ return;
+ }
+
+ // Finally, assume that the last word is the last name
name.familyName = tokens.mTokens[tokens.mEndPointer - 1];
tokens.mEndPointer--;
- // Take care of last names like "D'Onofrio" and "von Cliburn"
+ // Take care of last names like "de Sade" and "von Cliburn"
if ((tokens.mEndPointer - tokens.mStartPointer) > 0) {
String lastNamePrefix = tokens.mTokens[tokens.mEndPointer - 1];
- final String normalized = lastNamePrefix.toUpperCase();
- if (mLastNamePrefixesSet.contains(normalized)
- || mLastNamePrefixesSet.contains(normalized + ".")) {
+ if (isFamilyNamePrefix(lastNamePrefix)) {
if (tokens.hasDot(tokens.mEndPointer - 1)) {
lastNamePrefix += '.';
}
@@ -351,6 +745,16 @@
}
}
+ /**
+ * Returns true if the supplied word is an accepted last name prefix, e.g. "von", "de"
+ */
+ private boolean isFamilyNamePrefix(String word) {
+ final String normalized = word.toUpperCase();
+
+ return mLastNamePrefixesSet.contains(normalized)
+ || mLastNamePrefixesSet.contains(normalized + ".");
+ }
+
private void parseMiddleName(Name name, NameTokenizer tokens) {
if (tokens.mStartPointer == tokens.mEndPointer) {
@@ -362,6 +766,9 @@
|| !mConjuctions.contains(tokens.mTokens[tokens.mEndPointer - 2].
toUpperCase())) {
name.middleName = tokens.mTokens[tokens.mEndPointer - 1];
+ if (tokens.hasDot(tokens.mEndPointer - 1)) {
+ name.middleName += '.';
+ }
tokens.mEndPointer--;
}
}
@@ -388,4 +795,226 @@
name.givenNames = sb.toString();
}
}
+
+ /**
+ * Makes the best guess at the expected full name style based on the character set
+ * used in the supplied name. If the phonetic name is also supplied, tries to
+ * differentiate between Chinese, Japanese and Korean based on the alphabet used
+ * for the phonetic name.
+ */
+ public void guessNameStyle(Name name) {
+ guessFullNameStyle(name);
+ if (FullNameStyle.CJK == name.fullNameStyle) {
+ name.fullNameStyle = getAdjustedFullNameStyle(name.fullNameStyle);
+ }
+ guessPhoneticNameStyle(name);
+ name.fullNameStyle = getAdjustedNameStyleBasedOnPhoneticNameStyle(name.fullNameStyle,
+ name.phoneticNameStyle);
+ }
+
+ /**
+ * Updates the display name style according to the phonetic name style if we
+ * were unsure about display name style based on the name components, but
+ * phonetic name makes it more definitive.
+ */
+ public int getAdjustedNameStyleBasedOnPhoneticNameStyle(int nameStyle, int phoneticNameStyle) {
+ if (phoneticNameStyle != PhoneticNameStyle.UNDEFINED) {
+ if (nameStyle == FullNameStyle.UNDEFINED || nameStyle == FullNameStyle.CJK) {
+ if (phoneticNameStyle == PhoneticNameStyle.JAPANESE) {
+ return FullNameStyle.JAPANESE;
+ } else if (phoneticNameStyle == PhoneticNameStyle.KOREAN) {
+ return FullNameStyle.KOREAN;
+ }
+ if (nameStyle == FullNameStyle.CJK && phoneticNameStyle == PhoneticNameStyle.PINYIN) {
+ return FullNameStyle.CHINESE;
+ }
+ }
+ }
+ return nameStyle;
+ }
+
+ /**
+ * Makes the best guess at the expected full name style based on the character set
+ * used in the supplied name.
+ */
+ private void guessFullNameStyle(NameSplitter.Name name) {
+ if (name.fullNameStyle != FullNameStyle.UNDEFINED) {
+ return;
+ }
+
+ int bestGuess = guessFullNameStyle(name.givenNames);
+ // A mix of Hanzi and latin chars are common in China, so we have to go through all names
+ // if the name is not JANPANESE or KOREAN.
+ if (bestGuess != FullNameStyle.UNDEFINED && bestGuess != FullNameStyle.CJK
+ && bestGuess != FullNameStyle.WESTERN) {
+ name.fullNameStyle = bestGuess;
+ return;
+ }
+
+ int guess = guessFullNameStyle(name.familyName);
+ if (guess != FullNameStyle.UNDEFINED) {
+ if (guess != FullNameStyle.CJK && guess != FullNameStyle.WESTERN) {
+ name.fullNameStyle = guess;
+ return;
+ }
+ bestGuess = guess;
+ }
+
+ guess = guessFullNameStyle(name.middleName);
+ if (guess != FullNameStyle.UNDEFINED) {
+ if (guess != FullNameStyle.CJK && guess != FullNameStyle.WESTERN) {
+ name.fullNameStyle = guess;
+ return;
+ }
+ bestGuess = guess;
+ }
+
+ name.fullNameStyle = bestGuess;
+ }
+
+ public int guessFullNameStyle(String name) {
+ if (name == null) {
+ return FullNameStyle.UNDEFINED;
+ }
+
+ int nameStyle = FullNameStyle.UNDEFINED;
+ int length = name.length();
+ int offset = 0;
+ while (offset < length) {
+ int codePoint = Character.codePointAt(name, offset);
+ if (Character.isLetter(codePoint)) {
+ UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint);
+
+ if (!isLatinUnicodeBlock(unicodeBlock)) {
+
+ if (isCJKUnicodeBlock(unicodeBlock)) {
+ // We don't know if this is Chinese, Japanese or Korean -
+ // trying to figure out by looking at other characters in the name
+ return guessCJKNameStyle(name, offset + Character.charCount(codePoint));
+ }
+
+ if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) {
+ return FullNameStyle.JAPANESE;
+ }
+
+ if (isKoreanUnicodeBlock(unicodeBlock)) {
+ return FullNameStyle.KOREAN;
+ }
+ }
+ nameStyle = FullNameStyle.WESTERN;
+ }
+ offset += Character.charCount(codePoint);
+ }
+ return nameStyle;
+ }
+
+ private int guessCJKNameStyle(String name, int offset) {
+ int length = name.length();
+ while (offset < length) {
+ int codePoint = Character.codePointAt(name, offset);
+ if (Character.isLetter(codePoint)) {
+ UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint);
+ if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) {
+ return FullNameStyle.JAPANESE;
+ }
+ if (isKoreanUnicodeBlock(unicodeBlock)) {
+ return FullNameStyle.KOREAN;
+ }
+ }
+ offset += Character.charCount(codePoint);
+ }
+
+ return FullNameStyle.CJK;
+ }
+
+ private void guessPhoneticNameStyle(NameSplitter.Name name) {
+ if (name.phoneticNameStyle != PhoneticNameStyle.UNDEFINED) {
+ return;
+ }
+
+ int bestGuess = guessPhoneticNameStyle(name.phoneticFamilyName);
+ if (bestGuess != FullNameStyle.UNDEFINED && bestGuess != FullNameStyle.CJK) {
+ name.phoneticNameStyle = bestGuess;
+ return;
+ }
+
+ int guess = guessPhoneticNameStyle(name.phoneticGivenName);
+ if (guess != FullNameStyle.UNDEFINED) {
+ if (guess != FullNameStyle.CJK) {
+ name.phoneticNameStyle = guess;
+ return;
+ }
+ bestGuess = guess;
+ }
+
+ guess = guessPhoneticNameStyle(name.phoneticMiddleName);
+ if (guess != FullNameStyle.UNDEFINED) {
+ if (guess != FullNameStyle.CJK) {
+ name.phoneticNameStyle = guess;
+ return;
+ }
+ bestGuess = guess;
+ }
+ }
+
+ public int guessPhoneticNameStyle(String name) {
+ if (name == null) {
+ return PhoneticNameStyle.UNDEFINED;
+ }
+
+ int nameStyle = PhoneticNameStyle.UNDEFINED;
+ int length = name.length();
+ int offset = 0;
+ while (offset < length) {
+ int codePoint = Character.codePointAt(name, offset);
+ if (Character.isLetter(codePoint)) {
+ UnicodeBlock unicodeBlock = UnicodeBlock.of(codePoint);
+ if (isJapanesePhoneticUnicodeBlock(unicodeBlock)) {
+ return PhoneticNameStyle.JAPANESE;
+ }
+ if (isKoreanUnicodeBlock(unicodeBlock)) {
+ return PhoneticNameStyle.KOREAN;
+ }
+ if (isLatinUnicodeBlock(unicodeBlock)) {
+ return PhoneticNameStyle.PINYIN;
+ }
+ }
+ offset += Character.charCount(codePoint);
+ }
+
+ return nameStyle;
+ }
+
+ private static boolean isLatinUnicodeBlock(UnicodeBlock unicodeBlock) {
+ return unicodeBlock == UnicodeBlock.BASIC_LATIN ||
+ unicodeBlock == UnicodeBlock.LATIN_1_SUPPLEMENT ||
+ unicodeBlock == UnicodeBlock.LATIN_EXTENDED_A ||
+ unicodeBlock == UnicodeBlock.LATIN_EXTENDED_B ||
+ unicodeBlock == UnicodeBlock.LATIN_EXTENDED_ADDITIONAL;
+ }
+
+ private static boolean isCJKUnicodeBlock(UnicodeBlock block) {
+ return block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
+ || block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
+ || block == UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
+ || block == UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
+ || block == UnicodeBlock.CJK_RADICALS_SUPPLEMENT
+ || block == UnicodeBlock.CJK_COMPATIBILITY
+ || block == UnicodeBlock.CJK_COMPATIBILITY_FORMS
+ || block == UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
+ || block == UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT;
+ }
+
+ private static boolean isKoreanUnicodeBlock(UnicodeBlock unicodeBlock) {
+ return unicodeBlock == UnicodeBlock.HANGUL_SYLLABLES ||
+ unicodeBlock == UnicodeBlock.HANGUL_JAMO ||
+ unicodeBlock == UnicodeBlock.HANGUL_COMPATIBILITY_JAMO;
+ }
+
+ private static boolean isJapanesePhoneticUnicodeBlock(UnicodeBlock unicodeBlock) {
+ return unicodeBlock == UnicodeBlock.KATAKANA ||
+ unicodeBlock == UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS ||
+ unicodeBlock == UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS ||
+ unicodeBlock == UnicodeBlock.HIRAGANA;
+ }
}
diff --git a/src/com/android/providers/contacts/PostalSplitter.java b/src/com/android/providers/contacts/PostalSplitter.java
index 7729d71..812c34d 100644
--- a/src/com/android/providers/contacts/PostalSplitter.java
+++ b/src/com/android/providers/contacts/PostalSplitter.java
@@ -25,6 +25,8 @@
* Split and join {@link StructuredPostal} fields.
*/
public class PostalSplitter {
+ private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
+
private final Locale mLocale;
public static class Postal {
@@ -80,11 +82,87 @@
* storage in {@link StructuredPostal#FORMATTED_ADDRESS}.
*/
public String join(Postal postal) {
+ final String[] values = new String[] {
+ postal.street, postal.pobox, postal.neighborhood, postal.city,
+ postal.region, postal.postcode, postal.country
+ };
// TODO: split off to handle various locales
- return joinEnUs(postal);
+ if (mLocale != null &&
+ JAPANESE_LANGUAGE.equals(mLocale.getLanguage()) &&
+ !arePrintableAsciiOnly(values)) {
+ return joinJaJp(postal);
+ } else {
+ return joinEnUs(postal);
+ }
}
- private String joinEnUs(Postal postal) {
+ private String joinJaJp(final Postal postal) {
+ final boolean hasStreet = !TextUtils.isEmpty(postal.street);
+ final boolean hasPobox = !TextUtils.isEmpty(postal.pobox);
+ final boolean hasNeighborhood = !TextUtils.isEmpty(postal.neighborhood);
+ final boolean hasCity = !TextUtils.isEmpty(postal.city);
+ final boolean hasRegion = !TextUtils.isEmpty(postal.region);
+ final boolean hasPostcode = !TextUtils.isEmpty(postal.postcode);
+ final boolean hasCountry = !TextUtils.isEmpty(postal.country);
+
+ // First block: [country][ ][postcode]
+ // Second block: [region][ ][city][ ][neighborhood]
+ // Third block: [street][ ][pobox]
+
+ final StringBuilder builder = new StringBuilder();
+
+ final boolean hasFirstBlock = hasCountry || hasPostcode;
+ final boolean hasSecondBlock = hasRegion || hasCity || hasNeighborhood;
+ final boolean hasThirdBlock = hasStreet || hasPobox;
+
+ if (hasFirstBlock) {
+ if (hasCountry) {
+ builder.append(postal.country);
+ }
+ if (hasPostcode) {
+ if (hasCountry) builder.append(SPACE);
+ builder.append(postal.postcode);
+ }
+ }
+
+ if (hasSecondBlock) {
+ if (hasFirstBlock) {
+ builder.append(NEWLINE);
+ }
+ if (hasRegion) {
+ builder.append(postal.region);
+ }
+ if (hasCity) {
+ if (hasRegion) builder.append(SPACE);
+ builder.append(postal.city);
+ }
+ if (hasNeighborhood) {
+ if (hasRegion || hasCity) builder.append(SPACE);
+ builder.append(postal.neighborhood);
+ }
+ }
+
+ if (hasThirdBlock) {
+ if (hasFirstBlock || hasSecondBlock) {
+ builder.append(NEWLINE);
+ }
+ if (hasStreet) {
+ builder.append(postal.street);
+ }
+ if (hasPobox) {
+ if (hasStreet) builder.append(SPACE);
+ builder.append(postal.pobox);
+ }
+ }
+
+ if (builder.length() > 0) {
+ return builder.toString();
+ } else {
+ return null;
+ }
+ }
+
+ private String joinEnUs(final Postal postal) {
final boolean hasStreet = !TextUtils.isEmpty(postal.street);
final boolean hasPobox = !TextUtils.isEmpty(postal.pobox);
final boolean hasNeighborhood = !TextUtils.isEmpty(postal.neighborhood);
@@ -149,4 +227,19 @@
return null;
}
}
+
+ private static boolean arePrintableAsciiOnly(final String[] values) {
+ if (values == null) {
+ return true;
+ }
+ for (final String value : values) {
+ if (TextUtils.isEmpty(value)) {
+ continue;
+ }
+ if (!TextUtils.isPrintableAsciiOnly(value)) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/src/com/android/providers/contacts/SQLiteContentProvider.java b/src/com/android/providers/contacts/SQLiteContentProvider.java
index 4d1ec90..56903bf 100644
--- a/src/com/android/providers/contacts/SQLiteContentProvider.java
+++ b/src/com/android/providers/contacts/SQLiteContentProvider.java
@@ -44,6 +44,11 @@
private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
+ /**
+ * Maximum number of operations allowed in a batch between yield points.
+ */
+ private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
+
@Override
public boolean onCreate() {
Context context = getContext();
@@ -186,6 +191,8 @@
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
+ int ypCount = 0;
+ int opCount = 0;
mDb = mOpenHelper.getWritableDatabase();
mDb.beginTransactionWithListener(this);
try {
@@ -193,9 +200,18 @@
final int numOperations = operations.size();
final ContentProviderResult[] results = new ContentProviderResult[numOperations];
for (int i = 0; i < numOperations; i++) {
+ if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
+ throw new OperationApplicationException(
+ "Too many content provider operations between yield points. "
+ + "The maximum number of operations per yield point is "
+ + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
+ }
final ContentProviderOperation operation = operations.get(i);
if (i > 0 && operation.isYieldAllowed()) {
- mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY);
+ opCount = 0;
+ if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
+ ypCount++;
+ }
}
results[i] = operation.apply(this, results, i);
}
diff --git a/src/com/android/providers/contacts/SocialProvider.java b/src/com/android/providers/contacts/SocialProvider.java
index 349e1fc..8201d90 100644
--- a/src/com/android/providers/contacts/SocialProvider.java
+++ b/src/com/android/providers/contacts/SocialProvider.java
@@ -84,7 +84,8 @@
// Contacts projection map
columns = new HashMap<String, String>();
- columns.put(Contacts.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS "
+ // TODO: fix display name reference (in fact, use the contacts view instead of the table)
+ columns.put(Contacts.DISPLAY_NAME, "contact." + Contacts.DISPLAY_NAME + " AS "
+ Contacts.DISPLAY_NAME);
sContactsProjectionMap = columns;
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index bcc7f6e..622ace2 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -130,15 +130,34 @@
}
protected long createRawContactWithName() {
- return createRawContactWithName("John", "Doe");
+ return createRawContactWithName(null);
+ }
+
+ protected long createRawContactWithName(Account account) {
+ return createRawContactWithName("John", "Doe", account);
}
protected long createRawContactWithName(String firstName, String lastName) {
- long rawContactId = createRawContact(null);
+ return createRawContactWithName(firstName, lastName, null);
+ }
+
+ protected long createRawContactWithName(String firstName, String lastName, Account account) {
+ long rawContactId = createRawContact(account);
insertStructuredName(rawContactId, firstName, lastName);
return rawContactId;
}
+ protected Uri setCallerIsSyncAdapter(Uri uri, Account account) {
+ if (account == null) {
+ return uri;
+ }
+ final Uri.Builder builder = uri.buildUpon();
+ builder.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name);
+ builder.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type);
+ builder.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true");
+ return builder.build();
+ }
+
protected long createRawContact(Account account, String... extras) {
ContentValues values = new ContentValues();
for (int i = 0; i < extras.length; ) {
@@ -390,8 +409,15 @@
return displayName;
}
+ private String queryLookupKey(long contactId) {
+ Cursor c = queryContact(contactId);
+ assertTrue(c.moveToFirst());
+ String lookupKey = c.getString(c.getColumnIndex(Contacts.LOOKUP_KEY));
+ c.close();
+ return lookupKey;
+ }
+
protected void assertAggregated(long rawContactId1, long rawContactId2) {
- forceAggregation();
long contactId1 = queryContactId(rawContactId1);
long contactId2 = queryContactId(rawContactId2);
assertTrue(contactId1 == contactId2);
@@ -399,8 +425,6 @@
protected void assertAggregated(long rawContactId1, long rawContactId2,
String expectedDisplayName) {
- forceAggregation();
-
long contactId1 = queryContactId(rawContactId1);
long contactId2 = queryContactId(rawContactId2);
assertTrue(contactId1 == contactId2);
@@ -410,8 +434,6 @@
}
protected void assertNotAggregated(long rawContactId1, long rawContactId2) {
- forceAggregation();
-
long contactId1 = queryContactId(rawContactId1);
long contactId2 = queryContactId(rawContactId2);
assertTrue(contactId1 != contactId2);
@@ -588,6 +610,10 @@
mResolver.update(ContentUris.withAppendedId(contentUri, id), values, null, null);
}
+ protected void assertStoredValue(Uri contentUri, long id, String column, Object expectedValue) {
+ assertStoredValue(ContentUris.withAppendedId(contentUri, id), column, expectedValue);
+ }
+
protected void assertStoredValue(Uri rowUri, String column, Object expectedValue) {
String value = getStoredValue(rowUri, column);
if (expectedValue == null) {
@@ -816,11 +842,314 @@
c.close();
}
- protected void forceAggregation() {
- ((SynchronousContactsProvider2) mActor.provider).aggregate();
- }
-
protected void assertNetworkNotified(boolean expected) {
assertEquals(expected, ((SynchronousContactsProvider2)mActor.provider).isNetworkNotified());
}
+
+ /**
+ * A contact in the database, and the attributes used to create it. Construct using
+ * {@link GoldenContactBuilder#build()}.
+ */
+ public final class GoldenContact {
+
+ private final long rawContactId;
+
+ private final long contactId;
+
+ private final String givenName;
+
+ private final String familyName;
+
+ private final String nickname;
+
+ private final byte[] photo;
+
+ private final String company;
+
+ private final String title;
+
+ private final String phone;
+
+ private final String email;
+
+ private GoldenContact(GoldenContactBuilder builder, long rawContactId, long contactId) {
+
+ this.rawContactId = rawContactId;
+ this.contactId = contactId;
+ givenName = builder.givenName;
+ familyName = builder.familyName;
+ nickname = builder.nickname;
+ photo = builder.photo;
+ company = builder.company;
+ title = builder.title;
+ phone = builder.phone;
+ email = builder.email;
+ }
+
+ public void delete() {
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ mResolver.delete(rawContactUri, null, null);
+ }
+
+ /**
+ * Returns the index of the contact in table "raw_contacts"
+ */
+ public long getRawContactId() {
+ return rawContactId;
+ }
+
+ /**
+ * Returns the index of the contact in table "contacts"
+ */
+ public long getContactId() {
+ return contactId;
+ }
+
+ /**
+ * Returns the lookup key for the contact.
+ */
+ public String getLookupKey() {
+ return queryLookupKey(contactId);
+ }
+
+ /**
+ * Returns the contact's given name.
+ */
+ public String getGivenName() {
+ return givenName;
+ }
+
+ /**
+ * Returns the contact's family name.
+ */
+ public String getFamilyName() {
+ return familyName;
+ }
+
+ /**
+ * Returns the contact's nickname.
+ */
+ public String getNickname() {
+ return nickname;
+ }
+
+ /**
+ * Return's the contact's photo
+ */
+ public byte[] getPhoto() {
+ return photo;
+ }
+
+ /**
+ * Return's the company at which the contact works.
+ */
+ public String getCompany() {
+ return company;
+ }
+
+ /**
+ * Returns the contact's job title.
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Returns the contact's phone number
+ */
+ public String getPhone() {
+ return phone;
+ }
+
+ /**
+ * Returns the contact's email address
+ */
+ public String getEmail() {
+ return email;
+ }
+ }
+
+ /**
+ * Builds {@link GoldenContact} objects. Unspecified boolean objects default to false.
+ * Unspecified String objects default to null.
+ */
+ public final class GoldenContactBuilder {
+
+ private String givenName;
+
+ private String familyName;
+
+ private String nickname;
+
+ private byte[] photo;
+
+ private String company;
+
+ private String title;
+
+ private String phone;
+
+ private String email;
+
+ /**
+ * The contact's given and family names.
+ *
+ * TODO(dplotnikov): inline, or should we require them to set both names if they set either?
+ */
+ public GoldenContactBuilder name(String givenName, String familyName) {
+ return givenName(givenName).familyName(familyName);
+ }
+
+ /**
+ * The contact's given name.
+ */
+ public GoldenContactBuilder givenName(String value) {
+ givenName = value;
+ return this;
+ }
+
+ /**
+ * The contact's family name.
+ */
+ public GoldenContactBuilder familyName(String value) {
+ familyName = value;
+ return this;
+ }
+
+ /**
+ * The contact's nickname.
+ */
+ public GoldenContactBuilder nickname(String value) {
+ nickname = value;
+ return this;
+ }
+
+ /**
+ * The contact's photo.
+ */
+ public GoldenContactBuilder photo(byte[] value) {
+ photo = value;
+ return this;
+ }
+
+ /**
+ * The company at which the contact works.
+ */
+ public GoldenContactBuilder company(String value) {
+ company = value;
+ return this;
+ }
+
+ /**
+ * The contact's job title.
+ */
+ public GoldenContactBuilder title(String value) {
+ title = value;
+ return this;
+ }
+
+ /**
+ * The contact's phone number.
+ */
+ public GoldenContactBuilder phone(String value) {
+ phone = value;
+ return this;
+ }
+
+ /**
+ * The contact's email address; also sets their IM status to {@link StatusUpdates#OFFLINE}
+ * with a presence of "Coding for Android".
+ */
+ public GoldenContactBuilder email(String value) {
+ email = value;
+ return this;
+ }
+
+ /**
+ * Builds the {@link GoldenContact} specified by this builder.
+ */
+ public GoldenContact build() {
+
+ final long groupId = createGroup(mAccount, "gsid1", "title1");
+
+ long rawContactId = createRawContact();
+ insertGroupMembership(rawContactId, groupId);
+
+ if (givenName != null || familyName != null) {
+ insertStructuredName(rawContactId, givenName, familyName);
+ }
+ if (nickname != null) {
+ insertNickname(rawContactId, nickname);
+ }
+ if (photo != null) {
+ insertPhoto(rawContactId);
+ }
+ if (company != null || title != null) {
+ insertOrganization(rawContactId);
+ }
+ if (email != null) {
+ insertEmail(rawContactId);
+ }
+ if (phone != null) {
+ insertPhone(rawContactId);
+ }
+
+ long contactId = queryContactId(rawContactId);
+
+ return new GoldenContact(this, rawContactId, contactId);
+ }
+
+ private void insertPhoto(long rawContactId) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+ values.put(Photo.PHOTO, photo);
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ private void insertOrganization(long rawContactId) {
+
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+ values.put(Organization.TYPE, Organization.TYPE_WORK);
+ if (company != null) {
+ values.put(Organization.COMPANY, company);
+ }
+ if (title != null) {
+ values.put(Organization.TITLE, title);
+ }
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+
+ private void insertEmail(long rawContactId) {
+
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ values.put(Email.TYPE, Email.TYPE_WORK);
+ values.put(Email.DATA, "foo@acme.com");
+ mResolver.insert(Data.CONTENT_URI, values);
+
+ int protocol = Im.PROTOCOL_GOOGLE_TALK;
+
+ values.clear();
+ values.put(StatusUpdates.PROTOCOL, protocol);
+ values.put(StatusUpdates.IM_HANDLE, email);
+ values.put(StatusUpdates.IM_ACCOUNT, "foo");
+ values.put(StatusUpdates.PRESENCE_STATUS, StatusUpdates.OFFLINE);
+ values.put(StatusUpdates.PRESENCE_CUSTOM_STATUS, "Coding for Android");
+ mResolver.insert(StatusUpdates.CONTENT_URI, values);
+ }
+
+ private void insertPhone(long rawContactId) {
+ ContentValues values = new ContentValues();
+ values.put(Data.RAW_CONTACT_ID, rawContactId);
+ values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ values.put(Data.IS_PRIMARY, 1);
+ values.put(Phone.TYPE, Phone.TYPE_HOME);
+ values.put(Phone.NUMBER, phone);
+ mResolver.insert(Data.CONTENT_URI, values);
+ }
+ }
}
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index 6e7a95a..af5a1fe 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -43,11 +43,9 @@
@MediumTest
public class CallLogProviderTest extends BaseContactsProvider2Test {
- private static final boolean USE_LEGACY_PROVIDER = false;
-
@Override
protected Class<? extends ContentProvider> getProviderClass() {
- return USE_LEGACY_PROVIDER ? ContactsProvider.class : SynchronousContactsProvider2.class;
+ return SynchronousContactsProvider2.class;
}
@Override
@@ -58,11 +56,7 @@
@Override
protected void setUp() throws Exception {
super.setUp();
- if (USE_LEGACY_PROVIDER) {
- addAuthority(CallLog.AUTHORITY);
- } else {
- addProvider(TestCallLogProvider.class, CallLog.AUTHORITY);
- }
+ addProvider(TestCallLogProvider.class, CallLog.AUTHORITY);
}
public void testInsert() {
diff --git a/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java b/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
deleted file mode 100644
index 6c15e0e..0000000
--- a/tests/src/com/android/providers/contacts/ContactAggregationSchedulerTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2009 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 android.test.suitebuilder.annotation.SmallTest;
-
-import junit.framework.TestCase;
-
-/**
- * Tests from {@link ContactAggregationScheduler}.
- *
- * Run the test like this:
- * <code>
- * adb shell am instrument -e class com.android.providers.contacts.ContactAggregationSchedulerTest \
- * -w com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
- * </code>
- */
-@SmallTest
-public class ContactAggregationSchedulerTest extends TestCase {
-
- private TestContactAggregationScheduler mScheduler;
-
- @Override
- protected void setUp() throws Exception {
- mScheduler = new TestContactAggregationScheduler();
- }
-
- public void testScheduleInitial() {
- mScheduler.schedule();
- assertEquals(1, mScheduler.mRunNow);
- assertEquals(0, mScheduler.mRunDelayed);
- }
-
- public void testScheduleTwiceRapidly() {
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
- mScheduler.schedule();
- assertEquals(1, mScheduler.mRunNow);
- assertEquals(1, mScheduler.mRunDelayed);
- }
-
- public void testScheduleThriceRapidly() {
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.AGGREGATION_DELAY / 2;
- mScheduler.schedule();
-
- assertEquals(1, mScheduler.mRunNow);
- assertEquals(2, mScheduler.mRunDelayed);
- }
-
- public void testScheduleThriceExceedingMaxDelay() {
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
- mScheduler.schedule();
- assertEquals(1, mScheduler.mRunDelayed);
- }
-
- public void testScheduleWhileRunning() {
- mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
- boolean mInterruptCalled;
-
- public void interrupt() {
- mInterruptCalled = true;
- }
-
- public void run() {
- mScheduler.schedule();
- assertTrue(mInterruptCalled);
- }
- });
-
- mScheduler.run();
- assertEquals(1, mScheduler.mRunDelayed);
- }
-
- public void testRepeatedInterruptions() {
- mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
- boolean mInterruptCalled;
-
- public void interrupt() {
- mInterruptCalled = true;
- }
-
- public void run() {
- mScheduler.schedule();
- assertTrue(mInterruptCalled);
- }
- });
-
- mScheduler.run();
- assertEquals(1, mScheduler.mRunDelayed);
-
- mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
- mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
- boolean mInterruptCalled;
-
- public void interrupt() {
- mInterruptCalled = true;
- }
-
- public void run() {
- mScheduler.schedule();
- assertFalse(mInterruptCalled);
- }
- });
-
- mScheduler.run();
- }
-
- public void testScheduleWhileRunningExceedingMaxDelay() {
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.DELAYED_EXECUTION_TIMEOUT / 2;
- mScheduler.schedule();
-
- mScheduler.mTime += ContactAggregationScheduler.MAX_AGGREGATION_DELAY + 100;
-
- mScheduler.setAggregator(new ContactAggregationScheduler.Aggregator() {
- boolean mInterruptCalled;
-
- public void interrupt() {
- mInterruptCalled = true;
- }
-
- public void run() {
- mScheduler.schedule();
- assertFalse(mInterruptCalled);
- }
- });
-
- mScheduler.run();
- assertEquals(1, mScheduler.mRunNow);
- assertEquals(2, mScheduler.mRunDelayed);
- }
-
- private static class TestContactAggregationScheduler extends ContactAggregationScheduler {
-
- long mTime = 1000;
- int mRunDelayed;
- int mRunNow;
-
- @Override
- public void start() {
- }
-
- @Override
- public void stop() {
- }
-
- @Override
- long currentTime() {
- return mTime;
- }
-
- @Override
- void runNow() {
- mRunNow++;
- }
-
- @Override
- void runDelayed() {
- mRunDelayed++;
- }
- }
-}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
index e4dc629..a71c58b 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
@@ -95,7 +95,10 @@
if (TRACE) {
Debug.startMethodTracing("aggregation");
}
- provider.aggregate();
+
+ // TODO
+// provider.aggregate();
+
if (TRACE) {
Debug.stopMethodTracing();
}
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
index 04b8c47..4322c5c 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
@@ -22,6 +22,8 @@
import android.net.Uri;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.test.suitebuilder.annotation.LargeTest;
@@ -368,6 +370,20 @@
assertNotAggregated(rawContactId1, rawContactId2);
}
+ public void testNonAggregationOnOrganization() {
+ ContentValues values = new ContentValues();
+ values.put(Organization.TITLE, "Monsters, Inc");
+ long rawContactId1 = createRawContact();
+ insertOrganization(rawContactId1, values);
+ insertNickname(rawContactId1, "Boo");
+
+ long rawContactId2 = createRawContact();
+ insertOrganization(rawContactId2, values);
+ insertNickname(rawContactId2, "Rendall"); // To force reaggregation
+
+ assertNotAggregated(rawContactId1, rawContactId2);
+ }
+
public void testAggregationExceptionKeepIn() {
long rawContactId1 = createRawContact();
insertStructuredName(rawContactId1, "Johnk", "Smithk");
@@ -626,9 +642,61 @@
assertEquals(cupcakeId, queryPhotoId(queryContactId(rawContactId2)));
}
- private void assertSuggestions(long contactId, long... suggestions) {
- forceAggregation();
+ public void testDisplayNameSources() {
+ long rawContactId = createRawContact();
+ long contactId = queryContactId(rawContactId);
+ assertNull(queryDisplayName(contactId));
+
+ insertEmail(rawContactId, "eclair@android.com");
+ assertEquals("eclair@android.com", queryDisplayName(contactId));
+
+ insertPhoneNumber(rawContactId, "800-555-5555");
+ assertEquals("800-555-5555", queryDisplayName(contactId));
+
+ ContentValues values = new ContentValues();
+ values.put(Organization.COMPANY, "Android");
+ insertOrganization(rawContactId, values);
+ assertEquals("Android", queryDisplayName(contactId));
+
+ insertNickname(rawContactId, "Dro");
+ assertEquals("Dro", queryDisplayName(contactId));
+
+ values.clear();
+ values.put(StructuredName.GIVEN_NAME, "Eclair");
+ values.put(StructuredName.FAMILY_NAME, "Android");
+ insertStructuredName(rawContactId, values);
+ assertEquals("Eclair Android", queryDisplayName(contactId));
+ }
+
+ public void testVerifiedName() {
+ long rawContactId1 = createRawContactWithName("test1", "TEST1");
+ storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.NAME_VERIFIED, "1");
+ long rawContactId2 = createRawContactWithName("test2", "TEST2");
+ long rawContactId3 = createRawContactWithName("test3", "TEST3 LONG");
+
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId2);
+ setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
+ rawContactId3);
+
+ long contactId = queryContactId(rawContactId1);
+
+ // Should be the verified name
+ assertEquals("test1 TEST1", queryDisplayName(contactId));
+
+ // Mark a different name as verified - this should reset the NAME_VERIFIED field
+ // for the other rawContacts
+ storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.NAME_VERIFIED, "1");
+ assertStoredValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.NAME_VERIFIED, 0);
+ assertEquals("test2 TEST2", queryDisplayName(contactId));
+
+ // Reset the NAME_VERIFIED flag - now the most complex of the three names should win
+ storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.NAME_VERIFIED, "0");
+ assertEquals("test3 TEST3 LONG", queryDisplayName(contactId));
+ }
+
+ private void assertSuggestions(long contactId, long... suggestions) {
final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
Uri uri = Uri.withAppendedPath(aggregateUri,
Contacts.AggregationSuggestions.CONTENT_DIRECTORY);
diff --git a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
new file mode 100644
index 0000000..a859581
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2010 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 java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import com.android.providers.contacts.ContactLocaleUtils;
+
+import android.provider.ContactsContract.FullNameStyle;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+public class ContactLocaleUtilsTest extends AndroidTestCase {
+ private static final String LATIN_NAME = "John Smith";
+ private static final String CHINESE_NAME = "\u675C\u9D51";
+ private static final String CHINESE_LATIN_MIX_NAME_1 = "D\u675C\u9D51";
+ private static final String CHINESE_LATIN_MIX_NAME_2 = "MARY \u675C\u9D51";
+ private static final String[] CHINESE_NAME_KEY = {"\u9D51", "\u675C\u9D51", "JUAN", "DUJUAN",
+ "J", "DJ"};
+ private static final String[] CHINESE_LATIN_MIX_NAME_1_KEY = {"\u9D51", "\u675C\u9D51",
+ "D \u675C\u9D51", "JUAN", "DUJUAN", "J", "DJ", "D DUJUAN", "DDJ"};
+ private static final String[] CHINESE_LATIN_MIX_NAME_2_KEY = {"\u9D51", "\u675C\u9D51",
+ "MARY \u675C\u9D51", "JUAN", "DUJUAN", "MARY DUJUAN", "J", "DJ", "MDJ"};
+
+
+ public void testContactLocaleUtilsBase() throws Exception {
+ assertEquals(ContactLocaleUtils.getSortKey(LATIN_NAME, FullNameStyle.UNDEFINED),
+ LATIN_NAME);
+ assertNull(ContactLocaleUtils.getNameLookupKeys(LATIN_NAME,
+ FullNameStyle.UNDEFINED));
+ }
+
+ public void testChineseContactLocaleUtils() throws Exception {
+ assertTrue(ContactLocaleUtils.getSortKey(CHINESE_NAME,
+ FullNameStyle.CHINESE).equalsIgnoreCase("DU \u675C JUAN \u9D51"));
+
+ assertTrue(ContactLocaleUtils.getSortKey(CHINESE_LATIN_MIX_NAME_1,
+ FullNameStyle.CHINESE).equalsIgnoreCase("d DU \u675C JUAN \u9D51"));
+
+ assertTrue(ContactLocaleUtils.getSortKey(CHINESE_LATIN_MIX_NAME_2,
+ FullNameStyle.CHINESE).equalsIgnoreCase("mary DU \u675C JUAN \u9D51"));
+
+ Iterator<String> keys = ContactLocaleUtils.getNameLookupKeys(CHINESE_NAME, FullNameStyle.CHINESE);
+ verifyKeys(keys, CHINESE_NAME_KEY);
+
+ keys = ContactLocaleUtils.getNameLookupKeys(CHINESE_LATIN_MIX_NAME_1, FullNameStyle.CHINESE);
+ verifyKeys(keys, CHINESE_LATIN_MIX_NAME_1_KEY);
+
+ keys = ContactLocaleUtils.getNameLookupKeys(CHINESE_LATIN_MIX_NAME_2, FullNameStyle.CHINESE);
+ verifyKeys(keys, CHINESE_LATIN_MIX_NAME_2_KEY);
+ }
+
+ private void verifyKeys(final Iterator<String> resultKeys, final String[] expectedKeys) throws Exception {
+ HashSet<String> allKeys = new HashSet<String>();
+ while (resultKeys.hasNext()) {
+ allKeys.add(resultKeys.next());
+ }
+ Iterator<String> ks = allKeys.iterator();
+ Log.d("ContactLocaleUtilsTest", "Result keys");
+ while(ks.hasNext()) {
+ Log.d("ContactLocaleUtilsTest", ks.next());
+ }
+ Log.d("ContactLocaleUtilsTest", "Expected keys");
+ for(String ekey : expectedKeys) {
+ Log.d("ContactLocaleUtilsTest", ekey);
+ }
+ assertEquals(allKeys.size(), expectedKeys.length);
+ assertTrue(allKeys.containsAll(Arrays.asList(expectedKeys)));
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
index 1d57924..390b940 100644
--- a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
+++ b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
@@ -126,8 +126,6 @@
long rawContactId3 = createRawContactWithName("John", "Doe");
storeValue(RawContacts.CONTENT_URI, rawContactId3, RawContacts.SOURCE_ID, "3");
- forceAggregation();
-
String lookupKey = "0i1.0i2.0i3";
long contactId = queryContactId(rawContactId1);
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 4568e5f..7dd980c 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -23,8 +23,8 @@
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.res.Configuration;
import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
@@ -43,10 +43,10 @@
import android.test.mock.MockContext;
import android.test.mock.MockPackageManager;
import android.test.mock.MockResources;
-import android.util.Log;
import android.util.TypedValue;
import java.util.HashMap;
+import java.util.Locale;
/**
* Helper class that encapsulates an "actor" which is owned by a specific
@@ -131,7 +131,11 @@
mPackageManager.addPackage(3000, PACKAGE_GREEN);
mPackageManager.addPackage(4000, PACKAGE_BLUE);
- mRes = new RestrictionMockResources(overallContext.getResources());
+ Resources resources = overallContext.getResources();
+ Configuration configuration = new Configuration(resources.getConfiguration());
+ configuration.locale = Locale.US;
+ resources.updateConfiguration(configuration, resources.getDisplayMetrics());
+ mRes = new RestrictionMockResources(resources);
}
@Override
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index afeb377..1e6bc49 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -18,8 +18,11 @@
import com.android.internal.util.ArrayUtils;
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.google.android.collect.Lists;
import android.accounts.Account;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Entity;
@@ -32,9 +35,12 @@
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.PhoneticNameStyle;
import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
import android.provider.ContactsContract.Settings;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.Email;
@@ -48,6 +54,10 @@
import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.LargeTest;
+import java.text.Collator;
+import java.util.Arrays;
+import java.util.Locale;
+
/**
* Unit tests for {@link ContactsProvider2}.
@@ -174,8 +184,6 @@
long rawContactId2 = createRawContactWithName("Hot", "Tamale");
insertPhoneNumber(rawContactId2, "1-800-466-4411");
- forceAggregation();
-
Uri filterUri1 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "tamale");
ContentValues values = new ContentValues();
values.put(Contacts.DISPLAY_NAME, "Hot Tamale");
@@ -322,7 +330,6 @@
long rawContactId2 = createRawContactWithName("Hot", "Tamale");
insertEmail(rawContactId2, "tamale@acme.com");
- forceAggregation();
Uri filterUri1 = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "tam");
ContentValues values = new ContentValues();
@@ -570,7 +577,7 @@
values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
insertStructuredName(rawContactId, values);
- assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr");
+ assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr.");
}
public void testDisplayNameParsingWhenPartsAreNull() {
@@ -580,7 +587,7 @@
values.putNull(StructuredName.GIVEN_NAME);
values.putNull(StructuredName.FAMILY_NAME);
insertStructuredName(rawContactId, values);
- assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr");
+ assertStructuredName(rawContactId, "Mr", "John", "Kevin", "von Smith", "Jr.");
}
public void testDisplayNameParsingWhenPartsSpecified() {
@@ -593,6 +600,125 @@
assertStructuredName(rawContactId, null, null, null, "Johnson", null);
}
+ public void testContactWithoutPhoneticName() {
+ final long rawContactId = createRawContact(null);
+
+ ContentValues values = new ContentValues();
+ values.put(StructuredName.PREFIX, "Mr");
+ values.put(StructuredName.GIVEN_NAME, "John");
+ values.put(StructuredName.MIDDLE_NAME, "K.");
+ values.put(StructuredName.FAMILY_NAME, "Doe");
+ values.put(StructuredName.SUFFIX, "Jr.");
+ Uri dataUri = insertStructuredName(rawContactId, values);
+
+ values.clear();
+ values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+ values.put(RawContacts.DISPLAY_NAME_PRIMARY, "John K. Doe, Jr.");
+ values.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, "Doe, John K., Jr.");
+ values.putNull(RawContacts.PHONETIC_NAME);
+ values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+ values.put(RawContacts.SORT_KEY_PRIMARY, "John K. Doe, Jr.");
+ values.put(RawContacts.SORT_KEY_ALTERNATIVE, "Doe, John K., Jr.");
+
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ assertStoredValues(rawContactUri, values);
+
+ values.clear();
+ values.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+ values.put(Contacts.DISPLAY_NAME_PRIMARY, "John K. Doe, Jr.");
+ values.put(Contacts.DISPLAY_NAME_ALTERNATIVE, "Doe, John K., Jr.");
+ values.putNull(Contacts.PHONETIC_NAME);
+ values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+ values.put(Contacts.SORT_KEY_PRIMARY, "John K. Doe, Jr.");
+ values.put(Contacts.SORT_KEY_ALTERNATIVE, "Doe, John K., Jr.");
+
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
+ queryContactId(rawContactId));
+ assertStoredValues(contactUri, values);
+
+ // The same values should be available through a join with Data
+ assertStoredValues(dataUri, values);
+ }
+
+ public void testContactWithChineseName() {
+
+ // Only run this test when Chinese collation is supported
+ if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+ return;
+ }
+
+ long rawContactId = createRawContact(null);
+
+ ContentValues values = new ContentValues();
+ values.put(StructuredName.DISPLAY_NAME, "\u6BB5\u5C0F\u6D9B");
+ Uri dataUri = insertStructuredName(rawContactId, values);
+
+ values.clear();
+ values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+ values.put(RawContacts.DISPLAY_NAME_PRIMARY, "\u6BB5\u5C0F\u6D9B");
+ values.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, "\u6BB5\u5C0F\u6D9B");
+ values.putNull(RawContacts.PHONETIC_NAME);
+ values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+ values.put(RawContacts.SORT_KEY_PRIMARY, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+ values.put(RawContacts.SORT_KEY_ALTERNATIVE, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ assertStoredValues(rawContactUri, values);
+
+ values.clear();
+ values.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+ values.put(Contacts.DISPLAY_NAME_PRIMARY, "\u6BB5\u5C0F\u6D9B");
+ values.put(Contacts.DISPLAY_NAME_ALTERNATIVE, "\u6BB5\u5C0F\u6D9B");
+ values.putNull(Contacts.PHONETIC_NAME);
+ values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+ values.put(Contacts.SORT_KEY_PRIMARY, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+ values.put(Contacts.SORT_KEY_ALTERNATIVE, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
+ queryContactId(rawContactId));
+ assertStoredValues(contactUri, values);
+
+ // The same values should be available through a join with Data
+ assertStoredValues(dataUri, values);
+ }
+
+ public void testContactWithJapaneseName() {
+ long rawContactId = createRawContact(null);
+
+ ContentValues values = new ContentValues();
+ values.put(StructuredName.GIVEN_NAME, "\u7A7A\u6D77");
+ values.put(StructuredName.PHONETIC_GIVEN_NAME, "\u304B\u3044\u304F\u3046");
+ Uri dataUri = insertStructuredName(rawContactId, values);
+
+ values.clear();
+ values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+ values.put(RawContacts.DISPLAY_NAME_PRIMARY, "\u7A7A\u6D77");
+ values.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, "\u7A7A\u6D77");
+ values.put(RawContacts.PHONETIC_NAME, "\u304B\u3044\u304F\u3046");
+ values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
+ values.put(RawContacts.SORT_KEY_PRIMARY, "\u304B\u3044\u304F\u3046");
+ values.put(RawContacts.SORT_KEY_ALTERNATIVE, "\u304B\u3044\u304F\u3046");
+
+ Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ assertStoredValues(rawContactUri, values);
+
+ values.clear();
+ values.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+ values.put(Contacts.DISPLAY_NAME_PRIMARY, "\u7A7A\u6D77");
+ values.put(Contacts.DISPLAY_NAME_ALTERNATIVE, "\u7A7A\u6D77");
+ values.put(Contacts.PHONETIC_NAME, "\u304B\u3044\u304F\u3046");
+ values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
+ values.put(Contacts.SORT_KEY_PRIMARY, "\u304B\u3044\u304F\u3046");
+ values.put(Contacts.SORT_KEY_ALTERNATIVE, "\u304B\u3044\u304F\u3046");
+
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
+ queryContactId(rawContactId));
+ assertStoredValues(contactUri, values);
+
+ // The same values should be available through a join with Data
+ assertStoredValues(dataUri, values);
+ }
+
public void testDisplayNameUpdate() {
long rawContactId1 = createRawContact();
insertEmail(rawContactId1, "potato@acme.com", true);
@@ -628,16 +754,10 @@
insertPhoneNumber(rawContactId, "1-800-466-4411");
assertStoredValue(uri, Contacts.DISPLAY_NAME, "1-800-466-4411");
- // If there is title without company, the title is display name.
- values.clear();
- values.put(Organization.TITLE, "Protagonist");
- Uri organizationUri = insertOrganization(rawContactId, values);
- assertStoredValue(uri, Contacts.DISPLAY_NAME, "Protagonist");
-
// If there are title and company, the company is display name.
values.clear();
values.put(Organization.COMPANY, "Monsters Inc");
- mResolver.update(organizationUri, values, null, null);
+ Uri organizationUri = insertOrganization(rawContactId, values);
assertStoredValue(uri, Contacts.DISPLAY_NAME, "Monsters Inc");
// If there is nickname, that is display name.
@@ -650,7 +770,77 @@
values.put(StructuredName.MIDDLE_NAME, "P.");
values.put(StructuredName.FAMILY_NAME, "Sullivan");
insertStructuredName(rawContactId, values);
- assertStoredValue(uri, Contacts.DISPLAY_NAME, "James Sullivan");
+ assertStoredValue(uri, Contacts.DISPLAY_NAME, "James P. Sullivan");
+ }
+
+ public void testDisplayNameFromOrganizationWithoutPhoneticName() {
+ long rawContactId = createRawContact();
+ long contactId = queryContactId(rawContactId);
+ ContentValues values = new ContentValues();
+
+ Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+ // If there is title without company, the title is display name.
+ values.clear();
+ values.put(Organization.TITLE, "Protagonist");
+ Uri organizationUri = insertOrganization(rawContactId, values);
+ assertStoredValue(uri, Contacts.DISPLAY_NAME, "Protagonist");
+
+ // If there are title and company, the company is display name.
+ values.clear();
+ values.put(Organization.COMPANY, "Monsters Inc");
+ mResolver.update(organizationUri, values, null, null);
+
+ values.clear();
+ values.put(Contacts.DISPLAY_NAME, "Monsters Inc");
+ values.putNull(Contacts.PHONETIC_NAME);
+ values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+ values.put(Contacts.SORT_KEY_PRIMARY, "Monsters Inc");
+ values.put(Contacts.SORT_KEY_ALTERNATIVE, "Monsters Inc");
+ assertStoredValues(uri, values);
+ }
+
+ public void testDisplayNameFromOrganizationWithJapanesePhoneticName() {
+ long rawContactId = createRawContact();
+ long contactId = queryContactId(rawContactId);
+ ContentValues values = new ContentValues();
+
+ Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+ // If there is title without company, the title is display name.
+ values.clear();
+ values.put(Organization.COMPANY, "DoCoMo");
+ values.put(Organization.PHONETIC_NAME, "\u30C9\u30B3\u30E2");
+ Uri organizationUri = insertOrganization(rawContactId, values);
+
+ values.clear();
+ values.put(Contacts.DISPLAY_NAME, "DoCoMo");
+ values.put(Contacts.PHONETIC_NAME, "\u30C9\u30B3\u30E2");
+ values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
+ values.put(Contacts.SORT_KEY_PRIMARY, "\u30C9\u30B3\u30E2");
+ values.put(Contacts.SORT_KEY_ALTERNATIVE, "\u30C9\u30B3\u30E2");
+ assertStoredValues(uri, values);
+ }
+
+ public void testDisplayNameFromOrganizationWithChineseName() {
+ long rawContactId = createRawContact();
+ long contactId = queryContactId(rawContactId);
+ ContentValues values = new ContentValues();
+
+ Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+ // If there is title without company, the title is display name.
+ values.clear();
+ values.put(Organization.COMPANY, "\u4E2D\u56FD\u7535\u4FE1");
+ Uri organizationUri = insertOrganization(rawContactId, values);
+
+ values.clear();
+ values.put(Contacts.DISPLAY_NAME, "\u4E2D\u56FD\u7535\u4FE1");
+ values.putNull(Contacts.PHONETIC_NAME);
+ values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
+ values.put(Contacts.SORT_KEY_PRIMARY, "ZHONG \u4E2D GUO \u56FD DIAN \u7535 XIN \u4FE1");
+ values.put(Contacts.SORT_KEY_ALTERNATIVE, "ZHONG \u4E2D GUO \u56FD DIAN \u7535 XIN \u4FE1");
+ assertStoredValues(uri, values);
}
public void testDisplayNameUpdateFromStructuredNameUpdate() {
@@ -679,6 +869,23 @@
assertStoredValue(uri, Contacts.DISPLAY_NAME, "Dog");
}
+ public void testInsertDataWithContentProviderOperations() throws Exception {
+ ContentProviderOperation cpo1 = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValues(new ContentValues())
+ .build();
+ ContentProviderOperation cpo2 = ContentProviderOperation.newInsert(Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, 0)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.GIVEN_NAME, "John")
+ .withValue(StructuredName.FAMILY_NAME, "Doe")
+ .build();
+ ContentProviderResult[] results =
+ mResolver.applyBatch(ContactsContract.AUTHORITY, Lists.newArrayList(cpo1, cpo2));
+ long contactId = queryContactId(ContentUris.parseId(results[0].uri));
+ Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ assertStoredValue(uri, Contacts.DISPLAY_NAME, "John Doe");
+ }
+
public void testSendToVoicemailDefault() {
long rawContactId = createRawContactWithName();
long contactId = queryContactId(rawContactId);
@@ -894,11 +1101,53 @@
values.put(Contacts.CONTACT_STATUS, "Available");
assertStoredValuesWithProjection(contactUri, values);
- mResolver.delete(StatusUpdates.CONTENT_URI, StatusUpdates.DATA_ID + "=" + statusId, null);
- values.putNull(Contacts.CONTACT_PRESENCE);
+ // update status_updates table to set new values for
+ // status_updates.status
+ // status_updates.status_ts
+ // presence
+ long updatedTs = 200;
+ String testUpdate = "test_update";
+ String selection = StatusUpdates.DATA_ID + "=" + statusId;
+ values.clear();
+ values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+ values.put(StatusUpdates.STATUS, testUpdate);
+ values.put(StatusUpdates.PRESENCE, "presence_test");
+ mResolver.update(StatusUpdates.CONTENT_URI, values,
+ StatusUpdates.DATA_ID + "=" + statusId, null);
+ assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
- // Latest custom status update stays on the phone
- values.put(Contacts.CONTACT_STATUS, "Available");
+ // update status_updates table to set new values for columns in status_updates table ONLY
+ // i.e., no rows in presence table are to be updated.
+ updatedTs = 300;
+ testUpdate = "test_update_new";
+ selection = StatusUpdates.DATA_ID + "=" + statusId;
+ values.clear();
+ values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+ values.put(StatusUpdates.STATUS, testUpdate);
+ mResolver.update(StatusUpdates.CONTENT_URI, values,
+ StatusUpdates.DATA_ID + "=" + statusId, null);
+ // make sure the presence column value is still the old value
+ values.put(StatusUpdates.PRESENCE, "presence_test");
+ assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
+
+ // update status_updates table to set new values for columns in presence table ONLY
+ // i.e., no rows in status_updates table are to be updated.
+ selection = StatusUpdates.DATA_ID + "=" + statusId;
+ values.clear();
+ values.put(StatusUpdates.PRESENCE, "presence_test_new");
+ mResolver.update(StatusUpdates.CONTENT_URI, values,
+ StatusUpdates.DATA_ID + "=" + statusId, null);
+ // make sure the status_updates table is not updated
+ values.put(StatusUpdates.STATUS_TIMESTAMP, updatedTs);
+ values.put(StatusUpdates.STATUS, testUpdate);
+ assertStoredValuesWithProjection(StatusUpdates.CONTENT_URI, values);
+
+ // effect "delete status_updates" operation and expect the following
+ // data deleted from status_updates table
+ // presence set to null
+ mResolver.delete(StatusUpdates.CONTENT_URI, StatusUpdates.DATA_ID + "=" + statusId, null);
+ values.clear();
+ values.putNull(Contacts.CONTACT_PRESENCE);
assertStoredValuesWithProjection(contactUri, values);
}
@@ -1083,9 +1332,9 @@
Uri id_3_1 = insertEmail(id, "c3@email.com");
Uri id_3_2 = insertPhoneNumber(id, "5551212c3");
- EntityIterator iterator = mResolver.queryEntities(
- maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, mAccount),
- RawContacts.SOURCE_ID + " in ('c1', 'c2', 'c3')", null, null);
+ EntityIterator iterator = RawContacts.newEntityIterator(mResolver.query(
+ maybeAddAccountQueryParameters(RawContactsEntity.CONTENT_URI, mAccount), null,
+ RawContacts.SOURCE_ID + " in ('c1', 'c2', 'c3')", null, null));
Entity entity;
ContentValues[] subValues;
entity = iterator.next();
@@ -1225,7 +1474,7 @@
}
public void testRawContactDeletion() {
- long rawContactId = createRawContact();
+ long rawContactId = createRawContact(mAccount);
Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
insertImHandle(rawContactId, Im.PROTOCOL_GOOGLE_TALK, null, "deleteme@android.com");
@@ -1243,8 +1492,7 @@
assertStoredValue(uri, RawContacts.DELETED, "1");
assertNetworkNotified(true);
- Uri permanentDeletionUri = uri.buildUpon().appendQueryParameter(
- ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+ Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
mResolver.delete(permanentDeletionUri, null, null);
assertEquals(0, getCount(uri, null, null));
assertEquals(0, getCount(Uri.withAppendedPath(uri, RawContacts.Data.CONTENT_DIRECTORY),
@@ -1256,8 +1504,8 @@
}
public void testRawContactDeletionKeepingAggregateContact() {
- long rawContactId1 = createRawContactWithName();
- long rawContactId2 = createRawContactWithName();
+ long rawContactId1 = createRawContactWithName(mAccount);
+ long rawContactId2 = createRawContactWithName(mAccount);
// Same name - should be aggregated
assertAggregated(rawContactId1, rawContactId2);
@@ -1265,8 +1513,7 @@
long contactId = queryContactId(rawContactId1);
Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1);
- Uri permanentDeletionUri = uri.buildUpon().appendQueryParameter(
- ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+ Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
mResolver.delete(permanentDeletionUri, null, null);
assertEquals(0, getCount(uri, null, null));
assertEquals(1, getCount(Contacts.CONTENT_URI, Contacts._ID + "=" + contactId, null));
@@ -1334,7 +1581,6 @@
public void testContactDeletion() {
long rawContactId1 = createRawContactWithName("John", "Doe");
long rawContactId2 = createRawContactWithName("John", "Doe");
- forceAggregation();
long contactId = queryContactId(rawContactId1);
@@ -1352,8 +1598,7 @@
Uri uri = insertStructuredName(rawContactId, "John", "Doe");
clearDirty(rawContactUri);
- Uri updateUri = uri.buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+ Uri updateUri = setCallerIsSyncAdapter(uri, mAccount);
ContentValues values = new ContentValues();
values.put(StructuredName.FAMILY_NAME, "Dough");
@@ -1628,6 +1873,72 @@
c.close();
}
+ public void testReadBooleanQueryParameter() {
+ assertBooleanUriParameter("foo:bar", "bool", true, true);
+ assertBooleanUriParameter("foo:bar", "bool", false, false);
+ assertBooleanUriParameter("foo:bar?bool=0", "bool", true, false);
+ assertBooleanUriParameter("foo:bar?bool=1", "bool", false, true);
+ assertBooleanUriParameter("foo:bar?bool=false", "bool", true, false);
+ assertBooleanUriParameter("foo:bar?bool=true", "bool", false, true);
+ assertBooleanUriParameter("foo:bar?bool=FaLsE", "bool", true, false);
+ assertBooleanUriParameter("foo:bar?bool=false&some=some", "bool", true, false);
+ assertBooleanUriParameter("foo:bar?bool=1&some=some", "bool", false, true);
+ assertBooleanUriParameter("foo:bar?some=bool", "bool", true, true);
+ assertBooleanUriParameter("foo:bar?bool", "bool", true, true);
+ }
+
+ private void assertBooleanUriParameter(String uriString, String parameter,
+ boolean defaultValue, boolean expectedValue) {
+ assertEquals(expectedValue, ContactsProvider2.readBooleanQueryParameter(
+ Uri.parse(uriString), parameter, defaultValue));
+ }
+
+ public void testGetQueryParameter() {
+ assertQueryParameter("foo:bar", "param", null);
+ assertQueryParameter("foo:bar?param", "param", null);
+ assertQueryParameter("foo:bar?param=", "param", "");
+ assertQueryParameter("foo:bar?param=val", "param", "val");
+ assertQueryParameter("foo:bar?param=val&some=some", "param", "val");
+ assertQueryParameter("foo:bar?some=some¶m=val", "param", "val");
+ assertQueryParameter("foo:bar?some=some¶m=val&else=else", "param", "val");
+ assertQueryParameter("foo:bar?param=john%40doe.com", "param", "john@doe.com");
+ }
+
+ public void testMissingAccountTypeParameter() {
+ // Try querying for RawContacts only using ACCOUNT_NAME
+ final Uri queryUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(
+ RawContacts.ACCOUNT_NAME, "lolwut").build();
+ try {
+ final Cursor cursor = mResolver.query(queryUri, null, null, null, null);
+ fail("Able to query with incomplete account query parameters");
+ } catch (IllegalArgumentException e) {
+ // Expected behavior.
+ }
+ }
+
+ public void testInsertInconsistentAccountType() {
+ // Try inserting RawContact with inconsistent Accounts
+ final Account red = new Account("red", "red");
+ final Account blue = new Account("blue", "blue");
+
+ final ContentValues values = new ContentValues();
+ values.put(RawContacts.ACCOUNT_NAME, red.name);
+ values.put(RawContacts.ACCOUNT_TYPE, red.type);
+
+ final Uri insertUri = maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, blue);
+ try {
+ mResolver.insert(insertUri, values);
+ fail("Able to insert RawContact with inconsistent account details");
+ } catch (IllegalArgumentException e) {
+ // Expected behavior.
+ }
+ }
+
+ private void assertQueryParameter(String uriString, String parameter, String expectedValue) {
+ assertEquals(expectedValue, ContactsProvider2.getQueryParameter(
+ Uri.parse(uriString), parameter));
+ }
+
private long createContact(ContentValues values, String firstName, String givenName,
String phoneNumber, String email, int presenceStatus, int timesContacted, int starred,
long groupId) {
diff --git a/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java b/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
index d8e3205..fe15306 100644
--- a/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
+++ b/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
@@ -18,37 +18,28 @@
import android.accounts.Account;
import android.app.SearchManager;
-import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.provider.ContactsContract;
-import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Intents;
-import android.provider.ContactsContract.Presence;
-import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.test.suitebuilder.annotation.LargeTest;
-import java.io.IOException;
-
/**
* Unit tests for {@link GlobalSearchSupport}.
- *
+ * <p>
* Run the test like this:
- * <code>
+ * <p>
+ * <code><pre>
* adb shell am instrument -e class com.android.providers.contacts.GlobalSearchSupportTest -w \
* com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
- * </code>
+ * </pre></code>
*/
@LargeTest
public class GlobalSearchSupportTest extends BaseContactsProvider2Test {
+
public void testSearchSuggestionsNotInVisibleGroup() throws Exception {
Account account = new Account("actname", "acttype");
long rawContactId = createRawContact(account);
@@ -63,233 +54,61 @@
c.close();
}
- public void testSearchSuggestionsByName() throws Exception {
- long groupId = createGroup(mAccount, "gsid1", "title1");
-
- assertSearchSuggestion(groupId,
- true, // name
- false, // nickname
- true, // photo
- false, // company
- false, // title
- false, // phone
- false, // email
- "D", // query
- true, // expect icon URI
- null, "Deer Dough", null);
-
- assertSearchSuggestion(groupId,
- true, // name
- false, // nickname
- true, // photo
- true, // company
- false, // title
- false, // phone
- false, // email
- "D", // query
- true, // expect icon URI
- null, "Deer Dough", "Google");
-
- assertSearchSuggestion(groupId,
- true, // name
- false, // nickname
- true, // photo
- false, // company
- false, // title
- true, // phone
- false, // email
- "D", // query
- true, // expect icon URI
- null, "Deer Dough", "1-800-4664-411");
-
- assertSearchSuggestion(groupId,
- true, // name
- false, // nickname
- true, // photo
- false, // company
- false, // title
- false, // phone
- true, // email
- "D", // query
- true, // expect icon URI
- String.valueOf(StatusUpdates.getPresenceIconResourceId(StatusUpdates.OFFLINE)),
- "Deer Dough", "foo@acme.com");
-
- assertSearchSuggestion(groupId,
- true, // name
- false, // nickname
- false, // photo
- true, // company
- false, // title
- false, // phone
- false, // email
- "D", // query
- false, // expect icon URI
- null, "Deer Dough", "Google");
-
- // Nickname is searchale
- assertSearchSuggestion(groupId,
- true, // name
- true, // nickname
- false, // photo
- true, // company
- false, // title
- false, // phone
- false, // email
- "L", // query
- false, // expect icon URI
- null, "Deer Dough", "Google");
-
- // Company is searchable
- assertSearchSuggestion(groupId,
- true, // name
- false, // nickname
- false, // photo
- true, // company
- false, // title
- false, // phone
- false, // email
- "G", // query
- false, // expect icon URI
- null, "Deer Dough", "Google");
-
- // Title is searchable
- assertSearchSuggestion(groupId,
- true, // name
- false, // nickname
- false, // photo
- true, // company
- true, // title
- false, // phone
- false, // email
- "S", // query
- false, // expect icon URI
- null, "Deer Dough", "Google");
+ public void testSearchSuggestionsByNameWithPhoto() throws Exception {
+ GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
+ loadTestPhoto()).build();
+ new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedText1(
+ "Deer Dough").build().test();
}
- private void assertSearchSuggestion(long groupId, boolean name, boolean nickname, boolean photo,
- boolean company, boolean title, boolean phone, boolean email, String query,
- boolean expectIcon1Uri, String expectedIcon2, String expectedText1,
- String expectedText2) throws IOException {
- ContentValues values = new ContentValues();
+ public void testSearchSuggestionsByNameWithPhotoAndCompany() throws Exception {
+ GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
+ loadTestPhoto()).company("Google").build();
+ new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedText1(
+ "Deer Dough").expectedText2("Google").build().test();
+ }
- long rawContactId = createRawContact();
- insertGroupMembership(rawContactId, groupId);
+ public void testSearchSuggestionsByNameWithPhotoAndPhone() {
+ GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
+ loadTestPhoto()).phone("1-800-4664-411").build();
+ new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedText1(
+ "Deer Dough").expectedText2("1-800-4664-411").build().test();
+ }
- if (name) {
- insertStructuredName(rawContactId, "Deer", "Dough");
- }
+ public void testSearchSuggestionsByNameWithPhotoAndEmail() {
+ GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
+ loadTestPhoto()).email("foo@acme.com").build();
+ new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedIcon2(
+ String.valueOf(StatusUpdates.getPresenceIconResourceId(StatusUpdates.OFFLINE)))
+ .expectedText1("Deer Dough").expectedText2("foo@acme.com").build().test();
+ }
- if (nickname) {
- insertNickname(rawContactId, "Little Fawn");
- }
+ public void testSearchSuggestionsByNameWithCompany() {
+ GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
+ .build();
+ new SuggestionTesterBuilder(contact).query("D").expectedText1("Deer Dough").expectedText2(
+ "Google").build().test();
+ }
- final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
- if (photo) {
- values.clear();
- byte[] photoData = loadTestPhoto();
- values.put(Data.RAW_CONTACT_ID, rawContactId);
- values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
- values.put(Photo.PHOTO, photoData);
- mResolver.insert(Data.CONTENT_URI, values);
- }
+ public void testSearchByNicknameWithCompany() {
+ GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").nickname(
+ "Little Fawn").company("Google").build();
+ new SuggestionTesterBuilder(contact).query("L").expectedText1("Deer Dough").expectedText2(
+ "Google").build().test();
+ }
- if (company || title) {
- values.clear();
- values.put(Data.RAW_CONTACT_ID, rawContactId);
- values.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
- values.put(Organization.TYPE, Organization.TYPE_WORK);
- if (company) {
- values.put(Organization.COMPANY, "Google");
- }
- if (title) {
- values.put(Organization.TITLE, "Software Engineer");
- }
- mResolver.insert(Data.CONTENT_URI, values);
- }
+ public void testSearchByCompany() {
+ GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
+ .build();
+ new SuggestionTesterBuilder(contact).query("G").expectedText1("Deer Dough").expectedText2(
+ "Google").build().test();
+ }
- if (email) {
- values.clear();
- values.put(Data.RAW_CONTACT_ID, rawContactId);
- values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
- values.put(Email.TYPE, Email.TYPE_WORK);
- values.put(Email.DATA, "foo@acme.com");
- mResolver.insert(Data.CONTENT_URI, values);
-
- int protocol = Im.PROTOCOL_GOOGLE_TALK;
-
- values.clear();
- values.put(StatusUpdates.PROTOCOL, protocol);
- values.put(StatusUpdates.IM_HANDLE, "foo@acme.com");
- values.put(StatusUpdates.IM_ACCOUNT, "foo");
- values.put(StatusUpdates.PRESENCE_STATUS, StatusUpdates.OFFLINE);
- values.put(StatusUpdates.PRESENCE_CUSTOM_STATUS, "Coding for Android");
- mResolver.insert(StatusUpdates.CONTENT_URI, values);
- }
-
- if (phone) {
- values.clear();
- values.put(Data.RAW_CONTACT_ID, rawContactId);
- values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
- values.put(Data.IS_PRIMARY, 1);
- values.put(Phone.TYPE, Phone.TYPE_HOME);
- values.put(Phone.NUMBER, "1-800-4664-411");
- mResolver.insert(Data.CONTENT_URI, values);
- }
-
- long contactId = queryContactId(rawContactId);
- Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
- .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath(query).build();
-
- Cursor c = mResolver.query(searchUri, null, null, null, null);
- assertEquals(1, c.getCount());
- c.moveToFirst();
- values.clear();
-
- // SearchManager does not declare a constant for _id
- values.put("_id", contactId);
- values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
- values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
-
- String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
- if (expectIcon1Uri) {
- assertTrue(icon1.startsWith("content:"));
- } else {
- assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture), icon1);
- }
-
- values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
- values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, contactId);
- values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, contactId);
- assertCursorValues(c, values);
- c.close();
-
- // See if the same result is returned by a shortcut refresh
- Uri shortcutsUri = ContactsContract.AUTHORITY_URI.buildUpon()
- .appendPath(SearchManager.SUGGEST_URI_PATH_SHORTCUT).build();
- Uri refreshUri = ContentUris.withAppendedId(shortcutsUri, contactId);
-
- String[] projection = new String[]{
- SearchManager.SUGGEST_COLUMN_ICON_1,
- SearchManager.SUGGEST_COLUMN_ICON_2,
- SearchManager.SUGGEST_COLUMN_TEXT_1,
- SearchManager.SUGGEST_COLUMN_TEXT_2,
- SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
- SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
- "_id",
- };
-
- c = mResolver.query(refreshUri, projection, null, null, null);
- try {
- assertEquals("Record count", 1, c.getCount());
- c.moveToFirst();
- assertCursorValues(c, values);
- } finally {
- c.close();
- }
-
- // Cleanup
- mResolver.delete(rawContactUri, null, null);
+ public void testSearchByTitleWithCompany() {
+ GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
+ .title("Software Engineer").build();
+ new SuggestionTesterBuilder(contact).query("S").expectIcon1Uri(false).expectedText1(
+ "Deer Dough").expectedText2("Google").build().test();
}
public void testSearchSuggestionsByPhoneNumber() throws Exception {
@@ -327,5 +146,225 @@
assertCursorValues(c, values);
c.close();
}
-}
+ /**
+ * Tests that the quick search suggestion returns the expected contact
+ * information.
+ */
+ private final class SuggestionTester {
+
+ private final GoldenContact contact;
+
+ private final String query;
+
+ private final boolean expectIcon1Uri;
+
+ private final String expectedIcon2;
+
+ private final String expectedText1;
+
+ private final String expectedText2;
+
+ public SuggestionTester(SuggestionTesterBuilder builder) {
+ contact = builder.contact;
+ query = builder.query;
+ expectIcon1Uri = builder.expectIcon1Uri;
+ expectedIcon2 = builder.expectedIcon2;
+ expectedText1 = builder.expectedText1;
+ expectedText2 = builder.expectedText2;
+ }
+
+ /**
+ * Tests suggest and refresh queries from quick search box, then deletes the contact from
+ * the data base.
+ */
+ public void test() {
+
+ testQsbSuggest();
+ testContactIdQsbRefresh();
+ testLookupKeyQsbRefresh();
+
+ // Cleanup
+ contact.delete();
+ }
+
+ /**
+ * Tests that the contacts provider return the appropriate information from the golden
+ * contact in response to the suggestion query from the quick search box.
+ */
+ private void testQsbSuggest() {
+
+ Uri searchUri = new Uri.Builder().scheme("content").authority(
+ ContactsContract.AUTHORITY).appendPath(SearchManager.SUGGEST_URI_PATH_QUERY)
+ .appendPath(query).build();
+
+ Cursor c = mResolver.query(searchUri, null, null, null, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+
+ String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
+ if (expectIcon1Uri) {
+ assertTrue(icon1.startsWith("content:"));
+ } else {
+ assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture),
+ icon1);
+ }
+
+ // SearchManager does not declare a constant for _id
+ ContentValues values = getContactValues();
+ assertCursorValues(c, values);
+ c.close();
+ }
+
+ /**
+ * Returns the expected Quick Search Box content values for the golden contact.
+ */
+ private ContentValues getContactValues() {
+
+ ContentValues values = new ContentValues();
+ values.put("_id", contact.getContactId());
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
+ values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
+
+ values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
+ values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, contact.getLookupKey());
+ values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, contact.getLookupKey());
+ return values;
+ }
+
+ /**
+ * Performs the refresh query and returns a cursor to the results.
+ *
+ * @param refreshId the final component path of the refresh query, which identifies which
+ * contact to refresh.
+ */
+ private Cursor refreshQuery(String refreshId) {
+
+ // See if the same result is returned by a shortcut refresh
+ Uri refershUri = ContactsContract.AUTHORITY_URI.buildUpon().appendPath(
+ SearchManager.SUGGEST_URI_PATH_SHORTCUT)
+ .appendPath(refreshId).build();
+
+ String[] projection = new String[] {
+ SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_ICON_2,
+ SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
+ SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "_id",
+ };
+
+ return mResolver.query(refershUri, projection, null, null, null);
+ }
+
+ /**
+ * Tests that the contacts provider returns an empty result in response to a refresh query
+ * from the quick search box that uses the contact id to identify the contact. The empty
+ * result indicates that the shortcut is no longer valid, and the QSB will replace it with
+ * a new-style shortcut the next time they click on the contact.
+ *
+ * @see #testLookupKeyQsbRefresh()
+ */
+ private void testContactIdQsbRefresh() {
+
+ Cursor c = refreshQuery(String.valueOf(contact.getContactId()));
+ try {
+ assertEquals("Record count", 0, c.getCount());
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Tests that the contacts provider return the appropriate information from the golden
+ * contact in response to the refresh query from the quick search box. The refresh query
+ * uses the currently-supported mechanism of identifying the contact by the lookup key,
+ * which is more stable than the previously used contact id.
+ */
+ private void testLookupKeyQsbRefresh() {
+
+ Cursor c = refreshQuery(contact.getLookupKey());
+ try {
+ assertEquals("Record count", 1, c.getCount());
+ c.moveToFirst();
+ assertCursorValues(c, getContactValues());
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * Builds {@link SuggestionTester} objects. Unspecified boolean objects default to
+ * false. Unspecified String objects default to null.
+ */
+ private final class SuggestionTesterBuilder {
+
+ private final GoldenContact contact;
+
+ private String query;
+
+ private boolean expectIcon1Uri;
+
+ private String expectedIcon2;
+
+ private String expectedText1;
+
+ private String expectedText2;
+
+ public SuggestionTesterBuilder(GoldenContact contact) {
+ this.contact = contact;
+ }
+
+ /**
+ * Builds the {@link SuggestionTester} specified by this builder.
+ */
+ public SuggestionTester build() {
+ return new SuggestionTester(this);
+ }
+
+ /**
+ * The text of the user's query to quick search (i.e., what they typed
+ * in the search box).
+ */
+ public SuggestionTesterBuilder query(String value) {
+ query = value;
+ return this;
+ }
+
+ /**
+ * Whether to set Icon1, which in practice is the contact's photo.
+ * <p>
+ * TODO(tomo): Replace with actual expected value? This might be hard
+ * because the values look non-deterministic, such as
+ * "content://com.android.contacts/contacts/2015/photo"
+ */
+ public SuggestionTesterBuilder expectIcon1Uri(boolean value) {
+ expectIcon1Uri = value;
+ return this;
+ }
+
+ /**
+ * The value for Icon2, which in practice is the contact's Chat status
+ * (available, busy, etc.)
+ */
+ public SuggestionTesterBuilder expectedIcon2(String value) {
+ expectedIcon2 = value;
+ return this;
+ }
+
+ /**
+ * First line of suggestion text expected to be returned (required).
+ */
+ public SuggestionTesterBuilder expectedText1(String value) {
+ expectedText1 = value;
+ return this;
+ }
+
+ /**
+ * Second line of suggestion text expected to return (optional).
+ */
+ public SuggestionTesterBuilder expectedText2(String value) {
+ expectedText2 = value;
+ return this;
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/GroupsTest.java b/tests/src/com/android/providers/contacts/GroupsTest.java
index 085fef6..ebec559 100644
--- a/tests/src/com/android/providers/contacts/GroupsTest.java
+++ b/tests/src/com/android/providers/contacts/GroupsTest.java
@@ -156,8 +156,7 @@
Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
createGroup(mAccount, "gsid1", "title1"));
clearDirty(uri);
- Uri updateUri = uri.buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+ Uri updateUri = setCallerIsSyncAdapter(uri, mAccount);
ContentValues values = new ContentValues();
values.put(Groups.NOTES, "New notes");
@@ -189,8 +188,7 @@
assertEquals(1, getCount(uri, null, null));
assertStoredValue(uri, Groups.DELETED, "1");
- Uri permanentDeletionUri = uri.buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+ Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
mResolver.delete(permanentDeletionUri, null, null);
assertEquals(0, getCount(uri, null, null));
}
@@ -201,8 +199,7 @@
Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
assertEquals(1, getCount(uri, null, null));
- Uri permanentDeletionUri = uri.buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+ Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
mResolver.delete(permanentDeletionUri, null, null);
assertEquals(0, getCount(uri, null, null));
}
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
index bdfab7a..3919770 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
@@ -77,8 +77,7 @@
targetContext, "perf_imp.");
targetContextWrapper.makeExistingFilesAndDbsAccessible();
IsolatedContext providerContext = new IsolatedContext(resolver, targetContextWrapper);
- TestAggregationScheduler scheduler = new TestAggregationScheduler();
- SynchronousContactsProvider2 provider = new SynchronousContactsProvider2(scheduler);
+ SynchronousContactsProvider2 provider = new SynchronousContactsProvider2();
provider.setDataWipeEnabled(false);
provider.attachInfo(providerContext, null);
resolver.addProvider(ContactsContract.AUTHORITY, provider);
@@ -120,28 +119,4 @@
return mDbHelper;
}
}
-
- private static class TestAggregationScheduler extends ContactAggregationScheduler {
-
- @Override
- public void start() {
- }
-
- @Override
- public void stop() {
- }
-
- @Override
- long currentTime() {
- return 0;
- }
-
- @Override
- void runDelayed() {
- }
-
- void trigger() {
- run();
- }
- }
}
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
index af15b3d..08c7582 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
@@ -184,8 +184,6 @@
Calls.CACHED_NUMBER_TYPE,
});
- // Performing an aggregation pass should not change anything.
- provider.scheduleContactAggregation();
assertQueryResults("expected_contacts.txt", Contacts.CONTENT_URI, new String[]{
Contacts._ID,
Contacts.DISPLAY_NAME,
diff --git a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
index 55649d0..85ed6a7 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
@@ -52,11 +52,9 @@
@SuppressWarnings("deprecation")
public class LegacyContactsProviderTest extends BaseContactsProvider2Test {
- private static final boolean USE_LEGACY_PROVIDER = false;
-
@Override
protected Class<? extends ContentProvider> getProviderClass() {
- return USE_LEGACY_PROVIDER ? ContactsProvider.class : SynchronousContactsProvider2.class;
+ return SynchronousContactsProvider2.class;
}
@Override
@@ -730,23 +728,8 @@
* Capturing the search suggestion requirements in test cases as a reference.
*/
public void testSearchSuggestionsNotInMyContacts() throws Exception {
-
// We don't provide compatibility for search suggestions
- if (!USE_LEGACY_PROVIDER) {
- return;
- }
-
- ContentValues values = new ContentValues();
- putContactValues(values);
- mResolver.insert(People.CONTENT_URI, values);
-
- Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
- .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("D").build();
-
- // If the contact is not in the "my contacts" group, nothing should be found
- Cursor c = mResolver.query(searchUri, null, null, null, null);
- assertEquals(0, c.getCount());
- c.close();
+ return;
}
/**
@@ -755,50 +738,7 @@
public void testSearchSuggestionsByName() throws Exception {
// We don't provide compatibility for search suggestions
- if (!USE_LEGACY_PROVIDER) {
- return;
- }
-
- assertSearchSuggestion(
- true, // name
- true, // photo
- true, // organization
- false, // phone
- false, // email
- "D", // query
- true, // expect icon URI
- null, "Deer Dough", "Google");
-
- assertSearchSuggestion(
- true, // name
- true, // photo
- false, // organization
- true, // phone
- false, // email
- "D", // query
- true, // expect icon URI
- null, "Deer Dough", "1-800-4664-411");
-
- assertSearchSuggestion(
- true, // name
- true, // photo
- false, // organization
- false, // phone
- true, // email
- "D", // query
- true, // expect icon URI
- String.valueOf(Presence.getPresenceIconResourceId(Presence.OFFLINE)),
- "Deer Dough", "foo@acme.com");
-
- assertSearchSuggestion(
- true, // name
- false, // photo
- true, // organization
- false, // phone
- false, // email
- "D", // query
- false, // expect icon URI
- null, "Deer Dough", "Google");
+ return;
}
private void assertSearchSuggestion(boolean name, boolean photo, boolean organization,
@@ -897,44 +837,8 @@
* Capturing the search suggestion requirements in test cases as a reference.
*/
public void testSearchSuggestionsByPhoneNumber() throws Exception {
-
// We don't provide compatibility for search suggestions
- if (!USE_LEGACY_PROVIDER) {
- return;
- }
-
- ContentValues values = new ContentValues();
-
- Uri searchUri = new Uri.Builder().scheme("content").authority(Contacts.AUTHORITY)
- .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("12345").build();
-
- Cursor c = mResolver.query(searchUri, null, null, null, null);
- assertEquals(2, c.getCount());
- c.moveToFirst();
-
- values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Execute");
- values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "");
- values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
- String.valueOf(com.android.internal.R.drawable.call_contact));
- values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
- Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
- values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
- values.putNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
- assertCursorValues(c, values);
-
- c.moveToNext();
- values.clear();
- values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Dial number");
- values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345");
- values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
- String.valueOf(com.android.internal.R.drawable.create_contact));
- values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
- Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
- values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345");
- values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
- SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
- assertCursorValues(c, values);
- c.close();
+ return;
}
public void testSettings() throws Exception {
diff --git a/tests/src/com/android/providers/contacts/NameSplitterTest.java b/tests/src/com/android/providers/contacts/NameSplitterTest.java
index 9fa0d96..87c897e 100644
--- a/tests/src/com/android/providers/contacts/NameSplitterTest.java
+++ b/tests/src/com/android/providers/contacts/NameSplitterTest.java
@@ -18,6 +18,8 @@
import com.android.providers.contacts.NameSplitter.Name;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhoneticNameStyle;
import android.test.suitebuilder.annotation.SmallTest;
import java.util.Locale;
@@ -40,9 +42,12 @@
@Override
protected void setUp() throws Exception {
super.setUp();
+ createNameSplitter(Locale.US);
+ }
- mNameSplitter = new NameSplitter("Mr, Ms, Mrs", "d', st, st., von", "Jr, M.D., MD, D.D.S.",
- "&, AND", Locale.getDefault());
+ private void createNameSplitter(Locale locale) {
+ mNameSplitter = new NameSplitter("Mr, Ms, Mrs", "d', st, st., von", "Jr., M.D., MD, D.D.S.",
+ "&, AND", locale);
}
public void testNull() {
@@ -60,7 +65,7 @@
assertJoinedName(null, null, null, null, null, null);
}
- public void testLastName() {
+ public void testFamilyName() {
assertSplitName("Smith", null, "Smith", null, null, null);
assertJoinedName("Smith", null, "Smith", null, null, null);
}
@@ -70,104 +75,301 @@
assertJoinedName("MD", "Ms", null, null, "MD", null);
}
- public void testFirstLastName() {
+ public void testGivenFamilyName() {
assertSplitName("John Smith", null, "John", null, "Smith", null);
assertJoinedName("John Smith", null, "John", null, "Smith", null);
}
- public void testFirstMiddleLastName() {
+ public void testGivenMiddleFamilyName() {
assertSplitName("John Edward Smith", null, "John", "Edward", "Smith", null);
- assertJoinedName("John Smith", null, "John", "Edward", "Smith", null);
+ assertJoinedName("John Edward Smith", null, "John", "Edward", "Smith", null);
}
- public void testThreeNamesAndLastName() {
+ public void testThreeNamesAndFamilyName() {
assertSplitName("John Edward Kevin Smith", null, "John Edward", "Kevin", "Smith", null);
- assertJoinedName("John Edward Smith", null, "John Edward", "Kevin", "Smith", null);
+ assertJoinedName("John Edward Kevin Smith", null, "John Edward", "Kevin", "Smith", null);
}
- public void testPrefixFirstLastName() {
+ public void testPrefixFivenFamilyName() {
assertSplitName("Mr. John Smith", "Mr", "John", null, "Smith", null);
assertJoinedName("John Smith", "Mr", "John", null, "Smith", null);
assertSplitName("Mr.John Smith", "Mr", "John", null, "Smith", null);
assertJoinedName("John Smith", "Mr", "John", null, "Smith", null);
}
- public void testFirstLastNameSuffix() {
- assertSplitName("John Smith Jr.", null, "John", null, "Smith", "Jr");
- assertJoinedName("John Smith", null, "John", null, "Smith", "Jr");
+ public void testFivenFamilyNameSuffix() {
+ assertSplitName("John Smith Jr", null, "John", null, "Smith", "Jr");
+ assertJoinedName("John Smith, Jr.", null, "John", null, "Smith", "Jr");
}
- public void testFirstLastNameSuffixWithDot() {
+ public void testGivenFamilyNameSuffixWithDot() {
assertSplitName("John Smith M.D.", null, "John", null, "Smith", "M.D.");
- assertJoinedName("John Smith", null, "John", null, "Smith", "M.D.");
+ assertJoinedName("John Smith, M.D.", null, "John", null, "Smith", "M.D.");
assertSplitName("John Smith D D S", null, "John", null, "Smith", "D D S");
- assertJoinedName("John Smith", null, "John", null, "Smith", "D D S");
+ assertJoinedName("John Smith, D D S", null, "John", null, "Smith", "D D S");
}
- public void testFirstSuffixLastName() {
+ public void testGivenSuffixFamilyName() {
assertSplitName("John von Smith", null, "John", null, "von Smith", null);
assertJoinedName("John von Smith", null, "John", null, "von Smith", null);
}
- public void testFirstSuffixLastNameWithDot() {
+ public void testGivenSuffixFamilyNameWithDot() {
assertSplitName("John St.Smith", null, "John", null, "St. Smith", null);
assertJoinedName("John St. Smith", null, "John", null, "St. Smith", null);
}
- public void testPrefixFirstMiddleLast() {
+ public void testPrefixGivenMiddleFamily() {
assertSplitName("Mr. John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
- assertJoinedName("John Smith", "Mr", "John", "Kevin", "Smith", null);
+ assertJoinedName("John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
assertSplitName("Mr.John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
- assertJoinedName("John Smith", "Mr", "John", "Kevin", "Smith", null);
+ assertJoinedName("John Kevin Smith", "Mr", "John", "Kevin", "Smith", null);
}
- public void testPrefixFirstMiddleLastSuffix() {
- assertSplitName("Mr. John Kevin Smith Jr.", "Mr", "John", "Kevin", "Smith", "Jr");
- assertJoinedName("John Smith", "Mr", "John", "Kevin", "Smith", "Jr");
+ public void testPrefixGivenMiddleFamilySuffix() {
+ assertSplitName("Mr. John Kevin Smith Jr.", "Mr", "John", "Kevin", "Smith", "Jr.");
+ assertJoinedName("John Kevin Smith, Jr.", "Mr", "John", "Kevin", "Smith", "Jr");
}
- public void testPrefixFirstMiddlePrefixLastSuffixWrongCapitalization() {
- assertSplitName("MR. john keVin VON SmiTh JR.", "MR", "john", "keVin", "VON SmiTh", "JR");
- assertJoinedName("john VON SmiTh", "MR", "john", "keVin", "VON SmiTh", "JR");
+ public void testPrefixGivenMiddlePrefixFamilySuffixWrongCapitalization() {
+ assertSplitName("MR. john keVin VON SmiTh JR.", "MR", "john", "keVin", "VON SmiTh", "JR.");
+ assertJoinedName("john keVin VON SmiTh, JR.", "MR", "john", "keVin", "VON SmiTh", "JR");
}
- public void testPrefixLastSuffix() {
- assertSplitName("von Smith Jr.", null, null, null, "von Smith", "Jr");
- assertJoinedName("von Smith", null, null, null, "von Smith", "Jr");
+ public void testPrefixFamilySuffix() {
+ assertSplitName("von Smith Jr.", null, null, null, "von Smith", "Jr.");
+ assertJoinedName("von Smith, Jr.", null, null, null, "von Smith", "Jr");
}
- public void testTwoNamesAndLastNameWithAmpersand() {
+ public void testFamilyNameGiven() {
+ assertSplitName("Smith, John", null, "John", null, "Smith", null);
+ assertSplitName("Smith , John", null, "John", null, "Smith", null);
+ assertSplitName("Smith, John Kimble", null, "John", "Kimble", "Smith", null);
+ assertSplitName("Smith, John K.", null, "John", "K.", "Smith", null);
+ assertSplitName("Smith, John, Jr.", null, "John", null, "Smith", "Jr.");
+ assertSplitName("Smith, John Kimble, Jr.", null, "John", "Kimble", "Smith", "Jr.");
+ assertSplitName("von Braun, John, Jr.", null, "John", null, "von Braun", "Jr.");
+ assertSplitName("von Braun, John Kimble, Jr.", null, "John", "Kimble", "von Braun", "Jr.");
+ }
+
+ public void testTwoNamesAndFamilyNameWithAmpersand() {
assertSplitName("John & Edward Smith", null, "John & Edward", null, "Smith", null);
assertJoinedName("John & Edward Smith", null, "John & Edward", null, "Smith", null);
assertSplitName("John and Edward Smith", null, "John and Edward", null, "Smith", null);
+ assertSplitName("Smith, John and Edward", null, "John and Edward", null, "Smith", null);
assertJoinedName("John and Edward Smith", null, "John and Edward", null, "Smith", null);
}
public void testWithMiddleInitialAndNoDot() {
- assertSplitName("John E. Smith", null, "John", "E", "Smith", null);
- assertJoinedName("John Smith", null, "John", "E", "Smith", null);
+ assertSplitName("John E. Smith", null, "John", "E.", "Smith", null);
+ assertJoinedName("John E Smith", null, "John", "E", "Smith", null);
}
- public void testWithLongFirstNameAndDot() {
- assertSplitName("John Ed. K. Smith", null, "John Ed.", "K", "Smith", null);
- assertJoinedName("John Ed. Smith", null, "John Ed.", "K", "Smith", null);
+ public void testWithLongGivenNameAndDot() {
+ assertSplitName("John Ed. K. Smith", null, "John Ed.", "K.", "Smith", null);
+ assertJoinedName("John Ed. K Smith", null, "John Ed.", "K", "Smith", null);
+ }
+
+ public void testGuessFullNameStyleEmpty() {
+ assertFullNameStyle(FullNameStyle.UNDEFINED, null);
+ assertFullNameStyle(FullNameStyle.UNDEFINED, "");
+ }
+
+ public void testGuessFullNameStyleWestern() {
+
+ // Latin letters
+ assertFullNameStyle(FullNameStyle.WESTERN, "John Doe");
+
+ // Starts with a Latin letter, but contains Japanese letters
+ assertFullNameStyle(FullNameStyle.JAPANESE, "A\u3080\u308D\u306A\u307F\u3048");
+
+ // Starts with an Extended Latin letter "Latin Capital Ligature OE"
+ assertFullNameStyle(FullNameStyle.WESTERN, "\u0152uvre");
+
+ // Non-letters don't make a difference. This one starts with a vertical line
+ assertFullNameStyle(FullNameStyle.WESTERN, "\uFF5C.?+Smith");
+ }
+
+ public void testGuessFullNameStyleJapanese() {
+ createNameSplitter(Locale.JAPAN);
+
+ // Hiragana: always Japanese
+ assertFullNameStyle(FullNameStyle.JAPANESE, "\u3042\u3080\u308D\u306A\u307F\u3048");
+
+ // Katakana: always Japanese
+ assertFullNameStyle(FullNameStyle.JAPANESE, "\u30A2\u30E0\u30ED \u30CA\u30DF\u30A8");
+
+ // Half-width Katakana: always Japanese
+ assertFullNameStyle(FullNameStyle.JAPANESE, "\uFF71\uFF91\uFF9B \uFF85\uFF90\uFF74");
+
+ // Kanji: we cannot tell if this is Japanese, Chinese or Korean, but we are
+ // in Locale.JAPAN, so assume Japanese
+ assertFullNameStyle(FullNameStyle.JAPANESE, "\u5B89\u5BA4\u5948\u7F8E\u6075");
+
+ // TODO: mix
+
+ // Accompanied by a phonetic name in Hiragana, we can safely assume that the
+ // name is Japanese
+ assertFullNameStyle(FullNameStyle.JAPANESE, "\u5B89\u5BA4\u5948\u7F8E\u6075",
+ "\u3042\u3080\u308D", null, "\u306A\u307F\u3048");
+
+ // Starts with a latin letter - not Western
+ assertFullNameStyle(FullNameStyle.JAPANESE, "A\u3080\u308D\u306A\u307F\u3048");
+ }
+
+ public void testGuessFullNameStyleChinese() {
+ createNameSplitter(Locale.CHINA);
+
+ // Hanzi: we cannot tell if this is Chinese, Japanese or Korean,
+ // but we are in Locale.CHINA, so assume this is Chinese
+ assertFullNameStyle(FullNameStyle.CHINESE, "\u675C\u9D51");
+
+ // Accompanied by a phonetic name in Pinyin, we can safely assume that the
+ // name is Chinese
+ assertFullNameStyle(FullNameStyle.CHINESE, "\u675C\u9D51",
+ "du4", null, "juan1");
+
+ // Non-letters don't make a difference. This one starts with a vertical line
+ assertFullNameStyle(FullNameStyle.CHINESE, "\uFF5C--(\u675C\u9D51)");
+ }
+
+
+ public void testGuessPhoneticNameStyle() {
+
+ // Hiragana
+ assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, "\u3042\u3080\u308D", null, null);
+ assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, null, "\u3042\u3080\u308D", null);
+ assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, null, null, "\u306A\u307F\u3048");
+ assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, "\u3042\u3080\u308D", null,
+ "\u306A\u307F\u3048");
+
+ // Katakana
+ assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, "\u30A2\u30E0\u30ED", null,
+ "\u30CA\u30DF\u30A8");
+
+ // Half-width Katakana
+ assertPhoneticNameStyle(PhoneticNameStyle.JAPANESE, "\u30A2\u30E0\u30ED", null,
+ "\u30CA\u30DF\u30A8");
+
+ // Chinese
+ assertPhoneticNameStyle(PhoneticNameStyle.PINYIN, "du4", null, "juan1");
+ }
+
+ public void testSplitJapaneseName() {
+ createNameSplitter(Locale.JAPAN);
+
+ // One word is interpreted as given name only
+ assertSplitName("\u3042\u3080\u308D", null, "\u3042\u3080\u308D", null, null, null);
+
+ // Two words are interpreted as family + give name
+ assertSplitName("\u3042\u3080\u308D \u306A\u307F\u3048", null, "\u306A\u307F\u3048", null,
+ "\u3042\u3080\u308D", null);
+
+ // Multiple words are interpreted as "family - given names"
+ assertSplitName("\u3042\u3080\u308D \u3068\u304A\u308B \u306A\u307F\u3048", null,
+ "\u3068\u304A\u308B \u306A\u307F\u3048", null, "\u3042\u3080\u308D", null);
+
+ // Hanzi characters without spaces: lump them all in the given name
+ assertSplitName("\u6BB5\u5C0F\u6D9B", null, "\u6BB5\u5C0F\u6D9B", null, null, null);
+ }
+
+ public void testSplitChineseName() {
+ createNameSplitter(Locale.CHINA);
+
+ // Two Hanzi characters: familyName+givenName
+ assertSplitName("\u6BB5\u5C0F", null, "\u5C0F", null, "\u6BB5", null);
+
+ // Two Hanzi characters: familyName+middleName+givenName
+ assertSplitName("\u6BB5\u5C0F\u6D9B", null, "\u6D9B", "\u5C0F", "\u6BB5", null);
+
+ // Two Hanzi characters: familyName(2)+middleName+givenName
+ assertSplitName("\u6BB5\u5C0F\u6D9B\u6D9C", null, "\u6D9C", "\u6D9B", "\u6BB5\u5C0F", null);
+ }
+
+ public void testJoinJapaneseName() {
+ createNameSplitter(Locale.JAPAN);
+
+ assertJoinedName("\u3042\u3080\u308D", FullNameStyle.JAPANESE, null, "\u3042\u3080\u308D",
+ null, null, null, true);
+
+ // Given-name-first flag is ignored for CJK locales
+ assertJoinedName("\u3084\u307E\u3056\u304D \u3068\u304A\u308B", FullNameStyle.JAPANESE,
+ null, "\u3068\u304A\u308B", null, "\u3084\u307E\u3056\u304D", null, false);
+ assertJoinedName("\u3084\u307E\u3056\u304D \u3068\u304A\u308B \u3068\u304A\u308B",
+ FullNameStyle.JAPANESE, null, "\u3068\u304A\u308B", "\u3068\u304A\u308B",
+ "\u3084\u307E\u3056\u304D", null, false);
+ }
+
+ public void testJoinChineseName() {
+ createNameSplitter(Locale.CHINA);
+
+ // Given-name-first flag is ignored for CJK locales
+ assertJoinedName("\u6BB5\u5C0F\u6D9B", FullNameStyle.CHINESE, null,
+ "\u6D9B", "\u5C0F", "\u6BB5", null, true);
+ assertJoinedName("\u6BB5\u5C0F\u6D9B", FullNameStyle.CHINESE, null,
+ "\u6D9B", "\u5C0F", "\u6BB5", null, false);
}
private void assertSplitName(String fullName, String prefix, String givenNames,
- String middleName, String lastName, String suffix) {
+ String middleName, String familyName, String suffix) {
final Name name = new Name();
mNameSplitter.split(name, fullName);
assertEquals(prefix, name.getPrefix());
assertEquals(givenNames, name.getGivenNames());
assertEquals(middleName, name.getMiddleName());
- assertEquals(lastName, name.getFamilyName());
+ assertEquals(familyName, name.getFamilyName());
assertEquals(suffix, name.getSuffix());
}
- private void assertJoinedName(String fullName, String prefix, String givenNames, String middleName,
- String lastName, String suffix) {
- final Name name = new Name(prefix, givenNames, middleName, lastName, suffix);
- final String joined = mNameSplitter.join(name);
- assertEquals(fullName, joined);
+ private void assertJoinedName(String expected, String prefix, String givenNames,
+ String middleName, String familyName, String suffix) {
+ assertJoinedName(expected, FullNameStyle.WESTERN, prefix, givenNames, middleName,
+ familyName, suffix, true);
+ }
+
+ private void assertJoinedName(String expected, int nameStyle, String prefix, String givenNames,
+ String middleName, String familyName, String suffix, boolean givenNameFirst) {
+ Name name = new Name();
+ name.fullNameStyle = nameStyle;
+ name.prefix = prefix;
+ name.givenNames = givenNames;
+ name.middleName = middleName;
+ name.familyName = familyName;
+ name.suffix = suffix;
+ String actual = mNameSplitter.join(name, givenNameFirst);
+ assertEquals(expected, actual);
+ }
+
+ private void assertFullNameStyle(int expectedFullNameStyle, String fullName) {
+ Name name = new Name();
+ mNameSplitter.split(name, fullName);
+ mNameSplitter.guessNameStyle(name);
+
+ assertEquals(expectedFullNameStyle, name.fullNameStyle);
+ }
+
+ private void assertFullNameStyle(int expectedFullNameStyle, String fullName,
+ String phoneticFamilyName, String phoneticMiddleName, String phoneticGivenName) {
+ Name name = new Name();
+ mNameSplitter.split(name, fullName);
+ name.phoneticFamilyName = phoneticFamilyName;
+ name.phoneticMiddleName = phoneticMiddleName;
+ name.phoneticGivenName = phoneticGivenName;
+
+ mNameSplitter.guessNameStyle(name);
+
+ assertEquals(expectedFullNameStyle, name.fullNameStyle);
+ }
+
+ private void assertPhoneticNameStyle(int expectedPhoneticNameStyle, String phoneticFamilyName,
+ String phoneticMiddleName, String phoneticGivenName) {
+ Name name = new Name();
+ name.phoneticFamilyName = phoneticFamilyName;
+ name.phoneticMiddleName = phoneticMiddleName;
+ name.phoneticGivenName = phoneticGivenName;
+
+ mNameSplitter.guessNameStyle(name);
+
+ assertEquals(expectedPhoneticNameStyle, name.phoneticNameStyle);
}
}
diff --git a/tests/src/com/android/providers/contacts/PostalSplitterForJapaneseTest.java b/tests/src/com/android/providers/contacts/PostalSplitterForJapaneseTest.java
new file mode 100644
index 0000000..b4be173
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/PostalSplitterForJapaneseTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2009 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 com.android.providers.contacts.PostalSplitter.Postal;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Locale;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link PostalSplitter}, especially for ja_JP locale.
+ * This class depends on the assumption that all the tests in {@link NameSplitterTest} pass.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e class com.android.providers.contacts.PostalSplitterForJapaneseTest -w
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@SmallTest
+public class PostalSplitterForJapaneseTest extends TestCase {
+ private PostalSplitter mPostalSplitter;
+
+ // Postal address for Tokyo Metropolitan City Hall (Tokyo-Tocho) as of 2009 + pseudo PO box.
+ // Japanese don't use neighborhood, so it is not used in this test suite.
+ //
+ // "Nihon" in Kanji
+ private static final String COUNTRY = "\u65E5\u672C";
+ private static final String POSTCODE = "163-8001";
+ // "Tokyo-to" in Kanji
+ private static final String REGION = "\u6771\u4EAC\u90FD";
+ // "Sinjuku-ku" in Kanji
+ private static final String CITY = "\u65B0\u5BBF\u533A";
+ // Nishi-Sinjuku 2-8-1
+ private static final String STREET = "\u897F\u65B0\u5BBF 2-8-1";
+ // Pseudo PO box for test: "Sisyobako 404"
+ private static final String POBOX = "\u79C1\u66F8\u7BB1";
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mPostalSplitter = new PostalSplitter(Locale.JAPAN);
+ }
+
+ public void testNull() {
+ assertSplitPostal(null, null, null, null, null, null, null, null);
+ assertJoinedPostal(null, null, null, null, null, null, null, null);
+ }
+
+ public void testEmpty() {
+ assertSplitPostal("", null, null, null, null, null, null, null);
+ assertJoinedPostal(null, null, null, null, null, null, null, null);
+ }
+
+ public void testSpaces() {
+ assertSplitPostal(" ", " ", null, null, null, null, null, null);
+ assertJoinedPostal(" ", " ", null, null, null, null, null, null);
+ }
+
+ public void testPobox() {
+ assertJoinedPostal(CITY + "\n" + POBOX, null, POBOX, null, CITY, null, null, null);
+ }
+
+ public void testNormal() {
+ assertJoinedPostal(POSTCODE + "\n" + REGION + " " + CITY + "\n" + STREET,
+ STREET, null, null, CITY, REGION, POSTCODE, null);
+ }
+
+ public void testMissingRegion() {
+ assertJoinedPostal(POSTCODE + "\n" + REGION + "\n" + STREET,
+ STREET, null, null, REGION, null, POSTCODE, null);
+
+ assertJoinedPostal(POSTCODE + "\n" + STREET,
+ STREET, null, null, null, null, POSTCODE, null);
+
+ assertJoinedPostal(COUNTRY + " " + POSTCODE + "\n" + STREET,
+ STREET, null, null, null, null, POSTCODE, COUNTRY);
+ }
+
+ public void testMissingPostcode() {
+ assertJoinedPostal(REGION + " " + CITY + "\n" + STREET,
+ STREET, null, null, CITY, REGION, null, null);
+
+ assertJoinedPostal(COUNTRY + "\n" + REGION + " " + CITY + "\n" + STREET,
+ STREET, null, null, CITY, REGION, null, COUNTRY);
+
+ assertJoinedPostal(COUNTRY + "\n" + STREET,
+ STREET, null, null, null, null, null, COUNTRY);
+ }
+
+ public void testMissingStreet() {
+ assertJoinedPostal(COUNTRY + "\n" + STREET,
+ null, null, STREET, null, null, null, COUNTRY);
+ }
+
+ private void assertSplitPostal(String formattedPostal, String street, String pobox,
+ String neighborhood, String city, String region, String postcode, String country) {
+ final Postal postal = new Postal();
+ mPostalSplitter.split(postal, formattedPostal);
+ assertEquals(street, postal.street);
+ assertEquals(pobox, postal.pobox);
+ assertEquals(neighborhood, postal.neighborhood);
+ assertEquals(city, postal.city);
+ assertEquals(region, postal.region);
+ assertEquals(postcode, postal.postcode);
+ assertEquals(country, postal.country);
+ }
+
+ private void assertJoinedPostal(String formattedPostal, String street, String pobox,
+ String neighborhood, String city, String region, String postcode, String country) {
+ final Postal postal = new Postal();
+ postal.street = street;
+ postal.pobox = pobox;
+ postal.neighborhood = neighborhood;
+ postal.city = city;
+ postal.region = region;
+ postal.postcode = postcode;
+ postal.country = country;
+
+ final String joined = mPostalSplitter.join(postal);
+ assertEquals(formattedPostal, joined);
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/PostalSplitterTest.java b/tests/src/com/android/providers/contacts/PostalSplitterTest.java
index 52dbc28..6778b79 100644
--- a/tests/src/com/android/providers/contacts/PostalSplitterTest.java
+++ b/tests/src/com/android/providers/contacts/PostalSplitterTest.java
@@ -25,7 +25,7 @@
import junit.framework.TestCase;
/**
- * Tests for {@link PostalSplitter}.
+ * Tests for {@link PostalSplitter}, especially for en_US locale.
*
* Run the test like this:
* <code>
diff --git a/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
index 7928d22..dd7c626 100644
--- a/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
+++ b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
@@ -225,13 +225,15 @@
final long greyContact = mGrey.createContact(true, GENERIC_NAME);
final long greyPhone = mGrey.createPhone(greyContact, PHONE_GREY);
- final Uri greyUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, greyContact);
- final Uri redUri = greyUri.buildUpon().appendQueryParameter(
+ Uri greyUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, greyContact);
+ greyUri = Uri.withAppendedPath(greyUri, RawContacts.Entity.CONTENT_DIRECTORY);
+ Uri redUri = greyUri.buildUpon().appendQueryParameter(
ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, mRed.packageName).build();
// When calling normally, we have access to protected
mGrey.ensureCallingPackage();
- EntityIterator iterator = mGrey.resolver.queryEntities(greyUri, null, null, null);
+ EntityIterator iterator = RawContacts.newEntityIterator(
+ mGrey.resolver.query(greyUri, null, null, null, null));
while (iterator.hasNext()) {
final Entity entity = iterator.next();
final long rawContactId = entity.getEntityValues().getAsLong(RawContacts._ID);
@@ -240,7 +242,8 @@
// When calling on behalf of another package, protected is omitted
mGrey.ensureCallingPackage();
- iterator = mGrey.resolver.queryEntities(redUri, null, null, null);
+ iterator = RawContacts.newEntityIterator(
+ mGrey.resolver.query(redUri, null, null, null, null));
while (iterator.hasNext()) {
final Entity entity = iterator.next();
final long rawContactId = entity.getEntityValues().getAsLong(RawContacts._ID);
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index f5a3572..865a4c9 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -20,6 +20,8 @@
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
+import java.util.Locale;
+
/**
* A version of {@link ContactsProvider2} class that performs aggregation
* synchronously and wipes all data at construction time.
@@ -31,14 +33,6 @@
private Account mAccount;
private boolean mNetworkNotified;
- public SynchronousContactsProvider2() {
- this(new SynchronousAggregationScheduler());
- }
-
- public SynchronousContactsProvider2(ContactAggregationScheduler scheduler) {
- super(scheduler);
- }
-
@Override
protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
if (mDbHelper == null) {
@@ -97,6 +91,11 @@
}
@Override
+ protected Locale getLocale() {
+ return Locale.US;
+ }
+
+ @Override
protected boolean isWritableAccount(Account account) {
return true;
}
@@ -135,31 +134,4 @@
// We have an explicit test for data conversion - no need to do it every time
return false;
}
-
- public void aggregate() {
- ContactAggregationScheduler scheduler = getContactAggregationScheduler();
- scheduler.run();
- }
-
- private static class SynchronousAggregationScheduler extends ContactAggregationScheduler {
-
- @Override
- public void start() {
- }
-
- @Override
- public void stop() {
- }
-
- @Override
- long currentTime() {
- return 0;
- }
-
- @Override
- void runDelayed() {
-// super.run();
- }
-
- }
}