• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.providers.contacts;
18 
19 import com.android.internal.content.SyncStateContentProviderHelper;
20 import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
21 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
22 import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
23 import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
24 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
25 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
26 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
27 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
28 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
29 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
30 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
31 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
32 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
33 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
34 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
35 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
36 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
37 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
38 import com.google.android.collect.Lists;
39 import com.google.android.collect.Maps;
40 import com.google.android.collect.Sets;
41 
42 import android.accounts.Account;
43 import android.accounts.AccountManager;
44 import android.accounts.OnAccountsUpdateListener;
45 import android.app.Notification;
46 import android.app.NotificationManager;
47 import android.app.PendingIntent;
48 import android.app.SearchManager;
49 import android.content.ContentProviderOperation;
50 import android.content.ContentProviderResult;
51 import android.content.ContentResolver;
52 import android.content.ContentUris;
53 import android.content.ContentValues;
54 import android.content.Context;
55 import android.content.IContentService;
56 import android.content.Intent;
57 import android.content.OperationApplicationException;
58 import android.content.SharedPreferences;
59 import android.content.SyncAdapterType;
60 import android.content.UriMatcher;
61 import android.content.res.AssetFileDescriptor;
62 import android.content.res.Configuration;
63 import android.database.CharArrayBuffer;
64 import android.database.Cursor;
65 import android.database.CursorWrapper;
66 import android.database.DatabaseUtils;
67 import android.database.MatrixCursor;
68 import android.database.MatrixCursor.RowBuilder;
69 import android.database.sqlite.SQLiteConstraintException;
70 import android.database.sqlite.SQLiteContentHelper;
71 import android.database.sqlite.SQLiteDatabase;
72 import android.database.sqlite.SQLiteQueryBuilder;
73 import android.database.sqlite.SQLiteStatement;
74 import android.net.Uri;
75 import android.os.AsyncTask;
76 import android.os.Bundle;
77 import android.os.MemoryFile;
78 import android.os.RemoteException;
79 import android.os.SystemClock;
80 import android.os.SystemProperties;
81 import android.pim.vcard.VCardComposer;
82 import android.pim.vcard.VCardConfig;
83 import android.preference.PreferenceManager;
84 import android.provider.BaseColumns;
85 import android.provider.ContactsContract;
86 import android.provider.LiveFolders;
87 import android.provider.OpenableColumns;
88 import android.provider.SyncStateContract;
89 import android.provider.ContactsContract.AggregationExceptions;
90 import android.provider.ContactsContract.ContactCounts;
91 import android.provider.ContactsContract.Contacts;
92 import android.provider.ContactsContract.Data;
93 import android.provider.ContactsContract.DisplayNameSources;
94 import android.provider.ContactsContract.FullNameStyle;
95 import android.provider.ContactsContract.Groups;
96 import android.provider.ContactsContract.Intents;
97 import android.provider.ContactsContract.PhoneLookup;
98 import android.provider.ContactsContract.PhoneticNameStyle;
99 import android.provider.ContactsContract.ProviderStatus;
100 import android.provider.ContactsContract.RawContacts;
101 import android.provider.ContactsContract.SearchSnippetColumns;
102 import android.provider.ContactsContract.Settings;
103 import android.provider.ContactsContract.StatusUpdates;
104 import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
105 import android.provider.ContactsContract.CommonDataKinds.Email;
106 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
107 import android.provider.ContactsContract.CommonDataKinds.Im;
108 import android.provider.ContactsContract.CommonDataKinds.Nickname;
109 import android.provider.ContactsContract.CommonDataKinds.Organization;
110 import android.provider.ContactsContract.CommonDataKinds.Phone;
111 import android.provider.ContactsContract.CommonDataKinds.Photo;
112 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
113 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
114 import android.telephony.PhoneNumberUtils;
115 import android.text.TextUtils;
116 import android.util.Log;
117 
118 import java.io.ByteArrayOutputStream;
119 import java.io.FileNotFoundException;
120 import java.io.IOException;
121 import java.io.OutputStream;
122 import java.text.SimpleDateFormat;
123 import java.util.ArrayList;
124 import java.util.Collections;
125 import java.util.Date;
126 import java.util.HashMap;
127 import java.util.HashSet;
128 import java.util.List;
129 import java.util.Locale;
130 import java.util.Map;
131 import java.util.Set;
132 import java.util.concurrent.CountDownLatch;
133 
134 /**
135  * Contacts content provider. The contract between this provider and applications
136  * is defined in {@link ContactsContract}.
137  */
138 public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
139 
140     private static final String TAG = "ContactsProvider";
141 
142     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
143 
144     // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
145     // TODO: check for restricted flag during insert(), update(), and delete() calls
146 
147     /** Default for the maximum number of returned aggregation suggestions. */
148     private static final int DEFAULT_MAX_SUGGESTIONS = 5;
149 
150     private static final String GOOGLE_MY_CONTACTS_GROUP_TITLE = "System Group: My Contacts";
151     /**
152      * Property key for the legacy contact import version. The need for a version
153      * as opposed to a boolean flag is that if we discover bugs in the contact import process,
154      * we can trigger re-import by incrementing the import version.
155      */
156     private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1";
157     private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1;
158     private static final String PREF_LOCALE = "locale";
159 
160     private static final String PROPERTY_AGGREGATION_ALGORITHM = "aggregation_v2";
161     private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 2;
162 
163     private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
164 
165     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
166 
167     private static final String TIMES_CONTACED_SORT_COLUMN = "times_contacted_sort";
168 
169     private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
170             + TIMES_CONTACED_SORT_COLUMN + " DESC, "
171             + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
172     private static final String STREQUENT_LIMIT =
173             "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
174             + Contacts.STARRED + "=1) + 25";
175 
176     /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
177             "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
178             " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
179             " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?";
180 
181     /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
182             "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
183             " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
184             " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?";
185 
186     /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
187 
188     private static final int CONTACTS = 1000;
189     private static final int CONTACTS_ID = 1001;
190     private static final int CONTACTS_LOOKUP = 1002;
191     private static final int CONTACTS_LOOKUP_ID = 1003;
192     private static final int CONTACTS_DATA = 1004;
193     private static final int CONTACTS_FILTER = 1005;
194     private static final int CONTACTS_STREQUENT = 1006;
195     private static final int CONTACTS_STREQUENT_FILTER = 1007;
196     private static final int CONTACTS_GROUP = 1008;
197     private static final int CONTACTS_PHOTO = 1009;
198     private static final int CONTACTS_AS_VCARD = 1010;
199     private static final int CONTACTS_AS_MULTI_VCARD = 1011;
200 
201     private static final int RAW_CONTACTS = 2002;
202     private static final int RAW_CONTACTS_ID = 2003;
203     private static final int RAW_CONTACTS_DATA = 2004;
204     private static final int RAW_CONTACT_ENTITY_ID = 2005;
205 
206     private static final int DATA = 3000;
207     private static final int DATA_ID = 3001;
208     private static final int PHONES = 3002;
209     private static final int PHONES_ID = 3003;
210     private static final int PHONES_FILTER = 3004;
211     private static final int EMAILS = 3005;
212     private static final int EMAILS_ID = 3006;
213     private static final int EMAILS_LOOKUP = 3007;
214     private static final int EMAILS_FILTER = 3008;
215     private static final int POSTALS = 3009;
216     private static final int POSTALS_ID = 3010;
217 
218     private static final int PHONE_LOOKUP = 4000;
219 
220     private static final int AGGREGATION_EXCEPTIONS = 6000;
221     private static final int AGGREGATION_EXCEPTION_ID = 6001;
222 
223     private static final int STATUS_UPDATES = 7000;
224     private static final int STATUS_UPDATES_ID = 7001;
225 
226     private static final int AGGREGATION_SUGGESTIONS = 8000;
227 
228     private static final int SETTINGS = 9000;
229 
230     private static final int GROUPS = 10000;
231     private static final int GROUPS_ID = 10001;
232     private static final int GROUPS_SUMMARY = 10003;
233 
234     private static final int SYNCSTATE = 11000;
235     private static final int SYNCSTATE_ID = 11001;
236 
237     private static final int SEARCH_SUGGESTIONS = 12001;
238     private static final int SEARCH_SHORTCUT = 12002;
239 
240     private static final int LIVE_FOLDERS_CONTACTS = 14000;
241     private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001;
242     private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002;
243     private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003;
244 
245     private static final int RAW_CONTACT_ENTITIES = 15001;
246 
247     private static final int PROVIDER_STATUS = 16001;
248 
249     private interface DataContactsQuery {
250         public static final String TABLE = "data "
251                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
252                 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
253 
254         public static final String[] PROJECTION = new String[] {
255             RawContactsColumns.CONCRETE_ID,
256             DataColumns.CONCRETE_ID,
257             ContactsColumns.CONCRETE_ID
258         };
259 
260         public static final int RAW_CONTACT_ID = 0;
261         public static final int DATA_ID = 1;
262         public static final int CONTACT_ID = 2;
263     }
264 
265     private interface DataDeleteQuery {
266         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
267 
268         public static final String[] CONCRETE_COLUMNS = new String[] {
269             DataColumns.CONCRETE_ID,
270             MimetypesColumns.MIMETYPE,
271             Data.RAW_CONTACT_ID,
272             Data.IS_PRIMARY,
273             Data.DATA1,
274         };
275 
276         public static final String[] COLUMNS = new String[] {
277             Data._ID,
278             MimetypesColumns.MIMETYPE,
279             Data.RAW_CONTACT_ID,
280             Data.IS_PRIMARY,
281             Data.DATA1,
282         };
283 
284         public static final int _ID = 0;
285         public static final int MIMETYPE = 1;
286         public static final int RAW_CONTACT_ID = 2;
287         public static final int IS_PRIMARY = 3;
288         public static final int DATA1 = 4;
289     }
290 
291     private interface DataUpdateQuery {
292         String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
293 
294         int _ID = 0;
295         int RAW_CONTACT_ID = 1;
296         int MIMETYPE = 2;
297     }
298 
299 
300     private interface RawContactsQuery {
301         String TABLE = Tables.RAW_CONTACTS;
302 
303         String[] COLUMNS = new String[] {
304                 RawContacts.DELETED,
305                 RawContacts.ACCOUNT_TYPE,
306                 RawContacts.ACCOUNT_NAME,
307         };
308 
309         int DELETED = 0;
310         int ACCOUNT_TYPE = 1;
311         int ACCOUNT_NAME = 2;
312     }
313 
314     public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
315     public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google";
316 
317     /** Sql where statement for filtering on groups. */
318     private static final String CONTACTS_IN_GROUP_SELECT =
319             Contacts._ID + " IN "
320                     + "(SELECT " + RawContacts.CONTACT_ID
321                     + " FROM " + Tables.RAW_CONTACTS
322                     + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
323                             + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
324                             + " FROM " + Tables.DATA_JOIN_MIMETYPES
325                             + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
326                                     + "' AND " + GroupMembership.GROUP_ROW_ID + "="
327                                     + "(SELECT " + Tables.GROUPS + "." + Groups._ID
328                                     + " FROM " + Tables.GROUPS
329                                     + " WHERE " + Groups.TITLE + "=?)))";
330 
331     /** Sql for updating DIRTY flag on multiple raw contacts */
332     private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
333             "UPDATE " + Tables.RAW_CONTACTS +
334             " SET " + RawContacts.DIRTY + "=1" +
335             " WHERE " + RawContacts._ID + " IN (";
336 
337     /** Sql for updating VERSION on multiple raw contacts */
338     private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
339             "UPDATE " + Tables.RAW_CONTACTS +
340             " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
341             " WHERE " + RawContacts._ID + " IN (";
342 
343     /** Name lookup types used for contact filtering */
344     private static final String CONTACT_LOOKUP_NAME_TYPES =
345             NameLookupType.NAME_COLLATION_KEY + "," +
346             NameLookupType.EMAIL_BASED_NICKNAME + "," +
347             NameLookupType.NICKNAME + "," +
348             NameLookupType.NAME_SHORTHAND + "," +
349             NameLookupType.ORGANIZATION + "," +
350             NameLookupType.NAME_CONSONANTS;
351 
352 
353     /** Contains just BaseColumns._COUNT */
354     private static final HashMap<String, String> sCountProjectionMap;
355     /** Contains just the contacts columns */
356     private static final HashMap<String, String> sContactsProjectionMap;
357     /** Contains just the contacts columns */
358     private static final HashMap<String, String> sContactsProjectionWithSnippetMap;
359 
360     /** Used for pushing starred contacts to the top of a times contacted list **/
361     private static final HashMap<String, String> sStrequentStarredProjectionMap;
362     private static final HashMap<String, String> sStrequentFrequentProjectionMap;
363     /** Contains just the contacts vCard columns */
364     private static final HashMap<String, String> sContactsVCardProjectionMap;
365     /** Contains just the raw contacts columns */
366     private static final HashMap<String, String> sRawContactsProjectionMap;
367     /** Contains the columns from the raw contacts entity view*/
368     private static final HashMap<String, String> sRawContactsEntityProjectionMap;
369     /** Contains columns from the data view */
370     private static final HashMap<String, String> sDataProjectionMap;
371     /** Contains columns from the data view */
372     private static final HashMap<String, String> sDistinctDataProjectionMap;
373     /** Contains the data and contacts columns, for joined tables */
374     private static final HashMap<String, String> sPhoneLookupProjectionMap;
375     /** Contains the just the {@link Groups} columns */
376     private static final HashMap<String, String> sGroupsProjectionMap;
377     /** Contains {@link Groups} columns along with summary details */
378     private static final HashMap<String, String> sGroupsSummaryProjectionMap;
379     /** Contains the agg_exceptions columns */
380     private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
381     /** Contains the agg_exceptions columns */
382     private static final HashMap<String, String> sSettingsProjectionMap;
383     /** Contains StatusUpdates columns */
384     private static final HashMap<String, String> sStatusUpdatesProjectionMap;
385     /** Contains Live Folders columns */
386     private static final HashMap<String, String> sLiveFoldersProjectionMap;
387 
388     // where clause to update the status_updates table
389     private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
390             StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
391             " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
392             " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
393 
394     private static final String[] EMPTY_STRING_ARRAY = new String[0];
395 
396     /**
397      * Notification ID for failure to import contacts.
398      */
399     private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1;
400 
401     /** Precompiled sql statement for setting a data record to the primary. */
402     private SQLiteStatement mSetPrimaryStatement;
403     /** Precompiled sql statement for setting a data record to the super primary. */
404     private SQLiteStatement mSetSuperPrimaryStatement;
405     /** Precompiled sql statement for updating a contact display name */
406     private SQLiteStatement mRawContactDisplayNameUpdate;
407     /** Precompiled sql statement for updating an aggregated status update */
408     private SQLiteStatement mLastStatusUpdate;
409     private SQLiteStatement mNameLookupInsert;
410     private SQLiteStatement mNameLookupDelete;
411     private SQLiteStatement mStatusUpdateAutoTimestamp;
412     private SQLiteStatement mStatusUpdateInsert;
413     private SQLiteStatement mStatusUpdateReplace;
414     private SQLiteStatement mStatusAttributionUpdate;
415     private SQLiteStatement mStatusUpdateDelete;
416     private SQLiteStatement mResetNameVerifiedForOtherRawContacts;
417 
418     private long mMimeTypeIdEmail;
419     private long mMimeTypeIdIm;
420     private long mMimeTypeIdStructuredName;
421     private long mMimeTypeIdOrganization;
422     private long mMimeTypeIdNickname;
423     private long mMimeTypeIdPhone;
424     private StringBuilder mSb = new StringBuilder();
425     private String[] mSelectionArgs1 = new String[1];
426     private String[] mSelectionArgs2 = new String[2];
427     private ArrayList<String> mSelectionArgs = Lists.newArrayList();
428 
429     private Account mAccount;
430 
431     static {
432         // Contacts URI matching table
433         final UriMatcher matcher = sUriMatcher;
matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS)434         matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID)435         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA)436         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", AGGREGATION_SUGGESTIONS)437         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
438                 AGGREGATION_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*", AGGREGATION_SUGGESTIONS)439         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
440                 AGGREGATION_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO)441         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER)442         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP)443         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID)444         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD)445         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*", CONTACTS_AS_MULTI_VCARD)446         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
447                 CONTACTS_AS_MULTI_VCARD);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT)448         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", CONTACTS_STREQUENT_FILTER)449         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
450                 CONTACTS_STREQUENT_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP)451         matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);
452 
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS)453         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID)454         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA)455         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID)456         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);
457 
matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES)458         matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
459 
matcher.addURI(ContactsContract.AUTHORITY, "data", DATA)460         matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID)461         matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES)462         matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID)463         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER)464         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER)465         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS)466         matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID)467         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP)468         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER)469         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER)470         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS)471         matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID)472         matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
473 
matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS)474         matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID)475         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY)476         matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
477 
matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE)478         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#", SYNCSTATE_ID)479         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
480                 SYNCSTATE_ID);
481 
matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP)482         matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", AGGREGATION_EXCEPTIONS)483         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
484                 AGGREGATION_EXCEPTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", AGGREGATION_EXCEPTION_ID)485         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
486                 AGGREGATION_EXCEPTION_ID);
487 
matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS)488         matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
489 
matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES)490         matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID)491         matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);
492 
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGESTIONS)493         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
494                 SEARCH_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGESTIONS)495         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
496                 SEARCH_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", SEARCH_SHORTCUT)497         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
498                 SEARCH_SHORTCUT);
499 
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts", LIVE_FOLDERS_CONTACTS)500         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts",
501                 LIVE_FOLDERS_CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*", LIVE_FOLDERS_CONTACTS_GROUP_NAME)502         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*",
503                 LIVE_FOLDERS_CONTACTS_GROUP_NAME);
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones", LIVE_FOLDERS_CONTACTS_WITH_PHONES)504         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones",
505                 LIVE_FOLDERS_CONTACTS_WITH_PHONES);
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites", LIVE_FOLDERS_CONTACTS_FAVORITES)506         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites",
507                 LIVE_FOLDERS_CONTACTS_FAVORITES);
508 
matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS)509         matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
510     }
511 
512     static {
513         sCountProjectionMap = new HashMap<String, String>();
sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)")514         sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)");
515 
516         sContactsProjectionMap = new HashMap<String, String>();
sContactsProjectionMap.put(Contacts._ID, Contacts._ID)517         sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY)518         sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY);
sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, Contacts.DISPLAY_NAME_ALTERNATIVE)519         sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
520                 Contacts.DISPLAY_NAME_ALTERNATIVE);
sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE)521         sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME)522         sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE)523         sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY)524         sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE)525         sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED)526         sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED)527         sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED)528         sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP)529         sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID)530         sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE)531         sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER)532         sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL)533         sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY)534         sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
535 
536         // Handle projections for Contacts-level statuses
addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)537         addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE,
538                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sContactsProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)539         addProjection(sContactsProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
540                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS, ContactsStatusUpdatesColumns.CONCRETE_STATUS)541         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS,
542                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)543         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
544                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)545         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
546                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL, ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)547         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL,
548                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON, ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)549         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON,
550                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
551 
552         sContactsProjectionWithSnippetMap = new HashMap<String, String>();
553         sContactsProjectionWithSnippetMap.putAll(sContactsProjectionMap);
sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_MIMETYPE, SearchSnippetColumns.SNIPPET_MIMETYPE)554         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_MIMETYPE,
555                 SearchSnippetColumns.SNIPPET_MIMETYPE);
sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA_ID, SearchSnippetColumns.SNIPPET_DATA_ID)556         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA_ID,
557                 SearchSnippetColumns.SNIPPET_DATA_ID);
sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA1, SearchSnippetColumns.SNIPPET_DATA1)558         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA1,
559                 SearchSnippetColumns.SNIPPET_DATA1);
sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA2, SearchSnippetColumns.SNIPPET_DATA2)560         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA2,
561                 SearchSnippetColumns.SNIPPET_DATA2);
sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA3, SearchSnippetColumns.SNIPPET_DATA3)562         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA3,
563                 SearchSnippetColumns.SNIPPET_DATA3);
sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA4, SearchSnippetColumns.SNIPPET_DATA4)564         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA4,
565                 SearchSnippetColumns.SNIPPET_DATA4);
566 
567         sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN)568         sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
569                   Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN);
570 
571         sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN)572         sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
573                   Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN);
574 
575         sContactsVCardProjectionMap = Maps.newHashMap();
sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME)576         sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME
577                 + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME);
sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE)578         sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE);
579 
580         sRawContactsProjectionMap = new HashMap<String, String>();
sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID)581         sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID)582         sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME)583         sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE)584         sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID)585         sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION)586         sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY)587         sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED)588         sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY, RawContacts.DISPLAY_NAME_PRIMARY)589         sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY,
590                 RawContacts.DISPLAY_NAME_PRIMARY);
sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, RawContacts.DISPLAY_NAME_ALTERNATIVE)591         sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE,
592                 RawContacts.DISPLAY_NAME_ALTERNATIVE);
sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE, RawContacts.DISPLAY_NAME_SOURCE)593         sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE,
594                 RawContacts.DISPLAY_NAME_SOURCE);
sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME, RawContacts.PHONETIC_NAME)595         sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME,
596                 RawContacts.PHONETIC_NAME);
sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE, RawContacts.PHONETIC_NAME_STYLE)597         sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE,
598                 RawContacts.PHONETIC_NAME_STYLE);
sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED)599         sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED,
600                 RawContacts.NAME_VERIFIED);
sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY, RawContacts.SORT_KEY_PRIMARY)601         sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY,
602                 RawContacts.SORT_KEY_PRIMARY);
sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE, RawContacts.SORT_KEY_ALTERNATIVE)603         sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE,
604                 RawContacts.SORT_KEY_ALTERNATIVE);
sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED)605         sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED, RawContacts.LAST_TIME_CONTACTED)606         sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
607                 RawContacts.LAST_TIME_CONTACTED);
sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE)608         sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE);
sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL)609         sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL);
sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED)610         sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED);
sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE)611         sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1)612         sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1);
sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2)613         sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2);
sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3)614         sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3);
sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4)615         sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4);
616 
617         sDataProjectionMap = new HashMap<String, String>();
sDataProjectionMap.put(Data._ID, Data._ID)618         sDataProjectionMap.put(Data._ID, Data._ID);
sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID)619         sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION)620         sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY)621         sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY)622         sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE)623         sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE)624         sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
sDataProjectionMap.put(Data.DATA1, Data.DATA1)625         sDataProjectionMap.put(Data.DATA1, Data.DATA1);
sDataProjectionMap.put(Data.DATA2, Data.DATA2)626         sDataProjectionMap.put(Data.DATA2, Data.DATA2);
sDataProjectionMap.put(Data.DATA3, Data.DATA3)627         sDataProjectionMap.put(Data.DATA3, Data.DATA3);
sDataProjectionMap.put(Data.DATA4, Data.DATA4)628         sDataProjectionMap.put(Data.DATA4, Data.DATA4);
sDataProjectionMap.put(Data.DATA5, Data.DATA5)629         sDataProjectionMap.put(Data.DATA5, Data.DATA5);
sDataProjectionMap.put(Data.DATA6, Data.DATA6)630         sDataProjectionMap.put(Data.DATA6, Data.DATA6);
sDataProjectionMap.put(Data.DATA7, Data.DATA7)631         sDataProjectionMap.put(Data.DATA7, Data.DATA7);
sDataProjectionMap.put(Data.DATA8, Data.DATA8)632         sDataProjectionMap.put(Data.DATA8, Data.DATA8);
sDataProjectionMap.put(Data.DATA9, Data.DATA9)633         sDataProjectionMap.put(Data.DATA9, Data.DATA9);
sDataProjectionMap.put(Data.DATA10, Data.DATA10)634         sDataProjectionMap.put(Data.DATA10, Data.DATA10);
sDataProjectionMap.put(Data.DATA11, Data.DATA11)635         sDataProjectionMap.put(Data.DATA11, Data.DATA11);
sDataProjectionMap.put(Data.DATA12, Data.DATA12)636         sDataProjectionMap.put(Data.DATA12, Data.DATA12);
sDataProjectionMap.put(Data.DATA13, Data.DATA13)637         sDataProjectionMap.put(Data.DATA13, Data.DATA13);
sDataProjectionMap.put(Data.DATA14, Data.DATA14)638         sDataProjectionMap.put(Data.DATA14, Data.DATA14);
sDataProjectionMap.put(Data.DATA15, Data.DATA15)639         sDataProjectionMap.put(Data.DATA15, Data.DATA15);
sDataProjectionMap.put(Data.SYNC1, Data.SYNC1)640         sDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
sDataProjectionMap.put(Data.SYNC2, Data.SYNC2)641         sDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
sDataProjectionMap.put(Data.SYNC3, Data.SYNC3)642         sDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
sDataProjectionMap.put(Data.SYNC4, Data.SYNC4)643         sDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID)644         sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID);
sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME)645         sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE)646         sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID)647         sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION)648         sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY)649         sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED)650         sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY)651         sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME)652         sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, Contacts.DISPLAY_NAME_ALTERNATIVE)653         sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
654                 Contacts.DISPLAY_NAME_ALTERNATIVE);
sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE)655         sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME)656         sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE)657         sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY)658         sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE)659         sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE)660         sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL)661         sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED)662         sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED)663         sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED)664         sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID)665         sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP)666         sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
sDataProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID)667         sDataProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID);
sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID)668         sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
669 
670         HashMap<String, String> columns;
671         columns = new HashMap<String, String>();
columns.put(RawContacts._ID, RawContacts._ID)672         columns.put(RawContacts._ID, RawContacts._ID);
columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID)673         columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME)674         columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE)675         columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID)676         columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
columns.put(RawContacts.VERSION, RawContacts.VERSION)677         columns.put(RawContacts.VERSION, RawContacts.VERSION);
columns.put(RawContacts.DIRTY, RawContacts.DIRTY)678         columns.put(RawContacts.DIRTY, RawContacts.DIRTY);
columns.put(RawContacts.DELETED, RawContacts.DELETED)679         columns.put(RawContacts.DELETED, RawContacts.DELETED);
columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED)680         columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED);
columns.put(RawContacts.SYNC1, RawContacts.SYNC1)681         columns.put(RawContacts.SYNC1, RawContacts.SYNC1);
columns.put(RawContacts.SYNC2, RawContacts.SYNC2)682         columns.put(RawContacts.SYNC2, RawContacts.SYNC2);
columns.put(RawContacts.SYNC3, RawContacts.SYNC3)683         columns.put(RawContacts.SYNC3, RawContacts.SYNC3);
columns.put(RawContacts.SYNC4, RawContacts.SYNC4)684         columns.put(RawContacts.SYNC4, RawContacts.SYNC4);
columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED)685         columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE)686         columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
columns.put(Data.MIMETYPE, Data.MIMETYPE)687         columns.put(Data.MIMETYPE, Data.MIMETYPE);
columns.put(Data.DATA1, Data.DATA1)688         columns.put(Data.DATA1, Data.DATA1);
columns.put(Data.DATA2, Data.DATA2)689         columns.put(Data.DATA2, Data.DATA2);
columns.put(Data.DATA3, Data.DATA3)690         columns.put(Data.DATA3, Data.DATA3);
columns.put(Data.DATA4, Data.DATA4)691         columns.put(Data.DATA4, Data.DATA4);
columns.put(Data.DATA5, Data.DATA5)692         columns.put(Data.DATA5, Data.DATA5);
columns.put(Data.DATA6, Data.DATA6)693         columns.put(Data.DATA6, Data.DATA6);
columns.put(Data.DATA7, Data.DATA7)694         columns.put(Data.DATA7, Data.DATA7);
columns.put(Data.DATA8, Data.DATA8)695         columns.put(Data.DATA8, Data.DATA8);
columns.put(Data.DATA9, Data.DATA9)696         columns.put(Data.DATA9, Data.DATA9);
columns.put(Data.DATA10, Data.DATA10)697         columns.put(Data.DATA10, Data.DATA10);
columns.put(Data.DATA11, Data.DATA11)698         columns.put(Data.DATA11, Data.DATA11);
columns.put(Data.DATA12, Data.DATA12)699         columns.put(Data.DATA12, Data.DATA12);
columns.put(Data.DATA13, Data.DATA13)700         columns.put(Data.DATA13, Data.DATA13);
columns.put(Data.DATA14, Data.DATA14)701         columns.put(Data.DATA14, Data.DATA14);
columns.put(Data.DATA15, Data.DATA15)702         columns.put(Data.DATA15, Data.DATA15);
columns.put(Data.SYNC1, Data.SYNC1)703         columns.put(Data.SYNC1, Data.SYNC1);
columns.put(Data.SYNC2, Data.SYNC2)704         columns.put(Data.SYNC2, Data.SYNC2);
columns.put(Data.SYNC3, Data.SYNC3)705         columns.put(Data.SYNC3, Data.SYNC3);
columns.put(Data.SYNC4, Data.SYNC4)706         columns.put(Data.SYNC4, Data.SYNC4);
columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID)707         columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID);
columns.put(Data.STARRED, Data.STARRED)708         columns.put(Data.STARRED, Data.STARRED);
columns.put(Data.DATA_VERSION, Data.DATA_VERSION)709         columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY)710         columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY)711         columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID)712         columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
713         sRawContactsEntityProjectionMap = columns;
714 
715         // Handle projections for Contacts-level statuses
addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)716         addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE,
717                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sContactsProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)718         addProjection(sContactsProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
719                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS, ContactsStatusUpdatesColumns.CONCRETE_STATUS)720         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS,
721                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)722         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
723                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)724         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
725                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)726         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
727                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON, ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)728         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
729                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
730 
731         // Handle projections for Data-level statuses
addProjection(sDataProjectionMap, Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)732         addProjection(sDataProjectionMap, Data.PRESENCE,
733                 Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sDataProjectionMap, Data.CONTACT_CHAT_CAPABILITY, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)734         addProjection(sDataProjectionMap, Data.CONTACT_CHAT_CAPABILITY,
735                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
addProjection(sDataProjectionMap, Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)736         addProjection(sDataProjectionMap, Data.STATUS,
737                 StatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)738         addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP,
739                 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)740         addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE,
741                 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sDataProjectionMap, Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)742         addProjection(sDataProjectionMap, Data.STATUS_LABEL,
743                 StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sDataProjectionMap, Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)744         addProjection(sDataProjectionMap, Data.STATUS_ICON,
745                 StatusUpdatesColumns.CONCRETE_STATUS_ICON);
746 
747         // Projection map for data grouped by contact (not raw contact) and some data field(s)
748         sDistinctDataProjectionMap = new HashMap<String, String>();
sDistinctDataProjectionMap.put(Data._ID, "MIN(" + Data._ID + ") AS " + Data._ID)749         sDistinctDataProjectionMap.put(Data._ID,
750                 "MIN(" + Data._ID + ") AS " + Data._ID);
sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION)751         sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY)752         sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY)753         sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE)754         sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE)755         sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1)756         sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1);
sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2)757         sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2);
sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3)758         sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3);
sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4)759         sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4);
sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5)760         sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5);
sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6)761         sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6);
sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7)762         sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7);
sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8)763         sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8);
sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9)764         sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9);
sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10)765         sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10);
sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11)766         sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11);
sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12)767         sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12);
sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13)768         sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13);
sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14)769         sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14);
sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15)770         sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15);
sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1)771         sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2)772         sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3)773         sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4)774         sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID)775         sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY)776         sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME)777         sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, Contacts.DISPLAY_NAME_ALTERNATIVE)778         sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
779                 Contacts.DISPLAY_NAME_ALTERNATIVE);
sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE)780         sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME)781         sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE)782         sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY)783         sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE)784         sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE,
785                 Contacts.SORT_KEY_ALTERNATIVE);
sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE)786         sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL)787         sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED)788         sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED)789         sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED)790         sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID)791         sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP)792         sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID)793         sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID,
794                 GroupMembership.GROUP_SOURCE_ID);
795 
796         // Handle projections for Contacts-level statuses
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)797         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE,
798                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)799         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
800                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS, ContactsStatusUpdatesColumns.CONCRETE_STATUS)801         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS,
802                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)803         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
804                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)805         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
806                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)807         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
808                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON, ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)809         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
810                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
811 
812         // Handle projections for Data-level statuses
addProjection(sDistinctDataProjectionMap, Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)813         addProjection(sDistinctDataProjectionMap, Data.PRESENCE,
814                 Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sDistinctDataProjectionMap, Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)815         addProjection(sDistinctDataProjectionMap, Data.CHAT_CAPABILITY,
816                 Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
addProjection(sDistinctDataProjectionMap, Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)817         addProjection(sDistinctDataProjectionMap, Data.STATUS,
818                 StatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)819         addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP,
820                 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)821         addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE,
822                 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)823         addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL,
824                 StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)825         addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON,
826                 StatusUpdatesColumns.CONCRETE_STATUS_ICON);
827 
828         sPhoneLookupProjectionMap = new HashMap<String, String>();
sPhoneLookupProjectionMap.put(PhoneLookup._ID, "contacts_view." + Contacts._ID + " AS " + PhoneLookup._ID)829         sPhoneLookupProjectionMap.put(PhoneLookup._ID,
830                 "contacts_view." + Contacts._ID
831                         + " AS " + PhoneLookup._ID);
sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY + " AS " + PhoneLookup.LOOKUP_KEY)832         sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
833                 "contacts_view." + Contacts.LOOKUP_KEY
834                         + " AS " + PhoneLookup.LOOKUP_KEY);
sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME)835         sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
836                 "contacts_view." + Contacts.DISPLAY_NAME
837                         + " AS " + PhoneLookup.DISPLAY_NAME);
sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED + " AS " + PhoneLookup.LAST_TIME_CONTACTED)838         sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
839                 "contacts_view." + Contacts.LAST_TIME_CONTACTED
840                         + " AS " + PhoneLookup.LAST_TIME_CONTACTED);
sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED)841         sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
842                 "contacts_view." + Contacts.TIMES_CONTACTED
843                         + " AS " + PhoneLookup.TIMES_CONTACTED);
sPhoneLookupProjectionMap.put(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED + " AS " + PhoneLookup.STARRED)844         sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
845                 "contacts_view." + Contacts.STARRED
846                         + " AS " + PhoneLookup.STARRED);
sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP)847         sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
848                 "contacts_view." + Contacts.IN_VISIBLE_GROUP
849                         + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID)850         sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
851                 "contacts_view." + Contacts.PHOTO_ID
852                         + " AS " + PhoneLookup.PHOTO_ID);
sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE)853         sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
854                 "contacts_view." + Contacts.CUSTOM_RINGTONE
855                         + " AS " + PhoneLookup.CUSTOM_RINGTONE);
sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER)856         sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
857                 "contacts_view." + Contacts.HAS_PHONE_NUMBER
858                         + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL + " AS " + PhoneLookup.SEND_TO_VOICEMAIL)859         sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
860                 "contacts_view." + Contacts.SEND_TO_VOICEMAIL
861                         + " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER, Phone.NUMBER + " AS " + PhoneLookup.NUMBER)862         sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
863                 Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
sPhoneLookupProjectionMap.put(PhoneLookup.TYPE, Phone.TYPE + " AS " + PhoneLookup.TYPE)864         sPhoneLookupProjectionMap.put(PhoneLookup.TYPE,
865                 Phone.TYPE + " AS " + PhoneLookup.TYPE);
sPhoneLookupProjectionMap.put(PhoneLookup.LABEL, Phone.LABEL + " AS " + PhoneLookup.LABEL)866         sPhoneLookupProjectionMap.put(PhoneLookup.LABEL,
867                 Phone.LABEL + " AS " + PhoneLookup.LABEL);
868 
869         // Groups projection map
870         columns = new HashMap<String, String>();
columns.put(Groups._ID, Groups._ID)871         columns.put(Groups._ID, Groups._ID);
columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME)872         columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE)873         columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID)874         columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
columns.put(Groups.DIRTY, Groups.DIRTY)875         columns.put(Groups.DIRTY, Groups.DIRTY);
columns.put(Groups.VERSION, Groups.VERSION)876         columns.put(Groups.VERSION, Groups.VERSION);
columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE)877         columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE);
columns.put(Groups.TITLE, Groups.TITLE)878         columns.put(Groups.TITLE, Groups.TITLE);
columns.put(Groups.TITLE_RES, Groups.TITLE_RES)879         columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE)880         columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID)881         columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID);
columns.put(Groups.DELETED, Groups.DELETED)882         columns.put(Groups.DELETED, Groups.DELETED);
columns.put(Groups.NOTES, Groups.NOTES)883         columns.put(Groups.NOTES, Groups.NOTES);
columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC)884         columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
columns.put(Groups.SYNC1, Groups.SYNC1)885         columns.put(Groups.SYNC1, Groups.SYNC1);
columns.put(Groups.SYNC2, Groups.SYNC2)886         columns.put(Groups.SYNC2, Groups.SYNC2);
columns.put(Groups.SYNC3, Groups.SYNC3)887         columns.put(Groups.SYNC3, Groups.SYNC3);
columns.put(Groups.SYNC4, Groups.SYNC4)888         columns.put(Groups.SYNC4, Groups.SYNC4);
889         sGroupsProjectionMap = columns;
890 
891         // RawContacts and groups projection map
892         columns = new HashMap<String, String>();
893         columns.putAll(sGroupsProjectionMap);
columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP + ") AS " + Groups.SUMMARY_COUNT)894         columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
895                 + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
896                 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
897                 + ") AS " + Groups.SUMMARY_COUNT);
columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES)898         columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
899                 + ContactsColumns.CONCRETE_ID + ") FROM "
900                 + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
901                 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
902                 + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES);
903         sGroupsSummaryProjectionMap = columns;
904 
905         // Aggregate exception projection map
906         columns = new HashMap<String, String>();
columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id")907         columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE)908         columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1)909         columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1);
columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2)910         columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2);
911         sAggregationExceptionsProjectionMap = columns;
912 
913         // Settings projection map
914         columns = new HashMap<String, String>();
columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME)915         columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME);
columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE)916         columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE);
columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE)917         columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE);
columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC)918         columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC);
columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN(" + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS " + Settings.ANY_UNSYNCED)919         columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
920                 + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN("
921                 + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE "
922                 + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME
923                 + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
924                 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS "
925                 + Settings.ANY_UNSYNCED);
columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS " + Settings.UNGROUPED_COUNT)926         columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
927                 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY "
928                 + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS
929                 + ")) AS " + Settings.UNGROUPED_COUNT);
columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE " + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS " + Settings.UNGROUPED_WITH_PHONES)930         columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
931                 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE "
932                 + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
933                 + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS "
934                 + Settings.UNGROUPED_WITH_PHONES);
935         sSettingsProjectionMap = columns;
936 
937         columns = new HashMap<String, String>();
columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID)938         columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID);
columns.put(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID)939         columns.put(StatusUpdates.DATA_ID,
940                 DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID);
columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT)941         columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT);
columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE)942         columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE);
columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL)943         columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL);
944         // We cannot allow a null in the custom protocol field, because SQLite3 does not
945         // properly enforce uniqueness of null values
columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS " + StatusUpdates.CUSTOM_PROTOCOL)946         columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL
947                 + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS "
948                 + StatusUpdates.CUSTOM_PROTOCOL);
columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE)949         columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE);
columns.put(StatusUpdates.CHAT_CAPABILITY, StatusUpdates.CHAT_CAPABILITY)950         columns.put(StatusUpdates.CHAT_CAPABILITY, StatusUpdates.CHAT_CAPABILITY);
columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS)951         columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS);
columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP)952         columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP);
columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE)953         columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE);
columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON)954         columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON);
columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL)955         columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL);
956         sStatusUpdatesProjectionMap = columns;
957 
958         // Live folder projection
959         sLiveFoldersProjectionMap = new HashMap<String, String>();
sLiveFoldersProjectionMap.put(LiveFolders._ID, Contacts._ID + " AS " + LiveFolders._ID)960         sLiveFoldersProjectionMap.put(LiveFolders._ID,
961                 Contacts._ID + " AS " + LiveFolders._ID);
sLiveFoldersProjectionMap.put(LiveFolders.NAME, Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME)962         sLiveFoldersProjectionMap.put(LiveFolders.NAME,
963                 Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME);
964         // TODO: Put contact photo back when we have a way to display a default icon
965         // for contacts without a photo
966         // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP,
967         //      Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
968     }
969 
addProjection(HashMap<String, String> map, String toField, String fromField)970     private static void addProjection(HashMap<String, String> map, String toField, String fromField) {
971         map.put(toField, fromField + " AS " + toField);
972     }
973 
974     /**
975      * Handles inserts and update for a specific Data type.
976      */
977     private abstract class DataRowHandler {
978 
979         protected final String mMimetype;
980         protected long mMimetypeId;
981 
982         @SuppressWarnings("all")
DataRowHandler(String mimetype)983         public DataRowHandler(String mimetype) {
984             mMimetype = mimetype;
985 
986             // To ensure the data column position. This is dead code if properly configured.
987             if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1
988                     || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
989                     || Email.DATA != Data.DATA1) {
990                 throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
991                         + " data is not in DATA1 column");
992             }
993         }
994 
getMimeTypeId()995         protected long getMimeTypeId() {
996             if (mMimetypeId == 0) {
997                 mMimetypeId = mDbHelper.getMimeTypeId(mMimetype);
998             }
999             return mMimetypeId;
1000         }
1001 
1002         /**
1003          * Inserts a row into the {@link Data} table.
1004          */
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1005         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1006             final long dataId = db.insert(Tables.DATA, null, values);
1007 
1008             Integer primary = values.getAsInteger(Data.IS_PRIMARY);
1009             if (primary != null && primary != 0) {
1010                 setIsPrimary(rawContactId, dataId, getMimeTypeId());
1011             }
1012 
1013             return dataId;
1014         }
1015 
1016         /**
1017          * Validates data and updates a {@link Data} row using the cursor, which contains
1018          * the current data.
1019          *
1020          * @return true if update changed something
1021          */
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1022         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1023                 boolean callerIsSyncAdapter) {
1024             long dataId = c.getLong(DataUpdateQuery._ID);
1025             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1026 
1027             if (values.containsKey(Data.IS_SUPER_PRIMARY)) {
1028                 long mimeTypeId = getMimeTypeId();
1029                 setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
1030                 setIsPrimary(rawContactId, dataId, mimeTypeId);
1031 
1032                 // Now that we've taken care of setting these, remove them from "values".
1033                 values.remove(Data.IS_SUPER_PRIMARY);
1034                 values.remove(Data.IS_PRIMARY);
1035             } else if (values.containsKey(Data.IS_PRIMARY)) {
1036                 setIsPrimary(rawContactId, dataId, getMimeTypeId());
1037 
1038                 // Now that we've taken care of setting this, remove it from "values".
1039                 values.remove(Data.IS_PRIMARY);
1040             }
1041 
1042             if (values.size() > 0) {
1043                 mSelectionArgs1[0] = String.valueOf(dataId);
1044                 mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
1045             }
1046 
1047             if (!callerIsSyncAdapter) {
1048                 setRawContactDirty(rawContactId);
1049             }
1050 
1051             return true;
1052         }
1053 
delete(SQLiteDatabase db, Cursor c)1054         public int delete(SQLiteDatabase db, Cursor c) {
1055             long dataId = c.getLong(DataDeleteQuery._ID);
1056             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1057             boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
1058             mSelectionArgs1[0] = String.valueOf(dataId);
1059             int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
1060             mSelectionArgs1[0] = String.valueOf(rawContactId);
1061             db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
1062             if (count != 0 && primary) {
1063                 fixPrimary(db, rawContactId);
1064             }
1065             return count;
1066         }
1067 
fixPrimary(SQLiteDatabase db, long rawContactId)1068         private void fixPrimary(SQLiteDatabase db, long rawContactId) {
1069             long mimeTypeId = getMimeTypeId();
1070             long primaryId = -1;
1071             int primaryType = -1;
1072             mSelectionArgs1[0] = String.valueOf(rawContactId);
1073             Cursor c = db.query(DataDeleteQuery.TABLE,
1074                     DataDeleteQuery.CONCRETE_COLUMNS,
1075                     Data.RAW_CONTACT_ID + "=?" +
1076                         " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
1077                     mSelectionArgs1, null, null, null);
1078             try {
1079                 while (c.moveToNext()) {
1080                     long dataId = c.getLong(DataDeleteQuery._ID);
1081                     int type = c.getInt(DataDeleteQuery.DATA1);
1082                     if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
1083                         primaryId = dataId;
1084                         primaryType = type;
1085                     }
1086                 }
1087             } finally {
1088                 c.close();
1089             }
1090             if (primaryId != -1) {
1091                 setIsPrimary(rawContactId, primaryId, mimeTypeId);
1092             }
1093         }
1094 
1095         /**
1096          * Returns the rank of a specific record type to be used in determining the primary
1097          * row. Lower number represents higher priority.
1098          */
getTypeRank(int type)1099         protected int getTypeRank(int type) {
1100             return 0;
1101         }
1102 
fixRawContactDisplayName(SQLiteDatabase db, long rawContactId)1103         protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
1104             if (!isNewRawContact(rawContactId)) {
1105                 updateRawContactDisplayName(db, rawContactId);
1106                 mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
1107             }
1108         }
1109 
1110         /**
1111          * Return set of values, using current values at given {@link Data#_ID}
1112          * as baseline, but augmented with any updates.  Returns null if there is
1113          * no change.
1114          */
getAugmentedValues(SQLiteDatabase db, long dataId, ContentValues update)1115         public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
1116                 ContentValues update) {
1117             boolean changing = false;
1118             final ContentValues values = new ContentValues();
1119             mSelectionArgs1[0] = String.valueOf(dataId);
1120             final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
1121                     mSelectionArgs1, null, null, null);
1122             try {
1123                 if (cursor.moveToFirst()) {
1124                     for (int i = 0; i < cursor.getColumnCount(); i++) {
1125                         final String key = cursor.getColumnName(i);
1126                         final String value = cursor.getString(i);
1127                         if (!changing && update.containsKey(key)) {
1128                             Object newValue = update.get(key);
1129                             String newString = newValue == null ? null : newValue.toString();
1130                             changing |= !TextUtils.equals(newString, value);
1131                         }
1132                         values.put(key, value);
1133                     }
1134                 }
1135             } finally {
1136                 cursor.close();
1137             }
1138             if (!changing) {
1139                 return null;
1140             }
1141 
1142             values.putAll(update);
1143             return values;
1144         }
1145     }
1146 
1147     public class CustomDataRowHandler extends DataRowHandler {
1148 
CustomDataRowHandler(String mimetype)1149         public CustomDataRowHandler(String mimetype) {
1150             super(mimetype);
1151         }
1152     }
1153 
1154     public class StructuredNameRowHandler extends DataRowHandler {
1155         private final NameSplitter mSplitter;
1156 
StructuredNameRowHandler(NameSplitter splitter)1157         public StructuredNameRowHandler(NameSplitter splitter) {
1158             super(StructuredName.CONTENT_ITEM_TYPE);
1159             mSplitter = splitter;
1160         }
1161 
1162         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1163         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1164             fixStructuredNameComponents(values, values);
1165 
1166             long dataId = super.insert(db, rawContactId, values);
1167 
1168             String name = values.getAsString(StructuredName.DISPLAY_NAME);
1169             Integer fullNameStyle = values.getAsInteger(StructuredName.FULL_NAME_STYLE);
1170             insertNameLookupForStructuredName(rawContactId, dataId, name,
1171                     fullNameStyle != null
1172                             ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle)
1173                             : FullNameStyle.UNDEFINED);
1174             insertNameLookupForPhoneticName(rawContactId, dataId, values);
1175             fixRawContactDisplayName(db, rawContactId);
1176             triggerAggregation(rawContactId);
1177             return dataId;
1178         }
1179 
1180         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1181         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1182                 boolean callerIsSyncAdapter) {
1183             final long dataId = c.getLong(DataUpdateQuery._ID);
1184             final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1185 
1186             final ContentValues augmented = getAugmentedValues(db, dataId, values);
1187             if (augmented == null) {  // No change
1188                 return false;
1189             }
1190 
1191             fixStructuredNameComponents(augmented, values);
1192 
1193             super.update(db, values, c, callerIsSyncAdapter);
1194             if (values.containsKey(StructuredName.DISPLAY_NAME) ||
1195                     values.containsKey(StructuredName.PHONETIC_FAMILY_NAME) ||
1196                     values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME) ||
1197                     values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) {
1198                 augmented.putAll(values);
1199                 String name = augmented.getAsString(StructuredName.DISPLAY_NAME);
1200                 deleteNameLookup(dataId);
1201                 Integer fullNameStyle = augmented.getAsInteger(StructuredName.FULL_NAME_STYLE);
1202                 insertNameLookupForStructuredName(rawContactId, dataId, name,
1203                         fullNameStyle != null
1204                                 ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle)
1205                                 : FullNameStyle.UNDEFINED);
1206                 insertNameLookupForPhoneticName(rawContactId, dataId, augmented);
1207             }
1208             fixRawContactDisplayName(db, rawContactId);
1209             triggerAggregation(rawContactId);
1210             return true;
1211         }
1212 
1213         @Override
delete(SQLiteDatabase db, Cursor c)1214         public int delete(SQLiteDatabase db, Cursor c) {
1215             long dataId = c.getLong(DataDeleteQuery._ID);
1216             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1217 
1218             int count = super.delete(db, c);
1219 
1220             deleteNameLookup(dataId);
1221             fixRawContactDisplayName(db, rawContactId);
1222             triggerAggregation(rawContactId);
1223             return count;
1224         }
1225 
1226         /**
1227          * Specific list of structured fields.
1228          */
1229         private final String[] STRUCTURED_FIELDS = new String[] {
1230                 StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME,
1231                 StructuredName.FAMILY_NAME, StructuredName.SUFFIX
1232         };
1233 
1234         /**
1235          * Parses the supplied display name, but only if the incoming values do
1236          * not already contain structured name parts. Also, if the display name
1237          * is not provided, generate one by concatenating first name and last
1238          * name.
1239          */
fixStructuredNameComponents(ContentValues augmented, ContentValues update)1240         private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) {
1241             final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME);
1242 
1243             final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
1244             final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);
1245 
1246             if (touchedUnstruct && !touchedStruct) {
1247                 NameSplitter.Name name = new NameSplitter.Name();
1248                 mSplitter.split(name, unstruct);
1249                 name.toValues(update);
1250             } else if (!touchedUnstruct
1251                     && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
1252                 // We need to update the display name when any structured components
1253                 // are specified, even when they are null, which is why we are checking
1254                 // areAnySpecified.  The touchedStruct in the condition is an optimization:
1255                 // if there are non-null values, we know for a fact that some values are present.
1256                 NameSplitter.Name name = new NameSplitter.Name();
1257                 name.fromValues(augmented);
1258                 // As the name could be changed, let's guess the name style again.
1259                 name.fullNameStyle = FullNameStyle.UNDEFINED;
1260                 mSplitter.guessNameStyle(name);
1261                 int unadjustedFullNameStyle = name.fullNameStyle;
1262                 name.fullNameStyle = mSplitter.getAdjustedFullNameStyle(name.fullNameStyle);
1263                 final String joined = mSplitter.join(name, true);
1264                 update.put(StructuredName.DISPLAY_NAME, joined);
1265 
1266                 update.put(StructuredName.FULL_NAME_STYLE, unadjustedFullNameStyle);
1267                 update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle);
1268             } else if (touchedUnstruct && touchedStruct){
1269                 if (!update.containsKey(StructuredName.FULL_NAME_STYLE)) {
1270                     update.put(StructuredName.FULL_NAME_STYLE,
1271                             mSplitter.guessFullNameStyle(unstruct));
1272                 }
1273                 if (!update.containsKey(StructuredName.PHONETIC_NAME_STYLE)) {
1274                     update.put(StructuredName.PHONETIC_NAME_STYLE,
1275                             mSplitter.guessPhoneticNameStyle(unstruct));
1276                 }
1277             }
1278         }
1279     }
1280 
1281     public class StructuredPostalRowHandler extends DataRowHandler {
1282         private PostalSplitter mSplitter;
1283 
StructuredPostalRowHandler(PostalSplitter splitter)1284         public StructuredPostalRowHandler(PostalSplitter splitter) {
1285             super(StructuredPostal.CONTENT_ITEM_TYPE);
1286             mSplitter = splitter;
1287         }
1288 
1289         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1290         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1291             fixStructuredPostalComponents(values, values);
1292             return super.insert(db, rawContactId, values);
1293         }
1294 
1295         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1296         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1297                 boolean callerIsSyncAdapter) {
1298             final long dataId = c.getLong(DataUpdateQuery._ID);
1299             final ContentValues augmented = getAugmentedValues(db, dataId, values);
1300             if (augmented == null) {    // No change
1301                 return false;
1302             }
1303 
1304             fixStructuredPostalComponents(augmented, values);
1305             super.update(db, values, c, callerIsSyncAdapter);
1306             return true;
1307         }
1308 
1309         /**
1310          * Specific list of structured fields.
1311          */
1312         private final String[] STRUCTURED_FIELDS = new String[] {
1313                 StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD,
1314                 StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE,
1315                 StructuredPostal.COUNTRY,
1316         };
1317 
1318         /**
1319          * Prepares the given {@link StructuredPostal} row, building
1320          * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured
1321          * values when missing. When structured components are missing, the
1322          * unstructured value is assigned to {@link StructuredPostal#STREET}.
1323          */
fixStructuredPostalComponents(ContentValues augmented, ContentValues update)1324         private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) {
1325             final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS);
1326 
1327             final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
1328             final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);
1329 
1330             final PostalSplitter.Postal postal = new PostalSplitter.Postal();
1331 
1332             if (touchedUnstruct && !touchedStruct) {
1333                 mSplitter.split(postal, unstruct);
1334                 postal.toValues(update);
1335             } else if (!touchedUnstruct
1336                     && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
1337                 // See comment in
1338                 postal.fromValues(augmented);
1339                 final String joined = mSplitter.join(postal);
1340                 update.put(StructuredPostal.FORMATTED_ADDRESS, joined);
1341             }
1342         }
1343     }
1344 
1345     public class CommonDataRowHandler extends DataRowHandler {
1346 
1347         private final String mTypeColumn;
1348         private final String mLabelColumn;
1349 
CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn)1350         public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
1351             super(mimetype);
1352             mTypeColumn = typeColumn;
1353             mLabelColumn = labelColumn;
1354         }
1355 
1356         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1357         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1358             enforceTypeAndLabel(values, values);
1359             return super.insert(db, rawContactId, values);
1360         }
1361 
1362         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1363         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1364                 boolean callerIsSyncAdapter) {
1365             final long dataId = c.getLong(DataUpdateQuery._ID);
1366             final ContentValues augmented = getAugmentedValues(db, dataId, values);
1367             if (augmented == null) {        // No change
1368                 return false;
1369             }
1370             enforceTypeAndLabel(augmented, values);
1371             return super.update(db, values, c, callerIsSyncAdapter);
1372         }
1373 
1374         /**
1375          * If the given {@link ContentValues} defines {@link #mTypeColumn},
1376          * enforce that {@link #mLabelColumn} only appears when type is
1377          * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise.
1378          */
enforceTypeAndLabel(ContentValues augmented, ContentValues update)1379         private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) {
1380             final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn));
1381             final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn));
1382 
1383             if (hasLabel && !hasType) {
1384                 // When label exists, assert that some type is defined
1385                 throw new IllegalArgumentException(mTypeColumn + " must be specified when "
1386                         + mLabelColumn + " is defined.");
1387             }
1388         }
1389     }
1390 
1391     public class OrganizationDataRowHandler extends CommonDataRowHandler {
1392 
OrganizationDataRowHandler()1393         public OrganizationDataRowHandler() {
1394             super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
1395         }
1396 
1397         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1398         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1399             String company = values.getAsString(Organization.COMPANY);
1400             String title = values.getAsString(Organization.TITLE);
1401 
1402             long dataId = super.insert(db, rawContactId, values);
1403 
1404             fixRawContactDisplayName(db, rawContactId);
1405             insertNameLookupForOrganization(rawContactId, dataId, company, title);
1406             return dataId;
1407         }
1408 
1409         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1410         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1411                 boolean callerIsSyncAdapter) {
1412             if (!super.update(db, values, c, callerIsSyncAdapter)) {
1413                 return false;
1414             }
1415 
1416             boolean containsCompany = values.containsKey(Organization.COMPANY);
1417             boolean containsTitle = values.containsKey(Organization.TITLE);
1418             if (containsCompany || containsTitle) {
1419                 long dataId = c.getLong(DataUpdateQuery._ID);
1420                 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1421 
1422                 String company;
1423 
1424                 if (containsCompany) {
1425                     company = values.getAsString(Organization.COMPANY);
1426                 } else {
1427                     mSelectionArgs1[0] = String.valueOf(dataId);
1428                     company = DatabaseUtils.stringForQuery(db,
1429                             "SELECT " + Organization.COMPANY +
1430                             " FROM " + Tables.DATA +
1431                             " WHERE " + Data._ID + "=?", mSelectionArgs1);
1432                 }
1433 
1434                 String title;
1435                 if (containsTitle) {
1436                     title = values.getAsString(Organization.TITLE);
1437                 } else {
1438                     mSelectionArgs1[0] = String.valueOf(dataId);
1439                     title = DatabaseUtils.stringForQuery(db,
1440                             "SELECT " + Organization.TITLE +
1441                             " FROM " + Tables.DATA +
1442                             " WHERE " + Data._ID + "=?", mSelectionArgs1);
1443                 }
1444 
1445                 deleteNameLookup(dataId);
1446                 insertNameLookupForOrganization(rawContactId, dataId, company, title);
1447 
1448                 fixRawContactDisplayName(db, rawContactId);
1449             }
1450             return true;
1451         }
1452 
1453         @Override
delete(SQLiteDatabase db, Cursor c)1454         public int delete(SQLiteDatabase db, Cursor c) {
1455             long dataId = c.getLong(DataUpdateQuery._ID);
1456             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1457 
1458             int count = super.delete(db, c);
1459             fixRawContactDisplayName(db, rawContactId);
1460             deleteNameLookup(dataId);
1461             return count;
1462         }
1463 
1464         @Override
getTypeRank(int type)1465         protected int getTypeRank(int type) {
1466             switch (type) {
1467                 case Organization.TYPE_WORK: return 0;
1468                 case Organization.TYPE_CUSTOM: return 1;
1469                 case Organization.TYPE_OTHER: return 2;
1470                 default: return 1000;
1471             }
1472         }
1473     }
1474 
1475     public class EmailDataRowHandler extends CommonDataRowHandler {
1476 
EmailDataRowHandler()1477         public EmailDataRowHandler() {
1478             super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
1479         }
1480 
1481         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1482         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1483             String email = values.getAsString(Email.DATA);
1484 
1485             long dataId = super.insert(db, rawContactId, values);
1486 
1487             fixRawContactDisplayName(db, rawContactId);
1488             String address = insertNameLookupForEmail(rawContactId, dataId, email);
1489             if (address != null) {
1490                 triggerAggregation(rawContactId);
1491             }
1492             return dataId;
1493         }
1494 
1495         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1496         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1497                 boolean callerIsSyncAdapter) {
1498             if (!super.update(db, values, c, callerIsSyncAdapter)) {
1499                 return false;
1500             }
1501 
1502             if (values.containsKey(Email.DATA)) {
1503                 long dataId = c.getLong(DataUpdateQuery._ID);
1504                 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1505 
1506                 String address = values.getAsString(Email.DATA);
1507                 deleteNameLookup(dataId);
1508                 insertNameLookupForEmail(rawContactId, dataId, address);
1509                 fixRawContactDisplayName(db, rawContactId);
1510                 triggerAggregation(rawContactId);
1511             }
1512 
1513             return true;
1514         }
1515 
1516         @Override
delete(SQLiteDatabase db, Cursor c)1517         public int delete(SQLiteDatabase db, Cursor c) {
1518             long dataId = c.getLong(DataDeleteQuery._ID);
1519             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1520 
1521             int count = super.delete(db, c);
1522 
1523             deleteNameLookup(dataId);
1524             fixRawContactDisplayName(db, rawContactId);
1525             triggerAggregation(rawContactId);
1526             return count;
1527         }
1528 
1529         @Override
getTypeRank(int type)1530         protected int getTypeRank(int type) {
1531             switch (type) {
1532                 case Email.TYPE_HOME: return 0;
1533                 case Email.TYPE_WORK: return 1;
1534                 case Email.TYPE_CUSTOM: return 2;
1535                 case Email.TYPE_OTHER: return 3;
1536                 default: return 1000;
1537             }
1538         }
1539     }
1540 
1541     public class NicknameDataRowHandler extends CommonDataRowHandler {
1542 
NicknameDataRowHandler()1543         public NicknameDataRowHandler() {
1544             super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL);
1545         }
1546 
1547         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1548         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1549             String nickname = values.getAsString(Nickname.NAME);
1550 
1551             long dataId = super.insert(db, rawContactId, values);
1552 
1553             if (!TextUtils.isEmpty(nickname)) {
1554                 fixRawContactDisplayName(db, rawContactId);
1555                 insertNameLookupForNickname(rawContactId, dataId, nickname);
1556                 triggerAggregation(rawContactId);
1557             }
1558             return dataId;
1559         }
1560 
1561         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1562         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1563                 boolean callerIsSyncAdapter) {
1564             long dataId = c.getLong(DataUpdateQuery._ID);
1565             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1566 
1567             if (!super.update(db, values, c, callerIsSyncAdapter)) {
1568                 return false;
1569             }
1570 
1571             if (values.containsKey(Nickname.NAME)) {
1572                 String nickname = values.getAsString(Nickname.NAME);
1573                 deleteNameLookup(dataId);
1574                 insertNameLookupForNickname(rawContactId, dataId, nickname);
1575                 fixRawContactDisplayName(db, rawContactId);
1576                 triggerAggregation(rawContactId);
1577             }
1578 
1579             return true;
1580         }
1581 
1582         @Override
delete(SQLiteDatabase db, Cursor c)1583         public int delete(SQLiteDatabase db, Cursor c) {
1584             long dataId = c.getLong(DataDeleteQuery._ID);
1585             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1586 
1587             int count = super.delete(db, c);
1588 
1589             deleteNameLookup(dataId);
1590             fixRawContactDisplayName(db, rawContactId);
1591             triggerAggregation(rawContactId);
1592             return count;
1593         }
1594     }
1595 
1596     public class PhoneDataRowHandler extends CommonDataRowHandler {
1597 
PhoneDataRowHandler()1598         public PhoneDataRowHandler() {
1599             super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
1600         }
1601 
1602         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1603         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1604             long dataId;
1605             if (values.containsKey(Phone.NUMBER)) {
1606                 String number = values.getAsString(Phone.NUMBER);
1607                 String normalizedNumber = computeNormalizedNumber(number);
1608                 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
1609                 dataId = super.insert(db, rawContactId, values);
1610 
1611                 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
1612                 mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1613                 fixRawContactDisplayName(db, rawContactId);
1614                 if (normalizedNumber != null) {
1615                     triggerAggregation(rawContactId);
1616                 }
1617             } else {
1618                 dataId = super.insert(db, rawContactId, values);
1619             }
1620             return dataId;
1621         }
1622 
1623         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1624         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1625                 boolean callerIsSyncAdapter) {
1626             String number = null;
1627             String normalizedNumber = null;
1628             if (values.containsKey(Phone.NUMBER)) {
1629                 number = values.getAsString(Phone.NUMBER);
1630                 normalizedNumber = computeNormalizedNumber(number);
1631                 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
1632             }
1633 
1634             if (!super.update(db, values, c, callerIsSyncAdapter)) {
1635                 return false;
1636             }
1637 
1638             if (values.containsKey(Phone.NUMBER)) {
1639                 long dataId = c.getLong(DataUpdateQuery._ID);
1640                 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1641                 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
1642                 mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1643                 fixRawContactDisplayName(db, rawContactId);
1644                 triggerAggregation(rawContactId);
1645             }
1646             return true;
1647         }
1648 
1649         @Override
delete(SQLiteDatabase db, Cursor c)1650         public int delete(SQLiteDatabase db, Cursor c) {
1651             long dataId = c.getLong(DataDeleteQuery._ID);
1652             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1653 
1654             int count = super.delete(db, c);
1655 
1656             updatePhoneLookup(db, rawContactId, dataId, null, null);
1657             mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1658             fixRawContactDisplayName(db, rawContactId);
1659             triggerAggregation(rawContactId);
1660             return count;
1661         }
1662 
computeNormalizedNumber(String number)1663         private String computeNormalizedNumber(String number) {
1664             String normalizedNumber = null;
1665             if (number != null) {
1666                 normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
1667             }
1668             return normalizedNumber;
1669         }
1670 
updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId, String number, String normalizedNumber)1671         private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId,
1672                 String number, String normalizedNumber) {
1673             if (number != null) {
1674                 ContentValues phoneValues = new ContentValues();
1675                 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
1676                 phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
1677                 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
1678                 phoneValues.put(PhoneLookupColumns.MIN_MATCH,
1679                         PhoneNumberUtils.toCallerIDMinMatch(number));
1680 
1681                 db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
1682             } else {
1683                 mSelectionArgs1[0] = String.valueOf(dataId);
1684                 db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1);
1685             }
1686         }
1687 
1688         @Override
getTypeRank(int type)1689         protected int getTypeRank(int type) {
1690             switch (type) {
1691                 case Phone.TYPE_MOBILE: return 0;
1692                 case Phone.TYPE_WORK: return 1;
1693                 case Phone.TYPE_HOME: return 2;
1694                 case Phone.TYPE_PAGER: return 3;
1695                 case Phone.TYPE_CUSTOM: return 4;
1696                 case Phone.TYPE_OTHER: return 5;
1697                 case Phone.TYPE_FAX_WORK: return 6;
1698                 case Phone.TYPE_FAX_HOME: return 7;
1699                 default: return 1000;
1700             }
1701         }
1702     }
1703 
1704     public class GroupMembershipRowHandler extends DataRowHandler {
1705 
GroupMembershipRowHandler()1706         public GroupMembershipRowHandler() {
1707             super(GroupMembership.CONTENT_ITEM_TYPE);
1708         }
1709 
1710         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1711         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1712             resolveGroupSourceIdInValues(rawContactId, db, values, true);
1713             long dataId = super.insert(db, rawContactId, values);
1714             updateVisibility(rawContactId);
1715             return dataId;
1716         }
1717 
1718         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1719         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1720                 boolean callerIsSyncAdapter) {
1721             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1722             resolveGroupSourceIdInValues(rawContactId, db, values, false);
1723             if (!super.update(db, values, c, callerIsSyncAdapter)) {
1724                 return false;
1725             }
1726             updateVisibility(rawContactId);
1727             return true;
1728         }
1729 
1730         @Override
delete(SQLiteDatabase db, Cursor c)1731         public int delete(SQLiteDatabase db, Cursor c) {
1732             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1733             int count = super.delete(db, c);
1734             updateVisibility(rawContactId);
1735             return count;
1736         }
1737 
updateVisibility(long rawContactId)1738         private void updateVisibility(long rawContactId) {
1739             long contactId = mDbHelper.getContactId(rawContactId);
1740             if (contactId != 0) {
1741                 mDbHelper.updateContactVisible(contactId);
1742             }
1743         }
1744 
resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db, ContentValues values, boolean isInsert)1745         private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db,
1746                 ContentValues values, boolean isInsert) {
1747             boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
1748             boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
1749             if (containsGroupSourceId && containsGroupId) {
1750                 throw new IllegalArgumentException(
1751                         "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
1752                                 + "and GroupMembership.GROUP_ROW_ID");
1753             }
1754 
1755             if (!containsGroupSourceId && !containsGroupId) {
1756                 if (isInsert) {
1757                     throw new IllegalArgumentException(
1758                             "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
1759                                     + "and GroupMembership.GROUP_ROW_ID");
1760                 } else {
1761                     return;
1762                 }
1763             }
1764 
1765             if (containsGroupSourceId) {
1766                 final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
1767                 final long groupId = getOrMakeGroup(db, rawContactId, sourceId,
1768                         mInsertedRawContacts.get(rawContactId));
1769                 values.remove(GroupMembership.GROUP_SOURCE_ID);
1770                 values.put(GroupMembership.GROUP_ROW_ID, groupId);
1771             }
1772         }
1773     }
1774 
1775     public class PhotoDataRowHandler extends DataRowHandler {
1776 
PhotoDataRowHandler()1777         public PhotoDataRowHandler() {
1778             super(Photo.CONTENT_ITEM_TYPE);
1779         }
1780 
1781         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1782         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1783             long dataId = super.insert(db, rawContactId, values);
1784             if (!isNewRawContact(rawContactId)) {
1785                 mContactAggregator.updatePhotoId(db, rawContactId);
1786             }
1787             return dataId;
1788         }
1789 
1790         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1791         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
1792                 boolean callerIsSyncAdapter) {
1793             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1794             if (!super.update(db, values, c, callerIsSyncAdapter)) {
1795                 return false;
1796             }
1797 
1798             mContactAggregator.updatePhotoId(db, rawContactId);
1799             return true;
1800         }
1801 
1802         @Override
delete(SQLiteDatabase db, Cursor c)1803         public int delete(SQLiteDatabase db, Cursor c) {
1804             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1805             int count = super.delete(db, c);
1806             mContactAggregator.updatePhotoId(db, rawContactId);
1807             return count;
1808         }
1809     }
1810 
1811     /**
1812      * An entry in group id cache. It maps the combination of (account type, account name
1813      * and source id) to group row id.
1814      */
1815     public class GroupIdCacheEntry {
1816         String accountType;
1817         String accountName;
1818         String sourceId;
1819         long groupId;
1820     }
1821 
1822     private HashMap<String, DataRowHandler> mDataRowHandlers;
1823     private ContactsDatabaseHelper mDbHelper;
1824 
1825     private NameSplitter mNameSplitter;
1826     private NameLookupBuilder mNameLookupBuilder;
1827 
1828     private PostalSplitter mPostalSplitter;
1829 
1830     // We don't need a soft cache for groups - the assumption is that there will only
1831     // be a small number of contact groups. The cache is keyed off source id.  The value
1832     // is a list of groups with this group id.
1833     private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
1834 
1835     private ContactAggregator mContactAggregator;
1836     private LegacyApiSupport mLegacyApiSupport;
1837     private GlobalSearchSupport mGlobalSearchSupport;
1838     private CommonNicknameCache mCommonNicknameCache;
1839 
1840     private ContentValues mValues = new ContentValues();
1841     private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128);
1842     private NameSplitter.Name mName = new NameSplitter.Name();
1843     private HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
1844 
1845     private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
1846     private long mEstimatedStorageRequirement = 0;
1847     private volatile CountDownLatch mAccessLatch;
1848 
1849     private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap();
1850     private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
1851     private HashSet<Long> mDirtyRawContacts = Sets.newHashSet();
1852     private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();
1853 
1854     private boolean mVisibleTouched = false;
1855 
1856     private boolean mSyncToNetwork;
1857 
1858     private Locale mCurrentLocale;
1859 
1860 
1861     @Override
onCreate()1862     public boolean onCreate() {
1863         super.onCreate();
1864         try {
1865             return initialize();
1866         } catch (RuntimeException e) {
1867             Log.e(TAG, "Cannot start provider", e);
1868             return false;
1869         }
1870     }
1871 
initialize()1872     private boolean initialize() {
1873         final Context context = getContext();
1874         mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
1875         mGlobalSearchSupport = new GlobalSearchSupport(this);
1876         mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
1877         mContactAggregator = new ContactAggregator(this, mDbHelper,
1878                 createPhotoPriorityResolver(context));
1879         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
1880 
1881         mDb = mDbHelper.getWritableDatabase();
1882 
1883         initForDefaultLocale();
1884 
1885         mSetPrimaryStatement = mDb.compileStatement(
1886                 "UPDATE " + Tables.DATA +
1887                 " SET " + Data.IS_PRIMARY + "=(_id=?)" +
1888                 " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
1889                 "   AND " + Data.RAW_CONTACT_ID + "=?");
1890 
1891         mSetSuperPrimaryStatement = mDb.compileStatement(
1892                 "UPDATE " + Tables.DATA +
1893                 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
1894                 " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
1895                 "   AND " + Data.RAW_CONTACT_ID + " IN (" +
1896                         "SELECT " + RawContacts._ID +
1897                         " FROM " + Tables.RAW_CONTACTS +
1898                         " WHERE " + RawContacts.CONTACT_ID + " =(" +
1899                                 "SELECT " + RawContacts.CONTACT_ID +
1900                                 " FROM " + Tables.RAW_CONTACTS +
1901                                 " WHERE " + RawContacts._ID + "=?))");
1902 
1903         mRawContactDisplayNameUpdate = mDb.compileStatement(
1904                 "UPDATE " + Tables.RAW_CONTACTS +
1905                 " SET " +
1906                         RawContacts.DISPLAY_NAME_SOURCE + "=?," +
1907                         RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
1908                         RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
1909                         RawContacts.PHONETIC_NAME + "=?," +
1910                         RawContacts.PHONETIC_NAME_STYLE + "=?," +
1911                         RawContacts.SORT_KEY_PRIMARY + "=?," +
1912                         RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
1913                 " WHERE " + RawContacts._ID + "=?");
1914 
1915         mLastStatusUpdate = mDb.compileStatement(
1916                 "UPDATE " + Tables.CONTACTS +
1917                 " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
1918                         "(SELECT " + DataColumns.CONCRETE_ID +
1919                         " FROM " + Tables.STATUS_UPDATES +
1920                         " JOIN " + Tables.DATA +
1921                         "   ON (" + StatusUpdatesColumns.DATA_ID + "="
1922                                 + DataColumns.CONCRETE_ID + ")" +
1923                         " JOIN " + Tables.RAW_CONTACTS +
1924                         "   ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
1925                                 + RawContactsColumns.CONCRETE_ID + ")" +
1926                         " WHERE " + RawContacts.CONTACT_ID + "=?" +
1927                         " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
1928                                 + StatusUpdates.STATUS +
1929                         " LIMIT 1)" +
1930                 " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
1931 
1932         mNameLookupInsert = mDb.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
1933                 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
1934                 + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME
1935                 + ") VALUES (?,?,?,?)");
1936         mNameLookupDelete = mDb.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
1937                 + NameLookupColumns.DATA_ID + "=?");
1938 
1939         mStatusUpdateInsert = mDb.compileStatement(
1940                 "INSERT INTO " + Tables.STATUS_UPDATES + "("
1941                         + StatusUpdatesColumns.DATA_ID + ", "
1942                         + StatusUpdates.STATUS + ","
1943                         + StatusUpdates.STATUS_RES_PACKAGE + ","
1944                         + StatusUpdates.STATUS_ICON + ","
1945                         + StatusUpdates.STATUS_LABEL + ")" +
1946                 " VALUES (?,?,?,?,?)");
1947 
1948         mStatusUpdateReplace = mDb.compileStatement(
1949                 "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
1950                         + StatusUpdatesColumns.DATA_ID + ", "
1951                         + StatusUpdates.STATUS_TIMESTAMP + ","
1952                         + StatusUpdates.STATUS + ","
1953                         + StatusUpdates.STATUS_RES_PACKAGE + ","
1954                         + StatusUpdates.STATUS_ICON + ","
1955                         + StatusUpdates.STATUS_LABEL + ")" +
1956                 " VALUES (?,?,?,?,?,?)");
1957 
1958         mStatusUpdateAutoTimestamp = mDb.compileStatement(
1959                 "UPDATE " + Tables.STATUS_UPDATES +
1960                 " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
1961                         + StatusUpdates.STATUS + "=?" +
1962                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
1963                         + " AND " + StatusUpdates.STATUS + "!=?");
1964 
1965         mStatusAttributionUpdate = mDb.compileStatement(
1966                 "UPDATE " + Tables.STATUS_UPDATES +
1967                 " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
1968                         + StatusUpdates.STATUS_ICON + "=?,"
1969                         + StatusUpdates.STATUS_LABEL + "=?" +
1970                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
1971 
1972         mStatusUpdateDelete = mDb.compileStatement(
1973                 "DELETE FROM " + Tables.STATUS_UPDATES +
1974                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
1975 
1976         // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0
1977         // on all other raw contacts in the same aggregate
1978         mResetNameVerifiedForOtherRawContacts = mDb.compileStatement(
1979                 "UPDATE " + Tables.RAW_CONTACTS +
1980                 " SET " + RawContacts.NAME_VERIFIED + "=0" +
1981                 " WHERE " + RawContacts.CONTACT_ID + "=(" +
1982                         "SELECT " + RawContacts.CONTACT_ID +
1983                         " FROM " + Tables.RAW_CONTACTS +
1984                         " WHERE " + RawContacts._ID + "=?)" +
1985                 " AND " + RawContacts._ID + "!=?");
1986 
1987         mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
1988         mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
1989         mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
1990         mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
1991         mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
1992         mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
1993 
1994         verifyAccounts();
1995 
1996         if (isLegacyContactImportNeeded()) {
1997             importLegacyContactsAsync();
1998         } else {
1999             verifyLocale();
2000         }
2001 
2002         if (isAggregationUpgradeNeeded()) {
2003             upgradeAggregationAlgorithm();
2004         }
2005 
2006         return (mDb != null);
2007     }
2008 
initDataRowHandlers()2009     private void initDataRowHandlers() {
2010       mDataRowHandlers = new HashMap<String, DataRowHandler>();
2011 
2012       mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
2013       mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
2014               new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
2015       mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
2016               StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
2017       mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
2018       mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
2019       mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler());
2020       mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
2021               new StructuredNameRowHandler(mNameSplitter));
2022       mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE,
2023               new StructuredPostalRowHandler(mPostalSplitter));
2024       mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler());
2025       mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler());
2026     }
2027     /**
2028      * Visible for testing.
2029      */
createPhotoPriorityResolver(Context context)2030     /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
2031         return new PhotoPriorityResolver(context);
2032     }
2033 
2034     /**
2035      * (Re)allocates all locale-sensitive structures.
2036      */
initForDefaultLocale()2037     private void initForDefaultLocale() {
2038         mCurrentLocale = getLocale();
2039         mNameSplitter = mDbHelper.createNameSplitter();
2040         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
2041         mPostalSplitter = new PostalSplitter(mCurrentLocale);
2042         mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase());
2043         ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
2044         initDataRowHandlers();
2045     }
2046 
2047     @Override
onConfigurationChanged(Configuration newConfig)2048     public void onConfigurationChanged(Configuration newConfig) {
2049         if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
2050             return;
2051         }
2052 
2053         initForDefaultLocale();
2054         verifyLocale();
2055     }
2056 
verifyAccounts()2057     protected void verifyAccounts() {
2058         AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
2059         onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
2060     }
2061 
2062     /**
2063      * Verifies that the contacts database is properly configured for the current locale.
2064      * If not, changes the database locale to the current locale using an asynchronous task.
2065      * This needs to be done asynchronously because the process involves rebuilding
2066      * large data structures (name lookup, sort keys), which can take minutes on
2067      * a large set of contacts.
2068      */
verifyLocale()2069     protected void verifyLocale() {
2070 
2071         // The process is already running - postpone the change
2072         if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) {
2073             return;
2074         }
2075 
2076         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
2077         final String providerLocale = prefs.getString(PREF_LOCALE, null);
2078         final Locale currentLocale = mCurrentLocale;
2079         if (currentLocale.toString().equals(providerLocale)) {
2080             return;
2081         }
2082 
2083         int providerStatus = mProviderStatus;
2084         setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
2085 
2086         AsyncTask<Integer, Void, Void> task = new AsyncTask<Integer, Void, Void>() {
2087 
2088             int savedProviderStatus;
2089 
2090             @Override
2091             protected Void doInBackground(Integer... params) {
2092                 savedProviderStatus = params[0];
2093                 mDbHelper.setLocale(ContactsProvider2.this, currentLocale);
2094                 return null;
2095             }
2096 
2097             @Override
2098             protected void onPostExecute(Void result) {
2099                 prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply();
2100                 setProviderStatus(savedProviderStatus);
2101 
2102                 // Recursive invocation, needed to cover the case where locale
2103                 // changes once and then changes again before the db upgrade is completed.
2104                 verifyLocale();
2105             }
2106         };
2107 
2108         task.execute(providerStatus);
2109     }
2110 
2111     /* Visible for testing */
2112     @Override
getDatabaseHelper(final Context context)2113     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
2114         return ContactsDatabaseHelper.getInstance(context);
2115     }
2116 
getNameSplitter()2117     /* package */ NameSplitter getNameSplitter() {
2118         return mNameSplitter;
2119     }
2120 
2121     /* Visible for testing */
getLocale()2122     protected Locale getLocale() {
2123         return Locale.getDefault();
2124     }
2125 
isLegacyContactImportNeeded()2126     protected boolean isLegacyContactImportNeeded() {
2127         int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0"));
2128         return version < PROPERTY_CONTACTS_IMPORT_VERSION;
2129     }
2130 
getLegacyContactImporter()2131     protected LegacyContactImporter getLegacyContactImporter() {
2132         return new LegacyContactImporter(getContext(), this);
2133     }
2134 
2135     /**
2136      * Imports legacy contacts in a separate thread.  As long as the import process is running
2137      * all other access to the contacts is blocked.
2138      */
importLegacyContactsAsync()2139     private void importLegacyContactsAsync() {
2140         Log.v(TAG, "Importing legacy contacts");
2141         setProviderStatus(ProviderStatus.STATUS_UPGRADING);
2142         if (mAccessLatch == null) {
2143             mAccessLatch = new CountDownLatch(1);
2144         }
2145 
2146         Thread importThread = new Thread("LegacyContactImport") {
2147             @Override
2148             public void run() {
2149                 final SharedPreferences prefs =
2150                     PreferenceManager.getDefaultSharedPreferences(getContext());
2151                 mDbHelper.setLocale(ContactsProvider2.this, mCurrentLocale);
2152                 prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit();
2153 
2154                 LegacyContactImporter importer = getLegacyContactImporter();
2155                 if (importLegacyContacts(importer)) {
2156                     onLegacyContactImportSuccess();
2157                 } else {
2158                     onLegacyContactImportFailure();
2159                 }
2160             }
2161         };
2162 
2163         importThread.start();
2164     }
2165 
2166     /**
2167      * Unlocks the provider and declares that the import process is complete.
2168      */
onLegacyContactImportSuccess()2169     private void onLegacyContactImportSuccess() {
2170         NotificationManager nm =
2171             (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
2172         nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION);
2173 
2174         // Store a property in the database indicating that the conversion process succeeded
2175         mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED,
2176                 String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION));
2177         setProviderStatus(ProviderStatus.STATUS_NORMAL);
2178         mAccessLatch.countDown();
2179         mAccessLatch = null;
2180         Log.v(TAG, "Completed import of legacy contacts");
2181     }
2182 
2183     /**
2184      * Announces the provider status and keeps the provider locked.
2185      */
onLegacyContactImportFailure()2186     private void onLegacyContactImportFailure() {
2187         Context context = getContext();
2188         NotificationManager nm =
2189             (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
2190 
2191         // Show a notification
2192         Notification n = new Notification(android.R.drawable.stat_notify_error,
2193                 context.getString(R.string.upgrade_out_of_memory_notification_ticker),
2194                 System.currentTimeMillis());
2195         n.setLatestEventInfo(context,
2196                 context.getString(R.string.upgrade_out_of_memory_notification_title),
2197                 context.getString(R.string.upgrade_out_of_memory_notification_text),
2198                 PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0));
2199         n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
2200 
2201         nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n);
2202 
2203         setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY);
2204         Log.v(TAG, "Failed to import legacy contacts");
2205     }
2206 
2207     /* Visible for testing */
importLegacyContacts(LegacyContactImporter importer)2208     /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
2209         boolean aggregatorEnabled = mContactAggregator.isEnabled();
2210         mContactAggregator.setEnabled(false);
2211         try {
2212             if (importer.importContacts()) {
2213 
2214                 // TODO aggregate all newly added raw contacts
2215                 mContactAggregator.setEnabled(aggregatorEnabled);
2216                 return true;
2217             }
2218         } catch (Throwable e) {
2219            Log.e(TAG, "Legacy contact import failed", e);
2220         }
2221         mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement();
2222         return false;
2223     }
2224 
2225     /**
2226      * Wipes all data from the contacts database.
2227      */
wipeData()2228     /* package */ void wipeData() {
2229         mDbHelper.wipeData();
2230     }
2231 
2232     /**
2233      * While importing and aggregating contacts, this content provider will
2234      * block all attempts to change contacts data. In particular, it will hold
2235      * up all contact syncs. As soon as the import process is complete, all
2236      * processes waiting to write to the provider are unblocked and can proceed
2237      * to compete for the database transaction monitor.
2238      */
waitForAccess()2239     private void waitForAccess() {
2240         CountDownLatch latch = mAccessLatch;
2241         if (latch != null) {
2242             while (true) {
2243                 try {
2244                     latch.await();
2245                     mAccessLatch = null;
2246                     return;
2247                 } catch (InterruptedException e) {
2248                     Thread.currentThread().interrupt();
2249                 }
2250             }
2251         }
2252     }
2253 
2254     @Override
insert(Uri uri, ContentValues values)2255     public Uri insert(Uri uri, ContentValues values) {
2256         waitForAccess();
2257         return super.insert(uri, values);
2258     }
2259 
2260     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)2261     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
2262         if (mAccessLatch != null) {
2263             // We are stuck trying to upgrade contacts db.  The only update request
2264             // allowed in this case is an update of provider status, which will trigger
2265             // an attempt to upgrade contacts again.
2266             int match = sUriMatcher.match(uri);
2267             if (match == PROVIDER_STATUS && isLegacyContactImportNeeded()) {
2268                 Integer newStatus = values.getAsInteger(ProviderStatus.STATUS);
2269                 if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) {
2270                     importLegacyContactsAsync();
2271                     return 1;
2272                 } else {
2273                     return 0;
2274                 }
2275             }
2276         }
2277         waitForAccess();
2278         return super.update(uri, values, selection, selectionArgs);
2279     }
2280 
2281     @Override
delete(Uri uri, String selection, String[] selectionArgs)2282     public int delete(Uri uri, String selection, String[] selectionArgs) {
2283         waitForAccess();
2284         return super.delete(uri, selection, selectionArgs);
2285     }
2286 
2287     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)2288     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
2289             throws OperationApplicationException {
2290         waitForAccess();
2291         return super.applyBatch(operations);
2292     }
2293 
2294     @Override
onBeginTransaction()2295     protected void onBeginTransaction() {
2296         if (VERBOSE_LOGGING) {
2297             Log.v(TAG, "onBeginTransaction");
2298         }
2299         super.onBeginTransaction();
2300         mContactAggregator.clearPendingAggregations();
2301         clearTransactionalChanges();
2302     }
2303 
clearTransactionalChanges()2304     private void clearTransactionalChanges() {
2305         mInsertedRawContacts.clear();
2306         mUpdatedRawContacts.clear();
2307         mUpdatedSyncStates.clear();
2308         mDirtyRawContacts.clear();
2309     }
2310 
2311     @Override
beforeTransactionCommit()2312     protected void beforeTransactionCommit() {
2313 
2314         if (VERBOSE_LOGGING) {
2315             Log.v(TAG, "beforeTransactionCommit");
2316         }
2317         super.beforeTransactionCommit();
2318         flushTransactionalChanges();
2319         mContactAggregator.aggregateInTransaction(mDb);
2320         if (mVisibleTouched) {
2321             mVisibleTouched = false;
2322             mDbHelper.updateAllVisible();
2323         }
2324     }
2325 
flushTransactionalChanges()2326     private void flushTransactionalChanges() {
2327         if (VERBOSE_LOGGING) {
2328             Log.v(TAG, "flushTransactionChanges");
2329         }
2330 
2331         for (long rawContactId : mInsertedRawContacts.keySet()) {
2332             updateRawContactDisplayName(mDb, rawContactId);
2333             mContactAggregator.onRawContactInsert(mDb, rawContactId);
2334         }
2335 
2336         if (!mDirtyRawContacts.isEmpty()) {
2337             mSb.setLength(0);
2338             mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
2339             appendIds(mSb, mDirtyRawContacts);
2340             mSb.append(")");
2341             mDb.execSQL(mSb.toString());
2342         }
2343 
2344         if (!mUpdatedRawContacts.isEmpty()) {
2345             mSb.setLength(0);
2346             mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
2347             appendIds(mSb, mUpdatedRawContacts);
2348             mSb.append(")");
2349             mDb.execSQL(mSb.toString());
2350         }
2351 
2352         for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
2353             long id = entry.getKey();
2354             if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) {
2355                 throw new IllegalStateException(
2356                         "unable to update sync state, does it still exist?");
2357             }
2358         }
2359 
2360         clearTransactionalChanges();
2361     }
2362 
2363     /**
2364      * Appends comma separated ids.
2365      * @param ids Should not be empty
2366      */
appendIds(StringBuilder sb, HashSet<Long> ids)2367     private void appendIds(StringBuilder sb, HashSet<Long> ids) {
2368         for (long id : ids) {
2369             sb.append(id).append(',');
2370         }
2371 
2372         sb.setLength(sb.length() - 1); // Yank the last comma
2373     }
2374 
2375     @Override
notifyChange()2376     protected void notifyChange() {
2377         notifyChange(mSyncToNetwork);
2378         mSyncToNetwork = false;
2379     }
2380 
notifyChange(boolean syncToNetwork)2381     protected void notifyChange(boolean syncToNetwork) {
2382         getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
2383                 syncToNetwork);
2384     }
2385 
setProviderStatus(int status)2386     protected void setProviderStatus(int status) {
2387         mProviderStatus = status;
2388         getContext().getContentResolver().notifyChange(ContactsContract.ProviderStatus.CONTENT_URI,
2389                 null, false);
2390     }
2391 
isNewRawContact(long rawContactId)2392     private boolean isNewRawContact(long rawContactId) {
2393         return mInsertedRawContacts.containsKey(rawContactId);
2394     }
2395 
getDataRowHandler(final String mimeType)2396     private DataRowHandler getDataRowHandler(final String mimeType) {
2397         DataRowHandler handler = mDataRowHandlers.get(mimeType);
2398         if (handler == null) {
2399             handler = new CustomDataRowHandler(mimeType);
2400             mDataRowHandlers.put(mimeType, handler);
2401         }
2402         return handler;
2403     }
2404 
2405     @Override
insertInTransaction(Uri uri, ContentValues values)2406     protected Uri insertInTransaction(Uri uri, ContentValues values) {
2407         if (VERBOSE_LOGGING) {
2408             Log.v(TAG, "insertInTransaction: " + uri + " " + values);
2409         }
2410 
2411         final boolean callerIsSyncAdapter =
2412                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2413 
2414         final int match = sUriMatcher.match(uri);
2415         long id = 0;
2416 
2417         switch (match) {
2418             case SYNCSTATE:
2419                 id = mDbHelper.getSyncState().insert(mDb, values);
2420                 break;
2421 
2422             case CONTACTS: {
2423                 insertContact(values);
2424                 break;
2425             }
2426 
2427             case RAW_CONTACTS: {
2428                 id = insertRawContact(uri, values);
2429                 mSyncToNetwork |= !callerIsSyncAdapter;
2430                 break;
2431             }
2432 
2433             case RAW_CONTACTS_DATA: {
2434                 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
2435                 id = insertData(values, callerIsSyncAdapter);
2436                 mSyncToNetwork |= !callerIsSyncAdapter;
2437                 break;
2438             }
2439 
2440             case DATA: {
2441                 id = insertData(values, callerIsSyncAdapter);
2442                 mSyncToNetwork |= !callerIsSyncAdapter;
2443                 break;
2444             }
2445 
2446             case GROUPS: {
2447                 id = insertGroup(uri, values, callerIsSyncAdapter);
2448                 mSyncToNetwork |= !callerIsSyncAdapter;
2449                 break;
2450             }
2451 
2452             case SETTINGS: {
2453                 id = insertSettings(uri, values);
2454                 mSyncToNetwork |= !callerIsSyncAdapter;
2455                 break;
2456             }
2457 
2458             case STATUS_UPDATES: {
2459                 id = insertStatusUpdate(values);
2460                 break;
2461             }
2462 
2463             default:
2464                 mSyncToNetwork = true;
2465                 return mLegacyApiSupport.insert(uri, values);
2466         }
2467 
2468         if (id < 0) {
2469             return null;
2470         }
2471 
2472         return ContentUris.withAppendedId(uri, id);
2473     }
2474 
2475     /**
2476      * If account is non-null then store it in the values. If the account is
2477      * already specified in the values then it must be consistent with the
2478      * account, if it is non-null.
2479      *
2480      * @param uri Current {@link Uri} being operated on.
2481      * @param values {@link ContentValues} to read and possibly update.
2482      * @throws IllegalArgumentException when only one of
2483      *             {@link RawContacts#ACCOUNT_NAME} or
2484      *             {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
2485      *             other undefined.
2486      * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
2487      *             and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
2488      *             the given {@link Uri} and {@link ContentValues}.
2489      */
resolveAccount(Uri uri, ContentValues values)2490     private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
2491         String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
2492         String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
2493         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
2494 
2495         String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
2496         String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
2497         final boolean partialValues = TextUtils.isEmpty(valueAccountName)
2498                 ^ TextUtils.isEmpty(valueAccountType);
2499 
2500         if (partialUri || partialValues) {
2501             // Throw when either account is incomplete
2502             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
2503                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
2504         }
2505 
2506         // Accounts are valid by only checking one parameter, since we've
2507         // already ruled out partial accounts.
2508         final boolean validUri = !TextUtils.isEmpty(accountName);
2509         final boolean validValues = !TextUtils.isEmpty(valueAccountName);
2510 
2511         if (validValues && validUri) {
2512             // Check that accounts match when both present
2513             final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
2514                     && TextUtils.equals(accountType, valueAccountType);
2515             if (!accountMatch) {
2516                 throw new IllegalArgumentException(mDbHelper.exceptionMessage(
2517                         "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
2518             }
2519         } else if (validUri) {
2520             // Fill values from Uri when not present
2521             values.put(RawContacts.ACCOUNT_NAME, accountName);
2522             values.put(RawContacts.ACCOUNT_TYPE, accountType);
2523         } else if (validValues) {
2524             accountName = valueAccountName;
2525             accountType = valueAccountType;
2526         } else {
2527             return null;
2528         }
2529 
2530         // Use cached Account object when matches, otherwise create
2531         if (mAccount == null
2532                 || !mAccount.name.equals(accountName)
2533                 || !mAccount.type.equals(accountType)) {
2534             mAccount = new Account(accountName, accountType);
2535         }
2536 
2537         return mAccount;
2538     }
2539 
2540     /**
2541      * Inserts an item in the contacts table
2542      *
2543      * @param values the values for the new row
2544      * @return the row ID of the newly created row
2545      */
insertContact(ContentValues values)2546     private long insertContact(ContentValues values) {
2547         throw new UnsupportedOperationException("Aggregate contacts are created automatically");
2548     }
2549 
2550     /**
2551      * Inserts an item in the contacts table
2552      *
2553      * @param uri the values for the new row
2554      * @param values the account this contact should be associated with. may be null.
2555      * @return the row ID of the newly created row
2556      */
insertRawContact(Uri uri, ContentValues values)2557     private long insertRawContact(Uri uri, ContentValues values) {
2558         mValues.clear();
2559         mValues.putAll(values);
2560         mValues.putNull(RawContacts.CONTACT_ID);
2561 
2562         final Account account = resolveAccount(uri, mValues);
2563 
2564         if (values.containsKey(RawContacts.DELETED)
2565                 && values.getAsInteger(RawContacts.DELETED) != 0) {
2566             mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
2567         }
2568 
2569         long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues);
2570         int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
2571         if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) {
2572             aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE);
2573         }
2574         mContactAggregator.markNewForAggregation(rawContactId, aggregationMode);
2575 
2576         // Trigger creation of a Contact based on this RawContact at the end of transaction
2577         mInsertedRawContacts.put(rawContactId, account);
2578 
2579         return rawContactId;
2580     }
2581 
2582     /**
2583      * Inserts an item in the data table
2584      *
2585      * @param values the values for the new row
2586      * @return the row ID of the newly created row
2587      */
insertData(ContentValues values, boolean callerIsSyncAdapter)2588     private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
2589         long id = 0;
2590         mValues.clear();
2591         mValues.putAll(values);
2592 
2593         long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
2594 
2595         // Replace package with internal mapping
2596         final String packageName = mValues.getAsString(Data.RES_PACKAGE);
2597         if (packageName != null) {
2598             mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
2599         }
2600         mValues.remove(Data.RES_PACKAGE);
2601 
2602         // Replace mimetype with internal mapping
2603         final String mimeType = mValues.getAsString(Data.MIMETYPE);
2604         if (TextUtils.isEmpty(mimeType)) {
2605             throw new IllegalArgumentException(Data.MIMETYPE + " is required");
2606         }
2607 
2608         mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
2609         mValues.remove(Data.MIMETYPE);
2610 
2611         DataRowHandler rowHandler = getDataRowHandler(mimeType);
2612         id = rowHandler.insert(mDb, rawContactId, mValues);
2613         if (!callerIsSyncAdapter) {
2614             setRawContactDirty(rawContactId);
2615         }
2616         mUpdatedRawContacts.add(rawContactId);
2617         return id;
2618     }
2619 
triggerAggregation(long rawContactId)2620     private void triggerAggregation(long rawContactId) {
2621         if (!mContactAggregator.isEnabled()) {
2622             return;
2623         }
2624 
2625         int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
2626         switch (aggregationMode) {
2627             case RawContacts.AGGREGATION_MODE_DISABLED:
2628                 break;
2629 
2630             case RawContacts.AGGREGATION_MODE_DEFAULT: {
2631                 mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
2632                 break;
2633             }
2634 
2635             case RawContacts.AGGREGATION_MODE_SUSPENDED: {
2636                 long contactId = mDbHelper.getContactId(rawContactId);
2637 
2638                 if (contactId != 0) {
2639                     mContactAggregator.updateAggregateData(contactId);
2640                 }
2641                 break;
2642             }
2643 
2644             case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
2645                 mContactAggregator.aggregateContact(mDb, rawContactId);
2646                 break;
2647             }
2648         }
2649     }
2650 
2651     /**
2652      * Returns the group id of the group with sourceId and the same account as rawContactId.
2653      * If the group doesn't already exist then it is first created,
2654      * @param db SQLiteDatabase to use for this operation
2655      * @param rawContactId the contact this group is associated with
2656      * @param sourceId the sourceIf of the group to query or create
2657      * @return the group id of the existing or created group
2658      * @throws IllegalArgumentException if the contact is not associated with an account
2659      * @throws IllegalStateException if a group needs to be created but the creation failed
2660      */
getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId, Account account)2661     private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
2662             Account account) {
2663 
2664         if (account == null) {
2665             mSelectionArgs1[0] = String.valueOf(rawContactId);
2666             Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
2667                     RawContacts._ID + "=?", mSelectionArgs1, null, null, null);
2668             try {
2669                 if (c.moveToFirst()) {
2670                     String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME);
2671                     String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
2672                     if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
2673                         account = new Account(accountName, accountType);
2674                     }
2675                 }
2676             } finally {
2677                 c.close();
2678             }
2679         }
2680 
2681         if (account == null) {
2682             throw new IllegalArgumentException("if the groupmembership only "
2683                     + "has a sourceid the the contact must be associated with "
2684                     + "an account");
2685         }
2686 
2687         ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
2688         if (entries == null) {
2689             entries = new ArrayList<GroupIdCacheEntry>(1);
2690             mGroupIdCache.put(sourceId, entries);
2691         }
2692 
2693         int count = entries.size();
2694         for (int i = 0; i < count; i++) {
2695             GroupIdCacheEntry entry = entries.get(i);
2696             if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) {
2697                 return entry.groupId;
2698             }
2699         }
2700 
2701         GroupIdCacheEntry entry = new GroupIdCacheEntry();
2702         entry.accountName = account.name;
2703         entry.accountType = account.type;
2704         entry.sourceId = sourceId;
2705         entries.add(0, entry);
2706 
2707         // look up the group that contains this sourceId and has the same account name and type
2708         // as the contact refered to by rawContactId
2709         Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
2710                 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
2711                 new String[]{sourceId, account.name, account.type}, null, null, null);
2712         try {
2713             if (c.moveToFirst()) {
2714                 entry.groupId = c.getLong(0);
2715             } else {
2716                 ContentValues groupValues = new ContentValues();
2717                 groupValues.put(Groups.ACCOUNT_NAME, account.name);
2718                 groupValues.put(Groups.ACCOUNT_TYPE, account.type);
2719                 groupValues.put(Groups.SOURCE_ID, sourceId);
2720                 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
2721                 if (groupId < 0) {
2722                     throw new IllegalStateException("unable to create a new group with "
2723                             + "this sourceid: " + groupValues);
2724                 }
2725                 entry.groupId = groupId;
2726             }
2727         } finally {
2728             c.close();
2729         }
2730 
2731         return entry.groupId;
2732     }
2733 
2734     private interface DisplayNameQuery {
2735         public static final String RAW_SQL =
2736                 "SELECT "
2737                         + DataColumns.MIMETYPE_ID + ","
2738                         + Data.IS_PRIMARY + ","
2739                         + Data.DATA1 + ","
2740                         + Data.DATA2 + ","
2741                         + Data.DATA3 + ","
2742                         + Data.DATA4 + ","
2743                         + Data.DATA5 + ","
2744                         + Data.DATA6 + ","
2745                         + Data.DATA7 + ","
2746                         + Data.DATA8 + ","
2747                         + Data.DATA9 + ","
2748                         + Data.DATA10 + ","
2749                         + Data.DATA11 +
2750                 " FROM " + Tables.DATA +
2751                 " WHERE " + Data.RAW_CONTACT_ID + "=?" +
2752                         " AND (" + Data.DATA1 + " NOT NULL OR " +
2753                                 Organization.TITLE + " NOT NULL)";
2754 
2755         public static final int MIMETYPE = 0;
2756         public static final int IS_PRIMARY = 1;
2757         public static final int DATA1 = 2;
2758         public static final int GIVEN_NAME = 3;                         // data2
2759         public static final int FAMILY_NAME = 4;                        // data3
2760         public static final int PREFIX = 5;                             // data4
2761         public static final int TITLE = 5;                              // data4
2762         public static final int MIDDLE_NAME = 6;                        // data5
2763         public static final int SUFFIX = 7;                             // data6
2764         public static final int PHONETIC_GIVEN_NAME = 8;                // data7
2765         public static final int PHONETIC_MIDDLE_NAME = 9;               // data8
2766         public static final int ORGANIZATION_PHONETIC_NAME = 9;         // data8
2767         public static final int PHONETIC_FAMILY_NAME = 10;              // data9
2768         public static final int FULL_NAME_STYLE = 11;                   // data10
2769         public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11;  // data10
2770         public static final int PHONETIC_NAME_STYLE = 12;               // data11
2771     }
2772 
2773     /**
2774      * Updates a raw contact display name based on data rows, e.g. structured name,
2775      * organization, email etc.
2776      */
updateRawContactDisplayName(SQLiteDatabase db, long rawContactId)2777     public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
2778         int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
2779         NameSplitter.Name bestName = null;
2780         String bestDisplayName = null;
2781         String bestPhoneticName = null;
2782         int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2783 
2784         mSelectionArgs1[0] = String.valueOf(rawContactId);
2785         Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1);
2786         try {
2787             while (c.moveToNext()) {
2788                 int mimeType = c.getInt(DisplayNameQuery.MIMETYPE);
2789                 int source = getDisplayNameSource(mimeType);
2790                 if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
2791                     continue;
2792                 }
2793 
2794                 if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) {
2795                     continue;
2796                 }
2797 
2798                 if (mimeType == mMimeTypeIdStructuredName) {
2799                     NameSplitter.Name name;
2800                     if (bestName != null) {
2801                         name = new NameSplitter.Name();
2802                     } else {
2803                         name = mName;
2804                         name.clear();
2805                     }
2806                     name.prefix = c.getString(DisplayNameQuery.PREFIX);
2807                     name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME);
2808                     name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME);
2809                     name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME);
2810                     name.suffix = c.getString(DisplayNameQuery.SUFFIX);
2811                     name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE)
2812                             ? FullNameStyle.UNDEFINED
2813                             : c.getInt(DisplayNameQuery.FULL_NAME_STYLE);
2814                     name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME);
2815                     name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME);
2816                     name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME);
2817                     name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE)
2818                             ? PhoneticNameStyle.UNDEFINED
2819                             : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE);
2820                     if (!name.isEmpty()) {
2821                         bestDisplayNameSource = source;
2822                         bestName = name;
2823                     }
2824                 } else if (mimeType == mMimeTypeIdOrganization) {
2825                     mCharArrayBuffer.sizeCopied = 0;
2826                     c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
2827                     if (mCharArrayBuffer.sizeCopied != 0) {
2828                         bestDisplayNameSource = source;
2829                         bestDisplayName = new String(mCharArrayBuffer.data, 0,
2830                                 mCharArrayBuffer.sizeCopied);
2831                         bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME);
2832                         bestPhoneticNameStyle =
2833                                 c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE)
2834                                     ? PhoneticNameStyle.UNDEFINED
2835                                     : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE);
2836                     } else {
2837                         c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer);
2838                         if (mCharArrayBuffer.sizeCopied != 0) {
2839                             bestDisplayNameSource = source;
2840                             bestDisplayName = new String(mCharArrayBuffer.data, 0,
2841                                     mCharArrayBuffer.sizeCopied);
2842                             bestPhoneticName = null;
2843                             bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2844                         }
2845                     }
2846                 } else {
2847                     // Display name is at DATA1 in all other types.
2848                     // This is ensured in the constructor.
2849 
2850                     mCharArrayBuffer.sizeCopied = 0;
2851                     c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
2852                     if (mCharArrayBuffer.sizeCopied != 0) {
2853                         bestDisplayNameSource = source;
2854                         bestDisplayName = new String(mCharArrayBuffer.data, 0,
2855                                 mCharArrayBuffer.sizeCopied);
2856                         bestPhoneticName = null;
2857                         bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
2858                     }
2859                 }
2860             }
2861 
2862         } finally {
2863             c.close();
2864         }
2865 
2866         String displayNamePrimary;
2867         String displayNameAlternative;
2868         String sortKeyPrimary = null;
2869         String sortKeyAlternative = null;
2870         int displayNameStyle = FullNameStyle.UNDEFINED;
2871 
2872         if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) {
2873             displayNameStyle = bestName.fullNameStyle;
2874             if (displayNameStyle == FullNameStyle.CJK
2875                     || displayNameStyle == FullNameStyle.UNDEFINED) {
2876                 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
2877                 bestName.fullNameStyle = displayNameStyle;
2878             }
2879 
2880             displayNamePrimary = mNameSplitter.join(bestName, true);
2881             displayNameAlternative = mNameSplitter.join(bestName, false);
2882 
2883             bestPhoneticName = mNameSplitter.joinPhoneticName(bestName);
2884             bestPhoneticNameStyle = bestName.phoneticNameStyle;
2885         } else {
2886             displayNamePrimary = displayNameAlternative = bestDisplayName;
2887         }
2888 
2889         if (bestPhoneticName != null) {
2890             sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
2891             if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
2892                 bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
2893             }
2894         } else {
2895             if (displayNameStyle == FullNameStyle.UNDEFINED) {
2896                 displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
2897                 if (displayNameStyle == FullNameStyle.UNDEFINED
2898                         || displayNameStyle == FullNameStyle.CJK) {
2899                     displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle(
2900                             displayNameStyle, bestPhoneticNameStyle);
2901                 }
2902                 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
2903             }
2904             if (displayNameStyle == FullNameStyle.CHINESE ||
2905                     displayNameStyle == FullNameStyle.CJK) {
2906                 sortKeyPrimary = sortKeyAlternative =
2907                         ContactLocaleUtils.getIntance().getSortKey(
2908                                 displayNamePrimary, displayNameStyle);
2909             }
2910         }
2911 
2912         if (sortKeyPrimary == null) {
2913             sortKeyPrimary = displayNamePrimary;
2914             sortKeyAlternative = displayNameAlternative;
2915         }
2916 
2917         setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary,
2918                 displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle,
2919                 sortKeyPrimary, sortKeyAlternative);
2920     }
2921 
getDisplayNameSource(int mimeTypeId)2922     private int getDisplayNameSource(int mimeTypeId) {
2923         if (mimeTypeId == mMimeTypeIdStructuredName) {
2924             return DisplayNameSources.STRUCTURED_NAME;
2925         } else if (mimeTypeId == mMimeTypeIdEmail) {
2926             return DisplayNameSources.EMAIL;
2927         } else if (mimeTypeId == mMimeTypeIdPhone) {
2928             return DisplayNameSources.PHONE;
2929         } else if (mimeTypeId == mMimeTypeIdOrganization) {
2930             return DisplayNameSources.ORGANIZATION;
2931         } else if (mimeTypeId == mMimeTypeIdNickname) {
2932             return DisplayNameSources.NICKNAME;
2933         } else {
2934             return DisplayNameSources.UNDEFINED;
2935         }
2936     }
2937 
2938     /**
2939      * Delete data row by row so that fixing of primaries etc work correctly.
2940      */
deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter)2941     private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
2942         int count = 0;
2943 
2944         // Note that the query will return data according to the access restrictions,
2945         // so we don't need to worry about deleting data we don't have permission to read.
2946         Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null);
2947         try {
2948             while(c.moveToNext()) {
2949                 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
2950                 String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
2951                 DataRowHandler rowHandler = getDataRowHandler(mimeType);
2952                 count += rowHandler.delete(mDb, c);
2953                 if (!callerIsSyncAdapter) {
2954                     setRawContactDirty(rawContactId);
2955                 }
2956             }
2957         } finally {
2958             c.close();
2959         }
2960 
2961         return count;
2962     }
2963 
2964     /**
2965      * Delete a data row provided that it is one of the allowed mime types.
2966      */
deleteData(long dataId, String[] allowedMimeTypes)2967     public int deleteData(long dataId, String[] allowedMimeTypes) {
2968 
2969         // Note that the query will return data according to the access restrictions,
2970         // so we don't need to worry about deleting data we don't have permission to read.
2971         mSelectionArgs1[0] = String.valueOf(dataId);
2972         Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?",
2973                 mSelectionArgs1, null);
2974 
2975         try {
2976             if (!c.moveToFirst()) {
2977                 return 0;
2978             }
2979 
2980             String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
2981             boolean valid = false;
2982             for (int i = 0; i < allowedMimeTypes.length; i++) {
2983                 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
2984                     valid = true;
2985                     break;
2986                 }
2987             }
2988 
2989             if (!valid) {
2990                 throw new IllegalArgumentException("Data type mismatch: expected "
2991                         + Lists.newArrayList(allowedMimeTypes));
2992             }
2993 
2994             DataRowHandler rowHandler = getDataRowHandler(mimeType);
2995             return rowHandler.delete(mDb, c);
2996         } finally {
2997             c.close();
2998         }
2999     }
3000 
3001     /**
3002      * Inserts an item in the groups table
3003      */
insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter)3004     private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
3005         mValues.clear();
3006         mValues.putAll(values);
3007 
3008         final Account account = resolveAccount(uri, mValues);
3009 
3010         // Replace package with internal mapping
3011         final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
3012         if (packageName != null) {
3013             mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
3014         }
3015         mValues.remove(Groups.RES_PACKAGE);
3016 
3017         if (!callerIsSyncAdapter) {
3018             mValues.put(Groups.DIRTY, 1);
3019         }
3020 
3021         long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
3022 
3023         if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
3024             mVisibleTouched = true;
3025         }
3026 
3027         return result;
3028     }
3029 
insertSettings(Uri uri, ContentValues values)3030     private long insertSettings(Uri uri, ContentValues values) {
3031         final long id = mDb.insert(Tables.SETTINGS, null, values);
3032 
3033         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
3034             mVisibleTouched = true;
3035         }
3036 
3037         return id;
3038     }
3039 
3040     /**
3041      * Inserts a status update.
3042      */
insertStatusUpdate(ContentValues values)3043     public long insertStatusUpdate(ContentValues values) {
3044         final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
3045         final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
3046         String customProtocol = null;
3047 
3048         if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
3049             customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
3050             if (TextUtils.isEmpty(customProtocol)) {
3051                 throw new IllegalArgumentException(
3052                         "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
3053             }
3054         }
3055 
3056         long rawContactId = -1;
3057         long contactId = -1;
3058         Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
3059         mSb.setLength(0);
3060         mSelectionArgs.clear();
3061         if (dataId != null) {
3062             // Lookup the contact info for the given data row.
3063 
3064             mSb.append(Tables.DATA + "." + Data._ID + "=?");
3065             mSelectionArgs.add(String.valueOf(dataId));
3066         } else {
3067             // Lookup the data row to attach this presence update to
3068 
3069             if (TextUtils.isEmpty(handle) || protocol == null) {
3070                 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
3071             }
3072 
3073             // TODO: generalize to allow other providers to match against email
3074             boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
3075 
3076             String mimeTypeIdIm = String.valueOf(mMimeTypeIdIm);
3077             if (matchEmail) {
3078                 String mimeTypeIdEmail = String.valueOf(mMimeTypeIdEmail);
3079 
3080                 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
3081                 // the "OR" conjunction confuses it and it switches to a full scan of
3082                 // the raw_contacts table.
3083 
3084                 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
3085                 // column - Data.DATA1
3086                 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" +
3087                         " AND " + Data.DATA1 + "=?" +
3088                         " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?");
3089                 mSelectionArgs.add(mimeTypeIdEmail);
3090                 mSelectionArgs.add(mimeTypeIdIm);
3091                 mSelectionArgs.add(handle);
3092                 mSelectionArgs.add(mimeTypeIdIm);
3093                 mSelectionArgs.add(String.valueOf(protocol));
3094                 if (customProtocol != null) {
3095                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
3096                     mSelectionArgs.add(customProtocol);
3097                 }
3098                 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))");
3099                 mSelectionArgs.add(mimeTypeIdEmail);
3100             } else {
3101                 mSb.append(DataColumns.MIMETYPE_ID + "=?" +
3102                         " AND " + Im.PROTOCOL + "=?" +
3103                         " AND " + Im.DATA + "=?");
3104                 mSelectionArgs.add(mimeTypeIdIm);
3105                 mSelectionArgs.add(String.valueOf(protocol));
3106                 mSelectionArgs.add(handle);
3107                 if (customProtocol != null) {
3108                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
3109                     mSelectionArgs.add(customProtocol);
3110                 }
3111             }
3112 
3113             if (values.containsKey(StatusUpdates.DATA_ID)) {
3114                 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?");
3115                 mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID));
3116             }
3117         }
3118         mSb.append(" AND ").append(getContactsRestrictions());
3119 
3120         Cursor cursor = null;
3121         try {
3122             cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
3123                     mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
3124                     Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID);
3125             if (cursor.moveToFirst()) {
3126                 dataId = cursor.getLong(DataContactsQuery.DATA_ID);
3127                 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
3128                 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
3129             } else {
3130                 // No contact found, return a null URI
3131                 return -1;
3132             }
3133         } finally {
3134             if (cursor != null) {
3135                 cursor.close();
3136             }
3137         }
3138 
3139         if (values.containsKey(StatusUpdates.PRESENCE)) {
3140             if (customProtocol == null) {
3141                 // We cannot allow a null in the custom protocol field, because SQLite3 does not
3142                 // properly enforce uniqueness of null values
3143                 customProtocol = "";
3144             }
3145 
3146             mValues.clear();
3147             mValues.put(StatusUpdates.DATA_ID, dataId);
3148             mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
3149             mValues.put(PresenceColumns.CONTACT_ID, contactId);
3150             mValues.put(StatusUpdates.PROTOCOL, protocol);
3151             mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
3152             mValues.put(StatusUpdates.IM_HANDLE, handle);
3153             if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
3154                 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
3155             }
3156             mValues.put(StatusUpdates.PRESENCE,
3157                     values.getAsString(StatusUpdates.PRESENCE));
3158             mValues.put(StatusUpdates.CHAT_CAPABILITY,
3159                     values.getAsString(StatusUpdates.CHAT_CAPABILITY));
3160 
3161             // Insert the presence update
3162             mDb.replace(Tables.PRESENCE, null, mValues);
3163         }
3164 
3165 
3166         if (values.containsKey(StatusUpdates.STATUS)) {
3167             String status = values.getAsString(StatusUpdates.STATUS);
3168             String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
3169             Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL);
3170 
3171             if (TextUtils.isEmpty(resPackage)
3172                     && (labelResource == null || labelResource == 0)
3173                     && protocol != null) {
3174                 labelResource = Im.getProtocolLabelResource(protocol);
3175             }
3176 
3177             Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON);
3178             // TODO compute the default icon based on the protocol
3179 
3180             if (TextUtils.isEmpty(status)) {
3181                 mStatusUpdateDelete.bindLong(1, dataId);
3182                 mStatusUpdateDelete.execute();
3183             } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) {
3184                 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
3185                 mStatusUpdateReplace.bindLong(1, dataId);
3186                 mStatusUpdateReplace.bindLong(2, timestamp);
3187                 bindString(mStatusUpdateReplace, 3, status);
3188                 bindString(mStatusUpdateReplace, 4, resPackage);
3189                 bindLong(mStatusUpdateReplace, 5, iconResource);
3190                 bindLong(mStatusUpdateReplace, 6, labelResource);
3191                 mStatusUpdateReplace.execute();
3192             } else {
3193 
3194                 try {
3195                     mStatusUpdateInsert.bindLong(1, dataId);
3196                     bindString(mStatusUpdateInsert, 2, status);
3197                     bindString(mStatusUpdateInsert, 3, resPackage);
3198                     bindLong(mStatusUpdateInsert, 4, iconResource);
3199                     bindLong(mStatusUpdateInsert, 5, labelResource);
3200                     mStatusUpdateInsert.executeInsert();
3201                 } catch (SQLiteConstraintException e) {
3202                     // The row already exists - update it
3203                     long timestamp = System.currentTimeMillis();
3204                     mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
3205                     bindString(mStatusUpdateAutoTimestamp, 2, status);
3206                     mStatusUpdateAutoTimestamp.bindLong(3, dataId);
3207                     bindString(mStatusUpdateAutoTimestamp, 4, status);
3208                     mStatusUpdateAutoTimestamp.execute();
3209 
3210                     bindString(mStatusAttributionUpdate, 1, resPackage);
3211                     bindLong(mStatusAttributionUpdate, 2, iconResource);
3212                     bindLong(mStatusAttributionUpdate, 3, labelResource);
3213                     mStatusAttributionUpdate.bindLong(4, dataId);
3214                     mStatusAttributionUpdate.execute();
3215                 }
3216             }
3217         }
3218 
3219         if (contactId != -1) {
3220             mLastStatusUpdate.bindLong(1, contactId);
3221             mLastStatusUpdate.bindLong(2, contactId);
3222             mLastStatusUpdate.execute();
3223         }
3224 
3225         return dataId;
3226     }
3227 
3228     @Override
deleteInTransaction(Uri uri, String selection, String[] selectionArgs)3229     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
3230         if (VERBOSE_LOGGING) {
3231             Log.v(TAG, "deleteInTransaction: " + uri);
3232         }
3233         flushTransactionalChanges();
3234         final boolean callerIsSyncAdapter =
3235                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
3236         final int match = sUriMatcher.match(uri);
3237         switch (match) {
3238             case SYNCSTATE:
3239                 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
3240 
3241             case SYNCSTATE_ID:
3242                 String selectionWithId =
3243                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
3244                         + (selection == null ? "" : " AND (" + selection + ")");
3245                 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);
3246 
3247             case CONTACTS: {
3248                 // TODO
3249                 return 0;
3250             }
3251 
3252             case CONTACTS_ID: {
3253                 long contactId = ContentUris.parseId(uri);
3254                 return deleteContact(contactId);
3255             }
3256 
3257             case CONTACTS_LOOKUP: {
3258                 final List<String> pathSegments = uri.getPathSegments();
3259                 final int segmentCount = pathSegments.size();
3260                 if (segmentCount < 3) {
3261                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3262                             "Missing a lookup key", uri));
3263                 }
3264                 final String lookupKey = pathSegments.get(2);
3265                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
3266                 return deleteContact(contactId);
3267             }
3268 
3269             case CONTACTS_LOOKUP_ID: {
3270                 // lookup contact by id and lookup key to see if they still match the actual record
3271                 long contactId = ContentUris.parseId(uri);
3272                 final List<String> pathSegments = uri.getPathSegments();
3273                 final String lookupKey = pathSegments.get(2);
3274                 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
3275                 setTablesAndProjectionMapForContacts(lookupQb, uri, null);
3276                 String[] args;
3277                 if (selectionArgs == null) {
3278                     args = new String[2];
3279                 } else {
3280                     args = new String[selectionArgs.length + 2];
3281                     System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
3282                 }
3283                 args[0] = String.valueOf(contactId);
3284                 args[1] = Uri.encode(lookupKey);
3285                 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
3286                 final SQLiteDatabase db = mDbHelper.getReadableDatabase();
3287                 Cursor c = query(db, lookupQb, null, selection, args, null, null, null);
3288                 try {
3289                     if (c.getCount() == 1) {
3290                         // contact was unmodified so go ahead and delete it
3291                         return deleteContact(contactId);
3292                     } else {
3293                         // row was changed (e.g. the merging might have changed), we got multiple
3294                         // rows or the supplied selection filtered the record out
3295                         return 0;
3296                     }
3297                 } finally {
3298                     c.close();
3299                 }
3300             }
3301 
3302             case RAW_CONTACTS: {
3303                 int numDeletes = 0;
3304                 Cursor c = mDb.query(Tables.RAW_CONTACTS,
3305                         new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
3306                         appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
3307                 try {
3308                     while (c.moveToNext()) {
3309                         final long rawContactId = c.getLong(0);
3310                         long contactId = c.getLong(1);
3311                         numDeletes += deleteRawContact(rawContactId, contactId,
3312                                 callerIsSyncAdapter);
3313                     }
3314                 } finally {
3315                     c.close();
3316                 }
3317                 return numDeletes;
3318             }
3319 
3320             case RAW_CONTACTS_ID: {
3321                 final long rawContactId = ContentUris.parseId(uri);
3322                 return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId),
3323                         callerIsSyncAdapter);
3324             }
3325 
3326             case DATA: {
3327                 mSyncToNetwork |= !callerIsSyncAdapter;
3328                 return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
3329                         callerIsSyncAdapter);
3330             }
3331 
3332             case DATA_ID:
3333             case PHONES_ID:
3334             case EMAILS_ID:
3335             case POSTALS_ID: {
3336                 long dataId = ContentUris.parseId(uri);
3337                 mSyncToNetwork |= !callerIsSyncAdapter;
3338                 mSelectionArgs1[0] = String.valueOf(dataId);
3339                 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
3340             }
3341 
3342             case GROUPS_ID: {
3343                 mSyncToNetwork |= !callerIsSyncAdapter;
3344                 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
3345             }
3346 
3347             case GROUPS: {
3348                 int numDeletes = 0;
3349                 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
3350                         appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
3351                 try {
3352                     while (c.moveToNext()) {
3353                         numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
3354                     }
3355                 } finally {
3356                     c.close();
3357                 }
3358                 if (numDeletes > 0) {
3359                     mSyncToNetwork |= !callerIsSyncAdapter;
3360                 }
3361                 return numDeletes;
3362             }
3363 
3364             case SETTINGS: {
3365                 mSyncToNetwork |= !callerIsSyncAdapter;
3366                 return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
3367             }
3368 
3369             case STATUS_UPDATES: {
3370                 return deleteStatusUpdates(selection, selectionArgs);
3371             }
3372 
3373             default: {
3374                 mSyncToNetwork = true;
3375                 return mLegacyApiSupport.delete(uri, selection, selectionArgs);
3376             }
3377         }
3378     }
3379 
deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter)3380     public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
3381         mGroupIdCache.clear();
3382         final long groupMembershipMimetypeId = mDbHelper
3383                 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
3384         mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
3385                 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
3386                 + groupId, null);
3387 
3388         try {
3389             if (callerIsSyncAdapter) {
3390                 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
3391             } else {
3392                 mValues.clear();
3393                 mValues.put(Groups.DELETED, 1);
3394                 mValues.put(Groups.DIRTY, 1);
3395                 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
3396             }
3397         } finally {
3398             mVisibleTouched = true;
3399         }
3400     }
3401 
deleteSettings(Uri uri, String selection, String[] selectionArgs)3402     private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
3403         final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
3404         mVisibleTouched = true;
3405         return count;
3406     }
3407 
deleteContact(long contactId)3408     private int deleteContact(long contactId) {
3409         mSelectionArgs1[0] = Long.toString(contactId);
3410         Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
3411                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
3412                 null, null, null);
3413         try {
3414             while (c.moveToNext()) {
3415                 long rawContactId = c.getLong(0);
3416                 markRawContactAsDeleted(rawContactId);
3417             }
3418         } finally {
3419             c.close();
3420         }
3421 
3422         return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
3423     }
3424 
deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter)3425     public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
3426         mContactAggregator.invalidateAggregationExceptionCache();
3427         if (callerIsSyncAdapter) {
3428             mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
3429             int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
3430             mContactAggregator.updateDisplayNameForContact(mDb, contactId);
3431             return count;
3432         } else {
3433             mDbHelper.removeContactIfSingleton(rawContactId);
3434             return markRawContactAsDeleted(rawContactId);
3435         }
3436     }
3437 
deleteStatusUpdates(String selection, String[] selectionArgs)3438     private int deleteStatusUpdates(String selection, String[] selectionArgs) {
3439       // delete from both tables: presence and status_updates
3440       // TODO should account type/name be appended to the where clause?
3441       if (VERBOSE_LOGGING) {
3442           Log.v(TAG, "deleting data from status_updates for " + selection);
3443       }
3444       mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
3445           selectionArgs);
3446       return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
3447     }
3448 
markRawContactAsDeleted(long rawContactId)3449     private int markRawContactAsDeleted(long rawContactId) {
3450         mSyncToNetwork = true;
3451 
3452         mValues.clear();
3453         mValues.put(RawContacts.DELETED, 1);
3454         mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
3455         mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
3456         mValues.putNull(RawContacts.CONTACT_ID);
3457         mValues.put(RawContacts.DIRTY, 1);
3458         return updateRawContact(rawContactId, mValues);
3459     }
3460 
3461     @Override
updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs)3462     protected int updateInTransaction(Uri uri, ContentValues values, String selection,
3463             String[] selectionArgs) {
3464         if (VERBOSE_LOGGING) {
3465             Log.v(TAG, "updateInTransaction: " + uri);
3466         }
3467 
3468         int count = 0;
3469 
3470         final int match = sUriMatcher.match(uri);
3471         if (match == SYNCSTATE_ID && selection == null) {
3472             long rowId = ContentUris.parseId(uri);
3473             Object data = values.get(ContactsContract.SyncState.DATA);
3474             mUpdatedSyncStates.put(rowId, data);
3475             return 1;
3476         }
3477         flushTransactionalChanges();
3478         final boolean callerIsSyncAdapter =
3479                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
3480         switch(match) {
3481             case SYNCSTATE:
3482                 return mDbHelper.getSyncState().update(mDb, values,
3483                         appendAccountToSelection(uri, selection), selectionArgs);
3484 
3485             case SYNCSTATE_ID: {
3486                 selection = appendAccountToSelection(uri, selection);
3487                 String selectionWithId =
3488                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
3489                         + (selection == null ? "" : " AND (" + selection + ")");
3490                 return mDbHelper.getSyncState().update(mDb, values,
3491                         selectionWithId, selectionArgs);
3492             }
3493 
3494             case CONTACTS: {
3495                 count = updateContactOptions(values, selection, selectionArgs);
3496                 break;
3497             }
3498 
3499             case CONTACTS_ID: {
3500                 count = updateContactOptions(ContentUris.parseId(uri), values);
3501                 break;
3502             }
3503 
3504             case CONTACTS_LOOKUP:
3505             case CONTACTS_LOOKUP_ID: {
3506                 final List<String> pathSegments = uri.getPathSegments();
3507                 final int segmentCount = pathSegments.size();
3508                 if (segmentCount < 3) {
3509                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
3510                             "Missing a lookup key", uri));
3511                 }
3512                 final String lookupKey = pathSegments.get(2);
3513                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
3514                 count = updateContactOptions(contactId, values);
3515                 break;
3516             }
3517 
3518             case RAW_CONTACTS_DATA: {
3519                 final String rawContactId = uri.getPathSegments().get(1);
3520                 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
3521                     + (selection == null ? "" : " AND " + selection);
3522 
3523                 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
3524 
3525                 break;
3526             }
3527 
3528             case DATA: {
3529                 count = updateData(uri, values, appendAccountToSelection(uri, selection),
3530                         selectionArgs, callerIsSyncAdapter);
3531                 if (count > 0) {
3532                     mSyncToNetwork |= !callerIsSyncAdapter;
3533                 }
3534                 break;
3535             }
3536 
3537             case DATA_ID:
3538             case PHONES_ID:
3539             case EMAILS_ID:
3540             case POSTALS_ID: {
3541                 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
3542                 if (count > 0) {
3543                     mSyncToNetwork |= !callerIsSyncAdapter;
3544                 }
3545                 break;
3546             }
3547 
3548             case RAW_CONTACTS: {
3549                 selection = appendAccountToSelection(uri, selection);
3550                 count = updateRawContacts(values, selection, selectionArgs);
3551                 break;
3552             }
3553 
3554             case RAW_CONTACTS_ID: {
3555                 long rawContactId = ContentUris.parseId(uri);
3556                 if (selection != null) {
3557                     selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
3558                     count = updateRawContacts(values, RawContacts._ID + "=?"
3559                                     + " AND(" + selection + ")", selectionArgs);
3560                 } else {
3561                     mSelectionArgs1[0] = String.valueOf(rawContactId);
3562                     count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1);
3563                 }
3564                 break;
3565             }
3566 
3567             case GROUPS: {
3568                 count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
3569                         selectionArgs, callerIsSyncAdapter);
3570                 if (count > 0) {
3571                     mSyncToNetwork |= !callerIsSyncAdapter;
3572                 }
3573                 break;
3574             }
3575 
3576             case GROUPS_ID: {
3577                 long groupId = ContentUris.parseId(uri);
3578                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
3579                 String selectionWithId = Groups._ID + "=? "
3580                         + (selection == null ? "" : " AND " + selection);
3581                 count = updateGroups(uri, values, selectionWithId, selectionArgs,
3582                         callerIsSyncAdapter);
3583                 if (count > 0) {
3584                     mSyncToNetwork |= !callerIsSyncAdapter;
3585                 }
3586                 break;
3587             }
3588 
3589             case AGGREGATION_EXCEPTIONS: {
3590                 count = updateAggregationException(mDb, values);
3591                 break;
3592             }
3593 
3594             case SETTINGS: {
3595                 count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
3596                         selectionArgs);
3597                 mSyncToNetwork |= !callerIsSyncAdapter;
3598                 break;
3599             }
3600 
3601             case STATUS_UPDATES: {
3602                 count = updateStatusUpdate(uri, values, selection, selectionArgs);
3603                 break;
3604             }
3605 
3606             default: {
3607                 mSyncToNetwork = true;
3608                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
3609             }
3610         }
3611 
3612         return count;
3613     }
3614 
updateStatusUpdate(Uri uri, ContentValues values, String selection, String[] selectionArgs)3615     private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
3616         String[] selectionArgs) {
3617         // update status_updates table, if status is provided
3618         // TODO should account type/name be appended to the where clause?
3619         int updateCount = 0;
3620         ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
3621         if (settableValues.size() > 0) {
3622           updateCount = mDb.update(Tables.STATUS_UPDATES,
3623                     settableValues,
3624                     getWhereClauseForStatusUpdatesTable(selection),
3625                     selectionArgs);
3626         }
3627 
3628         // now update the Presence table
3629         settableValues = getSettableColumnsForPresenceTable(values);
3630         if (settableValues.size() > 0) {
3631           updateCount = mDb.update(Tables.PRESENCE, settableValues,
3632                     selection, selectionArgs);
3633         }
3634         // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
3635         // potentially get updated in this method.
3636         return updateCount;
3637     }
3638 
3639     /**
3640      * Build a where clause to select the rows to be updated in status_updates table.
3641      */
getWhereClauseForStatusUpdatesTable(String selection)3642     private String getWhereClauseForStatusUpdatesTable(String selection) {
3643         mSb.setLength(0);
3644         mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
3645         mSb.append(selection);
3646         mSb.append(")");
3647         return mSb.toString();
3648     }
3649 
getSettableColumnsForStatusUpdatesTable(ContentValues values)3650     private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
3651         mValues.clear();
3652         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
3653             StatusUpdates.STATUS);
3654         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
3655             StatusUpdates.STATUS_TIMESTAMP);
3656         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
3657             StatusUpdates.STATUS_RES_PACKAGE);
3658         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
3659             StatusUpdates.STATUS_LABEL);
3660         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
3661             StatusUpdates.STATUS_ICON);
3662         return mValues;
3663     }
3664 
getSettableColumnsForPresenceTable(ContentValues values)3665     private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
3666         mValues.clear();
3667         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
3668             StatusUpdates.PRESENCE);
3669         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values,
3670                 StatusUpdates.CHAT_CAPABILITY);
3671         return mValues;
3672     }
3673 
updateGroups(Uri uri, ContentValues values, String selectionWithId, String[] selectionArgs, boolean callerIsSyncAdapter)3674     private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
3675             String[] selectionArgs, boolean callerIsSyncAdapter) {
3676 
3677         mGroupIdCache.clear();
3678 
3679         ContentValues updatedValues;
3680         if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
3681             updatedValues = mValues;
3682             updatedValues.clear();
3683             updatedValues.putAll(values);
3684             updatedValues.put(Groups.DIRTY, 1);
3685         } else {
3686             updatedValues = values;
3687         }
3688 
3689         int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
3690         if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
3691             mVisibleTouched = true;
3692         }
3693         if (updatedValues.containsKey(Groups.SHOULD_SYNC)
3694                 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
3695             Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
3696                     Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
3697                     null, null);
3698             String accountName;
3699             String accountType;
3700             try {
3701                 while (c.moveToNext()) {
3702                     accountName = c.getString(0);
3703                     accountType = c.getString(1);
3704                     if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
3705                         Account account = new Account(accountName, accountType);
3706                         ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
3707                                 new Bundle());
3708                         break;
3709                     }
3710                 }
3711             } finally {
3712                 c.close();
3713             }
3714         }
3715         return count;
3716     }
3717 
updateSettings(Uri uri, ContentValues values, String selection, String[] selectionArgs)3718     private int updateSettings(Uri uri, ContentValues values, String selection,
3719             String[] selectionArgs) {
3720         final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
3721         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
3722             mVisibleTouched = true;
3723         }
3724         return count;
3725     }
3726 
updateRawContacts(ContentValues values, String selection, String[] selectionArgs)3727     private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) {
3728         if (values.containsKey(RawContacts.CONTACT_ID)) {
3729             throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
3730                     "in content values. Contact IDs are assigned automatically");
3731         }
3732 
3733         int count = 0;
3734         Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
3735                 new String[] { RawContacts._ID }, selection,
3736                 selectionArgs, null, null, null);
3737         try {
3738             while (cursor.moveToNext()) {
3739                 long rawContactId = cursor.getLong(0);
3740                 updateRawContact(rawContactId, values);
3741                 count++;
3742             }
3743         } finally {
3744             cursor.close();
3745         }
3746 
3747         return count;
3748     }
3749 
updateRawContact(long rawContactId, ContentValues values)3750     private int updateRawContact(long rawContactId, ContentValues values) {
3751         final String selection = RawContacts._ID + " = ?";
3752         mSelectionArgs1[0] = Long.toString(rawContactId);
3753         final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
3754                 && values.getAsInteger(RawContacts.DELETED) == 0);
3755         int previousDeleted = 0;
3756         String accountType = null;
3757         String accountName = null;
3758         if (requestUndoDelete) {
3759             Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
3760                     mSelectionArgs1, null, null, null);
3761             try {
3762                 if (cursor.moveToFirst()) {
3763                     previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
3764                     accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
3765                     accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
3766                 }
3767             } finally {
3768                 cursor.close();
3769             }
3770             values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
3771                     ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
3772         }
3773 
3774         int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
3775         if (count != 0) {
3776             if (values.containsKey(RawContacts.AGGREGATION_MODE)) {
3777                 int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE);
3778 
3779                 // As per ContactsContract documentation, changing aggregation mode
3780                 // to DEFAULT should not trigger aggregation
3781                 if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
3782                     mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
3783                 }
3784             }
3785             if (values.containsKey(RawContacts.STARRED)) {
3786                 mContactAggregator.updateStarred(rawContactId);
3787             }
3788             if (values.containsKey(RawContacts.SOURCE_ID)) {
3789                 mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId);
3790             }
3791             if (values.containsKey(RawContacts.NAME_VERIFIED)) {
3792 
3793                 // If setting NAME_VERIFIED for this raw contact, reset it for all
3794                 // other raw contacts in the same aggregate
3795                 if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
3796                     mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId);
3797                     mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId);
3798                     mResetNameVerifiedForOtherRawContacts.execute();
3799                 }
3800                 mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId);
3801             }
3802             if (requestUndoDelete && previousDeleted == 1) {
3803                 // undo delete, needs aggregation again.
3804                 mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType));
3805             }
3806         }
3807         return count;
3808     }
3809 
updateData(Uri uri, ContentValues values, String selection, String[] selectionArgs, boolean callerIsSyncAdapter)3810     private int updateData(Uri uri, ContentValues values, String selection,
3811             String[] selectionArgs, boolean callerIsSyncAdapter) {
3812         mValues.clear();
3813         mValues.putAll(values);
3814         mValues.remove(Data._ID);
3815         mValues.remove(Data.RAW_CONTACT_ID);
3816         mValues.remove(Data.MIMETYPE);
3817 
3818         String packageName = values.getAsString(Data.RES_PACKAGE);
3819         if (packageName != null) {
3820             mValues.remove(Data.RES_PACKAGE);
3821             mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
3822         }
3823 
3824         boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
3825         boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
3826 
3827         // Remove primary or super primary values being set to 0. This is disallowed by the
3828         // content provider.
3829         if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
3830             containsIsSuperPrimary = false;
3831             mValues.remove(Data.IS_SUPER_PRIMARY);
3832         }
3833         if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
3834             containsIsPrimary = false;
3835             mValues.remove(Data.IS_PRIMARY);
3836         }
3837 
3838         int count = 0;
3839 
3840         // Note that the query will return data according to the access restrictions,
3841         // so we don't need to worry about updating data we don't have permission to read.
3842         Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null);
3843         try {
3844             while(c.moveToNext()) {
3845                 count += updateData(mValues, c, callerIsSyncAdapter);
3846             }
3847         } finally {
3848             c.close();
3849         }
3850 
3851         return count;
3852     }
3853 
updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter)3854     private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
3855         if (values.size() == 0) {
3856             return 0;
3857         }
3858 
3859         final String mimeType = c.getString(DataUpdateQuery.MIMETYPE);
3860         DataRowHandler rowHandler = getDataRowHandler(mimeType);
3861         if (rowHandler.update(mDb, values, c, callerIsSyncAdapter)) {
3862             return 1;
3863         } else {
3864             return 0;
3865         }
3866     }
3867 
updateContactOptions(ContentValues values, String selection, String[] selectionArgs)3868     private int updateContactOptions(ContentValues values, String selection,
3869             String[] selectionArgs) {
3870         int count = 0;
3871         Cursor cursor = mDb.query(mDbHelper.getContactView(),
3872                 new String[] { Contacts._ID }, selection,
3873                 selectionArgs, null, null, null);
3874         try {
3875             while (cursor.moveToNext()) {
3876                 long contactId = cursor.getLong(0);
3877                 updateContactOptions(contactId, values);
3878                 count++;
3879             }
3880         } finally {
3881             cursor.close();
3882         }
3883 
3884         return count;
3885     }
3886 
updateContactOptions(long contactId, ContentValues values)3887     private int updateContactOptions(long contactId, ContentValues values) {
3888 
3889         mValues.clear();
3890         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
3891                 values, Contacts.CUSTOM_RINGTONE);
3892         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
3893                 values, Contacts.SEND_TO_VOICEMAIL);
3894         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
3895                 values, Contacts.LAST_TIME_CONTACTED);
3896         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
3897                 values, Contacts.TIMES_CONTACTED);
3898         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
3899                 values, Contacts.STARRED);
3900 
3901         // Nothing to update - just return
3902         if (mValues.size() == 0) {
3903             return 0;
3904         }
3905 
3906         if (mValues.containsKey(RawContacts.STARRED)) {
3907             // Mark dirty when changing starred to trigger sync
3908             mValues.put(RawContacts.DIRTY, 1);
3909         }
3910 
3911         mSelectionArgs1[0] = String.valueOf(contactId);
3912         mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
3913 
3914         // Copy changeable values to prevent automatically managed fields from
3915         // being explicitly updated by clients.
3916         mValues.clear();
3917         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
3918                 values, Contacts.CUSTOM_RINGTONE);
3919         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
3920                 values, Contacts.SEND_TO_VOICEMAIL);
3921         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
3922                 values, Contacts.LAST_TIME_CONTACTED);
3923         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
3924                 values, Contacts.TIMES_CONTACTED);
3925         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
3926                 values, Contacts.STARRED);
3927 
3928         int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
3929 
3930         if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
3931                 !values.containsKey(Contacts.TIMES_CONTACTED)) {
3932             mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
3933             mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
3934         }
3935         return rslt;
3936     }
3937 
updateAggregationException(SQLiteDatabase db, ContentValues values)3938     private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
3939         int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
3940         long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
3941         long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
3942 
3943         long rawContactId1, rawContactId2;
3944         if (rcId1 < rcId2) {
3945             rawContactId1 = rcId1;
3946             rawContactId2 = rcId2;
3947         } else {
3948             rawContactId2 = rcId1;
3949             rawContactId1 = rcId2;
3950         }
3951 
3952         if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
3953             mSelectionArgs2[0] = String.valueOf(rawContactId1);
3954             mSelectionArgs2[1] = String.valueOf(rawContactId2);
3955             db.delete(Tables.AGGREGATION_EXCEPTIONS,
3956                     AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
3957                     + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
3958         } else {
3959             ContentValues exceptionValues = new ContentValues(3);
3960             exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
3961             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
3962             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
3963             db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
3964                     exceptionValues);
3965         }
3966 
3967         mContactAggregator.invalidateAggregationExceptionCache();
3968         mContactAggregator.markForAggregation(rawContactId1,
3969                 RawContacts.AGGREGATION_MODE_DEFAULT, true);
3970         mContactAggregator.markForAggregation(rawContactId2,
3971                 RawContacts.AGGREGATION_MODE_DEFAULT, true);
3972 
3973         mContactAggregator.aggregateContact(db, rawContactId1);
3974         mContactAggregator.aggregateContact(db, rawContactId2);
3975 
3976         // The return value is fake - we just confirm that we made a change, not count actual
3977         // rows changed.
3978         return 1;
3979     }
3980 
3981     /**
3982      * Check whether GOOGLE_MY_CONTACTS_GROUP exists, otherwise create it.
3983      *
3984      * @return the group id
3985      */
getOrCreateMyContactsGroupInTransaction(String accountName, String accountType)3986     private long getOrCreateMyContactsGroupInTransaction(String accountName, String accountType) {
3987         Cursor cursor = mDb.query(Tables.GROUPS, new String[] {"_id"},
3988                 Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =? AND "
3989                     + Groups.TITLE + " =?",
3990                 new String[] {accountName, accountType, GOOGLE_MY_CONTACTS_GROUP_TITLE},
3991                 null, null, null);
3992         try {
3993             if(cursor.moveToNext()) {
3994                 return cursor.getLong(0);
3995             }
3996         } finally {
3997             cursor.close();
3998         }
3999 
4000         ContentValues values = new ContentValues();
4001         values.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP_TITLE);
4002         values.put(Groups.ACCOUNT_NAME, accountName);
4003         values.put(Groups.ACCOUNT_TYPE, accountType);
4004         values.put(Groups.GROUP_VISIBLE, "1");
4005         return mDb.insert(Tables.GROUPS, null, values);
4006     }
4007 
onAccountsUpdated(Account[] accounts)4008     public void onAccountsUpdated(Account[] accounts) {
4009         // TODO : Check the unit test.
4010         HashSet<Account> existingAccounts = new HashSet<Account>();
4011         boolean hasUnassignedContacts[] = new boolean[]{false};
4012         mDb.beginTransaction();
4013         try {
4014             findValidAccounts(existingAccounts, hasUnassignedContacts);
4015 
4016             // Add a row to the ACCOUNTS table for each new account
4017             for (Account account : accounts) {
4018                 if (!existingAccounts.contains(account)) {
4019                     mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
4020                             + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)",
4021                             new String[] {account.name, account.type});
4022                 }
4023             }
4024 
4025             // Remove all valid accounts from the existing account set. What is left
4026             // in the accountsToDelete set will be extra accounts whose data must be deleted.
4027             HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts);
4028             for (Account account : accounts) {
4029                 accountsToDelete.remove(account);
4030             }
4031 
4032             for (Account account : accountsToDelete) {
4033                 Log.d(TAG, "removing data for removed account " + account);
4034                 String[] params = new String[] {account.name, account.type};
4035                 mDb.execSQL(
4036                         "DELETE FROM " + Tables.GROUPS +
4037                         " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
4038                                 " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
4039                 mDb.execSQL(
4040                         "DELETE FROM " + Tables.PRESENCE +
4041                         " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
4042                                 "SELECT " + RawContacts._ID +
4043                                 " FROM " + Tables.RAW_CONTACTS +
4044                                 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
4045                                 " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
4046                 mDb.execSQL(
4047                         "DELETE FROM " + Tables.RAW_CONTACTS +
4048                         " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
4049                         " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
4050                 mDb.execSQL(
4051                         "DELETE FROM " + Tables.SETTINGS +
4052                         " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
4053                         " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
4054                 mDb.execSQL(
4055                         "DELETE FROM " + Tables.ACCOUNTS +
4056                         " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
4057                         " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
4058             }
4059 
4060             if (!accountsToDelete.isEmpty()) {
4061                 // Find all aggregated contacts that used to contain the raw contacts
4062                 // we have just deleted and see if they are still referencing the deleted
4063                 // names of photos.  If so, fix up those contacts.
4064                 HashSet<Long> orphanContactIds = Sets.newHashSet();
4065                 Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID +
4066                         " FROM " + Tables.CONTACTS +
4067                         " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
4068                                 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
4069                                         "(SELECT " + RawContacts._ID +
4070                                         " FROM " + Tables.RAW_CONTACTS + "))" +
4071                         " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
4072                                 Contacts.PHOTO_ID + " NOT IN " +
4073                                         "(SELECT " + Data._ID +
4074                                         " FROM " + Tables.DATA + "))", null);
4075                 try {
4076                     while (cursor.moveToNext()) {
4077                         orphanContactIds.add(cursor.getLong(0));
4078                     }
4079                 } finally {
4080                     cursor.close();
4081                 }
4082 
4083                 for (Long contactId : orphanContactIds) {
4084                     mContactAggregator.updateAggregateData(contactId);
4085                 }
4086             }
4087 
4088             if (hasUnassignedContacts[0]) {
4089 
4090                 Account primaryAccount = null;
4091                 for (Account account : accounts) {
4092                     if (isWritableAccount(account.type)) {
4093                         primaryAccount = account;
4094                         break;
4095                     }
4096                 }
4097 
4098                 if (primaryAccount != null) {
4099                     String[] params = new String[] {primaryAccount.name, primaryAccount.type};
4100                     if (primaryAccount.type.equals(DEFAULT_ACCOUNT_TYPE)) {
4101                         long groupId = getOrCreateMyContactsGroupInTransaction(
4102                                 primaryAccount.name, primaryAccount.type);
4103                         if (groupId != -1) {
4104                             long mimeTypeId = mDbHelper.getMimeTypeId(
4105                                     GroupMembership.CONTENT_ITEM_TYPE);
4106                             mDb.execSQL(
4107                                     "INSERT INTO " + Tables.DATA + "(" + DataColumns.MIMETYPE_ID +
4108                                         ", " + Data.RAW_CONTACT_ID + ", "
4109                                         + GroupMembership.GROUP_ROW_ID + ") " +
4110                                     "SELECT " + mimeTypeId + ", "
4111                                             + RawContacts._ID + ", " + groupId +
4112                                     " FROM " + Tables.RAW_CONTACTS +
4113                                     " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
4114                                     " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL"
4115                             );
4116                         }
4117                     }
4118                     mDb.execSQL(
4119                             "UPDATE " + Tables.RAW_CONTACTS +
4120                             " SET " + RawContacts.ACCOUNT_NAME + "=?,"
4121                                     + RawContacts.ACCOUNT_TYPE + "=?" +
4122                             " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
4123                             " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params);
4124 
4125                     // We don't currently support groups for unsynced accounts, so this is for
4126                     // the future
4127                     mDb.execSQL(
4128                             "UPDATE " + Tables.GROUPS +
4129                             " SET " + Groups.ACCOUNT_NAME + "=?,"
4130                                     + Groups.ACCOUNT_TYPE + "=?" +
4131                             " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" +
4132                             " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params);
4133 
4134                     mDb.execSQL(
4135                             "DELETE FROM " + Tables.ACCOUNTS +
4136                             " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
4137                             " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL");
4138                 }
4139             }
4140 
4141             mDbHelper.updateAllVisible();
4142 
4143             mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
4144             mDb.setTransactionSuccessful();
4145         } finally {
4146             mDb.endTransaction();
4147         }
4148         mAccountWritability.clear();
4149     }
4150 
4151     /**
4152      * Finds all distinct accounts present in the specified table.
4153      */
findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts)4154     private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts) {
4155         Cursor c = mDb.rawQuery(
4156                 "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
4157                 " FROM " + Tables.ACCOUNTS, null);
4158         try {
4159             while (c.moveToNext()) {
4160                 if (c.isNull(0) && c.isNull(1)) {
4161                     hasUnassignedContacts[0] = true;
4162                 } else {
4163                     validAccounts.add(new Account(c.getString(0), c.getString(1)));
4164                 }
4165             }
4166         } finally {
4167             c.close();
4168         }
4169     }
4170 
4171     /**
4172      * Test all against {@link TextUtils#isEmpty(CharSequence)}.
4173      */
areAllEmpty(ContentValues values, String[] keys)4174     private static boolean areAllEmpty(ContentValues values, String[] keys) {
4175         for (String key : keys) {
4176             if (!TextUtils.isEmpty(values.getAsString(key))) {
4177                 return false;
4178             }
4179         }
4180         return true;
4181     }
4182 
4183     /**
4184      * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
4185      */
areAnySpecified(ContentValues values, String[] keys)4186     private static boolean areAnySpecified(ContentValues values, String[] keys) {
4187         for (String key : keys) {
4188             if (values.containsKey(key)) {
4189                 return true;
4190             }
4191         }
4192         return false;
4193     }
4194 
4195     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)4196     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
4197             String sortOrder) {
4198         if (VERBOSE_LOGGING) {
4199             Log.v(TAG, "query: " + uri);
4200         }
4201 
4202         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
4203 
4204         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
4205         String groupBy = null;
4206         String limit = getLimit(uri);
4207 
4208         // TODO: Consider writing a test case for RestrictionExceptions when you
4209         // write a new query() block to make sure it protects restricted data.
4210         final int match = sUriMatcher.match(uri);
4211         switch (match) {
4212             case SYNCSTATE:
4213                 return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
4214                         sortOrder);
4215 
4216             case CONTACTS: {
4217                 setTablesAndProjectionMapForContacts(qb, uri, projection);
4218                 break;
4219             }
4220 
4221             case CONTACTS_ID: {
4222                 long contactId = ContentUris.parseId(uri);
4223                 setTablesAndProjectionMapForContacts(qb, uri, projection);
4224                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4225                 qb.appendWhere(Contacts._ID + "=?");
4226                 break;
4227             }
4228 
4229             case CONTACTS_LOOKUP:
4230             case CONTACTS_LOOKUP_ID: {
4231                 List<String> pathSegments = uri.getPathSegments();
4232                 int segmentCount = pathSegments.size();
4233                 if (segmentCount < 3) {
4234                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
4235                             "Missing a lookup key", uri));
4236                 }
4237                 String lookupKey = pathSegments.get(2);
4238                 if (segmentCount == 4) {
4239                     // TODO: pull this out into a method and generalize to not require contactId
4240                     long contactId = Long.parseLong(pathSegments.get(3));
4241                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
4242                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
4243                     String[] args;
4244                     if (selectionArgs == null) {
4245                         args = new String[2];
4246                     } else {
4247                         args = new String[selectionArgs.length + 2];
4248                         System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
4249                     }
4250                     args[0] = String.valueOf(contactId);
4251                     args[1] = Uri.encode(lookupKey);
4252                     lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
4253                     Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
4254                             groupBy, limit);
4255                     if (c.getCount() != 0) {
4256                         return c;
4257                     }
4258 
4259                     c.close();
4260                 }
4261 
4262                 setTablesAndProjectionMapForContacts(qb, uri, projection);
4263                 selectionArgs = insertSelectionArg(selectionArgs,
4264                         String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4265                 qb.appendWhere(Contacts._ID + "=?");
4266                 break;
4267             }
4268 
4269             case CONTACTS_AS_VCARD: {
4270                 // When reading as vCard always use restricted view
4271                 final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
4272                 qb.setTables(mDbHelper.getContactView(true /* require restricted */));
4273                 qb.setProjectionMap(sContactsVCardProjectionMap);
4274                 selectionArgs = insertSelectionArg(selectionArgs,
4275                         String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
4276                 qb.appendWhere(Contacts._ID + "=?");
4277                 break;
4278             }
4279 
4280             case CONTACTS_AS_MULTI_VCARD: {
4281                 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
4282                 String currentDateString = dateFormat.format(new Date()).toString();
4283                 return db.rawQuery(
4284                     "SELECT" +
4285                     " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," +
4286                     " NULL AS " + OpenableColumns.SIZE,
4287                     new String[] { currentDateString });
4288             }
4289 
4290             case CONTACTS_FILTER: {
4291                 String filterParam = "";
4292                 if (uri.getPathSegments().size() > 2) {
4293                     filterParam = uri.getLastPathSegment();
4294                 }
4295                 setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam);
4296                 break;
4297             }
4298 
4299             case CONTACTS_STREQUENT_FILTER:
4300             case CONTACTS_STREQUENT: {
4301                 String filterSql = null;
4302                 if (match == CONTACTS_STREQUENT_FILTER
4303                         && uri.getPathSegments().size() > 3) {
4304                     String filterParam = uri.getLastPathSegment();
4305                     StringBuilder sb = new StringBuilder();
4306                     sb.append(Contacts._ID + " IN ");
4307                     appendContactFilterAsNestedQuery(sb, filterParam);
4308                     filterSql = sb.toString();
4309                 }
4310 
4311                 setTablesAndProjectionMapForContacts(qb, uri, projection);
4312 
4313                 String[] starredProjection = null;
4314                 String[] frequentProjection = null;
4315                 if (projection != null) {
4316                     starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
4317                     frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
4318                 }
4319 
4320                 // Build the first query for starred
4321                 if (filterSql != null) {
4322                     qb.appendWhere(filterSql);
4323                 }
4324                 qb.setProjectionMap(sStrequentStarredProjectionMap);
4325                 final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1",
4326                         null, Contacts._ID, null, null, null);
4327 
4328                 // Build the second query for frequent
4329                 qb = new SQLiteQueryBuilder();
4330                 setTablesAndProjectionMapForContacts(qb, uri, projection);
4331                 if (filterSql != null) {
4332                     qb.appendWhere(filterSql);
4333                 }
4334                 qb.setProjectionMap(sStrequentFrequentProjectionMap);
4335                 final String frequentQuery = qb.buildQuery(frequentProjection,
4336                         Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
4337                         + " = 0 OR " + Contacts.STARRED + " IS NULL)",
4338                         null, Contacts._ID, null, null, null);
4339 
4340                 // Put them together
4341                 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
4342                         STREQUENT_ORDER_BY, STREQUENT_LIMIT);
4343                 Cursor c = db.rawQuery(query, null);
4344                 if (c != null) {
4345                     c.setNotificationUri(getContext().getContentResolver(),
4346                             ContactsContract.AUTHORITY_URI);
4347                 }
4348                 return c;
4349             }
4350 
4351             case CONTACTS_GROUP: {
4352                 setTablesAndProjectionMapForContacts(qb, uri, projection);
4353                 if (uri.getPathSegments().size() > 2) {
4354                     qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
4355                     selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4356                 }
4357                 break;
4358             }
4359 
4360             case CONTACTS_DATA: {
4361                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
4362                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4363                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4364                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
4365                 break;
4366             }
4367 
4368             case CONTACTS_PHOTO: {
4369                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
4370                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4371                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
4372                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
4373                 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
4374                 break;
4375             }
4376 
4377             case PHONES: {
4378                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4379                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4380                 break;
4381             }
4382 
4383             case PHONES_ID: {
4384                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4385                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4386                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4387                 qb.appendWhere(" AND " + Data._ID + "=?");
4388                 break;
4389             }
4390 
4391             case PHONES_FILTER: {
4392                 setTablesAndProjectionMapForData(qb, uri, projection, true);
4393                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
4394                 if (uri.getPathSegments().size() > 2) {
4395                     String filterParam = uri.getLastPathSegment();
4396                     StringBuilder sb = new StringBuilder();
4397                     sb.append(" AND (");
4398 
4399                     boolean hasCondition = false;
4400                     boolean orNeeded = false;
4401                     String normalizedName = NameNormalizer.normalize(filterParam);
4402                     if (normalizedName.length() > 0) {
4403                         sb.append(Data.RAW_CONTACT_ID + " IN ");
4404                         appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
4405                         orNeeded = true;
4406                         hasCondition = true;
4407                     }
4408 
4409                     if (isPhoneNumber(filterParam)) {
4410                         if (orNeeded) {
4411                             sb.append(" OR ");
4412                         }
4413                         String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam);
4414                         String reversed = PhoneNumberUtils.getStrippedReversed(number);
4415                         sb.append(Data._ID +
4416                                 " IN (SELECT " + PhoneLookupColumns.DATA_ID
4417                                   + " FROM " + Tables.PHONE_LOOKUP
4418                                   + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%");
4419                         sb.append(reversed);
4420                         sb.append("')");
4421                         hasCondition = true;
4422                     }
4423 
4424                     if (!hasCondition) {
4425                         // If it is neither a phone number nor a name, the query should return
4426                         // an empty cursor.  Let's ensure that.
4427                         sb.append("0");
4428                     }
4429                     sb.append(")");
4430                     qb.appendWhere(sb);
4431                 }
4432                 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
4433                 if (sortOrder == null) {
4434                     sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
4435                 }
4436                 break;
4437             }
4438 
4439             case EMAILS: {
4440                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4441                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
4442                 break;
4443             }
4444 
4445             case EMAILS_ID: {
4446                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4447                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4448                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"
4449                         + " AND " + Data._ID + "=?");
4450                 break;
4451             }
4452 
4453             case EMAILS_LOOKUP: {
4454                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4455                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
4456                 if (uri.getPathSegments().size() > 2) {
4457                     String email = uri.getLastPathSegment();
4458                     String address = mDbHelper.extractAddressFromEmailAddress(email);
4459                     selectionArgs = insertSelectionArg(selectionArgs, address);
4460                     qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)");
4461                 }
4462                 break;
4463             }
4464 
4465             case EMAILS_FILTER: {
4466                 setTablesAndProjectionMapForData(qb, uri, projection, true);
4467                 String filterParam = null;
4468                 if (uri.getPathSegments().size() > 3) {
4469                     filterParam = uri.getLastPathSegment();
4470                     if (TextUtils.isEmpty(filterParam)) {
4471                         filterParam = null;
4472                     }
4473                 }
4474 
4475                 if (filterParam == null) {
4476                     // If the filter is unspecified, return nothing
4477                     qb.appendWhere(" AND 0");
4478                 } else {
4479                     StringBuilder sb = new StringBuilder();
4480                     sb.append(" AND " + Data._ID + " IN (");
4481                     sb.append(
4482                             "SELECT " + Data._ID +
4483                             " FROM " + Tables.DATA +
4484                             " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
4485                             " AND " + Data.DATA1 + " LIKE ");
4486                     DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
4487                     if (!filterParam.contains("@")) {
4488                         String normalizedName = NameNormalizer.normalize(filterParam);
4489                         if (normalizedName.length() > 0) {
4490 
4491                             /*
4492                              * Using a UNION instead of an "OR" to make SQLite use the right
4493                              * indexes. We need it to use the (mimetype,data1) index for the
4494                              * email lookup (see above), but not for the name lookup.
4495                              * SQLite is not smart enough to use the index on one side of an OR
4496                              * but not on the other. Using two separate nested queries
4497                              * and a UNION between them does the job.
4498                              */
4499                             sb.append(
4500                                     " UNION SELECT " + Data._ID +
4501                                     " FROM " + Tables.DATA +
4502                                     " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
4503                                     " AND " + Data.RAW_CONTACT_ID + " IN ");
4504                             appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
4505                         }
4506                     }
4507                     sb.append(")");
4508                     qb.appendWhere(sb);
4509                 }
4510                 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
4511                 if (sortOrder == null) {
4512                     sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
4513                 }
4514                 break;
4515             }
4516 
4517             case POSTALS: {
4518                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4519                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
4520                         + StructuredPostal.CONTENT_ITEM_TYPE + "'");
4521                 break;
4522             }
4523 
4524             case POSTALS_ID: {
4525                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4526                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4527                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
4528                         + StructuredPostal.CONTENT_ITEM_TYPE + "'");
4529                 qb.appendWhere(" AND " + Data._ID + "=?");
4530                 break;
4531             }
4532 
4533             case RAW_CONTACTS: {
4534                 setTablesAndProjectionMapForRawContacts(qb, uri);
4535                 break;
4536             }
4537 
4538             case RAW_CONTACTS_ID: {
4539                 long rawContactId = ContentUris.parseId(uri);
4540                 setTablesAndProjectionMapForRawContacts(qb, uri);
4541                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
4542                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
4543                 break;
4544             }
4545 
4546             case RAW_CONTACTS_DATA: {
4547                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
4548                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4549                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
4550                 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
4551                 break;
4552             }
4553 
4554             case DATA: {
4555                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4556                 break;
4557             }
4558 
4559             case DATA_ID: {
4560                 setTablesAndProjectionMapForData(qb, uri, projection, false);
4561                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4562                 qb.appendWhere(" AND " + Data._ID + "=?");
4563                 break;
4564             }
4565 
4566             case PHONE_LOOKUP: {
4567 
4568                 if (TextUtils.isEmpty(sortOrder)) {
4569                     // Default the sort order to something reasonable so we get consistent
4570                     // results when callers don't request an ordering
4571                     sortOrder = RawContactsColumns.CONCRETE_ID;
4572                 }
4573 
4574                 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
4575                 mDbHelper.buildPhoneLookupAndContactQuery(qb, number);
4576                 qb.setProjectionMap(sPhoneLookupProjectionMap);
4577 
4578                 // Phone lookup cannot be combined with a selection
4579                 selection = null;
4580                 selectionArgs = null;
4581                 break;
4582             }
4583 
4584             case GROUPS: {
4585                 qb.setTables(mDbHelper.getGroupView());
4586                 qb.setProjectionMap(sGroupsProjectionMap);
4587                 appendAccountFromParameter(qb, uri);
4588                 break;
4589             }
4590 
4591             case GROUPS_ID: {
4592                 qb.setTables(mDbHelper.getGroupView());
4593                 qb.setProjectionMap(sGroupsProjectionMap);
4594                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4595                 qb.appendWhere(Groups._ID + "=?");
4596                 break;
4597             }
4598 
4599             case GROUPS_SUMMARY: {
4600                 qb.setTables(mDbHelper.getGroupView() + " AS groups");
4601                 qb.setProjectionMap(sGroupsSummaryProjectionMap);
4602                 appendAccountFromParameter(qb, uri);
4603                 groupBy = Groups._ID;
4604                 break;
4605             }
4606 
4607             case AGGREGATION_EXCEPTIONS: {
4608                 qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
4609                 qb.setProjectionMap(sAggregationExceptionsProjectionMap);
4610                 break;
4611             }
4612 
4613             case AGGREGATION_SUGGESTIONS: {
4614                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
4615                 String filter = null;
4616                 if (uri.getPathSegments().size() > 3) {
4617                     filter = uri.getPathSegments().get(3);
4618                 }
4619                 final int maxSuggestions;
4620                 if (limit != null) {
4621                     maxSuggestions = Integer.parseInt(limit);
4622                 } else {
4623                     maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
4624                 }
4625 
4626                 setTablesAndProjectionMapForContacts(qb, uri, projection);
4627 
4628                 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
4629                         maxSuggestions, filter);
4630             }
4631 
4632             case SETTINGS: {
4633                 qb.setTables(Tables.SETTINGS);
4634                 qb.setProjectionMap(sSettingsProjectionMap);
4635                 appendAccountFromParameter(qb, uri);
4636 
4637                 // When requesting specific columns, this query requires
4638                 // late-binding of the GroupMembership MIME-type.
4639                 final String groupMembershipMimetypeId = Long.toString(mDbHelper
4640                         .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
4641                 if (projection != null && projection.length != 0 &&
4642                         mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
4643                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
4644                 }
4645                 if (projection != null && projection.length != 0 &&
4646                         mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
4647                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
4648                 }
4649 
4650                 break;
4651             }
4652 
4653             case STATUS_UPDATES: {
4654                 setTableAndProjectionMapForStatusUpdates(qb, projection);
4655                 break;
4656             }
4657 
4658             case STATUS_UPDATES_ID: {
4659                 setTableAndProjectionMapForStatusUpdates(qb, projection);
4660                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4661                 qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
4662                 break;
4663             }
4664 
4665             case SEARCH_SUGGESTIONS: {
4666                 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
4667             }
4668 
4669             case SEARCH_SHORTCUT: {
4670                 String lookupKey = uri.getLastPathSegment();
4671                 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
4672             }
4673 
4674             case LIVE_FOLDERS_CONTACTS:
4675                 qb.setTables(mDbHelper.getContactView());
4676                 qb.setProjectionMap(sLiveFoldersProjectionMap);
4677                 break;
4678 
4679             case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
4680                 qb.setTables(mDbHelper.getContactView());
4681                 qb.setProjectionMap(sLiveFoldersProjectionMap);
4682                 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
4683                 break;
4684 
4685             case LIVE_FOLDERS_CONTACTS_FAVORITES:
4686                 qb.setTables(mDbHelper.getContactView());
4687                 qb.setProjectionMap(sLiveFoldersProjectionMap);
4688                 qb.appendWhere(Contacts.STARRED + "=1");
4689                 break;
4690 
4691             case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
4692                 qb.setTables(mDbHelper.getContactView());
4693                 qb.setProjectionMap(sLiveFoldersProjectionMap);
4694                 qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
4695                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
4696                 break;
4697 
4698             case RAW_CONTACT_ENTITIES: {
4699                 setTablesAndProjectionMapForRawContactsEntities(qb, uri);
4700                 break;
4701             }
4702 
4703             case RAW_CONTACT_ENTITY_ID: {
4704                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
4705                 setTablesAndProjectionMapForRawContactsEntities(qb, uri);
4706                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
4707                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
4708                 break;
4709             }
4710 
4711             case PROVIDER_STATUS: {
4712                 return queryProviderStatus(uri, projection);
4713             }
4714 
4715             default:
4716                 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
4717                         sortOrder, limit);
4718         }
4719 
4720         qb.setStrictProjectionMap(true);
4721 
4722         Cursor cursor =
4723                 query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
4724         if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
4725             cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder);
4726         }
4727         return cursor;
4728     }
4729 
query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit)4730     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
4731             String selection, String[] selectionArgs, String sortOrder, String groupBy,
4732             String limit) {
4733         if (projection != null && projection.length == 1
4734                 && BaseColumns._COUNT.equals(projection[0])) {
4735             qb.setProjectionMap(sCountProjectionMap);
4736         }
4737         final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
4738                 sortOrder, limit);
4739         if (c != null) {
4740             c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
4741         }
4742         return c;
4743     }
4744 
4745     /**
4746      * Creates a single-row cursor containing the current status of the provider.
4747      */
queryProviderStatus(Uri uri, String[] projection)4748     private Cursor queryProviderStatus(Uri uri, String[] projection) {
4749         MatrixCursor cursor = new MatrixCursor(projection);
4750         RowBuilder row = cursor.newRow();
4751         for (int i = 0; i < projection.length; i++) {
4752             if (ProviderStatus.STATUS.equals(projection[i])) {
4753                 row.add(mProviderStatus);
4754             } else if (ProviderStatus.DATA1.equals(projection[i])) {
4755                 row.add(mEstimatedStorageRequirement);
4756             }
4757         }
4758         return cursor;
4759     }
4760 
4761 
4762     private static final class AddressBookIndexQuery {
4763         public static final String LETTER = "letter";
4764         public static final String TITLE = "title";
4765         public static final String COUNT = "count";
4766 
4767         public static final String[] COLUMNS = new String[] {
4768                 LETTER, TITLE, COUNT
4769         };
4770 
4771         public static final int COLUMN_LETTER = 0;
4772         public static final int COLUMN_TITLE = 1;
4773         public static final int COLUMN_COUNT = 2;
4774 
4775         public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
4776     }
4777 
4778     /**
4779      * Computes counts by the address book index titles and adds the resulting tally
4780      * to the returned cursor as a bundle of extras.
4781      */
bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db, SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder)4782     private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db,
4783             SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) {
4784         String sortKey;
4785 
4786         // The sort order suffix could be something like "DESC".
4787         // We want to preserve it in the query even though we will change
4788         // the sort column itself.
4789         String sortOrderSuffix = "";
4790         if (sortOrder != null) {
4791             int spaceIndex = sortOrder.indexOf(' ');
4792             if (spaceIndex != -1) {
4793                 sortKey = sortOrder.substring(0, spaceIndex);
4794                 sortOrderSuffix = sortOrder.substring(spaceIndex);
4795             } else {
4796                 sortKey = sortOrder;
4797             }
4798         } else {
4799             sortKey = Contacts.SORT_KEY_PRIMARY;
4800         }
4801 
4802         String locale = getLocale().toString();
4803         HashMap<String, String> projectionMap = Maps.newHashMap();
4804         projectionMap.put(AddressBookIndexQuery.LETTER,
4805                 "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER);
4806 
4807         /**
4808          * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
4809          * to map the first letter of the sort key to a character that is traditionally
4810          * used in phonebooks to represent that letter.  For example, in Korean it will
4811          * be the first consonant in the letter; for Japanese it will be Hiragana rather
4812          * than Katakana.
4813          */
4814         projectionMap.put(AddressBookIndexQuery.TITLE,
4815                 "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')"
4816                         + " AS " + AddressBookIndexQuery.TITLE);
4817         projectionMap.put(AddressBookIndexQuery.COUNT,
4818                 "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT);
4819         qb.setProjectionMap(projectionMap);
4820 
4821         Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
4822                 AddressBookIndexQuery.ORDER_BY, null /* having */,
4823                 AddressBookIndexQuery.ORDER_BY + sortOrderSuffix);
4824 
4825         try {
4826             int groupCount = indexCursor.getCount();
4827             String titles[] = new String[groupCount];
4828             int counts[] = new int[groupCount];
4829             int indexCount = 0;
4830             String currentTitle = null;
4831 
4832             // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
4833             // with multiple entries for the same title.  The following code
4834             // collapses those duplicates.
4835             for (int i = 0; i < groupCount; i++) {
4836                 indexCursor.moveToNext();
4837                 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
4838                 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
4839                 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
4840                     titles[indexCount] = currentTitle = title;
4841                     counts[indexCount] = count;
4842                     indexCount++;
4843                 } else {
4844                     counts[indexCount - 1] += count;
4845                 }
4846             }
4847 
4848             if (indexCount < groupCount) {
4849                 String[] newTitles = new String[indexCount];
4850                 System.arraycopy(titles, 0, newTitles, 0, indexCount);
4851                 titles = newTitles;
4852 
4853                 int[] newCounts = new int[indexCount];
4854                 System.arraycopy(counts, 0, newCounts, 0, indexCount);
4855                 counts = newCounts;
4856             }
4857 
4858             final Bundle bundle = new Bundle();
4859             bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
4860             bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
4861             return new CursorWrapper(cursor) {
4862 
4863                 @Override
4864                 public Bundle getExtras() {
4865                     return bundle;
4866                 }
4867             };
4868         } finally {
4869             indexCursor.close();
4870         }
4871     }
4872 
4873     /**
4874      * Returns the contact Id for the contact identified by the lookupKey.
4875      * Robust against changes in the lookup key: if the key has changed, will
4876      * look up the contact by the raw contact IDs or name encoded in the lookup
4877      * key.
4878      */
4879     public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
4880         ContactLookupKey key = new ContactLookupKey();
4881         ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
4882 
4883         long contactId = -1;
4884         if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) {
4885             contactId = lookupContactIdBySourceIds(db, segments);
4886             if (contactId != -1) {
4887                 return contactId;
4888             }
4889         }
4890 
4891         boolean hasRawContactIds =
4892                 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID);
4893         if (hasRawContactIds) {
4894             contactId = lookupContactIdByRawContactIds(db, segments);
4895             if (contactId != -1) {
4896                 return contactId;
4897             }
4898         }
4899 
4900         if (hasRawContactIds
4901                 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) {
4902             contactId = lookupContactIdByDisplayNames(db, segments);
4903         }
4904 
4905         return contactId;
4906     }
4907 
4908     private interface LookupBySourceIdQuery {
4909         String TABLE = Tables.RAW_CONTACTS;
4910 
4911         String COLUMNS[] = {
4912                 RawContacts.CONTACT_ID,
4913                 RawContacts.ACCOUNT_TYPE,
4914                 RawContacts.ACCOUNT_NAME,
4915                 RawContacts.SOURCE_ID
4916         };
4917 
4918         int CONTACT_ID = 0;
4919         int ACCOUNT_TYPE = 1;
4920         int ACCOUNT_NAME = 2;
4921         int SOURCE_ID = 3;
4922     }
4923 
4924     private long lookupContactIdBySourceIds(SQLiteDatabase db,
4925                 ArrayList<LookupKeySegment> segments) {
4926         StringBuilder sb = new StringBuilder();
4927         sb.append(RawContacts.SOURCE_ID + " IN (");
4928         for (int i = 0; i < segments.size(); i++) {
4929             LookupKeySegment segment = segments.get(i);
4930             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) {
4931                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
4932                 sb.append(",");
4933             }
4934         }
4935         sb.setLength(sb.length() - 1);      // Last comma
4936         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
4937 
4938         Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
4939                  sb.toString(), null, null, null, null);
4940         try {
4941             while (c.moveToNext()) {
4942                 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
4943                 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
4944                 int accountHashCode =
4945                         ContactLookupKey.getAccountHashCode(accountType, accountName);
4946                 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
4947                 for (int i = 0; i < segments.size(); i++) {
4948                     LookupKeySegment segment = segments.get(i);
4949                     if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID
4950                             && accountHashCode == segment.accountHashCode
4951                             && segment.key.equals(sourceId)) {
4952                         segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
4953                         break;
4954                     }
4955                 }
4956             }
4957         } finally {
4958             c.close();
4959         }
4960 
4961         return getMostReferencedContactId(segments);
4962     }
4963 
4964     private interface LookupByRawContactIdQuery {
4965         String TABLE = Tables.RAW_CONTACTS;
4966 
4967         String COLUMNS[] = {
4968                 RawContacts.CONTACT_ID,
4969                 RawContacts.ACCOUNT_TYPE,
4970                 RawContacts.ACCOUNT_NAME,
4971                 RawContacts._ID,
4972         };
4973 
4974         int CONTACT_ID = 0;
4975         int ACCOUNT_TYPE = 1;
4976         int ACCOUNT_NAME = 2;
4977         int ID = 3;
4978     }
4979 
4980     private long lookupContactIdByRawContactIds(SQLiteDatabase db,
4981             ArrayList<LookupKeySegment> segments) {
4982         StringBuilder sb = new StringBuilder();
4983         sb.append(RawContacts._ID + " IN (");
4984         for (int i = 0; i < segments.size(); i++) {
4985             LookupKeySegment segment = segments.get(i);
4986             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
4987                 sb.append(segment.rawContactId);
4988                 sb.append(",");
4989             }
4990         }
4991         sb.setLength(sb.length() - 1);      // Last comma
4992         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
4993 
4994         Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS,
4995                  sb.toString(), null, null, null, null);
4996         try {
4997             while (c.moveToNext()) {
4998                 String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE);
4999                 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME);
5000                 int accountHashCode =
5001                         ContactLookupKey.getAccountHashCode(accountType, accountName);
5002                 String rawContactId = c.getString(LookupByRawContactIdQuery.ID);
5003                 for (int i = 0; i < segments.size(); i++) {
5004                     LookupKeySegment segment = segments.get(i);
5005                     if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID
5006                             && accountHashCode == segment.accountHashCode
5007                             && segment.rawContactId.equals(rawContactId)) {
5008                         segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID);
5009                         break;
5010                     }
5011                 }
5012             }
5013         } finally {
5014             c.close();
5015         }
5016 
5017         return getMostReferencedContactId(segments);
5018     }
5019 
5020     private interface LookupByDisplayNameQuery {
5021         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
5022 
5023         String COLUMNS[] = {
5024                 RawContacts.CONTACT_ID,
5025                 RawContacts.ACCOUNT_TYPE,
5026                 RawContacts.ACCOUNT_NAME,
5027                 NameLookupColumns.NORMALIZED_NAME
5028         };
5029 
5030         int CONTACT_ID = 0;
5031         int ACCOUNT_TYPE = 1;
5032         int ACCOUNT_NAME = 2;
5033         int NORMALIZED_NAME = 3;
5034     }
5035 
5036     private long lookupContactIdByDisplayNames(SQLiteDatabase db,
5037                 ArrayList<LookupKeySegment> segments) {
5038         StringBuilder sb = new StringBuilder();
5039         sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
5040         for (int i = 0; i < segments.size(); i++) {
5041             LookupKeySegment segment = segments.get(i);
5042             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
5043                     || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
5044                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
5045                 sb.append(",");
5046             }
5047         }
5048         sb.setLength(sb.length() - 1);      // Last comma
5049         sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
5050                 + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
5051 
5052         Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
5053                  sb.toString(), null, null, null, null);
5054         try {
5055             while (c.moveToNext()) {
5056                 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
5057                 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
5058                 int accountHashCode =
5059                         ContactLookupKey.getAccountHashCode(accountType, accountName);
5060                 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
5061                 for (int i = 0; i < segments.size(); i++) {
5062                     LookupKeySegment segment = segments.get(i);
5063                     if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
5064                             || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID)
5065                             && accountHashCode == segment.accountHashCode
5066                             && segment.key.equals(name)) {
5067                         segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
5068                         break;
5069                     }
5070                 }
5071             }
5072         } finally {
5073             c.close();
5074         }
5075 
5076         return getMostReferencedContactId(segments);
5077     }
5078 
5079     private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) {
5080         for (int i = 0; i < segments.size(); i++) {
5081             LookupKeySegment segment = segments.get(i);
5082             if (segment.lookupType == lookupType) {
5083                 return true;
5084             }
5085         }
5086 
5087         return false;
5088     }
5089 
5090     public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
5091         mContactAggregator.updateLookupKeyForRawContact(db, rawContactId);
5092     }
5093 
5094     /**
5095      * Returns the contact ID that is mentioned the highest number of times.
5096      */
5097     private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
5098         Collections.sort(segments);
5099 
5100         long bestContactId = -1;
5101         int bestRefCount = 0;
5102 
5103         long contactId = -1;
5104         int count = 0;
5105 
5106         int segmentCount = segments.size();
5107         for (int i = 0; i < segmentCount; i++) {
5108             LookupKeySegment segment = segments.get(i);
5109             if (segment.contactId != -1) {
5110                 if (segment.contactId == contactId) {
5111                     count++;
5112                 } else {
5113                     if (count > bestRefCount) {
5114                         bestContactId = contactId;
5115                         bestRefCount = count;
5116                     }
5117                     contactId = segment.contactId;
5118                     count = 1;
5119                 }
5120             }
5121         }
5122         if (count > bestRefCount) {
5123             return contactId;
5124         } else {
5125             return bestContactId;
5126         }
5127     }
5128 
5129     private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
5130             String[] projection) {
5131         StringBuilder sb = new StringBuilder();
5132         appendContactsTables(sb, uri, projection);
5133         qb.setTables(sb.toString());
5134         qb.setProjectionMap(sContactsProjectionMap);
5135     }
5136 
5137     /**
5138      * Finds name lookup records matching the supplied filter, picks one arbitrary match per
5139      * contact and joins that with other contacts tables.
5140      */
5141     private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
5142             String[] projection, String filter) {
5143 
5144         StringBuilder sb = new StringBuilder();
5145         appendContactsTables(sb, uri, projection);
5146 
5147         sb.append(" JOIN (SELECT " +
5148                 RawContacts.CONTACT_ID + " AS snippet_contact_id");
5149 
5150         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA_ID)) {
5151             sb.append(", " + DataColumns.CONCRETE_ID + " AS "
5152                     + SearchSnippetColumns.SNIPPET_DATA_ID);
5153         }
5154 
5155         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA1)) {
5156             sb.append(", " + Data.DATA1 + " AS " + SearchSnippetColumns.SNIPPET_DATA1);
5157         }
5158 
5159         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA2)) {
5160             sb.append(", " + Data.DATA2 + " AS " + SearchSnippetColumns.SNIPPET_DATA2);
5161         }
5162 
5163         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA3)) {
5164             sb.append(", " + Data.DATA3 + " AS " + SearchSnippetColumns.SNIPPET_DATA3);
5165         }
5166 
5167         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA4)) {
5168             sb.append(", " + Data.DATA4 + " AS " + SearchSnippetColumns.SNIPPET_DATA4);
5169         }
5170 
5171         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_MIMETYPE)) {
5172             sb.append(", (" +
5173                     "SELECT " + MimetypesColumns.MIMETYPE +
5174                     " FROM " + Tables.MIMETYPES +
5175                     " WHERE " + MimetypesColumns._ID + "=" + DataColumns.MIMETYPE_ID +
5176                     ") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE);
5177         }
5178 
5179         sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS +
5180                 " WHERE " + DataColumns.CONCRETE_ID +
5181                 " IN (");
5182 
5183         // Construct a query that gives us exactly one data _id per matching contact.
5184         // MIN stands in for ANY in this context.
5185         sb.append(
5186                 "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" +
5187                 " FROM " + Tables.NAME_LOOKUP +
5188                 " JOIN " + Tables.RAW_CONTACTS +
5189                 " ON (" + RawContactsColumns.CONCRETE_ID
5190                         + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" +
5191                 " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '");
5192         sb.append(NameNormalizer.normalize(filter));
5193         sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
5194                     " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" +
5195                 " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID);
5196 
5197         sb.append(")) ON (" + Contacts._ID + "=snippet_contact_id)");
5198 
5199         qb.setTables(sb.toString());
5200         qb.setProjectionMap(sContactsProjectionWithSnippetMap);
5201     }
5202 
5203     private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) {
5204         boolean excludeRestrictedData = false;
5205         String requestingPackage = getQueryParameter(uri,
5206                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5207         if (requestingPackage != null) {
5208             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5209         }
5210         sb.append(mDbHelper.getContactView(excludeRestrictedData));
5211         if (mDbHelper.isInProjection(projection,
5212                 Contacts.CONTACT_PRESENCE)) {
5213             sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
5214                     " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")");
5215         }
5216         if (mDbHelper.isInProjection(projection,
5217                 Contacts.CONTACT_STATUS,
5218                 Contacts.CONTACT_STATUS_RES_PACKAGE,
5219                 Contacts.CONTACT_STATUS_ICON,
5220                 Contacts.CONTACT_STATUS_LABEL,
5221                 Contacts.CONTACT_STATUS_TIMESTAMP)) {
5222             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
5223                     + ContactsStatusUpdatesColumns.ALIAS +
5224                     " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
5225                             + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
5226         }
5227     }
5228 
5229     private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
5230         StringBuilder sb = new StringBuilder();
5231         boolean excludeRestrictedData = false;
5232         String requestingPackage = getQueryParameter(uri,
5233                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5234         if (requestingPackage != null) {
5235             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5236         }
5237         sb.append(mDbHelper.getRawContactView(excludeRestrictedData));
5238         qb.setTables(sb.toString());
5239         qb.setProjectionMap(sRawContactsProjectionMap);
5240         appendAccountFromParameter(qb, uri);
5241     }
5242 
5243     private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) {
5244         // Note: currently, "export only" equals to "restricted", but may not in the future.
5245         boolean excludeRestrictedData = readBooleanQueryParameter(uri,
5246                 Data.FOR_EXPORT_ONLY, false);
5247 
5248         String requestingPackage = getQueryParameter(uri,
5249                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5250         if (requestingPackage != null) {
5251             excludeRestrictedData = excludeRestrictedData
5252                     || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5253         }
5254         qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData));
5255         qb.setProjectionMap(sRawContactsEntityProjectionMap);
5256         appendAccountFromParameter(qb, uri);
5257     }
5258 
5259     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
5260             String[] projection, boolean distinct) {
5261         StringBuilder sb = new StringBuilder();
5262         // Note: currently, "export only" equals to "restricted", but may not in the future.
5263         boolean excludeRestrictedData = readBooleanQueryParameter(uri,
5264                 Data.FOR_EXPORT_ONLY, false);
5265 
5266         String requestingPackage = getQueryParameter(uri,
5267                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
5268         if (requestingPackage != null) {
5269             excludeRestrictedData = excludeRestrictedData
5270                     || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
5271         }
5272 
5273         sb.append(mDbHelper.getDataView(excludeRestrictedData));
5274         sb.append(" data");
5275 
5276         // Include aggregated presence when requested
5277         if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) {
5278             sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
5279                     " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "="
5280                     + RawContacts.CONTACT_ID + ")");
5281         }
5282 
5283         // Include aggregated status updates when requested
5284         if (mDbHelper.isInProjection(projection,
5285                 Data.CONTACT_STATUS,
5286                 Data.CONTACT_STATUS_RES_PACKAGE,
5287                 Data.CONTACT_STATUS_ICON,
5288                 Data.CONTACT_STATUS_LABEL,
5289                 Data.CONTACT_STATUS_TIMESTAMP)) {
5290             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
5291                     + ContactsStatusUpdatesColumns.ALIAS +
5292                     " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
5293                             + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
5294         }
5295 
5296         // Include individual presence when requested
5297         if (mDbHelper.isInProjection(projection, Data.PRESENCE)) {
5298             sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
5299                     " ON (" + StatusUpdates.DATA_ID + "="
5300                     + DataColumns.CONCRETE_ID + ")");
5301         }
5302 
5303         // Include individual status updates when requested
5304         if (mDbHelper.isInProjection(projection,
5305                 Data.STATUS,
5306                 Data.STATUS_RES_PACKAGE,
5307                 Data.STATUS_ICON,
5308                 Data.STATUS_LABEL,
5309                 Data.STATUS_TIMESTAMP)) {
5310             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
5311                     " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
5312                             + DataColumns.CONCRETE_ID + ")");
5313         }
5314 
5315         qb.setTables(sb.toString());
5316         qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap);
5317         appendAccountFromParameter(qb, uri);
5318     }
5319 
5320     private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
5321             String[] projection) {
5322         StringBuilder sb = new StringBuilder();
5323         sb.append(mDbHelper.getDataView());
5324         sb.append(" data");
5325 
5326         if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) {
5327             sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
5328                     " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID
5329                     + "=" + DataColumns.CONCRETE_ID + ")");
5330         }
5331 
5332         if (mDbHelper.isInProjection(projection,
5333                 StatusUpdates.STATUS,
5334                 StatusUpdates.STATUS_RES_PACKAGE,
5335                 StatusUpdates.STATUS_ICON,
5336                 StatusUpdates.STATUS_LABEL,
5337                 StatusUpdates.STATUS_TIMESTAMP)) {
5338             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
5339                     " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID
5340                     + "=" + DataColumns.CONCRETE_ID + ")");
5341         }
5342         qb.setTables(sb.toString());
5343         qb.setProjectionMap(sStatusUpdatesProjectionMap);
5344     }
5345 
5346     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
5347         final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
5348         final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
5349 
5350         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
5351         if (partialUri) {
5352             // Throw when either account is incomplete
5353             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
5354                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
5355         }
5356 
5357         // Accounts are valid by only checking one parameter, since we've
5358         // already ruled out partial accounts.
5359         final boolean validAccount = !TextUtils.isEmpty(accountName);
5360         if (validAccount) {
5361             qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
5362                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
5363                     + RawContacts.ACCOUNT_TYPE + "="
5364                     + DatabaseUtils.sqlEscapeString(accountType));
5365         } else {
5366             qb.appendWhere("1");
5367         }
5368     }
5369 
5370     private String appendAccountToSelection(Uri uri, String selection) {
5371         final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
5372         final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
5373 
5374         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
5375         if (partialUri) {
5376             // Throw when either account is incomplete
5377             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
5378                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
5379         }
5380 
5381         // Accounts are valid by only checking one parameter, since we've
5382         // already ruled out partial accounts.
5383         final boolean validAccount = !TextUtils.isEmpty(accountName);
5384         if (validAccount) {
5385             StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
5386                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
5387                     + RawContacts.ACCOUNT_TYPE + "="
5388                     + DatabaseUtils.sqlEscapeString(accountType));
5389             if (!TextUtils.isEmpty(selection)) {
5390                 selectionSb.append(" AND (");
5391                 selectionSb.append(selection);
5392                 selectionSb.append(')');
5393             }
5394             return selectionSb.toString();
5395         } else {
5396             return selection;
5397         }
5398     }
5399 
5400     /**
5401      * Gets the value of the "limit" URI query parameter.
5402      *
5403      * @return A string containing a non-negative integer, or <code>null</code> if
5404      *         the parameter is not set, or is set to an invalid value.
5405      */
5406     private String getLimit(Uri uri) {
5407         String limitParam = getQueryParameter(uri, "limit");
5408         if (limitParam == null) {
5409             return null;
5410         }
5411         // make sure that the limit is a non-negative integer
5412         try {
5413             int l = Integer.parseInt(limitParam);
5414             if (l < 0) {
5415                 Log.w(TAG, "Invalid limit parameter: " + limitParam);
5416                 return null;
5417             }
5418             return String.valueOf(l);
5419         } catch (NumberFormatException ex) {
5420             Log.w(TAG, "Invalid limit parameter: " + limitParam);
5421             return null;
5422         }
5423     }
5424 
5425     /**
5426      * Returns true if all the characters are meaningful as digits
5427      * in a phone number -- letters, digits, and a few punctuation marks.
5428      */
5429     private boolean isPhoneNumber(CharSequence cons) {
5430         int len = cons.length();
5431 
5432         for (int i = 0; i < len; i++) {
5433             char c = cons.charAt(i);
5434 
5435             if ((c >= '0') && (c <= '9')) {
5436                 continue;
5437             }
5438             if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+')
5439                     || (c == '#') || (c == '*')) {
5440                 continue;
5441             }
5442             if ((c >= 'A') && (c <= 'Z')) {
5443                 continue;
5444             }
5445             if ((c >= 'a') && (c <= 'z')) {
5446                 continue;
5447             }
5448 
5449             return false;
5450         }
5451 
5452         return true;
5453     }
5454 
5455     String getContactsRestrictions() {
5456         if (mDbHelper.hasAccessToRestrictedData()) {
5457             return "1";
5458         } else {
5459             return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
5460         }
5461     }
5462 
5463     public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
5464         if (mDbHelper.hasAccessToRestrictedData()) {
5465             return "1";
5466         } else {
5467             return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
5468                     + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
5469         }
5470     }
5471 
5472     @Override
5473     public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
5474         int match = sUriMatcher.match(uri);
5475         switch (match) {
5476             case CONTACTS_PHOTO: {
5477                 return openPhotoAssetFile(uri, mode,
5478                         Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?",
5479                         new String[]{uri.getPathSegments().get(1)});
5480             }
5481 
5482             case DATA_ID: {
5483                 return openPhotoAssetFile(uri, mode,
5484                         Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'",
5485                         new String[]{uri.getPathSegments().get(1)});
5486             }
5487 
5488             case CONTACTS_AS_VCARD: {
5489                 final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
5490                 mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(mDb, lookupKey));
5491                 final String selection = Contacts._ID + "=?";
5492 
5493                 // When opening a contact as file, we pass back contents as a
5494                 // vCard-encoded stream. We build into a local buffer first,
5495                 // then pipe into MemoryFile once the exact size is known.
5496                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
5497                 outputRawContactsAsVCard(localStream, selection, mSelectionArgs1);
5498                 return buildAssetFileDescriptor(localStream);
5499             }
5500 
5501             case CONTACTS_AS_MULTI_VCARD: {
5502                 final String lookupKeys = uri.getPathSegments().get(2);
5503                 final String[] loopupKeyList = lookupKeys.split(":");
5504                 final StringBuilder inBuilder = new StringBuilder();
5505                 int index = 0;
5506                 // SQLite has limits on how many parameters can be used
5507                 // so the IDs are concatenated to a query string here instead
5508                 for (String lookupKey : loopupKeyList) {
5509                     if (index == 0) {
5510                         inBuilder.append("(");
5511                     } else {
5512                         inBuilder.append(",");
5513                     }
5514                     inBuilder.append(lookupContactIdByLookupKey(mDb, lookupKey));
5515                     index++;
5516                 }
5517                 inBuilder.append(')');
5518                 final String selection = Contacts._ID + " IN " + inBuilder.toString();
5519 
5520                 // When opening a contact as file, we pass back contents as a
5521                 // vCard-encoded stream. We build into a local buffer first,
5522                 // then pipe into MemoryFile once the exact size is known.
5523                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
5524                 outputRawContactsAsVCard(localStream, selection, null);
5525                 return buildAssetFileDescriptor(localStream);
5526             }
5527 
5528             default:
5529                 throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist",
5530                         uri));
5531         }
5532     }
5533 
5534     private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection,
5535             String[] selectionArgs)
5536             throws FileNotFoundException {
5537         if (!"r".equals(mode)) {
5538             throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode
5539                     + " not supported.", uri));
5540         }
5541 
5542         String sql =
5543                 "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
5544                 " WHERE " + selection;
5545         SQLiteDatabase db = mDbHelper.getReadableDatabase();
5546         return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql,
5547                 selectionArgs);
5548     }
5549 
5550     private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
5551 
5552     /**
5553      * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the
5554      * contents of the given {@link ByteArrayOutputStream}.
5555      */
5556     private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
5557         AssetFileDescriptor fd = null;
5558         try {
5559             stream.flush();
5560 
5561             final byte[] byteData = stream.toByteArray();
5562             final int size = byteData.length;
5563 
5564             final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size);
5565             memoryFile.writeBytes(byteData, 0, 0, size);
5566             memoryFile.deactivate();
5567 
5568             fd = AssetFileDescriptor.fromMemoryFile(memoryFile);
5569         } catch (IOException e) {
5570             Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString());
5571         }
5572         return fd;
5573     }
5574 
5575     /**
5576      * Output {@link RawContacts} matching the requested selection in the vCard
5577      * format to the given {@link OutputStream}. This method returns silently if
5578      * any errors encountered.
5579      */
5580     private void outputRawContactsAsVCard(OutputStream stream, String selection,
5581             String[] selectionArgs) {
5582         final Context context = this.getContext();
5583         final VCardComposer composer =
5584                 new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false);
5585         composer.addHandler(composer.new HandlerForOutputStream(stream));
5586 
5587         // No extra checks since composer always uses restricted views
5588         if (!composer.init(selection, selectionArgs)) {
5589             Log.w(TAG, "Failed to init VCardComposer");
5590             return;
5591         }
5592 
5593         while (!composer.isAfterLast()) {
5594             if (!composer.createOneEntry()) {
5595                 Log.w(TAG, "Failed to output a contact.");
5596             }
5597         }
5598         composer.terminate();
5599     }
5600 
5601     @Override
5602     public String getType(Uri uri) {
5603         final int match = sUriMatcher.match(uri);
5604         switch (match) {
5605             case CONTACTS:
5606                 return Contacts.CONTENT_TYPE;
5607             case CONTACTS_LOOKUP:
5608             case CONTACTS_ID:
5609             case CONTACTS_LOOKUP_ID:
5610                 return Contacts.CONTENT_ITEM_TYPE;
5611             case CONTACTS_AS_VCARD:
5612             case CONTACTS_AS_MULTI_VCARD:
5613                 return Contacts.CONTENT_VCARD_TYPE;
5614             case RAW_CONTACTS:
5615                 return RawContacts.CONTENT_TYPE;
5616             case RAW_CONTACTS_ID:
5617                 return RawContacts.CONTENT_ITEM_TYPE;
5618             case DATA:
5619                 return Data.CONTENT_TYPE;
5620             case DATA_ID:
5621                 return mDbHelper.getDataMimeType(ContentUris.parseId(uri));
5622             case PHONES:
5623                 return Phone.CONTENT_TYPE;
5624             case PHONES_ID:
5625                 return Phone.CONTENT_ITEM_TYPE;
5626             case PHONE_LOOKUP:
5627                 return PhoneLookup.CONTENT_TYPE;
5628             case EMAILS:
5629                 return Email.CONTENT_TYPE;
5630             case EMAILS_ID:
5631                 return Email.CONTENT_ITEM_TYPE;
5632             case POSTALS:
5633                 return StructuredPostal.CONTENT_TYPE;
5634             case POSTALS_ID:
5635                 return StructuredPostal.CONTENT_ITEM_TYPE;
5636             case AGGREGATION_EXCEPTIONS:
5637                 return AggregationExceptions.CONTENT_TYPE;
5638             case AGGREGATION_EXCEPTION_ID:
5639                 return AggregationExceptions.CONTENT_ITEM_TYPE;
5640             case SETTINGS:
5641                 return Settings.CONTENT_TYPE;
5642             case AGGREGATION_SUGGESTIONS:
5643                 return Contacts.CONTENT_TYPE;
5644             case SEARCH_SUGGESTIONS:
5645                 return SearchManager.SUGGEST_MIME_TYPE;
5646             case SEARCH_SHORTCUT:
5647                 return SearchManager.SHORTCUT_MIME_TYPE;
5648 
5649             default:
5650                 return mLegacyApiSupport.getType(uri);
5651         }
5652     }
5653 
5654     private void setDisplayName(long rawContactId, int displayNameSource,
5655             String displayNamePrimary, String displayNameAlternative, String phoneticName,
5656             int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) {
5657         mRawContactDisplayNameUpdate.bindLong(1, displayNameSource);
5658         bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary);
5659         bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative);
5660         bindString(mRawContactDisplayNameUpdate, 4, phoneticName);
5661         mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle);
5662         bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary);
5663         bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative);
5664         mRawContactDisplayNameUpdate.bindLong(8, rawContactId);
5665         mRawContactDisplayNameUpdate.execute();
5666     }
5667 
5668     /**
5669      * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
5670      */
5671     private void setRawContactDirty(long rawContactId) {
5672         mDirtyRawContacts.add(rawContactId);
5673     }
5674 
5675     /*
5676      * Sets the given dataId record in the "data" table to primary, and resets all data records of
5677      * the same mimetype and under the same contact to not be primary.
5678      *
5679      * @param dataId the id of the data record to be set to primary.
5680      */
5681     private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
5682         mSetPrimaryStatement.bindLong(1, dataId);
5683         mSetPrimaryStatement.bindLong(2, mimeTypeId);
5684         mSetPrimaryStatement.bindLong(3, rawContactId);
5685         mSetPrimaryStatement.execute();
5686     }
5687 
5688     /*
5689      * Sets the given dataId record in the "data" table to "super primary", and resets all data
5690      * records of the same mimetype and under the same aggregate to not be "super primary".
5691      *
5692      * @param dataId the id of the data record to be set to primary.
5693      */
5694     private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
5695         mSetSuperPrimaryStatement.bindLong(1, dataId);
5696         mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
5697         mSetSuperPrimaryStatement.bindLong(3, rawContactId);
5698         mSetSuperPrimaryStatement.execute();
5699     }
5700 
5701     public String insertNameLookupForEmail(long rawContactId, long dataId, String email) {
5702         if (TextUtils.isEmpty(email)) {
5703             return null;
5704         }
5705 
5706         String address = mDbHelper.extractHandleFromEmailAddress(email);
5707         if (address == null) {
5708             return null;
5709         }
5710 
5711         insertNameLookup(rawContactId, dataId,
5712                 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address));
5713         return address;
5714     }
5715 
5716     /**
5717      * Normalizes the nickname and inserts it in the name lookup table.
5718      */
5719     public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) {
5720         if (TextUtils.isEmpty(nickname)) {
5721             return;
5722         }
5723 
5724         insertNameLookup(rawContactId, dataId,
5725                 NameLookupType.NICKNAME, NameNormalizer.normalize(nickname));
5726     }
5727 
5728     public void insertNameLookupForOrganization(long rawContactId, long dataId, String company,
5729             String title) {
5730         if (!TextUtils.isEmpty(company)) {
5731             insertNameLookup(rawContactId, dataId,
5732                     NameLookupType.ORGANIZATION, NameNormalizer.normalize(company));
5733         }
5734         if (!TextUtils.isEmpty(title)) {
5735             insertNameLookup(rawContactId, dataId,
5736                     NameLookupType.ORGANIZATION, NameNormalizer.normalize(title));
5737         }
5738     }
5739 
5740     public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name,
5741             int fullNameStyle) {
5742         mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle);
5743     }
5744 
5745     private class StructuredNameLookupBuilder extends NameLookupBuilder {
5746 
5747         public StructuredNameLookupBuilder(NameSplitter splitter) {
5748             super(splitter);
5749         }
5750 
5751         @Override
5752         protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
5753                 String name) {
5754             ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name);
5755         }
5756 
5757         @Override
5758         protected String[] getCommonNicknameClusters(String normalizedName) {
5759             return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
5760         }
5761     }
5762 
5763     public void insertNameLookupForPhoneticName(long rawContactId, long dataId,
5764             ContentValues values) {
5765         if (values.containsKey(StructuredName.PHONETIC_FAMILY_NAME)
5766                 || values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)
5767                 || values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME)) {
5768             insertNameLookupForPhoneticName(rawContactId, dataId,
5769                     values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
5770                     values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
5771                     values.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
5772         }
5773     }
5774 
5775     public void insertNameLookupForPhoneticName(long rawContactId, long dataId, String familyName,
5776             String middleName, String givenName) {
5777         mSb.setLength(0);
5778         if (familyName != null) {
5779             mSb.append(familyName.trim());
5780         }
5781         if (middleName != null) {
5782             mSb.append(middleName.trim());
5783         }
5784         if (givenName != null) {
5785             mSb.append(givenName.trim());
5786         }
5787 
5788         if (mSb.length() > 0) {
5789             insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY,
5790                     NameNormalizer.normalize(mSb.toString()));
5791         }
5792 
5793         if (givenName != null) {
5794             // We want the phonetic given name to be used for search, but not for aggregation,
5795             // which is why we are using NAME_SHORTHAND rather than NAME_COLLATION_KEY
5796             insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND,
5797                     NameNormalizer.normalize(givenName.trim()));
5798         }
5799     }
5800 
5801     /**
5802      * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
5803      */
5804     public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
5805         mNameLookupInsert.bindLong(1, rawContactId);
5806         mNameLookupInsert.bindLong(2, dataId);
5807         mNameLookupInsert.bindLong(3, lookupType);
5808         bindString(mNameLookupInsert, 4, name);
5809         mNameLookupInsert.executeInsert();
5810     }
5811 
5812     /**
5813      * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
5814      */
5815     public void deleteNameLookup(long dataId) {
5816         mNameLookupDelete.bindLong(1, dataId);
5817         mNameLookupDelete.execute();
5818     }
5819 
5820     public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
5821         sb.append("(" +
5822                 "SELECT DISTINCT " + RawContacts.CONTACT_ID +
5823                 " FROM " + Tables.RAW_CONTACTS +
5824                 " JOIN " + Tables.NAME_LOOKUP +
5825                 " ON(" + RawContactsColumns.CONCRETE_ID + "="
5826                         + NameLookupColumns.RAW_CONTACT_ID + ")" +
5827                 " WHERE normalized_name GLOB '");
5828         sb.append(NameNormalizer.normalize(filterParam));
5829         sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
5830                     " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
5831     }
5832 
5833     public String getRawContactsByFilterAsNestedQuery(String filterParam) {
5834         StringBuilder sb = new StringBuilder();
5835         appendRawContactsByFilterAsNestedQuery(sb, filterParam);
5836         return sb.toString();
5837     }
5838 
5839     public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) {
5840         appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true);
5841     }
5842 
5843     private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
5844             boolean allowEmailMatch) {
5845         sb.append("(" +
5846                 "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
5847                 " FROM " + Tables.NAME_LOOKUP +
5848                 " WHERE " + NameLookupColumns.NORMALIZED_NAME +
5849                 " GLOB '");
5850         sb.append(normalizedName);
5851         sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
5852                 + NameLookupType.NAME_COLLATION_KEY + ","
5853                 + NameLookupType.NICKNAME + ","
5854                 + NameLookupType.NAME_SHORTHAND + ","
5855                 + NameLookupType.ORGANIZATION + ","
5856                 + NameLookupType.NAME_CONSONANTS);
5857         if (allowEmailMatch) {
5858             sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
5859         }
5860         sb.append("))");
5861     }
5862 
5863     /**
5864      * Inserts an argument at the beginning of the selection arg list.
5865      */
5866     private String[] insertSelectionArg(String[] selectionArgs, String arg) {
5867         if (selectionArgs == null) {
5868             return new String[] {arg};
5869         } else {
5870             int newLength = selectionArgs.length + 1;
5871             String[] newSelectionArgs = new String[newLength];
5872             newSelectionArgs[0] = arg;
5873             System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
5874             return newSelectionArgs;
5875         }
5876     }
5877 
5878     private String[] appendProjectionArg(String[] projection, String arg) {
5879         if (projection == null) {
5880             return null;
5881         }
5882         final int length = projection.length;
5883         String[] newProjection = new String[length + 1];
5884         System.arraycopy(projection, 0, newProjection, 0, length);
5885         newProjection[length] = arg;
5886         return newProjection;
5887     }
5888 
5889     protected Account getDefaultAccount() {
5890         AccountManager accountManager = AccountManager.get(getContext());
5891         try {
5892             Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
5893                     new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
5894             if (accounts != null && accounts.length > 0) {
5895                 return accounts[0];
5896             }
5897         } catch (Throwable e) {
5898             Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
5899         }
5900         return null;
5901     }
5902 
5903     /**
5904      * Returns true if the specified account type is writable.
5905      */
5906     protected boolean isWritableAccount(String accountType) {
5907         if (accountType == null) {
5908             return true;
5909         }
5910 
5911         Boolean writable = mAccountWritability.get(accountType);
5912         if (writable != null) {
5913             return writable;
5914         }
5915 
5916         IContentService contentService = ContentResolver.getContentService();
5917         try {
5918             for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
5919                 if (ContactsContract.AUTHORITY.equals(sync.authority) &&
5920                         accountType.equals(sync.accountType)) {
5921                     writable = sync.supportsUploading();
5922                     break;
5923                 }
5924             }
5925         } catch (RemoteException e) {
5926             Log.e(TAG, "Could not acquire sync adapter types");
5927         }
5928 
5929         if (writable == null) {
5930             writable = false;
5931         }
5932 
5933         mAccountWritability.put(accountType, writable);
5934         return writable;
5935     }
5936 
5937     /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
5938             boolean defaultValue) {
5939 
5940         // Manually parse the query, which is much faster than calling uri.getQueryParameter
5941         String query = uri.getEncodedQuery();
5942         if (query == null) {
5943             return defaultValue;
5944         }
5945 
5946         int index = query.indexOf(parameter);
5947         if (index == -1) {
5948             return defaultValue;
5949         }
5950 
5951         index += parameter.length();
5952 
5953         return !matchQueryParameter(query, index, "=0", false)
5954                 && !matchQueryParameter(query, index, "=false", true);
5955     }
5956 
5957     private static boolean matchQueryParameter(String query, int index, String value,
5958             boolean ignoreCase) {
5959         int length = value.length();
5960         return query.regionMatches(ignoreCase, index, value, 0, length)
5961                 && (query.length() == index + length || query.charAt(index + length) == '&');
5962     }
5963 
5964     /**
5965      * A fast re-implementation of {@link Uri#getQueryParameter}
5966      */
5967     /* package */ static String getQueryParameter(Uri uri, String parameter) {
5968         String query = uri.getEncodedQuery();
5969         if (query == null) {
5970             return null;
5971         }
5972 
5973         int queryLength = query.length();
5974         int parameterLength = parameter.length();
5975 
5976         String value;
5977         int index = 0;
5978         while (true) {
5979             index = query.indexOf(parameter, index);
5980             if (index == -1) {
5981                 return null;
5982             }
5983 
5984             index += parameterLength;
5985 
5986             if (queryLength == index) {
5987                 return null;
5988             }
5989 
5990             if (query.charAt(index) == '=') {
5991                 index++;
5992                 break;
5993             }
5994         }
5995 
5996         int ampIndex = query.indexOf('&', index);
5997         if (ampIndex == -1) {
5998             value = query.substring(index);
5999         } else {
6000             value = query.substring(index, ampIndex);
6001         }
6002 
6003         return Uri.decode(value);
6004     }
6005 
6006     private void bindString(SQLiteStatement stmt, int index, String value) {
6007         if (value == null) {
6008             stmt.bindNull(index);
6009         } else {
6010             stmt.bindString(index, value);
6011         }
6012     }
6013 
6014     private void bindLong(SQLiteStatement stmt, int index, Number value) {
6015         if (value == null) {
6016             stmt.bindNull(index);
6017         } else {
6018             stmt.bindLong(index, value.longValue());
6019         }
6020     }
6021 
6022     protected boolean isAggregationUpgradeNeeded() {
6023         if (!mContactAggregator.isEnabled()) {
6024             return false;
6025         }
6026 
6027         int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_AGGREGATION_ALGORITHM, "2"));
6028         return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION;
6029     }
6030 
6031     protected void upgradeAggregationAlgorithm() {
6032         // This upgrade will affect very few contacts, so it can be performed on the
6033         // main thread during the initial boot after an OTA
6034 
6035         Log.i(TAG, "Upgrading aggregation algorithm");
6036         int count = 0;
6037         long start = SystemClock.currentThreadTimeMillis();
6038         try {
6039             mDb.beginTransaction();
6040             Cursor cursor = mDb.query(true,
6041                     Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2",
6042                     new String[]{"r1." + RawContacts._ID},
6043                     "r1." + RawContacts._ID + "!=r2." + RawContacts._ID +
6044                     " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID +
6045                     " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME +
6046                     " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE,
6047                     null, null, null, null, null);
6048             try {
6049                 while (cursor.moveToNext()) {
6050                     long rawContactId = cursor.getLong(0);
6051                     mContactAggregator.markForAggregation(rawContactId,
6052                             RawContacts.AGGREGATION_MODE_DEFAULT, true);
6053                     count++;
6054                 }
6055             } finally {
6056                 cursor.close();
6057             }
6058             mContactAggregator.aggregateInTransaction(mDb);
6059             mDb.setTransactionSuccessful();
6060             mDbHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM,
6061                     String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
6062         } finally {
6063             mDb.endTransaction();
6064             long end = SystemClock.currentThreadTimeMillis();
6065             Log.i(TAG, "Aggregation algorithm upgraded for " + count
6066                     + " contacts, in " + (end - start) + "ms");
6067         }
6068     }
6069 }
6070