• 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.DisplayNameSources;
28 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
29 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
30 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
31 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
32 import com.android.providers.contacts.ContactsDatabaseHelper.NicknameLookupColumns;
33 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
34 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
35 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
36 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
37 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
38 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
39 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
40 import com.google.android.collect.Lists;
41 import com.google.android.collect.Maps;
42 import com.google.android.collect.Sets;
43 
44 import android.accounts.Account;
45 import android.accounts.AccountManager;
46 import android.accounts.OnAccountsUpdateListener;
47 import android.app.SearchManager;
48 import android.content.ContentProviderOperation;
49 import android.content.ContentProviderResult;
50 import android.content.ContentResolver;
51 import android.content.ContentUris;
52 import android.content.ContentValues;
53 import android.content.Context;
54 import android.content.Entity;
55 import android.content.EntityIterator;
56 import android.content.IContentService;
57 import android.content.OperationApplicationException;
58 import android.content.SharedPreferences;
59 import android.content.SyncAdapterType;
60 import android.content.UriMatcher;
61 import android.content.SharedPreferences.Editor;
62 import android.content.res.AssetFileDescriptor;
63 import android.database.Cursor;
64 import android.database.DatabaseUtils;
65 import android.database.sqlite.SQLiteConstraintException;
66 import android.database.sqlite.SQLiteContentHelper;
67 import android.database.sqlite.SQLiteCursor;
68 import android.database.sqlite.SQLiteDatabase;
69 import android.database.sqlite.SQLiteQueryBuilder;
70 import android.database.sqlite.SQLiteStatement;
71 import android.net.Uri;
72 import android.os.Bundle;
73 import android.os.MemoryFile;
74 import android.os.RemoteException;
75 import android.os.SystemProperties;
76 import android.pim.vcard.VCardComposer;
77 import android.preference.PreferenceManager;
78 import android.provider.BaseColumns;
79 import android.provider.ContactsContract;
80 import android.provider.LiveFolders;
81 import android.provider.OpenableColumns;
82 import android.provider.SyncStateContract;
83 import android.provider.ContactsContract.AggregationExceptions;
84 import android.provider.ContactsContract.Contacts;
85 import android.provider.ContactsContract.Data;
86 import android.provider.ContactsContract.Groups;
87 import android.provider.ContactsContract.PhoneLookup;
88 import android.provider.ContactsContract.RawContacts;
89 import android.provider.ContactsContract.Settings;
90 import android.provider.ContactsContract.StatusUpdates;
91 import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
92 import android.provider.ContactsContract.CommonDataKinds.Email;
93 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
94 import android.provider.ContactsContract.CommonDataKinds.Im;
95 import android.provider.ContactsContract.CommonDataKinds.Nickname;
96 import android.provider.ContactsContract.CommonDataKinds.Organization;
97 import android.provider.ContactsContract.CommonDataKinds.Phone;
98 import android.provider.ContactsContract.CommonDataKinds.Photo;
99 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
100 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
101 import android.telephony.PhoneNumberUtils;
102 import android.text.TextUtils;
103 import android.text.util.Rfc822Token;
104 import android.text.util.Rfc822Tokenizer;
105 import android.util.Log;
106 
107 import java.io.ByteArrayOutputStream;
108 import java.io.FileNotFoundException;
109 import java.io.IOException;
110 import java.io.OutputStream;
111 import java.lang.ref.SoftReference;
112 import java.util.ArrayList;
113 import java.util.Collections;
114 import java.util.HashMap;
115 import java.util.HashSet;
116 import java.util.List;
117 import java.util.Locale;
118 import java.util.Map;
119 import java.util.Set;
120 import java.util.concurrent.CountDownLatch;
121 
122 /**
123  * Contacts content provider. The contract between this provider and applications
124  * is defined in {@link ContactsContract}.
125  */
126 public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
127 
128     private static final String TAG = "ContactsProvider";
129 
130     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
131 
132     // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
133     // TODO: check for restricted flag during insert(), update(), and delete() calls
134 
135     /** Default for the maximum number of returned aggregation suggestions. */
136     private static final int DEFAULT_MAX_SUGGESTIONS = 5;
137 
138     /**
139      * Shared preference key for the legacy contact import version. The need for a version
140      * as opposed to a boolean flag is that if we discover bugs in the contact import process,
141      * we can trigger re-import by incrementing the import version.
142      */
143     private static final String PREF_CONTACTS_IMPORTED = "contacts_imported_v1";
144     private static final int PREF_CONTACTS_IMPORT_VERSION = 1;
145 
146     private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
147 
148     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
149 
150     private static final String TIMES_CONTACED_SORT_COLUMN = "times_contacted_sort";
151 
152     private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
153             + TIMES_CONTACED_SORT_COLUMN + " DESC, "
154             + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
155     private static final String STREQUENT_LIMIT =
156             "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
157             + Contacts.STARRED + "=1) + 25";
158 
159     private static final int CONTACTS = 1000;
160     private static final int CONTACTS_ID = 1001;
161     private static final int CONTACTS_LOOKUP = 1002;
162     private static final int CONTACTS_LOOKUP_ID = 1003;
163     private static final int CONTACTS_DATA = 1004;
164     private static final int CONTACTS_FILTER = 1005;
165     private static final int CONTACTS_STREQUENT = 1006;
166     private static final int CONTACTS_STREQUENT_FILTER = 1007;
167     private static final int CONTACTS_GROUP = 1008;
168     private static final int CONTACTS_PHOTO = 1009;
169     private static final int CONTACTS_AS_VCARD = 1010;
170 
171     private static final int RAW_CONTACTS = 2002;
172     private static final int RAW_CONTACTS_ID = 2003;
173     private static final int RAW_CONTACTS_DATA = 2004;
174     private static final int RAW_CONTACT_ENTITY_ID = 2005;
175 
176     private static final int DATA = 3000;
177     private static final int DATA_ID = 3001;
178     private static final int PHONES = 3002;
179     private static final int PHONES_ID = 3003;
180     private static final int PHONES_FILTER = 3004;
181     private static final int EMAILS = 3005;
182     private static final int EMAILS_ID = 3006;
183     private static final int EMAILS_LOOKUP = 3007;
184     private static final int EMAILS_FILTER = 3008;
185     private static final int POSTALS = 3009;
186     private static final int POSTALS_ID = 3010;
187 
188     private static final int PHONE_LOOKUP = 4000;
189 
190     private static final int AGGREGATION_EXCEPTIONS = 6000;
191     private static final int AGGREGATION_EXCEPTION_ID = 6001;
192 
193     private static final int STATUS_UPDATES = 7000;
194     private static final int STATUS_UPDATES_ID = 7001;
195 
196     private static final int AGGREGATION_SUGGESTIONS = 8000;
197 
198     private static final int SETTINGS = 9000;
199 
200     private static final int GROUPS = 10000;
201     private static final int GROUPS_ID = 10001;
202     private static final int GROUPS_SUMMARY = 10003;
203 
204     private static final int SYNCSTATE = 11000;
205     private static final int SYNCSTATE_ID = 11001;
206 
207     private static final int SEARCH_SUGGESTIONS = 12001;
208     private static final int SEARCH_SHORTCUT = 12002;
209 
210     private static final int LIVE_FOLDERS_CONTACTS = 14000;
211     private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001;
212     private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002;
213     private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003;
214 
215     private static final int RAW_CONTACT_ENTITIES = 15001;
216 
217     private interface ContactsQuery {
218         public static final String TABLE = Tables.RAW_CONTACTS;
219 
220         public static final String[] PROJECTION = new String[] {
221             RawContactsColumns.CONCRETE_ID,
222             RawContacts.ACCOUNT_NAME,
223             RawContacts.ACCOUNT_TYPE,
224         };
225 
226         public static final int RAW_CONTACT_ID = 0;
227         public static final int ACCOUNT_NAME = 1;
228         public static final int ACCOUNT_TYPE = 2;
229     }
230 
231     private interface DataContactsQuery {
232         public static final String TABLE = "data "
233                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
234                 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
235 
236         public static final String[] PROJECTION = new String[] {
237             RawContactsColumns.CONCRETE_ID,
238             DataColumns.CONCRETE_ID,
239             ContactsColumns.CONCRETE_ID
240         };
241 
242         public static final int RAW_CONTACT_ID = 0;
243         public static final int DATA_ID = 1;
244         public static final int CONTACT_ID = 2;
245     }
246 
247     private interface DisplayNameQuery {
248         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
249 
250         public static final String[] COLUMNS = new String[] {
251             MimetypesColumns.MIMETYPE,
252             Data.IS_PRIMARY,
253             Data.DATA1,
254             Organization.TITLE,
255         };
256 
257         public static final int MIMETYPE = 0;
258         public static final int IS_PRIMARY = 1;
259         public static final int DATA = 2;
260         public static final int TITLE = 3;
261     }
262 
263     private interface DataDeleteQuery {
264         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
265 
266         public static final String[] CONCRETE_COLUMNS = new String[] {
267             DataColumns.CONCRETE_ID,
268             MimetypesColumns.MIMETYPE,
269             Data.RAW_CONTACT_ID,
270             Data.IS_PRIMARY,
271             Data.DATA1,
272         };
273 
274         public static final String[] COLUMNS = new String[] {
275             Data._ID,
276             MimetypesColumns.MIMETYPE,
277             Data.RAW_CONTACT_ID,
278             Data.IS_PRIMARY,
279             Data.DATA1,
280         };
281 
282         public static final int _ID = 0;
283         public static final int MIMETYPE = 1;
284         public static final int RAW_CONTACT_ID = 2;
285         public static final int IS_PRIMARY = 3;
286         public static final int DATA1 = 4;
287     }
288 
289     private interface DataUpdateQuery {
290         String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
291 
292         int _ID = 0;
293         int RAW_CONTACT_ID = 1;
294         int MIMETYPE = 2;
295     }
296 
297 
298     private interface NicknameLookupQuery {
299         String TABLE = Tables.NICKNAME_LOOKUP;
300 
301         String[] COLUMNS = new String[] {
302             NicknameLookupColumns.CLUSTER
303         };
304 
305         int CLUSTER = 0;
306     }
307 
308     private interface RawContactsQuery {
309         String TABLE = Tables.RAW_CONTACTS;
310 
311         String[] COLUMNS = new String[] {
312                 ContactsContract.RawContacts.DELETED
313         };
314 
315         int DELETED = 0;
316     }
317 
318     private static final HashMap<String, Integer> sDisplayNameSources;
319     static {
320         sDisplayNameSources = new HashMap<String, Integer>();
sDisplayNameSources.put(StructuredName.CONTENT_ITEM_TYPE, DisplayNameSources.STRUCTURED_NAME)321         sDisplayNameSources.put(StructuredName.CONTENT_ITEM_TYPE,
322                 DisplayNameSources.STRUCTURED_NAME);
sDisplayNameSources.put(Nickname.CONTENT_ITEM_TYPE, DisplayNameSources.NICKNAME)323         sDisplayNameSources.put(Nickname.CONTENT_ITEM_TYPE,
324                 DisplayNameSources.NICKNAME);
sDisplayNameSources.put(Organization.CONTENT_ITEM_TYPE, DisplayNameSources.ORGANIZATION)325         sDisplayNameSources.put(Organization.CONTENT_ITEM_TYPE,
326                 DisplayNameSources.ORGANIZATION);
sDisplayNameSources.put(Phone.CONTENT_ITEM_TYPE, DisplayNameSources.PHONE)327         sDisplayNameSources.put(Phone.CONTENT_ITEM_TYPE,
328                 DisplayNameSources.PHONE);
sDisplayNameSources.put(Email.CONTENT_ITEM_TYPE, DisplayNameSources.EMAIL)329         sDisplayNameSources.put(Email.CONTENT_ITEM_TYPE,
330                 DisplayNameSources.EMAIL);
331     }
332 
333     public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
334     public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google";
335 
336     /** Sql where statement for filtering on groups. */
337     private static final String CONTACTS_IN_GROUP_SELECT =
338             Contacts._ID + " IN "
339                     + "(SELECT " + RawContacts.CONTACT_ID
340                     + " FROM " + Tables.RAW_CONTACTS
341                     + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
342                             + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
343                             + " FROM " + Tables.DATA_JOIN_MIMETYPES
344                             + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
345                                     + "' AND " + GroupMembership.GROUP_ROW_ID + "="
346                                     + "(SELECT " + Tables.GROUPS + "." + Groups._ID
347                                     + " FROM " + Tables.GROUPS
348                                     + " WHERE " + Groups.TITLE + "=?)))";
349 
350     /** Contains just BaseColumns._COUNT */
351     private static final HashMap<String, String> sCountProjectionMap;
352     /** Contains just the contacts columns */
353     private static final HashMap<String, String> sContactsProjectionMap;
354     /** Used for pushing starred contacts to the top of a times contacted list **/
355     private static final HashMap<String, String> sStrequentStarredProjectionMap;
356     private static final HashMap<String, String> sStrequentFrequentProjectionMap;
357     /** Contains just the contacts vCard columns */
358     private static final HashMap<String, String> sContactsVCardProjectionMap;
359     /** Contains just the raw contacts columns */
360     private static final HashMap<String, String> sRawContactsProjectionMap;
361     /** Contains the columns from the raw contacts entity view*/
362     private static final HashMap<String, String> sRawContactsEntityProjectionMap;
363     /** Contains columns from the data view */
364     private static final HashMap<String, String> sDataProjectionMap;
365     /** Contains columns from the data view */
366     private static final HashMap<String, String> sDistinctDataProjectionMap;
367     /** Contains the data and contacts columns, for joined tables */
368     private static final HashMap<String, String> sPhoneLookupProjectionMap;
369     /** Contains the just the {@link Groups} columns */
370     private static final HashMap<String, String> sGroupsProjectionMap;
371     /** Contains {@link Groups} columns along with summary details */
372     private static final HashMap<String, String> sGroupsSummaryProjectionMap;
373     /** Contains the agg_exceptions columns */
374     private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
375     /** Contains the agg_exceptions columns */
376     private static final HashMap<String, String> sSettingsProjectionMap;
377     /** Contains StatusUpdates columns */
378     private static final HashMap<String, String> sStatusUpdatesProjectionMap;
379     /** Contains Live Folders columns */
380     private static final HashMap<String, String> sLiveFoldersProjectionMap;
381 
382     /** Precompiled sql statement for setting a data record to the primary. */
383     private SQLiteStatement mSetPrimaryStatement;
384     /** Precompiled sql statement for setting a data record to the super primary. */
385     private SQLiteStatement mSetSuperPrimaryStatement;
386     /** Precompiled sql statement for incrementing times contacted for a contact */
387     private SQLiteStatement mContactsLastTimeContactedUpdate;
388     /** Precompiled sql statement for updating a contact display name */
389     private SQLiteStatement mRawContactDisplayNameUpdate;
390     /** Precompiled sql statement for marking a raw contact as dirty */
391     private SQLiteStatement mRawContactDirtyUpdate;
392     /** Precompiled sql statement for updating an aggregated status update */
393     private SQLiteStatement mLastStatusUpdate;
394     private SQLiteStatement mNameLookupInsert;
395     private SQLiteStatement mNameLookupDelete;
396     private SQLiteStatement mStatusUpdateAutoTimestamp;
397     private SQLiteStatement mStatusUpdateInsert;
398     private SQLiteStatement mStatusUpdateReplace;
399     private SQLiteStatement mStatusAttributionUpdate;
400     private SQLiteStatement mStatusUpdateDelete;
401 
402     private long mMimeTypeIdEmail;
403     private long mMimeTypeIdIm;
404     private StringBuilder mSb = new StringBuilder();
405 
406     static {
407         // Contacts URI matching table
408         final UriMatcher matcher = sUriMatcher;
matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS)409         matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID)410         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA)411         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", AGGREGATION_SUGGESTIONS)412         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
413                 AGGREGATION_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*", AGGREGATION_SUGGESTIONS)414         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
415                 AGGREGATION_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO)416         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER)417         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP)418         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID)419         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD)420         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT)421         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", CONTACTS_STREQUENT_FILTER)422         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
423                 CONTACTS_STREQUENT_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP)424         matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);
425 
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS)426         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID)427         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA)428         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID)429         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);
430 
matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES)431         matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
432 
matcher.addURI(ContactsContract.AUTHORITY, "data", DATA)433         matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID)434         matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES)435         matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID)436         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER)437         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER)438         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS)439         matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID)440         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP)441         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER)442         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER)443         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS)444         matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID)445         matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
446 
matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS)447         matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID)448         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY)449         matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
450 
matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE)451         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#", SYNCSTATE_ID)452         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
453                 SYNCSTATE_ID);
454 
matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP)455         matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", AGGREGATION_EXCEPTIONS)456         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
457                 AGGREGATION_EXCEPTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", AGGREGATION_EXCEPTION_ID)458         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
459                 AGGREGATION_EXCEPTION_ID);
460 
matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS)461         matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
462 
matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES)463         matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID)464         matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);
465 
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGESTIONS)466         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
467                 SEARCH_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGESTIONS)468         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
469                 SEARCH_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#", SEARCH_SHORTCUT)470         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
471                 SEARCH_SHORTCUT);
472 
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts", LIVE_FOLDERS_CONTACTS)473         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts",
474                 LIVE_FOLDERS_CONTACTS);
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*", LIVE_FOLDERS_CONTACTS_GROUP_NAME)475         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*",
476                 LIVE_FOLDERS_CONTACTS_GROUP_NAME);
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones", LIVE_FOLDERS_CONTACTS_WITH_PHONES)477         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones",
478                 LIVE_FOLDERS_CONTACTS_WITH_PHONES);
matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites", LIVE_FOLDERS_CONTACTS_FAVORITES)479         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites",
480                 LIVE_FOLDERS_CONTACTS_FAVORITES);
481     }
482 
483     static {
484         sCountProjectionMap = new HashMap<String, String>();
sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)")485         sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)");
486 
487         sContactsProjectionMap = new HashMap<String, String>();
sContactsProjectionMap.put(Contacts._ID, Contacts._ID)488         sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME)489         sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED)490         sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED)491         sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED)492         sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP)493         sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID)494         sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE)495         sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER)496         sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL)497         sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY)498         sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
499 
500         // Handle projections for Contacts-level statuses
addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)501         addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE,
502                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS, ContactsStatusUpdatesColumns.CONCRETE_STATUS)503         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS,
504                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)505         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
506                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)507         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
508                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL, ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)509         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL,
510                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON, ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)511         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON,
512                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
513 
514         sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN)515         sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
516                   Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN);
517 
518         sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN)519         sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
520                   Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN);
521 
522         sContactsVCardProjectionMap = Maps.newHashMap();
sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME)523         sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME
524                 + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME);
sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "0 AS " + OpenableColumns.SIZE)525         sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "0 AS " + OpenableColumns.SIZE);
526 
527         sRawContactsProjectionMap = new HashMap<String, String>();
sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID)528         sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID)529         sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME)530         sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE)531         sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID)532         sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION)533         sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY)534         sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED)535         sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED)536         sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED, RawContacts.LAST_TIME_CONTACTED)537         sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
538                 RawContacts.LAST_TIME_CONTACTED);
sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE)539         sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE);
sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL)540         sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL);
sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED)541         sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED);
sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE)542         sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1)543         sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1);
sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2)544         sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2);
sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3)545         sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3);
sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4)546         sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4);
547 
548         sDataProjectionMap = new HashMap<String, String>();
sDataProjectionMap.put(Data._ID, Data._ID)549         sDataProjectionMap.put(Data._ID, Data._ID);
sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID)550         sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION)551         sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY)552         sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY)553         sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE)554         sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE)555         sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
sDataProjectionMap.put(Data.DATA1, Data.DATA1)556         sDataProjectionMap.put(Data.DATA1, Data.DATA1);
sDataProjectionMap.put(Data.DATA2, Data.DATA2)557         sDataProjectionMap.put(Data.DATA2, Data.DATA2);
sDataProjectionMap.put(Data.DATA3, Data.DATA3)558         sDataProjectionMap.put(Data.DATA3, Data.DATA3);
sDataProjectionMap.put(Data.DATA4, Data.DATA4)559         sDataProjectionMap.put(Data.DATA4, Data.DATA4);
sDataProjectionMap.put(Data.DATA5, Data.DATA5)560         sDataProjectionMap.put(Data.DATA5, Data.DATA5);
sDataProjectionMap.put(Data.DATA6, Data.DATA6)561         sDataProjectionMap.put(Data.DATA6, Data.DATA6);
sDataProjectionMap.put(Data.DATA7, Data.DATA7)562         sDataProjectionMap.put(Data.DATA7, Data.DATA7);
sDataProjectionMap.put(Data.DATA8, Data.DATA8)563         sDataProjectionMap.put(Data.DATA8, Data.DATA8);
sDataProjectionMap.put(Data.DATA9, Data.DATA9)564         sDataProjectionMap.put(Data.DATA9, Data.DATA9);
sDataProjectionMap.put(Data.DATA10, Data.DATA10)565         sDataProjectionMap.put(Data.DATA10, Data.DATA10);
sDataProjectionMap.put(Data.DATA11, Data.DATA11)566         sDataProjectionMap.put(Data.DATA11, Data.DATA11);
sDataProjectionMap.put(Data.DATA12, Data.DATA12)567         sDataProjectionMap.put(Data.DATA12, Data.DATA12);
sDataProjectionMap.put(Data.DATA13, Data.DATA13)568         sDataProjectionMap.put(Data.DATA13, Data.DATA13);
sDataProjectionMap.put(Data.DATA14, Data.DATA14)569         sDataProjectionMap.put(Data.DATA14, Data.DATA14);
sDataProjectionMap.put(Data.DATA15, Data.DATA15)570         sDataProjectionMap.put(Data.DATA15, Data.DATA15);
sDataProjectionMap.put(Data.SYNC1, Data.SYNC1)571         sDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
sDataProjectionMap.put(Data.SYNC2, Data.SYNC2)572         sDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
sDataProjectionMap.put(Data.SYNC3, Data.SYNC3)573         sDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
sDataProjectionMap.put(Data.SYNC4, Data.SYNC4)574         sDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID)575         sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID);
sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME)576         sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE)577         sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID)578         sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION)579         sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY)580         sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY)581         sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME)582         sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE)583         sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL)584         sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED)585         sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED)586         sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED)587         sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID)588         sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP)589         sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID)590         sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
591 
592         HashMap<String, String> columns;
593         columns = new HashMap<String, String>();
columns.put(RawContacts._ID, RawContacts._ID)594         columns.put(RawContacts._ID, RawContacts._ID);
columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID)595         columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME)596         columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE)597         columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID)598         columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
columns.put(RawContacts.VERSION, RawContacts.VERSION)599         columns.put(RawContacts.VERSION, RawContacts.VERSION);
columns.put(RawContacts.DIRTY, RawContacts.DIRTY)600         columns.put(RawContacts.DIRTY, RawContacts.DIRTY);
columns.put(RawContacts.DELETED, RawContacts.DELETED)601         columns.put(RawContacts.DELETED, RawContacts.DELETED);
columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED)602         columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED);
columns.put(RawContacts.SYNC1, RawContacts.SYNC1)603         columns.put(RawContacts.SYNC1, RawContacts.SYNC1);
columns.put(RawContacts.SYNC2, RawContacts.SYNC2)604         columns.put(RawContacts.SYNC2, RawContacts.SYNC2);
columns.put(RawContacts.SYNC3, RawContacts.SYNC3)605         columns.put(RawContacts.SYNC3, RawContacts.SYNC3);
columns.put(RawContacts.SYNC4, RawContacts.SYNC4)606         columns.put(RawContacts.SYNC4, RawContacts.SYNC4);
columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE)607         columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
columns.put(Data.MIMETYPE, Data.MIMETYPE)608         columns.put(Data.MIMETYPE, Data.MIMETYPE);
columns.put(Data.DATA1, Data.DATA1)609         columns.put(Data.DATA1, Data.DATA1);
columns.put(Data.DATA2, Data.DATA2)610         columns.put(Data.DATA2, Data.DATA2);
columns.put(Data.DATA3, Data.DATA3)611         columns.put(Data.DATA3, Data.DATA3);
columns.put(Data.DATA4, Data.DATA4)612         columns.put(Data.DATA4, Data.DATA4);
columns.put(Data.DATA5, Data.DATA5)613         columns.put(Data.DATA5, Data.DATA5);
columns.put(Data.DATA6, Data.DATA6)614         columns.put(Data.DATA6, Data.DATA6);
columns.put(Data.DATA7, Data.DATA7)615         columns.put(Data.DATA7, Data.DATA7);
columns.put(Data.DATA8, Data.DATA8)616         columns.put(Data.DATA8, Data.DATA8);
columns.put(Data.DATA9, Data.DATA9)617         columns.put(Data.DATA9, Data.DATA9);
columns.put(Data.DATA10, Data.DATA10)618         columns.put(Data.DATA10, Data.DATA10);
columns.put(Data.DATA11, Data.DATA11)619         columns.put(Data.DATA11, Data.DATA11);
columns.put(Data.DATA12, Data.DATA12)620         columns.put(Data.DATA12, Data.DATA12);
columns.put(Data.DATA13, Data.DATA13)621         columns.put(Data.DATA13, Data.DATA13);
columns.put(Data.DATA14, Data.DATA14)622         columns.put(Data.DATA14, Data.DATA14);
columns.put(Data.DATA15, Data.DATA15)623         columns.put(Data.DATA15, Data.DATA15);
columns.put(Data.SYNC1, Data.SYNC1)624         columns.put(Data.SYNC1, Data.SYNC1);
columns.put(Data.SYNC2, Data.SYNC2)625         columns.put(Data.SYNC2, Data.SYNC2);
columns.put(Data.SYNC3, Data.SYNC3)626         columns.put(Data.SYNC3, Data.SYNC3);
columns.put(Data.SYNC4, Data.SYNC4)627         columns.put(Data.SYNC4, Data.SYNC4);
columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID)628         columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID);
columns.put(Data.STARRED, Data.STARRED)629         columns.put(Data.STARRED, Data.STARRED);
columns.put(Data.DATA_VERSION, Data.DATA_VERSION)630         columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY)631         columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY)632         columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID)633         columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
634         sRawContactsEntityProjectionMap = columns;
635 
636         // Handle projections for Contacts-level statuses
addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)637         addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE,
638                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS, ContactsStatusUpdatesColumns.CONCRETE_STATUS)639         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS,
640                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)641         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
642                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)643         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
644                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)645         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
646                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON, ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)647         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
648                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
649 
650         // Handle projections for Data-level statuses
addProjection(sDataProjectionMap, Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)651         addProjection(sDataProjectionMap, Data.PRESENCE,
652                 Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sDataProjectionMap, Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)653         addProjection(sDataProjectionMap, Data.STATUS,
654                 StatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)655         addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP,
656                 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)657         addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE,
658                 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sDataProjectionMap, Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)659         addProjection(sDataProjectionMap, Data.STATUS_LABEL,
660                 StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sDataProjectionMap, Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)661         addProjection(sDataProjectionMap, Data.STATUS_ICON,
662                 StatusUpdatesColumns.CONCRETE_STATUS_ICON);
663 
664         // Projection map for data grouped by contact (not raw contact) and some data field(s)
665         sDistinctDataProjectionMap = new HashMap<String, String>();
sDistinctDataProjectionMap.put(Data._ID, "MIN(" + Data._ID + ") AS " + Data._ID)666         sDistinctDataProjectionMap.put(Data._ID,
667                 "MIN(" + Data._ID + ") AS " + Data._ID);
sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION)668         sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY)669         sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY)670         sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE)671         sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE)672         sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1)673         sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1);
sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2)674         sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2);
sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3)675         sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3);
sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4)676         sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4);
sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5)677         sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5);
sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6)678         sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6);
sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7)679         sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7);
sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8)680         sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8);
sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9)681         sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9);
sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10)682         sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10);
sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11)683         sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11);
sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12)684         sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12);
sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13)685         sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13);
sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14)686         sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14);
sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15)687         sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15);
sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1)688         sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2)689         sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3)690         sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4)691         sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID)692         sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY)693         sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME)694         sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE)695         sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL)696         sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED)697         sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED)698         sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED)699         sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID)700         sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP)701         sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID)702         sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID,
703                 GroupMembership.GROUP_SOURCE_ID);
704 
705         // Handle projections for Contacts-level statuses
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE, Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)706         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE,
707                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS, ContactsStatusUpdatesColumns.CONCRETE_STATUS)708         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS,
709                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)710         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
711                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)712         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
713                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)714         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
715                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON, ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)716         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
717                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
718 
719         // Handle projections for Data-level statuses
addProjection(sDistinctDataProjectionMap, Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)720         addProjection(sDistinctDataProjectionMap, Data.PRESENCE,
721                 Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
addProjection(sDistinctDataProjectionMap, Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)722         addProjection(sDistinctDataProjectionMap, Data.STATUS,
723                 StatusUpdatesColumns.CONCRETE_STATUS);
addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)724         addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP,
725                 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)726         addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE,
727                 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)728         addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL,
729                 StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)730         addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON,
731                 StatusUpdatesColumns.CONCRETE_STATUS_ICON);
732 
733         sPhoneLookupProjectionMap = new HashMap<String, String>();
sPhoneLookupProjectionMap.put(PhoneLookup._ID, ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID)734         sPhoneLookupProjectionMap.put(PhoneLookup._ID,
735                 ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID);
sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY, Contacts.LOOKUP_KEY + " AS " + PhoneLookup.LOOKUP_KEY)736         sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
737                 Contacts.LOOKUP_KEY + " AS " + PhoneLookup.LOOKUP_KEY);
sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME, ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME)738         sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
739                 ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME);
sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED, ContactsColumns.CONCRETE_LAST_TIME_CONTACTED + " AS " + PhoneLookup.LAST_TIME_CONTACTED)740         sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
741                 ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
742                         + " AS " + PhoneLookup.LAST_TIME_CONTACTED);
sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED, ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED)743         sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
744                 ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED);
sPhoneLookupProjectionMap.put(PhoneLookup.STARRED, ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED)745         sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
746                 ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED);
sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP)747         sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
748                 Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID, Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID)749         sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
750                 Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID);
sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE, ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE)751         sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
752                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE);
sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER)753         sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
754                 Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL, ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL + " AS " + PhoneLookup.SEND_TO_VOICEMAIL)755         sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
756                 ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
757                         + " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER, Phone.NUMBER + " AS " + PhoneLookup.NUMBER)758         sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
759                 Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
sPhoneLookupProjectionMap.put(PhoneLookup.TYPE, Phone.TYPE + " AS " + PhoneLookup.TYPE)760         sPhoneLookupProjectionMap.put(PhoneLookup.TYPE,
761                 Phone.TYPE + " AS " + PhoneLookup.TYPE);
sPhoneLookupProjectionMap.put(PhoneLookup.LABEL, Phone.LABEL + " AS " + PhoneLookup.LABEL)762         sPhoneLookupProjectionMap.put(PhoneLookup.LABEL,
763                 Phone.LABEL + " AS " + PhoneLookup.LABEL);
764 
765         // Groups projection map
766         columns = new HashMap<String, String>();
columns.put(Groups._ID, Groups._ID)767         columns.put(Groups._ID, Groups._ID);
columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME)768         columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE)769         columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID)770         columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
columns.put(Groups.DIRTY, Groups.DIRTY)771         columns.put(Groups.DIRTY, Groups.DIRTY);
columns.put(Groups.VERSION, Groups.VERSION)772         columns.put(Groups.VERSION, Groups.VERSION);
columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE)773         columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE);
columns.put(Groups.TITLE, Groups.TITLE)774         columns.put(Groups.TITLE, Groups.TITLE);
columns.put(Groups.TITLE_RES, Groups.TITLE_RES)775         columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE)776         columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID)777         columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID);
columns.put(Groups.DELETED, Groups.DELETED)778         columns.put(Groups.DELETED, Groups.DELETED);
columns.put(Groups.NOTES, Groups.NOTES)779         columns.put(Groups.NOTES, Groups.NOTES);
columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC)780         columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
columns.put(Groups.SYNC1, Groups.SYNC1)781         columns.put(Groups.SYNC1, Groups.SYNC1);
columns.put(Groups.SYNC2, Groups.SYNC2)782         columns.put(Groups.SYNC2, Groups.SYNC2);
columns.put(Groups.SYNC3, Groups.SYNC3)783         columns.put(Groups.SYNC3, Groups.SYNC3);
columns.put(Groups.SYNC4, Groups.SYNC4)784         columns.put(Groups.SYNC4, Groups.SYNC4);
785         sGroupsProjectionMap = columns;
786 
787         // RawContacts and groups projection map
788         columns = new HashMap<String, String>();
789         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)790         columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
791                 + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
792                 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
793                 + ") 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)794         columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
795                 + ContactsColumns.CONCRETE_ID + ") FROM "
796                 + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
797                 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
798                 + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES);
799         sGroupsSummaryProjectionMap = columns;
800 
801         // Aggregate exception projection map
802         columns = new HashMap<String, String>();
columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id")803         columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE)804         columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1)805         columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1);
columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2)806         columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2);
807         sAggregationExceptionsProjectionMap = columns;
808 
809         // Settings projection map
810         columns = new HashMap<String, String>();
columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME)811         columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME);
columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE)812         columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE);
columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE)813         columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE);
columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC)814         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)815         columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
816                 + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN("
817                 + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE "
818                 + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME
819                 + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
820                 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS "
821                 + 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)822         columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
823                 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY "
824                 + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS
825                 + ")) 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)826         columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
827                 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE "
828                 + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
829                 + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS "
830                 + Settings.UNGROUPED_WITH_PHONES);
831         sSettingsProjectionMap = columns;
832 
833         columns = new HashMap<String, String>();
columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID)834         columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID);
columns.put(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID)835         columns.put(StatusUpdates.DATA_ID,
836                 DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID);
columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT)837         columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT);
columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE)838         columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE);
columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL)839         columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL);
840         // We cannot allow a null in the custom protocol field, because SQLite3 does not
841         // 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)842         columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL
843                 + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS "
844                 + StatusUpdates.CUSTOM_PROTOCOL);
columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE)845         columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE);
columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS)846         columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS);
columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP)847         columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP);
columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE)848         columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE);
columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON)849         columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON);
columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL)850         columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL);
851         sStatusUpdatesProjectionMap = columns;
852 
853         // Live folder projection
854         sLiveFoldersProjectionMap = new HashMap<String, String>();
sLiveFoldersProjectionMap.put(LiveFolders._ID, Contacts._ID + " AS " + LiveFolders._ID)855         sLiveFoldersProjectionMap.put(LiveFolders._ID,
856                 Contacts._ID + " AS " + LiveFolders._ID);
sLiveFoldersProjectionMap.put(LiveFolders.NAME, Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME)857         sLiveFoldersProjectionMap.put(LiveFolders.NAME,
858                 Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME);
859 
860         // TODO: Put contact photo back when we have a way to display a default icon
861         // for contacts without a photo
862         // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP,
863         //      Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
864     }
865 
addProjection(HashMap<String, String> map, String toField, String fromField)866     private static void addProjection(HashMap<String, String> map, String toField, String fromField) {
867         map.put(toField, fromField + " AS " + toField);
868     }
869 
870     /**
871      * Handles inserts and update for a specific Data type.
872      */
873     private abstract class DataRowHandler {
874 
875         protected final String mMimetype;
876         protected long mMimetypeId;
877 
DataRowHandler(String mimetype)878         public DataRowHandler(String mimetype) {
879             mMimetype = mimetype;
880 
881             // To ensure the data column position. This is dead code if properly configured.
882             if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1
883                     || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
884                     || Email.DATA != Data.DATA1) {
885                 throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
886                         + " data is not in DATA1 column");
887             }
888         }
889 
getMimeTypeId()890         protected long getMimeTypeId() {
891             if (mMimetypeId == 0) {
892                 mMimetypeId = mDbHelper.getMimeTypeId(mMimetype);
893             }
894             return mMimetypeId;
895         }
896 
897         /**
898          * Inserts a row into the {@link Data} table.
899          */
insert(SQLiteDatabase db, long rawContactId, ContentValues values)900         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
901             final long dataId = db.insert(Tables.DATA, null, values);
902 
903             Integer primary = values.getAsInteger(Data.IS_PRIMARY);
904             if (primary != null && primary != 0) {
905                 setIsPrimary(rawContactId, dataId, getMimeTypeId());
906             }
907 
908             return dataId;
909         }
910 
911         /**
912          * Validates data and updates a {@link Data} row using the cursor, which contains
913          * the current data.
914          */
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)915         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
916                 boolean callerIsSyncAdapter) {
917             long dataId = c.getLong(DataUpdateQuery._ID);
918             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
919 
920             if (values.containsKey(Data.IS_SUPER_PRIMARY)) {
921                 long mimeTypeId = getMimeTypeId();
922                 setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
923                 setIsPrimary(rawContactId, dataId, mimeTypeId);
924 
925                 // Now that we've taken care of setting these, remove them from "values".
926                 values.remove(Data.IS_SUPER_PRIMARY);
927                 values.remove(Data.IS_PRIMARY);
928             } else if (values.containsKey(Data.IS_PRIMARY)) {
929                 setIsPrimary(rawContactId, dataId, getMimeTypeId());
930 
931                 // Now that we've taken care of setting this, remove it from "values".
932                 values.remove(Data.IS_PRIMARY);
933             }
934 
935             if (values.size() > 0) {
936                 mDb.update(Tables.DATA, values, Data._ID + " = " + dataId, null);
937             }
938 
939             if (!callerIsSyncAdapter) {
940                 setRawContactDirty(rawContactId);
941             }
942         }
943 
delete(SQLiteDatabase db, Cursor c)944         public int delete(SQLiteDatabase db, Cursor c) {
945             long dataId = c.getLong(DataDeleteQuery._ID);
946             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
947             boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
948             int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
949             db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
950             if (count != 0 && primary) {
951                 fixPrimary(db, rawContactId);
952             }
953             return count;
954         }
955 
fixPrimary(SQLiteDatabase db, long rawContactId)956         private void fixPrimary(SQLiteDatabase db, long rawContactId) {
957             long newPrimaryId = findNewPrimaryDataId(db, rawContactId);
958             if (newPrimaryId != -1) {
959                 setIsPrimary(rawContactId, newPrimaryId, getMimeTypeId());
960             }
961         }
962 
findNewPrimaryDataId(SQLiteDatabase db, long rawContactId)963         protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) {
964             long primaryId = -1;
965             int primaryType = -1;
966             Cursor c = queryData(db, rawContactId);
967             try {
968                 while (c.moveToNext()) {
969                     long dataId = c.getLong(DataDeleteQuery._ID);
970                     int type = c.getInt(DataDeleteQuery.DATA1);
971                     if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
972                         primaryId = dataId;
973                         primaryType = type;
974                     }
975                 }
976             } finally {
977                 c.close();
978             }
979             return primaryId;
980         }
981 
982         /**
983          * Returns the rank of a specific record type to be used in determining the primary
984          * row. Lower number represents higher priority.
985          */
getTypeRank(int type)986         protected int getTypeRank(int type) {
987             return 0;
988         }
989 
queryData(SQLiteDatabase db, long rawContactId)990         protected Cursor queryData(SQLiteDatabase db, long rawContactId) {
991             return db.query(DataDeleteQuery.TABLE, DataDeleteQuery.CONCRETE_COLUMNS,
992                     Data.RAW_CONTACT_ID + "=" + rawContactId +
993                     " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
994                     null, null, null, null);
995         }
996 
fixRawContactDisplayName(SQLiteDatabase db, long rawContactId)997         protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
998             String bestDisplayName = null;
999             int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
1000 
1001             Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS,
1002                     Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null);
1003             try {
1004                 while (c.moveToNext()) {
1005                     String mimeType = c.getString(DisplayNameQuery.MIMETYPE);
1006 
1007                     // Display name is at DATA1 in all type.  This is ensured in the constructor.
1008                     String name = c.getString(DisplayNameQuery.DATA);
1009                     if (TextUtils.isEmpty(name)
1010                             && Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
1011                         name = c.getString(DisplayNameQuery.TITLE);
1012                     }
1013                     boolean primary = StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)
1014                         || (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);
1015 
1016                     if (name != null) {
1017                         Integer source = sDisplayNameSources.get(mimeType);
1018                         if (source != null
1019                                 && (source > bestDisplayNameSource
1020                                         || (source == bestDisplayNameSource && primary))) {
1021                             bestDisplayNameSource = source;
1022                             bestDisplayName = name;
1023                         }
1024                     }
1025                 }
1026 
1027             } finally {
1028                 c.close();
1029             }
1030 
1031             setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource);
1032             if (!isNewRawContact(rawContactId)) {
1033                 mContactAggregator.updateDisplayName(db, rawContactId);
1034             }
1035         }
1036 
isAggregationRequired()1037         public boolean isAggregationRequired() {
1038             return true;
1039         }
1040 
1041         /**
1042          * Return set of values, using current values at given {@link Data#_ID}
1043          * as baseline, but augmented with any updates.
1044          */
getAugmentedValues(SQLiteDatabase db, long dataId, ContentValues update)1045         public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
1046                 ContentValues update) {
1047             final ContentValues values = new ContentValues();
1048             final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=" + dataId,
1049                     null, null, null, null);
1050             try {
1051                 if (cursor.moveToFirst()) {
1052                     for (int i = 0; i < cursor.getColumnCount(); i++) {
1053                         final String key = cursor.getColumnName(i);
1054                         values.put(key, cursor.getString(i));
1055                     }
1056                 }
1057             } finally {
1058                 cursor.close();
1059             }
1060             values.putAll(update);
1061             return values;
1062         }
1063     }
1064 
1065     public class CustomDataRowHandler extends DataRowHandler {
1066 
CustomDataRowHandler(String mimetype)1067         public CustomDataRowHandler(String mimetype) {
1068             super(mimetype);
1069         }
1070     }
1071 
1072     public class StructuredNameRowHandler extends DataRowHandler {
1073         private final NameSplitter mSplitter;
1074 
StructuredNameRowHandler(NameSplitter splitter)1075         public StructuredNameRowHandler(NameSplitter splitter) {
1076             super(StructuredName.CONTENT_ITEM_TYPE);
1077             mSplitter = splitter;
1078         }
1079 
1080         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1081         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1082             fixStructuredNameComponents(values, values);
1083 
1084             long dataId = super.insert(db, rawContactId, values);
1085 
1086             String name = values.getAsString(StructuredName.DISPLAY_NAME);
1087             insertNameLookupForStructuredName(rawContactId, dataId, name);
1088             fixRawContactDisplayName(db, rawContactId);
1089             return dataId;
1090         }
1091 
1092         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1093         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1094                 boolean callerIsSyncAdapter) {
1095             final long dataId = c.getLong(DataUpdateQuery._ID);
1096             final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1097 
1098             final ContentValues augmented = getAugmentedValues(db, dataId, values);
1099             fixStructuredNameComponents(augmented, values);
1100 
1101             super.update(db, values, c, callerIsSyncAdapter);
1102 
1103             if (values.containsKey(StructuredName.DISPLAY_NAME)) {
1104                 String name = values.getAsString(StructuredName.DISPLAY_NAME);
1105                 deleteNameLookup(dataId);
1106                 insertNameLookupForStructuredName(rawContactId, dataId, name);
1107             }
1108             fixRawContactDisplayName(db, rawContactId);
1109         }
1110 
1111         @Override
delete(SQLiteDatabase db, Cursor c)1112         public int delete(SQLiteDatabase db, Cursor c) {
1113             long dataId = c.getLong(DataDeleteQuery._ID);
1114             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1115 
1116             int count = super.delete(db, c);
1117 
1118             deleteNameLookup(dataId);
1119             fixRawContactDisplayName(db, rawContactId);
1120             return count;
1121         }
1122 
1123         /**
1124          * Specific list of structured fields.
1125          */
1126         private final String[] STRUCTURED_FIELDS = new String[] {
1127                 StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME,
1128                 StructuredName.FAMILY_NAME, StructuredName.SUFFIX
1129         };
1130 
1131         /**
1132          * Parses the supplied display name, but only if the incoming values do
1133          * not already contain structured name parts. Also, if the display name
1134          * is not provided, generate one by concatenating first name and last
1135          * name.
1136          */
fixStructuredNameComponents(ContentValues augmented, ContentValues update)1137         private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) {
1138             final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME);
1139 
1140             final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
1141             final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);
1142 
1143             if (touchedUnstruct && !touchedStruct) {
1144                 NameSplitter.Name name = new NameSplitter.Name();
1145                 mSplitter.split(name, unstruct);
1146                 name.toValues(update);
1147             } else if (!touchedUnstruct
1148                     && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
1149                 // We need to update the display name when any structured components
1150                 // are specified, even when they are null, which is why we are checking
1151                 // areAnySpecified.  The touchedStruct in the condition is an optimization:
1152                 // if there are non-null values, we know for a fact that some values are present.
1153                 NameSplitter.Name name = new NameSplitter.Name();
1154                 name.fromValues(augmented);
1155                 final String joined = mSplitter.join(name);
1156                 update.put(StructuredName.DISPLAY_NAME, joined);
1157             }
1158         }
1159     }
1160 
1161     public class StructuredPostalRowHandler extends DataRowHandler {
1162         private PostalSplitter mSplitter;
1163 
StructuredPostalRowHandler(PostalSplitter splitter)1164         public StructuredPostalRowHandler(PostalSplitter splitter) {
1165             super(StructuredPostal.CONTENT_ITEM_TYPE);
1166             mSplitter = splitter;
1167         }
1168 
1169         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1170         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1171             fixStructuredPostalComponents(values, values);
1172             return super.insert(db, rawContactId, values);
1173         }
1174 
1175         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1176         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1177                 boolean callerIsSyncAdapter) {
1178             final long dataId = c.getLong(DataUpdateQuery._ID);
1179             final ContentValues augmented = getAugmentedValues(db, dataId, values);
1180             fixStructuredPostalComponents(augmented, values);
1181             super.update(db, values, c, callerIsSyncAdapter);
1182         }
1183 
1184         /**
1185          * Specific list of structured fields.
1186          */
1187         private final String[] STRUCTURED_FIELDS = new String[] {
1188                 StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD,
1189                 StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE,
1190                 StructuredPostal.COUNTRY,
1191         };
1192 
1193         /**
1194          * Prepares the given {@link StructuredPostal} row, building
1195          * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured
1196          * values when missing. When structured components are missing, the
1197          * unstructured value is assigned to {@link StructuredPostal#STREET}.
1198          */
fixStructuredPostalComponents(ContentValues augmented, ContentValues update)1199         private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) {
1200             final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS);
1201 
1202             final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
1203             final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);
1204 
1205             final PostalSplitter.Postal postal = new PostalSplitter.Postal();
1206 
1207             if (touchedUnstruct && !touchedStruct) {
1208                 mSplitter.split(postal, unstruct);
1209                 postal.toValues(update);
1210             } else if (!touchedUnstruct
1211                     && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
1212                 // See comment in
1213                 postal.fromValues(augmented);
1214                 final String joined = mSplitter.join(postal);
1215                 update.put(StructuredPostal.FORMATTED_ADDRESS, joined);
1216             }
1217         }
1218     }
1219 
1220     public class CommonDataRowHandler extends DataRowHandler {
1221 
1222         private final String mTypeColumn;
1223         private final String mLabelColumn;
1224 
CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn)1225         public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
1226             super(mimetype);
1227             mTypeColumn = typeColumn;
1228             mLabelColumn = labelColumn;
1229         }
1230 
1231         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1232         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1233             enforceTypeAndLabel(values, values);
1234             return super.insert(db, rawContactId, values);
1235         }
1236 
1237         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1238         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1239                 boolean callerIsSyncAdapter) {
1240             final long dataId = c.getLong(DataUpdateQuery._ID);
1241             final ContentValues augmented = getAugmentedValues(db, dataId, values);
1242             enforceTypeAndLabel(augmented, values);
1243             super.update(db, values, c, callerIsSyncAdapter);
1244         }
1245 
1246         /**
1247          * If the given {@link ContentValues} defines {@link #mTypeColumn},
1248          * enforce that {@link #mLabelColumn} only appears when type is
1249          * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise.
1250          */
enforceTypeAndLabel(ContentValues augmented, ContentValues update)1251         private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) {
1252             final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn));
1253             final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn));
1254 
1255             if (hasLabel && !hasType) {
1256                 // When label exists, assert that some type is defined
1257                 throw new IllegalArgumentException(mTypeColumn + " must be specified when "
1258                         + mLabelColumn + " is defined.");
1259             }
1260         }
1261     }
1262 
1263     public class OrganizationDataRowHandler extends CommonDataRowHandler {
1264 
OrganizationDataRowHandler()1265         public OrganizationDataRowHandler() {
1266             super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
1267         }
1268 
1269         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1270         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1271             String company = values.getAsString(Organization.COMPANY);
1272             String title = values.getAsString(Organization.TITLE);
1273 
1274             long dataId = super.insert(db, rawContactId, values);
1275 
1276             fixRawContactDisplayName(db, rawContactId);
1277             insertNameLookupForOrganization(rawContactId, dataId, company, title);
1278             return dataId;
1279         }
1280 
1281         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1282         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1283                 boolean callerIsSyncAdapter) {
1284             String company = values.getAsString(Organization.COMPANY);
1285             String title = values.getAsString(Organization.TITLE);
1286             long dataId = c.getLong(DataUpdateQuery._ID);
1287             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1288 
1289             super.update(db, values, c, callerIsSyncAdapter);
1290 
1291             fixRawContactDisplayName(db, rawContactId);
1292             deleteNameLookup(dataId);
1293             insertNameLookupForOrganization(rawContactId, dataId, company, title);
1294         }
1295 
1296         @Override
delete(SQLiteDatabase db, Cursor c)1297         public int delete(SQLiteDatabase db, Cursor c) {
1298             long dataId = c.getLong(DataUpdateQuery._ID);
1299             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1300 
1301             int count = super.delete(db, c);
1302             fixRawContactDisplayName(db, rawContactId);
1303             deleteNameLookup(dataId);
1304             return count;
1305         }
1306 
1307         @Override
getTypeRank(int type)1308         protected int getTypeRank(int type) {
1309             switch (type) {
1310                 case Organization.TYPE_WORK: return 0;
1311                 case Organization.TYPE_CUSTOM: return 1;
1312                 case Organization.TYPE_OTHER: return 2;
1313                 default: return 1000;
1314             }
1315         }
1316 
1317         @Override
isAggregationRequired()1318         public boolean isAggregationRequired() {
1319             return false;
1320         }
1321     }
1322 
1323     public class EmailDataRowHandler extends CommonDataRowHandler {
1324 
EmailDataRowHandler()1325         public EmailDataRowHandler() {
1326             super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
1327         }
1328 
1329         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1330         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1331             String address = values.getAsString(Email.DATA);
1332 
1333             long dataId = super.insert(db, rawContactId, values);
1334 
1335             fixRawContactDisplayName(db, rawContactId);
1336             insertNameLookupForEmail(rawContactId, dataId, address);
1337             return dataId;
1338         }
1339 
1340         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1341         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1342                 boolean callerIsSyncAdapter) {
1343             long dataId = c.getLong(DataUpdateQuery._ID);
1344             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1345             String address = values.getAsString(Email.DATA);
1346 
1347             super.update(db, values, c, callerIsSyncAdapter);
1348 
1349             deleteNameLookup(dataId);
1350             insertNameLookupForEmail(rawContactId, dataId, address);
1351             fixRawContactDisplayName(db, rawContactId);
1352         }
1353 
1354         @Override
delete(SQLiteDatabase db, Cursor c)1355         public int delete(SQLiteDatabase db, Cursor c) {
1356             long dataId = c.getLong(DataDeleteQuery._ID);
1357             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1358 
1359             int count = super.delete(db, c);
1360 
1361             deleteNameLookup(dataId);
1362             fixRawContactDisplayName(db, rawContactId);
1363             return count;
1364         }
1365 
1366         @Override
getTypeRank(int type)1367         protected int getTypeRank(int type) {
1368             switch (type) {
1369                 case Email.TYPE_HOME: return 0;
1370                 case Email.TYPE_WORK: return 1;
1371                 case Email.TYPE_CUSTOM: return 2;
1372                 case Email.TYPE_OTHER: return 3;
1373                 default: return 1000;
1374             }
1375         }
1376     }
1377 
1378     public class NicknameDataRowHandler extends CommonDataRowHandler {
1379 
NicknameDataRowHandler()1380         public NicknameDataRowHandler() {
1381             super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL);
1382         }
1383 
1384         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1385         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1386             String nickname = values.getAsString(Nickname.NAME);
1387 
1388             long dataId = super.insert(db, rawContactId, values);
1389 
1390             fixRawContactDisplayName(db, rawContactId);
1391             insertNameLookupForNickname(rawContactId, dataId, nickname);
1392             return dataId;
1393         }
1394 
1395         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1396         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1397                 boolean callerIsSyncAdapter) {
1398             long dataId = c.getLong(DataUpdateQuery._ID);
1399             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1400             String nickname = values.getAsString(Nickname.NAME);
1401 
1402             super.update(db, values, c, callerIsSyncAdapter);
1403 
1404             deleteNameLookup(dataId);
1405             insertNameLookupForNickname(rawContactId, dataId, nickname);
1406             fixRawContactDisplayName(db, rawContactId);
1407         }
1408 
1409         @Override
delete(SQLiteDatabase db, Cursor c)1410         public int delete(SQLiteDatabase db, Cursor c) {
1411             long dataId = c.getLong(DataDeleteQuery._ID);
1412             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1413 
1414             int count = super.delete(db, c);
1415 
1416             deleteNameLookup(dataId);
1417             fixRawContactDisplayName(db, rawContactId);
1418             return count;
1419         }
1420     }
1421 
1422     public class PhoneDataRowHandler extends CommonDataRowHandler {
1423 
PhoneDataRowHandler()1424         public PhoneDataRowHandler() {
1425             super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
1426         }
1427 
1428         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1429         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1430             long dataId;
1431             if (values.containsKey(Phone.NUMBER)) {
1432                 String number = values.getAsString(Phone.NUMBER);
1433                 String normalizedNumber = computeNormalizedNumber(number, values);
1434 
1435                 dataId = super.insert(db, rawContactId, values);
1436 
1437                 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
1438                 mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1439                 fixRawContactDisplayName(db, rawContactId);
1440             } else {
1441                 dataId = super.insert(db, rawContactId, values);
1442             }
1443             return dataId;
1444         }
1445 
1446         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1447         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1448                 boolean callerIsSyncAdapter) {
1449             long dataId = c.getLong(DataUpdateQuery._ID);
1450             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1451             if (values.containsKey(Phone.NUMBER)) {
1452                 String number = values.getAsString(Phone.NUMBER);
1453                 String normalizedNumber = computeNormalizedNumber(number, values);
1454 
1455                 super.update(db, values, c, callerIsSyncAdapter);
1456 
1457                 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
1458                 mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1459                 fixRawContactDisplayName(db, rawContactId);
1460             } else {
1461                 super.update(db, values, c, callerIsSyncAdapter);
1462             }
1463         }
1464 
1465         @Override
delete(SQLiteDatabase db, Cursor c)1466         public int delete(SQLiteDatabase db, Cursor c) {
1467             long dataId = c.getLong(DataDeleteQuery._ID);
1468             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1469 
1470             int count = super.delete(db, c);
1471 
1472             updatePhoneLookup(db, rawContactId, dataId, null, null);
1473             mContactAggregator.updateHasPhoneNumber(db, rawContactId);
1474             fixRawContactDisplayName(db, rawContactId);
1475             return count;
1476         }
1477 
computeNormalizedNumber(String number, ContentValues values)1478         private String computeNormalizedNumber(String number, ContentValues values) {
1479             String normalizedNumber = null;
1480             if (number != null) {
1481                 normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
1482             }
1483             values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
1484             return normalizedNumber;
1485         }
1486 
updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId, String number, String normalizedNumber)1487         private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId,
1488                 String number, String normalizedNumber) {
1489             if (number != null) {
1490                 ContentValues phoneValues = new ContentValues();
1491                 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
1492                 phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
1493                 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
1494                 db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
1495             } else {
1496                 db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=" + dataId, null);
1497             }
1498         }
1499 
1500         @Override
getTypeRank(int type)1501         protected int getTypeRank(int type) {
1502             switch (type) {
1503                 case Phone.TYPE_MOBILE: return 0;
1504                 case Phone.TYPE_WORK: return 1;
1505                 case Phone.TYPE_HOME: return 2;
1506                 case Phone.TYPE_PAGER: return 3;
1507                 case Phone.TYPE_CUSTOM: return 4;
1508                 case Phone.TYPE_OTHER: return 5;
1509                 case Phone.TYPE_FAX_WORK: return 6;
1510                 case Phone.TYPE_FAX_HOME: return 7;
1511                 default: return 1000;
1512             }
1513         }
1514     }
1515 
1516     public class GroupMembershipRowHandler extends DataRowHandler {
1517 
GroupMembershipRowHandler()1518         public GroupMembershipRowHandler() {
1519             super(GroupMembership.CONTENT_ITEM_TYPE);
1520         }
1521 
1522         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1523         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1524             resolveGroupSourceIdInValues(rawContactId, db, values, true);
1525             long dataId = super.insert(db, rawContactId, values);
1526             updateVisibility(rawContactId);
1527             return dataId;
1528         }
1529 
1530         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1531         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1532                 boolean callerIsSyncAdapter) {
1533             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1534             resolveGroupSourceIdInValues(rawContactId, db, values, false);
1535             super.update(db, values, c, callerIsSyncAdapter);
1536             updateVisibility(rawContactId);
1537         }
1538 
1539         @Override
delete(SQLiteDatabase db, Cursor c)1540         public int delete(SQLiteDatabase db, Cursor c) {
1541             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1542             int count = super.delete(db, c);
1543             updateVisibility(rawContactId);
1544             return count;
1545         }
1546 
updateVisibility(long rawContactId)1547         private void updateVisibility(long rawContactId) {
1548             long contactId = mDbHelper.getContactId(rawContactId);
1549             if (contactId != 0) {
1550                 mDbHelper.updateContactVisible(contactId);
1551             }
1552         }
1553 
resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db, ContentValues values, boolean isInsert)1554         private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db,
1555                 ContentValues values, boolean isInsert) {
1556             boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
1557             boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
1558             if (containsGroupSourceId && containsGroupId) {
1559                 throw new IllegalArgumentException(
1560                         "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
1561                                 + "and GroupMembership.GROUP_ROW_ID");
1562             }
1563 
1564             if (!containsGroupSourceId && !containsGroupId) {
1565                 if (isInsert) {
1566                     throw new IllegalArgumentException(
1567                             "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
1568                                     + "and GroupMembership.GROUP_ROW_ID");
1569                 } else {
1570                     return;
1571                 }
1572             }
1573 
1574             if (containsGroupSourceId) {
1575                 final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
1576                 final long groupId = getOrMakeGroup(db, rawContactId, sourceId);
1577                 values.remove(GroupMembership.GROUP_SOURCE_ID);
1578                 values.put(GroupMembership.GROUP_ROW_ID, groupId);
1579             }
1580         }
1581 
1582         @Override
isAggregationRequired()1583         public boolean isAggregationRequired() {
1584             return false;
1585         }
1586     }
1587 
1588     public class PhotoDataRowHandler extends DataRowHandler {
1589 
PhotoDataRowHandler()1590         public PhotoDataRowHandler() {
1591             super(Photo.CONTENT_ITEM_TYPE);
1592         }
1593 
1594         @Override
insert(SQLiteDatabase db, long rawContactId, ContentValues values)1595         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
1596             long dataId = super.insert(db, rawContactId, values);
1597             if (!isNewRawContact(rawContactId)) {
1598                 mContactAggregator.updatePhotoId(db, rawContactId);
1599             }
1600             return dataId;
1601         }
1602 
1603         @Override
update(SQLiteDatabase db, ContentValues values, Cursor c, boolean callerIsSyncAdapter)1604         public void update(SQLiteDatabase db, ContentValues values, Cursor c,
1605                 boolean callerIsSyncAdapter) {
1606             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
1607             super.update(db, values, c, callerIsSyncAdapter);
1608             mContactAggregator.updatePhotoId(db, rawContactId);
1609         }
1610 
1611         @Override
delete(SQLiteDatabase db, Cursor c)1612         public int delete(SQLiteDatabase db, Cursor c) {
1613             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
1614             int count = super.delete(db, c);
1615             mContactAggregator.updatePhotoId(db, rawContactId);
1616             return count;
1617         }
1618 
1619         @Override
isAggregationRequired()1620         public boolean isAggregationRequired() {
1621             return false;
1622         }
1623     }
1624 
1625 
1626     private HashMap<String, DataRowHandler> mDataRowHandlers;
1627     private final ContactAggregationScheduler mAggregationScheduler;
1628     private ContactsDatabaseHelper mDbHelper;
1629 
1630     private NameSplitter mNameSplitter;
1631     private NameLookupBuilder mNameLookupBuilder;
1632     private HashMap<String, SoftReference<String[]>> mNicknameClusterCache =
1633             new HashMap<String, SoftReference<String[]>>();
1634     private PostalSplitter mPostalSplitter;
1635 
1636     private ContactAggregator mContactAggregator;
1637     private LegacyApiSupport mLegacyApiSupport;
1638     private GlobalSearchSupport mGlobalSearchSupport;
1639 
1640     private ContentValues mValues = new ContentValues();
1641 
1642     private volatile CountDownLatch mAccessLatch;
1643     private boolean mImportMode;
1644 
1645     private HashSet<Long> mInsertedRawContacts = Sets.newHashSet();
1646     private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
1647     private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();
1648 
1649     private boolean mVisibleTouched = false;
1650 
1651     private boolean mSyncToNetwork;
1652 
ContactsProvider2()1653     public ContactsProvider2() {
1654         this(new ContactAggregationScheduler());
1655     }
1656 
1657     /**
1658      * Constructor for testing.
1659      */
ContactsProvider2(ContactAggregationScheduler scheduler)1660     /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
1661         mAggregationScheduler = scheduler;
1662     }
1663 
1664     @Override
onCreate()1665     public boolean onCreate() {
1666         super.onCreate();
1667         try {
1668             return initialize();
1669         } catch (RuntimeException e) {
1670             Log.e(TAG, "Cannot start provider", e);
1671             return false;
1672         }
1673     }
1674 
initialize()1675     private boolean initialize() {
1676         final Context context = getContext();
1677         mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
1678         mGlobalSearchSupport = new GlobalSearchSupport(this);
1679         mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
1680         mContactAggregator = new ContactAggregator(this, mDbHelper, mAggregationScheduler);
1681         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
1682 
1683         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
1684 
1685         mSetPrimaryStatement = db.compileStatement(
1686                 "UPDATE " + Tables.DATA +
1687                 " SET " + Data.IS_PRIMARY + "=(_id=?)" +
1688                 " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
1689                 "   AND " + Data.RAW_CONTACT_ID + "=?");
1690 
1691         mSetSuperPrimaryStatement = db.compileStatement(
1692                 "UPDATE " + Tables.DATA +
1693                 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
1694                 " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
1695                 "   AND " + Data.RAW_CONTACT_ID + " IN (" +
1696                         "SELECT " + RawContacts._ID +
1697                         " FROM " + Tables.RAW_CONTACTS +
1698                         " WHERE " + RawContacts.CONTACT_ID + " =(" +
1699                                 "SELECT " + RawContacts.CONTACT_ID +
1700                                 " FROM " + Tables.RAW_CONTACTS +
1701                                 " WHERE " + RawContacts._ID + "=?))");
1702 
1703         mContactsLastTimeContactedUpdate = db.compileStatement(
1704                 "UPDATE " + Tables.CONTACTS +
1705                 " SET " + Contacts.LAST_TIME_CONTACTED + "=? " +
1706                 "WHERE " + Contacts._ID + "=?");
1707 
1708         mRawContactDisplayNameUpdate = db.compileStatement(
1709                 "UPDATE " + Tables.RAW_CONTACTS +
1710                 " SET " + RawContactsColumns.DISPLAY_NAME + "=?,"
1711                         + RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" +
1712                 " WHERE " + RawContacts._ID + "=?");
1713 
1714         mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
1715                 + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?");
1716 
1717         mLastStatusUpdate = db.compileStatement(
1718                 "UPDATE " + Tables.CONTACTS
1719                 + " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
1720                         "(SELECT " + DataColumns.CONCRETE_ID +
1721                         " FROM " + Tables.STATUS_UPDATES +
1722                         " JOIN " + Tables.DATA +
1723                         "   ON (" + StatusUpdatesColumns.DATA_ID + "="
1724                                 + DataColumns.CONCRETE_ID + ")" +
1725                         " JOIN " + Tables.RAW_CONTACTS +
1726                         "   ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
1727                                 + RawContactsColumns.CONCRETE_ID + ")" +
1728                         " WHERE " + RawContacts.CONTACT_ID + "=?" +
1729                         " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
1730                                 + StatusUpdates.STATUS +
1731                         " LIMIT 1)"
1732                 + " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
1733 
1734         final Locale locale = Locale.getDefault();
1735         mNameSplitter = new NameSplitter(
1736                 context.getString(com.android.internal.R.string.common_name_prefixes),
1737                 context.getString(com.android.internal.R.string.common_last_name_prefixes),
1738                 context.getString(com.android.internal.R.string.common_name_suffixes),
1739                 context.getString(com.android.internal.R.string.common_name_conjunctions),
1740                 locale);
1741         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
1742         mPostalSplitter = new PostalSplitter(locale);
1743 
1744         mNameLookupInsert = db.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
1745                 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
1746                 + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME
1747                 + ") VALUES (?,?,?,?)");
1748         mNameLookupDelete = db.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
1749                 + NameLookupColumns.DATA_ID + "=?");
1750 
1751         mStatusUpdateInsert = db.compileStatement(
1752                 "INSERT INTO " + Tables.STATUS_UPDATES + "("
1753                         + StatusUpdatesColumns.DATA_ID + ", "
1754                         + StatusUpdates.STATUS + ","
1755                         + StatusUpdates.STATUS_RES_PACKAGE + ","
1756                         + StatusUpdates.STATUS_ICON + ","
1757                         + StatusUpdates.STATUS_LABEL + ")" +
1758                 " VALUES (?,?,?,?,?)");
1759 
1760         mStatusUpdateReplace = db.compileStatement(
1761                 "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
1762                         + StatusUpdatesColumns.DATA_ID + ", "
1763                         + StatusUpdates.STATUS_TIMESTAMP + ","
1764                         + StatusUpdates.STATUS + ","
1765                         + StatusUpdates.STATUS_RES_PACKAGE + ","
1766                         + StatusUpdates.STATUS_ICON + ","
1767                         + StatusUpdates.STATUS_LABEL + ")" +
1768                 " VALUES (?,?,?,?,?,?)");
1769 
1770         mStatusUpdateAutoTimestamp = db.compileStatement(
1771                 "UPDATE " + Tables.STATUS_UPDATES +
1772                 " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
1773                         + StatusUpdates.STATUS + "=?" +
1774                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
1775                         + " AND " + StatusUpdates.STATUS + "!=?");
1776 
1777         mStatusAttributionUpdate = db.compileStatement(
1778                 "UPDATE " + Tables.STATUS_UPDATES +
1779                 " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
1780                         + StatusUpdates.STATUS_ICON + "=?,"
1781                         + StatusUpdates.STATUS_LABEL + "=?" +
1782                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
1783 
1784         mStatusUpdateDelete = db.compileStatement(
1785                 "DELETE FROM " + Tables.STATUS_UPDATES +
1786                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
1787 
1788         mDataRowHandlers = new HashMap<String, DataRowHandler>();
1789 
1790         mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
1791         mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
1792                 new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
1793         mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
1794                 StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
1795         mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
1796         mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
1797         mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler());
1798         mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
1799                 new StructuredNameRowHandler(mNameSplitter));
1800         mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE,
1801                 new StructuredPostalRowHandler(mPostalSplitter));
1802         mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler());
1803         mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler());
1804 
1805         if (isLegacyContactImportNeeded()) {
1806             importLegacyContactsAsync();
1807         }
1808 
1809         verifyAccounts();
1810 
1811         mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
1812         mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
1813         return (db != null);
1814     }
1815 
verifyAccounts()1816     protected void verifyAccounts() {
1817         AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
1818         onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
1819     }
1820 
1821     /* Visible for testing */
1822     @Override
getDatabaseHelper(final Context context)1823     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
1824         return ContactsDatabaseHelper.getInstance(context);
1825     }
1826 
getContactAggregationScheduler()1827     /* package */ ContactAggregationScheduler getContactAggregationScheduler() {
1828         return mAggregationScheduler;
1829     }
1830 
getNameSplitter()1831     /* package */ NameSplitter getNameSplitter() {
1832         return mNameSplitter;
1833     }
1834 
isLegacyContactImportNeeded()1835     protected boolean isLegacyContactImportNeeded() {
1836         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1837         return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION;
1838     }
1839 
getLegacyContactImporter()1840     protected LegacyContactImporter getLegacyContactImporter() {
1841         return new LegacyContactImporter(getContext(), this);
1842     }
1843 
1844     /**
1845      * Imports legacy contacts in a separate thread.  As long as the import process is running
1846      * all other access to the contacts is blocked.
1847      */
importLegacyContactsAsync()1848     private void importLegacyContactsAsync() {
1849         mAccessLatch = new CountDownLatch(1);
1850 
1851         Thread importThread = new Thread("LegacyContactImport") {
1852             @Override
1853             public void run() {
1854                 if (importLegacyContacts()) {
1855 
1856                     /*
1857                      * When the import process is done, we can unlock the provider and
1858                      * start aggregating the imported contacts asynchronously.
1859                      */
1860                     mAccessLatch.countDown();
1861                     mAccessLatch = null;
1862                     scheduleContactAggregation();
1863                 }
1864             }
1865         };
1866 
1867         importThread.start();
1868     }
1869 
importLegacyContacts()1870     private boolean importLegacyContacts() {
1871         LegacyContactImporter importer = getLegacyContactImporter();
1872         if (importLegacyContacts(importer)) {
1873             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1874             Editor editor = prefs.edit();
1875             editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION);
1876             editor.commit();
1877             return true;
1878         } else {
1879             return false;
1880         }
1881     }
1882 
1883     /* Visible for testing */
importLegacyContacts(LegacyContactImporter importer)1884     /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
1885         boolean aggregatorEnabled = mContactAggregator.isEnabled();
1886         mContactAggregator.setEnabled(false);
1887         mImportMode = true;
1888         try {
1889             importer.importContacts();
1890             mContactAggregator.setEnabled(aggregatorEnabled);
1891             return true;
1892         } catch (Throwable e) {
1893            Log.e(TAG, "Legacy contact import failed", e);
1894            return false;
1895         } finally {
1896             mImportMode = false;
1897         }
1898     }
1899 
1900     @Override
finalize()1901     protected void finalize() throws Throwable {
1902         if (mContactAggregator != null) {
1903             mContactAggregator.quit();
1904         }
1905 
1906         super.finalize();
1907     }
1908 
1909     /**
1910      * Wipes all data from the contacts database.
1911      */
wipeData()1912     /* package */ void wipeData() {
1913         mDbHelper.wipeData();
1914     }
1915 
1916     /**
1917      * While importing and aggregating contacts, this content provider will
1918      * block all attempts to change contacts data. In particular, it will hold
1919      * up all contact syncs. As soon as the import process is complete, all
1920      * processes waiting to write to the provider are unblocked and can proceed
1921      * to compete for the database transaction monitor.
1922      */
waitForAccess()1923     private void waitForAccess() {
1924         CountDownLatch latch = mAccessLatch;
1925         if (latch != null) {
1926             while (true) {
1927                 try {
1928                     latch.await();
1929                     mAccessLatch = null;
1930                     return;
1931                 } catch (InterruptedException e) {
1932                     Thread.currentThread().interrupt();
1933                 }
1934             }
1935         }
1936     }
1937 
1938     @Override
insert(Uri uri, ContentValues values)1939     public Uri insert(Uri uri, ContentValues values) {
1940         waitForAccess();
1941         return super.insert(uri, values);
1942     }
1943 
1944     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)1945     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
1946         waitForAccess();
1947         return super.update(uri, values, selection, selectionArgs);
1948     }
1949 
1950     @Override
delete(Uri uri, String selection, String[] selectionArgs)1951     public int delete(Uri uri, String selection, String[] selectionArgs) {
1952         waitForAccess();
1953         return super.delete(uri, selection, selectionArgs);
1954     }
1955 
1956     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)1957     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
1958             throws OperationApplicationException {
1959         waitForAccess();
1960         return super.applyBatch(operations);
1961     }
1962 
1963     @Override
onBeginTransaction()1964     protected void onBeginTransaction() {
1965         if (VERBOSE_LOGGING) {
1966             Log.v(TAG, "onBeginTransaction");
1967         }
1968         super.onBeginTransaction();
1969         mContactAggregator.clearPendingAggregations();
1970         clearTransactionalChanges();
1971     }
1972 
clearTransactionalChanges()1973     private void clearTransactionalChanges() {
1974         mInsertedRawContacts.clear();
1975         mUpdatedRawContacts.clear();
1976         mUpdatedSyncStates.clear();
1977     }
1978 
1979     @Override
beforeTransactionCommit()1980     protected void beforeTransactionCommit() {
1981         if (VERBOSE_LOGGING) {
1982             Log.v(TAG, "beforeTransactionCommit");
1983         }
1984         super.beforeTransactionCommit();
1985         flushTransactionalChanges();
1986         mContactAggregator.aggregateInTransaction(mDb);
1987         if (mVisibleTouched) {
1988             mVisibleTouched = false;
1989             mDbHelper.updateAllVisible();
1990         }
1991     }
1992 
flushTransactionalChanges()1993     private void flushTransactionalChanges() {
1994         if (VERBOSE_LOGGING) {
1995             Log.v(TAG, "flushTransactionChanges");
1996         }
1997         for (long rawContactId : mInsertedRawContacts) {
1998             mContactAggregator.insertContact(mDb, rawContactId);
1999         }
2000 
2001         String ids;
2002         if (!mUpdatedRawContacts.isEmpty()) {
2003             ids = buildIdsString(mUpdatedRawContacts);
2004             mDb.execSQL("UPDATE raw_contacts SET version = version + 1 WHERE _id in " + ids,
2005                     new Object[]{});
2006         }
2007 
2008         for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
2009             long id = entry.getKey();
2010             mDbHelper.getSyncState().update(mDb, id, entry.getValue());
2011         }
2012 
2013         clearTransactionalChanges();
2014     }
2015 
buildIdsString(HashSet<Long> ids)2016     private String buildIdsString(HashSet<Long> ids) {
2017         StringBuilder idsBuilder = null;
2018         for (long id : ids) {
2019             if (idsBuilder == null) {
2020                 idsBuilder = new StringBuilder();
2021                 idsBuilder.append("(");
2022             } else {
2023                 idsBuilder.append(",");
2024             }
2025             idsBuilder.append(id);
2026         }
2027         idsBuilder.append(")");
2028         return idsBuilder.toString();
2029     }
2030 
2031     @Override
notifyChange()2032     protected void notifyChange() {
2033         notifyChange(mSyncToNetwork);
2034         mSyncToNetwork = false;
2035     }
2036 
notifyChange(boolean syncToNetwork)2037     protected void notifyChange(boolean syncToNetwork) {
2038         getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
2039                 syncToNetwork);
2040     }
2041 
scheduleContactAggregation()2042     protected void scheduleContactAggregation() {
2043         mContactAggregator.schedule();
2044     }
2045 
isNewRawContact(long rawContactId)2046     private boolean isNewRawContact(long rawContactId) {
2047         return mInsertedRawContacts.contains(rawContactId);
2048     }
2049 
getDataRowHandler(final String mimeType)2050     private DataRowHandler getDataRowHandler(final String mimeType) {
2051         DataRowHandler handler = mDataRowHandlers.get(mimeType);
2052         if (handler == null) {
2053             handler = new CustomDataRowHandler(mimeType);
2054             mDataRowHandlers.put(mimeType, handler);
2055         }
2056         return handler;
2057     }
2058 
2059     @Override
insertInTransaction(Uri uri, ContentValues values)2060     protected Uri insertInTransaction(Uri uri, ContentValues values) {
2061         if (VERBOSE_LOGGING) {
2062             Log.v(TAG, "insertInTransaction: " + uri);
2063         }
2064 
2065         final boolean callerIsSyncAdapter =
2066                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2067 
2068         final int match = sUriMatcher.match(uri);
2069         long id = 0;
2070 
2071         switch (match) {
2072             case SYNCSTATE:
2073                 id = mDbHelper.getSyncState().insert(mDb, values);
2074                 break;
2075 
2076             case CONTACTS: {
2077                 insertContact(values);
2078                 break;
2079             }
2080 
2081             case RAW_CONTACTS: {
2082                 final Account account = readAccountFromQueryParams(uri);
2083                 id = insertRawContact(values, account);
2084                 mSyncToNetwork |= !callerIsSyncAdapter;
2085                 break;
2086             }
2087 
2088             case RAW_CONTACTS_DATA: {
2089                 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
2090                 id = insertData(values, callerIsSyncAdapter);
2091                 mSyncToNetwork |= !callerIsSyncAdapter;
2092                 break;
2093             }
2094 
2095             case DATA: {
2096                 id = insertData(values, callerIsSyncAdapter);
2097                 mSyncToNetwork |= !callerIsSyncAdapter;
2098                 break;
2099             }
2100 
2101             case GROUPS: {
2102                 final Account account = readAccountFromQueryParams(uri);
2103                 id = insertGroup(uri, values, account, callerIsSyncAdapter);
2104                 mSyncToNetwork |= !callerIsSyncAdapter;
2105                 break;
2106             }
2107 
2108             case SETTINGS: {
2109                 id = insertSettings(uri, values);
2110                 mSyncToNetwork |= !callerIsSyncAdapter;
2111                 break;
2112             }
2113 
2114             case STATUS_UPDATES: {
2115                 id = insertStatusUpdate(values);
2116                 break;
2117             }
2118 
2119             default:
2120                 mSyncToNetwork = true;
2121                 return mLegacyApiSupport.insert(uri, values);
2122         }
2123 
2124         if (id < 0) {
2125             return null;
2126         }
2127 
2128         return ContentUris.withAppendedId(uri, id);
2129     }
2130 
2131     /**
2132      * If account is non-null then store it in the values. If the account is already
2133      * specified in the values then it must be consistent with the account, if it is non-null.
2134      * @param values the ContentValues to read from and update
2135      * @param account the explicitly provided Account
2136      * @return false if the accounts are inconsistent
2137      */
resolveAccount(ContentValues values, Account account)2138     private boolean resolveAccount(ContentValues values, Account account) {
2139         // If either is specified then both must be specified.
2140         final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
2141         final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
2142         if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
2143             final Account valuesAccount = new Account(accountName, accountType);
2144             if (account != null && !valuesAccount.equals(account)) {
2145                 return false;
2146             }
2147             account = valuesAccount;
2148         }
2149         if (account != null) {
2150             values.put(RawContacts.ACCOUNT_NAME, account.name);
2151             values.put(RawContacts.ACCOUNT_TYPE, account.type);
2152         }
2153         return true;
2154     }
2155 
2156     /**
2157      * Inserts an item in the contacts table
2158      *
2159      * @param values the values for the new row
2160      * @return the row ID of the newly created row
2161      */
insertContact(ContentValues values)2162     private long insertContact(ContentValues values) {
2163         throw new UnsupportedOperationException("Aggregate contacts are created automatically");
2164     }
2165 
2166     /**
2167      * Inserts an item in the contacts table
2168      *
2169      * @param values the values for the new row
2170      * @param account the account this contact should be associated with. may be null.
2171      * @return the row ID of the newly created row
2172      */
insertRawContact(ContentValues values, Account account)2173     private long insertRawContact(ContentValues values, Account account) {
2174         ContentValues overriddenValues = new ContentValues(values);
2175         overriddenValues.putNull(RawContacts.CONTACT_ID);
2176         if (!resolveAccount(overriddenValues, account)) {
2177             return -1;
2178         }
2179 
2180         if (values.containsKey(RawContacts.DELETED)
2181                 && values.getAsInteger(RawContacts.DELETED) != 0) {
2182             overriddenValues.put(RawContacts.AGGREGATION_MODE,
2183                     RawContacts.AGGREGATION_MODE_DISABLED);
2184         }
2185 
2186         long rawContactId =
2187                 mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
2188         mContactAggregator.markNewForAggregation(rawContactId);
2189 
2190         // Trigger creation of a Contact based on this RawContact at the end of transaction
2191         mInsertedRawContacts.add(rawContactId);
2192         return rawContactId;
2193     }
2194 
2195     /**
2196      * Inserts an item in the data table
2197      *
2198      * @param values the values for the new row
2199      * @return the row ID of the newly created row
2200      */
insertData(ContentValues values, boolean callerIsSyncAdapter)2201     private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
2202         long id = 0;
2203         mValues.clear();
2204         mValues.putAll(values);
2205 
2206         long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
2207 
2208         // Replace package with internal mapping
2209         final String packageName = mValues.getAsString(Data.RES_PACKAGE);
2210         if (packageName != null) {
2211             mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
2212         }
2213         mValues.remove(Data.RES_PACKAGE);
2214 
2215         // Replace mimetype with internal mapping
2216         final String mimeType = mValues.getAsString(Data.MIMETYPE);
2217         if (TextUtils.isEmpty(mimeType)) {
2218             throw new IllegalArgumentException(Data.MIMETYPE + " is required");
2219         }
2220 
2221         mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
2222         mValues.remove(Data.MIMETYPE);
2223 
2224         DataRowHandler rowHandler = getDataRowHandler(mimeType);
2225         id = rowHandler.insert(mDb, rawContactId, mValues);
2226         if (!callerIsSyncAdapter) {
2227             setRawContactDirty(rawContactId);
2228         }
2229         mUpdatedRawContacts.add(rawContactId);
2230 
2231         if (rowHandler.isAggregationRequired()) {
2232             triggerAggregation(rawContactId);
2233         }
2234         return id;
2235     }
2236 
triggerAggregation(long rawContactId)2237     private void triggerAggregation(long rawContactId) {
2238         if (!mContactAggregator.isEnabled()) {
2239             return;
2240         }
2241 
2242         int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
2243         switch (aggregationMode) {
2244             case RawContacts.AGGREGATION_MODE_DISABLED:
2245                 break;
2246 
2247             case RawContacts.AGGREGATION_MODE_DEFAULT: {
2248                 mContactAggregator.markForAggregation(rawContactId);
2249                 break;
2250             }
2251 
2252             case RawContacts.AGGREGATION_MODE_SUSPENDED: {
2253                 long contactId = mDbHelper.getContactId(rawContactId);
2254 
2255                 if (contactId != 0) {
2256                     mContactAggregator.updateAggregateData(contactId);
2257                 }
2258                 break;
2259             }
2260 
2261             case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
2262                 long contactId = mDbHelper.getContactId(rawContactId);
2263                 mContactAggregator.aggregateContact(mDb, rawContactId, contactId);
2264                 break;
2265             }
2266         }
2267     }
2268 
2269     /**
2270      * Returns the group id of the group with sourceId and the same account as rawContactId.
2271      * If the group doesn't already exist then it is first created,
2272      * @param db SQLiteDatabase to use for this operation
2273      * @param rawContactId the contact this group is associated with
2274      * @param sourceId the sourceIf of the group to query or create
2275      * @return the group id of the existing or created group
2276      * @throws IllegalArgumentException if the contact is not associated with an account
2277      * @throws IllegalStateException if a group needs to be created but the creation failed
2278      */
getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId)2279     private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
2280         Account account = null;
2281         Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
2282                 + rawContactId, null, null, null, null);
2283         try {
2284             if (c.moveToNext()) {
2285                 final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
2286                 final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
2287                 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
2288                     account = new Account(accountName, accountType);
2289                 }
2290             }
2291         } finally {
2292             c.close();
2293         }
2294         if (account == null) {
2295             throw new IllegalArgumentException("if the groupmembership only "
2296                     + "has a sourceid the the contact must be associate with "
2297                     + "an account");
2298         }
2299 
2300         // look up the group that contains this sourceId and has the same account name and type
2301         // as the contact refered to by rawContactId
2302         c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
2303                 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
2304                 new String[]{sourceId, account.name, account.type}, null, null, null);
2305         try {
2306             if (c.moveToNext()) {
2307                 return c.getLong(0);
2308             } else {
2309                 ContentValues groupValues = new ContentValues();
2310                 groupValues.put(Groups.ACCOUNT_NAME, account.name);
2311                 groupValues.put(Groups.ACCOUNT_TYPE, account.type);
2312                 groupValues.put(Groups.SOURCE_ID, sourceId);
2313                 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
2314                 if (groupId < 0) {
2315                     throw new IllegalStateException("unable to create a new group with "
2316                             + "this sourceid: " + groupValues);
2317                 }
2318                 return groupId;
2319             }
2320         } finally {
2321             c.close();
2322         }
2323     }
2324 
2325     /**
2326      * Delete data row by row so that fixing of primaries etc work correctly.
2327      */
deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter)2328     private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
2329         int count = 0;
2330 
2331         // Note that the query will return data according to the access restrictions,
2332         // so we don't need to worry about deleting data we don't have permission to read.
2333         Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null);
2334         try {
2335             while(c.moveToNext()) {
2336                 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
2337                 String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
2338                 DataRowHandler rowHandler = getDataRowHandler(mimeType);
2339                 count += rowHandler.delete(mDb, c);
2340                 if (!callerIsSyncAdapter) {
2341                     setRawContactDirty(rawContactId);
2342                     if (rowHandler.isAggregationRequired()) {
2343                         triggerAggregation(rawContactId);
2344                     }
2345                 }
2346             }
2347         } finally {
2348             c.close();
2349         }
2350 
2351         return count;
2352     }
2353 
2354     /**
2355      * Delete a data row provided that it is one of the allowed mime types.
2356      */
deleteData(long dataId, String[] allowedMimeTypes)2357     public int deleteData(long dataId, String[] allowedMimeTypes) {
2358 
2359         // Note that the query will return data according to the access restrictions,
2360         // so we don't need to worry about deleting data we don't have permission to read.
2361         Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=" + dataId, null,
2362                 null);
2363 
2364         try {
2365             if (!c.moveToFirst()) {
2366                 return 0;
2367             }
2368 
2369             String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
2370             boolean valid = false;
2371             for (int i = 0; i < allowedMimeTypes.length; i++) {
2372                 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
2373                     valid = true;
2374                     break;
2375                 }
2376             }
2377 
2378             if (!valid) {
2379                 throw new IllegalArgumentException("Data type mismatch: expected "
2380                         + Lists.newArrayList(allowedMimeTypes));
2381             }
2382 
2383             DataRowHandler rowHandler = getDataRowHandler(mimeType);
2384             int count = rowHandler.delete(mDb, c);
2385             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
2386             if (rowHandler.isAggregationRequired()) {
2387                 triggerAggregation(rawContactId);
2388             }
2389             return count;
2390         } finally {
2391             c.close();
2392         }
2393     }
2394 
2395     /**
2396      * Inserts an item in the groups table
2397      */
insertGroup(Uri uri, ContentValues values, Account account, boolean callerIsSyncAdapter)2398     private long insertGroup(Uri uri, ContentValues values, Account account,
2399             boolean callerIsSyncAdapter) {
2400         ContentValues overriddenValues = new ContentValues(values);
2401         if (!resolveAccount(overriddenValues, account)) {
2402             return -1;
2403         }
2404 
2405         // Replace package with internal mapping
2406         final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
2407         if (packageName != null) {
2408             overriddenValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
2409         }
2410         overriddenValues.remove(Groups.RES_PACKAGE);
2411 
2412         if (!callerIsSyncAdapter) {
2413             overriddenValues.put(Groups.DIRTY, 1);
2414         }
2415 
2416         long result = mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);
2417 
2418         if (overriddenValues.containsKey(Groups.GROUP_VISIBLE)) {
2419             mVisibleTouched = true;
2420         }
2421 
2422         return result;
2423     }
2424 
insertSettings(Uri uri, ContentValues values)2425     private long insertSettings(Uri uri, ContentValues values) {
2426         final long id = mDb.insert(Tables.SETTINGS, null, values);
2427 
2428         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
2429             mVisibleTouched = true;
2430         }
2431 
2432         return id;
2433     }
2434 
2435     /**
2436      * Inserts a status update.
2437      */
insertStatusUpdate(ContentValues values)2438     public long insertStatusUpdate(ContentValues values) {
2439         final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
2440         final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
2441         String customProtocol = null;
2442 
2443         if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
2444             customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
2445             if (TextUtils.isEmpty(customProtocol)) {
2446                 throw new IllegalArgumentException(
2447                         "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
2448             }
2449         }
2450 
2451         long rawContactId = -1;
2452         long contactId = -1;
2453         Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
2454         mSb.setLength(0);
2455         if (dataId != null) {
2456             // Lookup the contact info for the given data row.
2457 
2458             mSb.append(Tables.DATA + "." + Data._ID + "=");
2459             mSb.append(dataId);
2460         } else {
2461             // Lookup the data row to attach this presence update to
2462 
2463             if (TextUtils.isEmpty(handle) || protocol == null) {
2464                 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
2465             }
2466 
2467             // TODO: generalize to allow other providers to match against email
2468             boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
2469 
2470             if (matchEmail) {
2471 
2472                 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
2473                 // the "OR" conjunction confuses it and it switches to a full scan of
2474                 // the raw_contacts table.
2475 
2476                 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
2477                 // column - Data.DATA1
2478                 mSb.append(DataColumns.MIMETYPE_ID + " IN (")
2479                         .append(mMimeTypeIdEmail)
2480                         .append(",")
2481                         .append(mMimeTypeIdIm)
2482                         .append(")" + " AND " + Data.DATA1 + "=");
2483                 DatabaseUtils.appendEscapedSQLString(mSb, handle);
2484                 mSb.append(" AND ((" + DataColumns.MIMETYPE_ID + "=")
2485                         .append(mMimeTypeIdIm)
2486                         .append(" AND " + Im.PROTOCOL + "=")
2487                         .append(protocol);
2488                 if (customProtocol != null) {
2489                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=");
2490                     DatabaseUtils.appendEscapedSQLString(mSb, customProtocol);
2491                 }
2492                 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=")
2493                         .append(mMimeTypeIdEmail)
2494                         .append("))");
2495             } else {
2496                 mSb.append(DataColumns.MIMETYPE_ID + "=")
2497                         .append(mMimeTypeIdIm)
2498                         .append(" AND " + Im.PROTOCOL + "=")
2499                         .append(protocol)
2500                         .append(" AND " + Im.DATA + "=");
2501                 DatabaseUtils.appendEscapedSQLString(mSb, handle);
2502                 if (customProtocol != null) {
2503                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=");
2504                     DatabaseUtils.appendEscapedSQLString(mSb, customProtocol);
2505                 }
2506             }
2507 
2508             if (values.containsKey(StatusUpdates.DATA_ID)) {
2509                 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=")
2510                         .append(values.getAsLong(StatusUpdates.DATA_ID));
2511             }
2512         }
2513         mSb.append(" AND ").append(getContactsRestrictions());
2514 
2515         Cursor cursor = null;
2516         try {
2517             cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
2518                     mSb.toString(), null, null, null,
2519                     Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID);
2520             if (cursor.moveToFirst()) {
2521                 dataId = cursor.getLong(DataContactsQuery.DATA_ID);
2522                 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
2523                 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
2524             } else {
2525                 // No contact found, return a null URI
2526                 return -1;
2527             }
2528         } finally {
2529             if (cursor != null) {
2530                 cursor.close();
2531             }
2532         }
2533 
2534         if (values.containsKey(StatusUpdates.PRESENCE)) {
2535             if (customProtocol == null) {
2536                 // We cannot allow a null in the custom protocol field, because SQLite3 does not
2537                 // properly enforce uniqueness of null values
2538                 customProtocol = "";
2539             }
2540 
2541             mValues.clear();
2542             mValues.put(StatusUpdates.DATA_ID, dataId);
2543             mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
2544             mValues.put(PresenceColumns.CONTACT_ID, contactId);
2545             mValues.put(StatusUpdates.PROTOCOL, protocol);
2546             mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
2547             mValues.put(StatusUpdates.IM_HANDLE, handle);
2548             if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
2549                 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
2550             }
2551             mValues.put(StatusUpdates.PRESENCE,
2552                     values.getAsString(StatusUpdates.PRESENCE));
2553 
2554             // Insert the presence update
2555             mDb.replace(Tables.PRESENCE, null, mValues);
2556         }
2557 
2558 
2559         if (values.containsKey(StatusUpdates.STATUS)) {
2560             String status = values.getAsString(StatusUpdates.STATUS);
2561             String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
2562             Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL);
2563 
2564             if (TextUtils.isEmpty(resPackage)
2565                     && (labelResource == null || labelResource == 0)
2566                     && protocol != null) {
2567                 labelResource = Im.getProtocolLabelResource(protocol);
2568             }
2569 
2570             Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON);
2571             // TODO compute the default icon based on the protocol
2572 
2573             if (TextUtils.isEmpty(status)) {
2574                 mStatusUpdateDelete.bindLong(1, dataId);
2575                 mStatusUpdateDelete.execute();
2576             } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) {
2577                 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
2578                 mStatusUpdateReplace.bindLong(1, dataId);
2579                 mStatusUpdateReplace.bindLong(2, timestamp);
2580                 DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 3, status);
2581                 DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 4, resPackage);
2582                 DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 5, iconResource);
2583                 DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 6, labelResource);
2584                 mStatusUpdateReplace.execute();
2585             } else {
2586 
2587                 try {
2588                     mStatusUpdateInsert.bindLong(1, dataId);
2589                     DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 2, status);
2590                     DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 3, resPackage);
2591                     DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 4, iconResource);
2592                     DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 5, labelResource);
2593                     mStatusUpdateInsert.executeInsert();
2594                 } catch (SQLiteConstraintException e) {
2595                     // The row already exists - update it
2596                     long timestamp = System.currentTimeMillis();
2597                     mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
2598                     DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 2, status);
2599                     mStatusUpdateAutoTimestamp.bindLong(3, dataId);
2600                     DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 4, status);
2601                     mStatusUpdateAutoTimestamp.execute();
2602 
2603                     DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 1, resPackage);
2604                     DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 2, iconResource);
2605                     DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 3, labelResource);
2606                     mStatusAttributionUpdate.bindLong(4, dataId);
2607                     mStatusAttributionUpdate.execute();
2608                 }
2609             }
2610         }
2611 
2612         if (contactId != -1) {
2613             mLastStatusUpdate.bindLong(1, contactId);
2614             mLastStatusUpdate.bindLong(2, contactId);
2615             mLastStatusUpdate.execute();
2616         }
2617 
2618         return dataId;
2619     }
2620 
2621     @Override
deleteInTransaction(Uri uri, String selection, String[] selectionArgs)2622     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
2623         if (VERBOSE_LOGGING) {
2624             Log.v(TAG, "deleteInTransaction: " + uri);
2625         }
2626         flushTransactionalChanges();
2627         final boolean callerIsSyncAdapter =
2628                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2629         final int match = sUriMatcher.match(uri);
2630         switch (match) {
2631             case SYNCSTATE:
2632                 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
2633 
2634             case SYNCSTATE_ID:
2635                 String selectionWithId =
2636                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
2637                         + (selection == null ? "" : " AND (" + selection + ")");
2638                 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);
2639 
2640             case CONTACTS: {
2641                 // TODO
2642                 return 0;
2643             }
2644 
2645             case CONTACTS_ID: {
2646                 long contactId = ContentUris.parseId(uri);
2647                 return deleteContact(contactId);
2648             }
2649 
2650             case CONTACTS_LOOKUP:
2651             case CONTACTS_LOOKUP_ID: {
2652                 final List<String> pathSegments = uri.getPathSegments();
2653                 final int segmentCount = pathSegments.size();
2654                 if (segmentCount < 3) {
2655                     throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
2656                 }
2657                 final String lookupKey = pathSegments.get(2);
2658                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
2659                 return deleteContact(contactId);
2660             }
2661 
2662             case RAW_CONTACTS: {
2663                 int numDeletes = 0;
2664                 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
2665                         appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
2666                 try {
2667                     while (c.moveToNext()) {
2668                         final long rawContactId = c.getLong(0);
2669                         numDeletes += deleteRawContact(rawContactId, callerIsSyncAdapter);
2670                     }
2671                 } finally {
2672                     c.close();
2673                 }
2674                 return numDeletes;
2675             }
2676 
2677             case RAW_CONTACTS_ID: {
2678                 final long rawContactId = ContentUris.parseId(uri);
2679                 return deleteRawContact(rawContactId, callerIsSyncAdapter);
2680             }
2681 
2682             case DATA: {
2683                 mSyncToNetwork |= !callerIsSyncAdapter;
2684                 return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
2685                         callerIsSyncAdapter);
2686             }
2687 
2688             case DATA_ID:
2689             case PHONES_ID:
2690             case EMAILS_ID:
2691             case POSTALS_ID: {
2692                 long dataId = ContentUris.parseId(uri);
2693                 mSyncToNetwork |= !callerIsSyncAdapter;
2694                 return deleteData(Data._ID + "=" + dataId, null, callerIsSyncAdapter);
2695             }
2696 
2697             case GROUPS_ID: {
2698                 mSyncToNetwork |= !callerIsSyncAdapter;
2699                 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
2700             }
2701 
2702             case GROUPS: {
2703                 int numDeletes = 0;
2704                 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
2705                         appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
2706                 try {
2707                     while (c.moveToNext()) {
2708                         numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
2709                     }
2710                 } finally {
2711                     c.close();
2712                 }
2713                 if (numDeletes > 0) {
2714                     mSyncToNetwork |= !callerIsSyncAdapter;
2715                 }
2716                 return numDeletes;
2717             }
2718 
2719             case SETTINGS: {
2720                 mSyncToNetwork |= !callerIsSyncAdapter;
2721                 return deleteSettings(uri, selection, selectionArgs);
2722             }
2723 
2724             case STATUS_UPDATES: {
2725                 return deleteStatusUpdates(selection, selectionArgs);
2726             }
2727 
2728             default: {
2729                 mSyncToNetwork = true;
2730                 return mLegacyApiSupport.delete(uri, selection, selectionArgs);
2731             }
2732         }
2733     }
2734 
readBooleanQueryParameter(Uri uri, String name, boolean defaultValue)2735     private static boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) {
2736         final String flag = uri.getQueryParameter(name);
2737         return flag == null
2738                 ? defaultValue
2739                 : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
2740     }
2741 
deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter)2742     public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
2743         final long groupMembershipMimetypeId = mDbHelper
2744                 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
2745         mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
2746                 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
2747                 + groupId, null);
2748 
2749         try {
2750             if (callerIsSyncAdapter) {
2751                 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
2752             } else {
2753                 mValues.clear();
2754                 mValues.put(Groups.DELETED, 1);
2755                 mValues.put(Groups.DIRTY, 1);
2756                 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
2757             }
2758         } finally {
2759             mVisibleTouched = true;
2760         }
2761     }
2762 
deleteSettings(Uri uri, String selection, String[] selectionArgs)2763     private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
2764         final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
2765         mVisibleTouched = true;
2766         return count;
2767     }
2768 
deleteContact(long contactId)2769     private int deleteContact(long contactId) {
2770         Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
2771                 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
2772         try {
2773             while (c.moveToNext()) {
2774                 long rawContactId = c.getLong(0);
2775                 markRawContactAsDeleted(rawContactId);
2776             }
2777         } finally {
2778             c.close();
2779         }
2780 
2781         return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
2782     }
2783 
deleteRawContact(long rawContactId, boolean callerIsSyncAdapter)2784     public int deleteRawContact(long rawContactId, boolean callerIsSyncAdapter) {
2785         if (callerIsSyncAdapter) {
2786             mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
2787             return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
2788         } else {
2789             mDbHelper.removeContactIfSingleton(rawContactId);
2790             return markRawContactAsDeleted(rawContactId);
2791         }
2792     }
2793 
deleteStatusUpdates(String selection, String[] selectionArgs)2794     private int deleteStatusUpdates(String selection, String[] selectionArgs) {
2795         // TODO delete from both tables: presence and status_updates
2796         return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
2797     }
2798 
markRawContactAsDeleted(long rawContactId)2799     private int markRawContactAsDeleted(long rawContactId) {
2800         mSyncToNetwork = true;
2801 
2802         mValues.clear();
2803         mValues.put(RawContacts.DELETED, 1);
2804         mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
2805         mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
2806         mValues.putNull(RawContacts.CONTACT_ID);
2807         mValues.put(RawContacts.DIRTY, 1);
2808         return updateRawContact(rawContactId, mValues);
2809     }
2810 
2811     @Override
updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs)2812     protected int updateInTransaction(Uri uri, ContentValues values, String selection,
2813             String[] selectionArgs) {
2814         if (VERBOSE_LOGGING) {
2815             Log.v(TAG, "updateInTransaction: " + uri);
2816         }
2817 
2818         int count = 0;
2819 
2820         final int match = sUriMatcher.match(uri);
2821         if (match == SYNCSTATE_ID && selection == null) {
2822             long rowId = ContentUris.parseId(uri);
2823             Object data = values.get(ContactsContract.SyncStateColumns.DATA);
2824             mUpdatedSyncStates.put(rowId, data);
2825             return 1;
2826         }
2827         flushTransactionalChanges();
2828         final boolean callerIsSyncAdapter =
2829                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
2830         switch(match) {
2831             case SYNCSTATE:
2832                 return mDbHelper.getSyncState().update(mDb, values,
2833                         appendAccountToSelection(uri, selection), selectionArgs);
2834 
2835             case SYNCSTATE_ID: {
2836                 selection = appendAccountToSelection(uri, selection);
2837                 String selectionWithId =
2838                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
2839                         + (selection == null ? "" : " AND (" + selection + ")");
2840                 return mDbHelper.getSyncState().update(mDb, values,
2841                         selectionWithId, selectionArgs);
2842             }
2843 
2844             case CONTACTS: {
2845                 count = updateContactOptions(values, selection, selectionArgs);
2846                 break;
2847             }
2848 
2849             case CONTACTS_ID: {
2850                 count = updateContactOptions(ContentUris.parseId(uri), values);
2851                 break;
2852             }
2853 
2854             case CONTACTS_LOOKUP:
2855             case CONTACTS_LOOKUP_ID: {
2856                 final List<String> pathSegments = uri.getPathSegments();
2857                 final int segmentCount = pathSegments.size();
2858                 if (segmentCount < 3) {
2859                     throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
2860                 }
2861                 final String lookupKey = pathSegments.get(2);
2862                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
2863                 count = updateContactOptions(contactId, values);
2864                 break;
2865             }
2866 
2867             case RAW_CONTACTS_DATA: {
2868                 final String rawContactId = uri.getPathSegments().get(1);
2869                 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
2870                     + (selection == null ? "" : " AND " + selection);
2871 
2872                 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
2873 
2874                 break;
2875             }
2876 
2877             case DATA: {
2878                 count = updateData(uri, values, appendAccountToSelection(uri, selection),
2879                         selectionArgs, callerIsSyncAdapter);
2880                 if (count > 0) {
2881                     mSyncToNetwork |= !callerIsSyncAdapter;
2882                 }
2883                 break;
2884             }
2885 
2886             case DATA_ID:
2887             case PHONES_ID:
2888             case EMAILS_ID:
2889             case POSTALS_ID: {
2890                 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
2891                 if (count > 0) {
2892                     mSyncToNetwork |= !callerIsSyncAdapter;
2893                 }
2894                 break;
2895             }
2896 
2897             case RAW_CONTACTS: {
2898                 selection = appendAccountToSelection(uri, selection);
2899                 count = updateRawContacts(values, selection, selectionArgs);
2900                 break;
2901             }
2902 
2903             case RAW_CONTACTS_ID: {
2904                 long rawContactId = ContentUris.parseId(uri);
2905                 if (selection != null) {
2906                     count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId
2907                                     + " AND(" + selection + ")", selectionArgs);
2908                 } else {
2909                     count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId, null);
2910                 }
2911                 break;
2912             }
2913 
2914             case GROUPS: {
2915                 count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
2916                         selectionArgs, callerIsSyncAdapter);
2917                 if (count > 0) {
2918                     mSyncToNetwork |= !callerIsSyncAdapter;
2919                 }
2920                 break;
2921             }
2922 
2923             case GROUPS_ID: {
2924                 long groupId = ContentUris.parseId(uri);
2925                 String selectionWithId = (Groups._ID + "=" + groupId + " ")
2926                         + (selection == null ? "" : " AND " + selection);
2927                 count = updateGroups(uri, values, selectionWithId, selectionArgs,
2928                         callerIsSyncAdapter);
2929                 if (count > 0) {
2930                     mSyncToNetwork |= !callerIsSyncAdapter;
2931                 }
2932                 break;
2933             }
2934 
2935             case AGGREGATION_EXCEPTIONS: {
2936                 count = updateAggregationException(mDb, values);
2937                 break;
2938             }
2939 
2940             case SETTINGS: {
2941                 count = updateSettings(uri, values, selection, selectionArgs);
2942                 mSyncToNetwork |= !callerIsSyncAdapter;
2943                 break;
2944             }
2945 
2946             default: {
2947                 mSyncToNetwork = true;
2948                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
2949             }
2950         }
2951 
2952         return count;
2953     }
2954 
updateGroups(Uri uri, ContentValues values, String selectionWithId, String[] selectionArgs, boolean callerIsSyncAdapter)2955     private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
2956             String[] selectionArgs, boolean callerIsSyncAdapter) {
2957 
2958         ContentValues updatedValues;
2959         if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
2960             updatedValues = mValues;
2961             updatedValues.clear();
2962             updatedValues.putAll(values);
2963             updatedValues.put(Groups.DIRTY, 1);
2964         } else {
2965             updatedValues = values;
2966         }
2967 
2968         int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
2969         if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
2970             mVisibleTouched = true;
2971         }
2972         if (updatedValues.containsKey(Groups.SHOULD_SYNC)
2973                 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
2974             final long groupId = ContentUris.parseId(uri);
2975             Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
2976                     Groups.ACCOUNT_TYPE}, Groups._ID + "=" + groupId, null, null,
2977                     null, null);
2978             String accountName;
2979             String accountType;
2980             try {
2981                 while (c.moveToNext()) {
2982                     accountName = c.getString(0);
2983                     accountType = c.getString(1);
2984                     if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
2985                         Account account = new Account(accountName, accountType);
2986                     ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
2987                                 new Bundle());
2988                         break;
2989                     }
2990                 }
2991             } finally {
2992                 c.close();
2993             }
2994         }
2995         return count;
2996     }
2997 
updateSettings(Uri uri, ContentValues values, String selection, String[] selectionArgs)2998     private int updateSettings(Uri uri, ContentValues values, String selection,
2999             String[] selectionArgs) {
3000         final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
3001         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
3002             mVisibleTouched = true;
3003         }
3004         return count;
3005     }
3006 
updateRawContacts(ContentValues values, String selection, String[] selectionArgs)3007     private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) {
3008         if (values.containsKey(RawContacts.CONTACT_ID)) {
3009             throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
3010                     "in content values. Contact IDs are assigned automatically");
3011         }
3012 
3013         int count = 0;
3014         Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
3015                 new String[] { RawContacts._ID }, selection,
3016                 selectionArgs, null, null, null);
3017         try {
3018             while (cursor.moveToNext()) {
3019                 long rawContactId = cursor.getLong(0);
3020                 updateRawContact(rawContactId, values);
3021                 count++;
3022             }
3023         } finally {
3024             cursor.close();
3025         }
3026 
3027         return count;
3028     }
3029 
updateRawContact(long rawContactId, ContentValues values)3030     private int updateRawContact(long rawContactId, ContentValues values) {
3031         final String selection = RawContacts._ID + " = " + rawContactId;
3032         final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
3033                 && values.getAsInteger(RawContacts.DELETED) == 0);
3034         int previousDeleted = 0;
3035         if (requestUndoDelete) {
3036             Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
3037                     null, null, null, null);
3038             try {
3039                 if (cursor.moveToFirst()) {
3040                     previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
3041                 }
3042             } finally {
3043                 cursor.close();
3044             }
3045             values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
3046                     ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
3047         }
3048         int count = mDb.update(Tables.RAW_CONTACTS, values, selection, null);
3049         if (count != 0) {
3050             if (values.containsKey(RawContacts.STARRED)) {
3051                 mContactAggregator.updateStarred(rawContactId);
3052             }
3053             if (values.containsKey(RawContacts.SOURCE_ID)) {
3054                 mContactAggregator.updateLookupKey(mDb, rawContactId);
3055             }
3056             if (requestUndoDelete && previousDeleted == 1) {
3057                 // undo delete, needs aggregation again.
3058                 mInsertedRawContacts.add(rawContactId);
3059             }
3060         }
3061         return count;
3062     }
3063 
updateData(Uri uri, ContentValues values, String selection, String[] selectionArgs, boolean callerIsSyncAdapter)3064     private int updateData(Uri uri, ContentValues values, String selection,
3065             String[] selectionArgs, boolean callerIsSyncAdapter) {
3066         mValues.clear();
3067         mValues.putAll(values);
3068         mValues.remove(Data._ID);
3069         mValues.remove(Data.RAW_CONTACT_ID);
3070         mValues.remove(Data.MIMETYPE);
3071 
3072         String packageName = values.getAsString(Data.RES_PACKAGE);
3073         if (packageName != null) {
3074             mValues.remove(Data.RES_PACKAGE);
3075             mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
3076         }
3077 
3078         boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
3079         boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
3080 
3081         // Remove primary or super primary values being set to 0. This is disallowed by the
3082         // content provider.
3083         if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
3084             containsIsSuperPrimary = false;
3085             mValues.remove(Data.IS_SUPER_PRIMARY);
3086         }
3087         if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
3088             containsIsPrimary = false;
3089             mValues.remove(Data.IS_PRIMARY);
3090         }
3091 
3092         int count = 0;
3093 
3094         // Note that the query will return data according to the access restrictions,
3095         // so we don't need to worry about updating data we don't have permission to read.
3096         Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null);
3097         try {
3098             while(c.moveToNext()) {
3099                 count += updateData(mValues, c, callerIsSyncAdapter);
3100             }
3101         } finally {
3102             c.close();
3103         }
3104 
3105         return count;
3106     }
3107 
updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter)3108     private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
3109         if (values.size() == 0) {
3110             return 0;
3111         }
3112 
3113         final String mimeType = c.getString(DataUpdateQuery.MIMETYPE);
3114         DataRowHandler rowHandler = getDataRowHandler(mimeType);
3115         rowHandler.update(mDb, values, c, callerIsSyncAdapter);
3116         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
3117         if (rowHandler.isAggregationRequired()) {
3118             triggerAggregation(rawContactId);
3119         }
3120 
3121         return 1;
3122     }
3123 
updateContactOptions(ContentValues values, String selection, String[] selectionArgs)3124     private int updateContactOptions(ContentValues values, String selection,
3125             String[] selectionArgs) {
3126         int count = 0;
3127         Cursor cursor = mDb.query(mDbHelper.getContactView(),
3128                 new String[] { Contacts._ID }, selection,
3129                 selectionArgs, null, null, null);
3130         try {
3131             while (cursor.moveToNext()) {
3132                 long contactId = cursor.getLong(0);
3133                 updateContactOptions(contactId, values);
3134                 count++;
3135             }
3136         } finally {
3137             cursor.close();
3138         }
3139 
3140         return count;
3141     }
3142 
updateContactOptions(long contactId, ContentValues values)3143     private int updateContactOptions(long contactId, ContentValues values) {
3144 
3145         mValues.clear();
3146         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
3147                 values, Contacts.CUSTOM_RINGTONE);
3148         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
3149                 values, Contacts.SEND_TO_VOICEMAIL);
3150         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
3151                 values, Contacts.LAST_TIME_CONTACTED);
3152         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
3153                 values, Contacts.TIMES_CONTACTED);
3154         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
3155                 values, Contacts.STARRED);
3156 
3157         // Nothing to update - just return
3158         if (mValues.size() == 0) {
3159             return 0;
3160         }
3161 
3162         if (mValues.containsKey(RawContacts.STARRED)) {
3163             // Mark dirty when changing starred to trigger sync
3164             mValues.put(RawContacts.DIRTY, 1);
3165         }
3166 
3167         mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=" + contactId, null);
3168 
3169         // Copy changeable values to prevent automatically managed fields from
3170         // being explicitly updated by clients.
3171         mValues.clear();
3172         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
3173                 values, Contacts.CUSTOM_RINGTONE);
3174         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
3175                 values, Contacts.SEND_TO_VOICEMAIL);
3176         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
3177                 values, Contacts.LAST_TIME_CONTACTED);
3178         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
3179                 values, Contacts.TIMES_CONTACTED);
3180         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
3181                 values, Contacts.STARRED);
3182 
3183         return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=" + contactId, null);
3184     }
3185 
updateContactLastContactedTime(long contactId, long lastTimeContacted)3186     public void updateContactLastContactedTime(long contactId, long lastTimeContacted) {
3187         mContactsLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
3188         mContactsLastTimeContactedUpdate.bindLong(2, contactId);
3189         mContactsLastTimeContactedUpdate.execute();
3190     }
3191 
updateAggregationException(SQLiteDatabase db, ContentValues values)3192     private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
3193         int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
3194         long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
3195         long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
3196 
3197         long rawContactId1, rawContactId2;
3198         if (rcId1 < rcId2) {
3199             rawContactId1 = rcId1;
3200             rawContactId2 = rcId2;
3201         } else {
3202             rawContactId2 = rcId1;
3203             rawContactId1 = rcId2;
3204         }
3205 
3206         if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
3207             db.delete(Tables.AGGREGATION_EXCEPTIONS,
3208                     AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND "
3209                     + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null);
3210         } else {
3211             ContentValues exceptionValues = new ContentValues(3);
3212             exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
3213             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
3214             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
3215             db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
3216                     exceptionValues);
3217         }
3218 
3219         mContactAggregator.markForAggregation(rawContactId1);
3220         mContactAggregator.markForAggregation(rawContactId2);
3221 
3222         long contactId1 = mDbHelper.getContactId(rawContactId1);
3223         mContactAggregator.aggregateContact(db, rawContactId1, contactId1);
3224 
3225         long contactId2 = mDbHelper.getContactId(rawContactId2);
3226         mContactAggregator.aggregateContact(db, rawContactId2, contactId2);
3227 
3228         // The return value is fake - we just confirm that we made a change, not count actual
3229         // rows changed.
3230         return 1;
3231     }
3232 
onAccountsUpdated(Account[] accounts)3233     public void onAccountsUpdated(Account[] accounts) {
3234         mDb = mDbHelper.getWritableDatabase();
3235         if (mDb == null) return;
3236 
3237         HashSet<Account> existingAccounts = new HashSet<Account>();
3238         boolean hasUnassignedContacts[] = new boolean[]{false};
3239         mDb.beginTransaction();
3240         try {
3241             findValidAccounts(existingAccounts, hasUnassignedContacts,
3242                     Tables.RAW_CONTACTS, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE);
3243             findValidAccounts(existingAccounts, hasUnassignedContacts,
3244                     Tables.GROUPS, Groups.ACCOUNT_NAME, Groups.ACCOUNT_TYPE);
3245             findValidAccounts(existingAccounts, hasUnassignedContacts,
3246                     Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE);
3247 
3248             // Remove all valid accounts from the existing account set. What is left
3249             // in the existingAccounts set will be extra accounts whose data must be deleted.
3250             HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts);
3251             for (Account account : accounts) {
3252                 accountsToDelete.remove(account);
3253             }
3254 
3255             for (Account account : accountsToDelete) {
3256                 Log.d(TAG, "removing data for removed account " + account);
3257                 String[] params = new String[] {account.name, account.type};
3258                 mDb.execSQL(
3259                         "DELETE FROM " + Tables.GROUPS +
3260                         " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
3261                                 " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
3262                 mDb.execSQL(
3263                         "DELETE FROM " + Tables.PRESENCE +
3264                         " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
3265                                 "SELECT " + RawContacts._ID +
3266                                 " FROM " + Tables.RAW_CONTACTS +
3267                                 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
3268                                 " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
3269                 mDb.execSQL(
3270                         "DELETE FROM " + Tables.RAW_CONTACTS +
3271                         " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
3272                         " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
3273                 mDb.execSQL(
3274                         "DELETE FROM " + Tables.SETTINGS +
3275                         " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
3276                         " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
3277             }
3278 
3279             if (hasUnassignedContacts[0]) {
3280 
3281                 Account primaryAccount = null;
3282                 for (Account account : accounts) {
3283                     if (isWritableAccount(account)) {
3284                         primaryAccount = account;
3285                         break;
3286                     }
3287                 }
3288 
3289                 if (primaryAccount != null) {
3290                     String[] params = new String[] {primaryAccount.name, primaryAccount.type};
3291 
3292                     mDb.execSQL(
3293                             "UPDATE " + Tables.RAW_CONTACTS +
3294                             " SET " + RawContacts.ACCOUNT_NAME + "=?,"
3295                                     + RawContacts.ACCOUNT_TYPE + "=?" +
3296                             " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
3297                             " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params);
3298 
3299                     // We don't currently support groups for unsynced accounts, so this is for
3300                     // the future
3301                     mDb.execSQL(
3302                             "UPDATE " + Tables.GROUPS +
3303                             " SET " + Groups.ACCOUNT_NAME + "=?,"
3304                                     + Groups.ACCOUNT_TYPE + "=?" +
3305                             " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" +
3306                             " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params);
3307                 }
3308             }
3309 
3310             mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
3311             mDb.setTransactionSuccessful();
3312         } finally {
3313             mDb.endTransaction();
3314         }
3315     }
3316 
3317     /**
3318      * Finds all distinct accounts present in the specified table.
3319      */
findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts, String table, String accountNameColumn, String accountTypeColumn)3320     private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts,
3321             String table, String accountNameColumn, String accountTypeColumn) {
3322         Cursor c = mDb.rawQuery("SELECT DISTINCT " + accountNameColumn + "," + accountTypeColumn
3323                 + " FROM " + table, null);
3324         try {
3325             while (c.moveToNext()) {
3326                 if (c.isNull(0) && c.isNull(1)) {
3327                     hasUnassignedContacts[0] = true;
3328                 } else {
3329                     validAccounts.add(new Account(c.getString(0), c.getString(1)));
3330                 }
3331             }
3332         } finally {
3333             c.close();
3334         }
3335     }
3336 
3337     /**
3338      * Test all against {@link TextUtils#isEmpty(CharSequence)}.
3339      */
areAllEmpty(ContentValues values, String[] keys)3340     private static boolean areAllEmpty(ContentValues values, String[] keys) {
3341         for (String key : keys) {
3342             if (!TextUtils.isEmpty(values.getAsString(key))) {
3343                 return false;
3344             }
3345         }
3346         return true;
3347     }
3348 
3349     /**
3350      * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
3351      */
areAnySpecified(ContentValues values, String[] keys)3352     private static boolean areAnySpecified(ContentValues values, String[] keys) {
3353         for (String key : keys) {
3354             if (values.containsKey(key)) {
3355                 return true;
3356             }
3357         }
3358         return false;
3359     }
3360 
3361     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)3362     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
3363             String sortOrder) {
3364         if (VERBOSE_LOGGING) {
3365             Log.v(TAG, "query: " + uri);
3366         }
3367 
3368         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
3369 
3370         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3371         String groupBy = null;
3372         String limit = getLimit(uri);
3373 
3374         // TODO: Consider writing a test case for RestrictionExceptions when you
3375         // write a new query() block to make sure it protects restricted data.
3376         final int match = sUriMatcher.match(uri);
3377         switch (match) {
3378             case SYNCSTATE:
3379                 return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
3380                         sortOrder);
3381 
3382             case CONTACTS: {
3383                 setTablesAndProjectionMapForContacts(qb, uri, projection);
3384                 break;
3385             }
3386 
3387             case CONTACTS_ID: {
3388                 long contactId = ContentUris.parseId(uri);
3389                 setTablesAndProjectionMapForContacts(qb, uri, projection);
3390                 qb.appendWhere(Contacts._ID + "=" + contactId);
3391                 break;
3392             }
3393 
3394             case CONTACTS_LOOKUP:
3395             case CONTACTS_LOOKUP_ID: {
3396                 List<String> pathSegments = uri.getPathSegments();
3397                 int segmentCount = pathSegments.size();
3398                 if (segmentCount < 3) {
3399                     throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
3400                 }
3401                 String lookupKey = pathSegments.get(2);
3402                 if (segmentCount == 4) {
3403                     long contactId = Long.parseLong(pathSegments.get(3));
3404                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
3405                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
3406                     lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " +
3407                             Contacts.LOOKUP_KEY + "=");
3408                     lookupQb.appendWhereEscapeString(lookupKey);
3409                     Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder,
3410                             groupBy, limit);
3411                     if (c.getCount() != 0) {
3412                         return c;
3413                     }
3414 
3415                     c.close();
3416                 }
3417 
3418                 setTablesAndProjectionMapForContacts(qb, uri, projection);
3419                 qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
3420                 break;
3421             }
3422 
3423             case CONTACTS_AS_VCARD: {
3424                 // When reading as vCard always use restricted view
3425                 final String lookupKey = uri.getPathSegments().get(2);
3426                 qb.setTables(mDbHelper.getContactView(true /* require restricted */));
3427                 qb.setProjectionMap(sContactsVCardProjectionMap);
3428                 qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
3429                 break;
3430             }
3431 
3432             case CONTACTS_FILTER: {
3433                 setTablesAndProjectionMapForContacts(qb, uri, projection);
3434                 if (uri.getPathSegments().size() > 2) {
3435                     String filterParam = uri.getLastPathSegment();
3436                     StringBuilder sb = new StringBuilder();
3437                     sb.append(Contacts._ID + " IN ");
3438                     appendContactFilterAsNestedQuery(sb, filterParam);
3439                     qb.appendWhere(sb.toString());
3440                 }
3441                 break;
3442             }
3443 
3444             case CONTACTS_STREQUENT_FILTER:
3445             case CONTACTS_STREQUENT: {
3446                 String filterSql = null;
3447                 if (match == CONTACTS_STREQUENT_FILTER
3448                         && uri.getPathSegments().size() > 3) {
3449                     String filterParam = uri.getLastPathSegment();
3450                     StringBuilder sb = new StringBuilder();
3451                     sb.append(Contacts._ID + " IN ");
3452                     appendContactFilterAsNestedQuery(sb, filterParam);
3453                     filterSql = sb.toString();
3454                 }
3455 
3456                 setTablesAndProjectionMapForContacts(qb, uri, projection);
3457 
3458                 String[] starredProjection = null;
3459                 String[] frequentProjection = null;
3460                 if (projection != null) {
3461                     starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
3462                     frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
3463                 }
3464 
3465                 // Build the first query for starred
3466                 if (filterSql != null) {
3467                     qb.appendWhere(filterSql);
3468                 }
3469                 qb.setProjectionMap(sStrequentStarredProjectionMap);
3470                 final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1",
3471                         null, Contacts._ID, null, null, null);
3472 
3473                 // Build the second query for frequent
3474                 qb = new SQLiteQueryBuilder();
3475                 setTablesAndProjectionMapForContacts(qb, uri, projection);
3476                 if (filterSql != null) {
3477                     qb.appendWhere(filterSql);
3478                 }
3479                 qb.setProjectionMap(sStrequentFrequentProjectionMap);
3480                 final String frequentQuery = qb.buildQuery(frequentProjection,
3481                         Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
3482                         + " = 0 OR " + Contacts.STARRED + " IS NULL)",
3483                         null, Contacts._ID, null, null, null);
3484 
3485                 // Put them together
3486                 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
3487                         STREQUENT_ORDER_BY, STREQUENT_LIMIT);
3488                 Cursor c = db.rawQuery(query, null);
3489                 if (c != null) {
3490                     c.setNotificationUri(getContext().getContentResolver(),
3491                             ContactsContract.AUTHORITY_URI);
3492                 }
3493                 return c;
3494             }
3495 
3496             case CONTACTS_GROUP: {
3497                 setTablesAndProjectionMapForContacts(qb, uri, projection);
3498                 if (uri.getPathSegments().size() > 2) {
3499                     qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
3500                     selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3501                 }
3502                 break;
3503             }
3504 
3505             case CONTACTS_DATA: {
3506                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
3507                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3508                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
3509                 break;
3510             }
3511 
3512             case CONTACTS_PHOTO: {
3513                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
3514                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3515                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
3516                 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
3517                 break;
3518             }
3519 
3520             case PHONES: {
3521                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3522                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
3523                 break;
3524             }
3525 
3526             case PHONES_ID: {
3527                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3528                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
3529                 qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
3530                 break;
3531             }
3532 
3533             case PHONES_FILTER: {
3534                 setTablesAndProjectionMapForData(qb, uri, projection, true);
3535                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
3536                 if (uri.getPathSegments().size() > 2) {
3537                     String filterParam = uri.getLastPathSegment();
3538                     StringBuilder sb = new StringBuilder();
3539                     sb.append("(");
3540 
3541                     boolean orNeeded = false;
3542                     String normalizedName = NameNormalizer.normalize(filterParam);
3543                     if (normalizedName.length() > 0) {
3544                         sb.append(Data.RAW_CONTACT_ID + " IN ");
3545                         appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false);
3546                         orNeeded = true;
3547                     }
3548 
3549                     if (isPhoneNumber(filterParam)) {
3550                         if (orNeeded) {
3551                             sb.append(" OR ");
3552                         }
3553                         String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam);
3554                         String reversed = PhoneNumberUtils.getStrippedReversed(number);
3555                         sb.append(Data._ID +
3556                                 " IN (SELECT " + PhoneLookupColumns.DATA_ID
3557                                   + " FROM " + Tables.PHONE_LOOKUP
3558                                   + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%");
3559                         sb.append(reversed);
3560                         sb.append("')");
3561                     }
3562                     sb.append(")");
3563                     qb.appendWhere(" AND " + sb);
3564                 }
3565                 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
3566                 if (sortOrder == null) {
3567                     sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
3568                 }
3569                 break;
3570             }
3571 
3572             case EMAILS: {
3573                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3574                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3575                 break;
3576             }
3577 
3578             case EMAILS_ID: {
3579                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3580                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3581                 qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
3582                 break;
3583             }
3584 
3585             case EMAILS_LOOKUP: {
3586                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3587                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3588                 if (uri.getPathSegments().size() > 2) {
3589                     qb.appendWhere(" AND " + Email.DATA + "=");
3590                     qb.appendWhereEscapeString(uri.getLastPathSegment());
3591                 }
3592                 break;
3593             }
3594 
3595             case EMAILS_FILTER: {
3596                 setTablesAndProjectionMapForData(qb, uri, projection, true);
3597                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
3598                 if (uri.getPathSegments().size() > 2) {
3599                     String filterParam = uri.getLastPathSegment();
3600                     StringBuilder sb = new StringBuilder();
3601                     sb.append("(");
3602 
3603                     if (!filterParam.contains("@")) {
3604                         String normalizedName = NameNormalizer.normalize(filterParam);
3605                         if (normalizedName.length() > 0) {
3606                             sb.append(Data.RAW_CONTACT_ID + " IN ");
3607                             appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false);
3608                             sb.append(" OR ");
3609                         }
3610                     }
3611 
3612                     sb.append(Email.DATA + " LIKE ");
3613                     sb.append(DatabaseUtils.sqlEscapeString(filterParam + '%'));
3614                     sb.append(")");
3615                     qb.appendWhere(" AND " + sb);
3616                 }
3617                 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
3618                 if (sortOrder == null) {
3619                     sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
3620                 }
3621                 break;
3622             }
3623 
3624             case POSTALS: {
3625                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3626                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
3627                         + StructuredPostal.CONTENT_ITEM_TYPE + "'");
3628                 break;
3629             }
3630 
3631             case POSTALS_ID: {
3632                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3633                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
3634                         + StructuredPostal.CONTENT_ITEM_TYPE + "'");
3635                 qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
3636                 break;
3637             }
3638 
3639             case RAW_CONTACTS: {
3640                 setTablesAndProjectionMapForRawContacts(qb, uri);
3641                 break;
3642             }
3643 
3644             case RAW_CONTACTS_ID: {
3645                 long rawContactId = ContentUris.parseId(uri);
3646                 setTablesAndProjectionMapForRawContacts(qb, uri);
3647                 qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
3648                 break;
3649             }
3650 
3651             case RAW_CONTACTS_DATA: {
3652                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
3653                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3654                 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=" + rawContactId);
3655                 break;
3656             }
3657 
3658             case DATA: {
3659                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3660                 break;
3661             }
3662 
3663             case DATA_ID: {
3664                 setTablesAndProjectionMapForData(qb, uri, projection, false);
3665                 qb.appendWhere(" AND " + Data._ID + "=" + ContentUris.parseId(uri));
3666                 break;
3667             }
3668 
3669             case PHONE_LOOKUP: {
3670 
3671                 if (TextUtils.isEmpty(sortOrder)) {
3672                     // Default the sort order to something reasonable so we get consistent
3673                     // results when callers don't request an ordering
3674                     sortOrder = RawContactsColumns.CONCRETE_ID;
3675                 }
3676 
3677                 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
3678                 mDbHelper.buildPhoneLookupAndContactQuery(qb, number);
3679                 qb.setProjectionMap(sPhoneLookupProjectionMap);
3680 
3681                 // Phone lookup cannot be combined with a selection
3682                 selection = null;
3683                 selectionArgs = null;
3684                 break;
3685             }
3686 
3687             case GROUPS: {
3688                 qb.setTables(mDbHelper.getGroupView());
3689                 qb.setProjectionMap(sGroupsProjectionMap);
3690                 appendAccountFromParameter(qb, uri);
3691                 break;
3692             }
3693 
3694             case GROUPS_ID: {
3695                 long groupId = ContentUris.parseId(uri);
3696                 qb.setTables(mDbHelper.getGroupView());
3697                 qb.setProjectionMap(sGroupsProjectionMap);
3698                 qb.appendWhere(Groups._ID + "=" + groupId);
3699                 break;
3700             }
3701 
3702             case GROUPS_SUMMARY: {
3703                 qb.setTables(mDbHelper.getGroupView() + " AS groups");
3704                 qb.setProjectionMap(sGroupsSummaryProjectionMap);
3705                 appendAccountFromParameter(qb, uri);
3706                 groupBy = Groups._ID;
3707                 break;
3708             }
3709 
3710             case AGGREGATION_EXCEPTIONS: {
3711                 qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
3712                 qb.setProjectionMap(sAggregationExceptionsProjectionMap);
3713                 break;
3714             }
3715 
3716             case AGGREGATION_SUGGESTIONS: {
3717                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
3718                 String filter = null;
3719                 if (uri.getPathSegments().size() > 3) {
3720                     filter = uri.getPathSegments().get(3);
3721                 }
3722                 final int maxSuggestions;
3723                 if (limit != null) {
3724                     maxSuggestions = Integer.parseInt(limit);
3725                 } else {
3726                     maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
3727                 }
3728 
3729                 setTablesAndProjectionMapForContacts(qb, uri, projection);
3730 
3731                 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
3732                         maxSuggestions, filter);
3733             }
3734 
3735             case SETTINGS: {
3736                 qb.setTables(Tables.SETTINGS);
3737                 qb.setProjectionMap(sSettingsProjectionMap);
3738                 appendAccountFromParameter(qb, uri);
3739 
3740                 // When requesting specific columns, this query requires
3741                 // late-binding of the GroupMembership MIME-type.
3742                 final String groupMembershipMimetypeId = Long.toString(mDbHelper
3743                         .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
3744                 if (projection != null && projection.length != 0 &&
3745                         mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
3746                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
3747                 }
3748                 if (projection != null && projection.length != 0 &&
3749                         mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
3750                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
3751                 }
3752 
3753                 break;
3754             }
3755 
3756             case STATUS_UPDATES: {
3757                 setTableAndProjectionMapForStatusUpdates(qb, projection);
3758                 break;
3759             }
3760 
3761             case STATUS_UPDATES_ID: {
3762                 setTableAndProjectionMapForStatusUpdates(qb, projection);
3763                 qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri));
3764                 break;
3765             }
3766 
3767             case SEARCH_SUGGESTIONS: {
3768                 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
3769             }
3770 
3771             case SEARCH_SHORTCUT: {
3772                 long contactId = ContentUris.parseId(uri);
3773                 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection);
3774             }
3775 
3776             case LIVE_FOLDERS_CONTACTS:
3777                 qb.setTables(mDbHelper.getContactView());
3778                 qb.setProjectionMap(sLiveFoldersProjectionMap);
3779                 break;
3780 
3781             case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
3782                 qb.setTables(mDbHelper.getContactView());
3783                 qb.setProjectionMap(sLiveFoldersProjectionMap);
3784                 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
3785                 break;
3786 
3787             case LIVE_FOLDERS_CONTACTS_FAVORITES:
3788                 qb.setTables(mDbHelper.getContactView());
3789                 qb.setProjectionMap(sLiveFoldersProjectionMap);
3790                 qb.appendWhere(Contacts.STARRED + "=1");
3791                 break;
3792 
3793             case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
3794                 qb.setTables(mDbHelper.getContactView());
3795                 qb.setProjectionMap(sLiveFoldersProjectionMap);
3796                 qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
3797                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
3798                 break;
3799 
3800             case RAW_CONTACT_ENTITIES: {
3801                 setTablesAndProjectionMapForRawContactsEntities(qb, uri);
3802                 break;
3803             }
3804 
3805             case RAW_CONTACT_ENTITY_ID: {
3806                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
3807                 setTablesAndProjectionMapForRawContactsEntities(qb, uri);
3808                 qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
3809                 break;
3810             }
3811 
3812             default:
3813                 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
3814                         sortOrder, limit);
3815         }
3816 
3817         return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
3818     }
3819 
query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, String selection, String[] selectionArgs, String sortOrder, String groupBy, String limit)3820     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
3821             String selection, String[] selectionArgs, String sortOrder, String groupBy,
3822             String limit) {
3823         if (projection != null && projection.length == 1
3824                 && BaseColumns._COUNT.equals(projection[0])) {
3825             qb.setProjectionMap(sCountProjectionMap);
3826         }
3827         final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
3828                 sortOrder, limit);
3829         if (c != null) {
3830             c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
3831         }
3832         return c;
3833     }
3834 
lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey)3835     private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
3836         ContactLookupKey key = new ContactLookupKey();
3837         ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
3838 
3839         long contactId = lookupContactIdBySourceIds(db, segments);
3840         if (contactId == -1) {
3841             contactId = lookupContactIdByDisplayNames(db, segments);
3842         }
3843 
3844         return contactId;
3845     }
3846 
3847     private interface LookupBySourceIdQuery {
3848         String TABLE = Tables.RAW_CONTACTS;
3849 
3850         String COLUMNS[] = {
3851                 RawContacts.CONTACT_ID,
3852                 RawContacts.ACCOUNT_TYPE,
3853                 RawContacts.ACCOUNT_NAME,
3854                 RawContacts.SOURCE_ID
3855         };
3856 
3857         int CONTACT_ID = 0;
3858         int ACCOUNT_TYPE = 1;
3859         int ACCOUNT_NAME = 2;
3860         int SOURCE_ID = 3;
3861     }
3862 
lookupContactIdBySourceIds(SQLiteDatabase db, ArrayList<LookupKeySegment> segments)3863     private long lookupContactIdBySourceIds(SQLiteDatabase db,
3864                 ArrayList<LookupKeySegment> segments) {
3865         int sourceIdCount = 0;
3866         for (int i = 0; i < segments.size(); i++) {
3867             LookupKeySegment segment = segments.get(i);
3868             if (segment.sourceIdLookup) {
3869                 sourceIdCount++;
3870             }
3871         }
3872 
3873         if (sourceIdCount == 0) {
3874             return -1;
3875         }
3876 
3877         // First try sync ids
3878         StringBuilder sb = new StringBuilder();
3879         sb.append(RawContacts.SOURCE_ID + " IN (");
3880         for (int i = 0; i < segments.size(); i++) {
3881             LookupKeySegment segment = segments.get(i);
3882             if (segment.sourceIdLookup) {
3883                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
3884                 sb.append(",");
3885             }
3886         }
3887         sb.setLength(sb.length() - 1);      // Last comma
3888         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
3889 
3890         Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
3891                  sb.toString(), null, null, null, null);
3892         try {
3893             while (c.moveToNext()) {
3894                 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
3895                 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
3896                 int accountHashCode =
3897                         ContactLookupKey.getAccountHashCode(accountType, accountName);
3898                 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
3899                 for (int i = 0; i < segments.size(); i++) {
3900                     LookupKeySegment segment = segments.get(i);
3901                     if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode
3902                             && segment.key.equals(sourceId)) {
3903                         segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
3904                         break;
3905                     }
3906                 }
3907             }
3908         } finally {
3909             c.close();
3910         }
3911 
3912         return getMostReferencedContactId(segments);
3913     }
3914 
3915     private interface LookupByDisplayNameQuery {
3916         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
3917 
3918         String COLUMNS[] = {
3919                 RawContacts.CONTACT_ID,
3920                 RawContacts.ACCOUNT_TYPE,
3921                 RawContacts.ACCOUNT_NAME,
3922                 NameLookupColumns.NORMALIZED_NAME
3923         };
3924 
3925         int CONTACT_ID = 0;
3926         int ACCOUNT_TYPE = 1;
3927         int ACCOUNT_NAME = 2;
3928         int NORMALIZED_NAME = 3;
3929     }
3930 
lookupContactIdByDisplayNames(SQLiteDatabase db, ArrayList<LookupKeySegment> segments)3931     private long lookupContactIdByDisplayNames(SQLiteDatabase db,
3932                 ArrayList<LookupKeySegment> segments) {
3933         int displayNameCount = 0;
3934         for (int i = 0; i < segments.size(); i++) {
3935             LookupKeySegment segment = segments.get(i);
3936             if (!segment.sourceIdLookup) {
3937                 displayNameCount++;
3938             }
3939         }
3940 
3941         if (displayNameCount == 0) {
3942             return -1;
3943         }
3944 
3945         // First try sync ids
3946         StringBuilder sb = new StringBuilder();
3947         sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
3948         for (int i = 0; i < segments.size(); i++) {
3949             LookupKeySegment segment = segments.get(i);
3950             if (!segment.sourceIdLookup) {
3951                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
3952                 sb.append(",");
3953             }
3954         }
3955         sb.setLength(sb.length() - 1);      // Last comma
3956         sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
3957                 + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
3958 
3959         Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
3960                  sb.toString(), null, null, null, null);
3961         try {
3962             while (c.moveToNext()) {
3963                 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
3964                 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
3965                 int accountHashCode =
3966                         ContactLookupKey.getAccountHashCode(accountType, accountName);
3967                 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
3968                 for (int i = 0; i < segments.size(); i++) {
3969                     LookupKeySegment segment = segments.get(i);
3970                     if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode
3971                             && segment.key.equals(name)) {
3972                         segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
3973                         break;
3974                     }
3975                 }
3976             }
3977         } finally {
3978             c.close();
3979         }
3980 
3981         return getMostReferencedContactId(segments);
3982     }
3983 
3984     /**
3985      * Returns the contact ID that is mentioned the highest number of times.
3986      */
getMostReferencedContactId(ArrayList<LookupKeySegment> segments)3987     private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
3988         Collections.sort(segments);
3989 
3990         long bestContactId = -1;
3991         int bestRefCount = 0;
3992 
3993         long contactId = -1;
3994         int count = 0;
3995 
3996         int segmentCount = segments.size();
3997         for (int i = 0; i < segmentCount; i++) {
3998             LookupKeySegment segment = segments.get(i);
3999             if (segment.contactId != -1) {
4000                 if (segment.contactId == contactId) {
4001                     count++;
4002                 } else {
4003                     if (count > bestRefCount) {
4004                         bestContactId = contactId;
4005                         bestRefCount = count;
4006                     }
4007                     contactId = segment.contactId;
4008                     count = 1;
4009                 }
4010             }
4011         }
4012         if (count > bestRefCount) {
4013             return contactId;
4014         } else {
4015             return bestContactId;
4016         }
4017     }
4018 
setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, String[] projection)4019     private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
4020             String[] projection) {
4021         StringBuilder sb = new StringBuilder();
4022         boolean excludeRestrictedData = false;
4023         String requestingPackage = uri.getQueryParameter(
4024                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
4025         if (requestingPackage != null) {
4026             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
4027         }
4028         sb.append(mDbHelper.getContactView(excludeRestrictedData));
4029         if (mDbHelper.isInProjection(projection,
4030                 Contacts.CONTACT_PRESENCE)) {
4031             sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
4032                     " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")");
4033         }
4034         if (mDbHelper.isInProjection(projection,
4035                 Contacts.CONTACT_STATUS,
4036                 Contacts.CONTACT_STATUS_RES_PACKAGE,
4037                 Contacts.CONTACT_STATUS_ICON,
4038                 Contacts.CONTACT_STATUS_LABEL,
4039                 Contacts.CONTACT_STATUS_TIMESTAMP)) {
4040             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
4041                     + ContactsStatusUpdatesColumns.ALIAS +
4042                     " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
4043                             + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
4044         }
4045         qb.setTables(sb.toString());
4046         qb.setProjectionMap(sContactsProjectionMap);
4047     }
4048 
setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri)4049     private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
4050         StringBuilder sb = new StringBuilder();
4051         boolean excludeRestrictedData = false;
4052         String requestingPackage = uri.getQueryParameter(
4053                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
4054         if (requestingPackage != null) {
4055             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
4056         }
4057         sb.append(mDbHelper.getRawContactView(excludeRestrictedData));
4058         qb.setTables(sb.toString());
4059         qb.setProjectionMap(sRawContactsProjectionMap);
4060         appendAccountFromParameter(qb, uri);
4061     }
4062 
setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri)4063     private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) {
4064         // Note: currently, "export only" equals to "restricted", but may not in the future.
4065         boolean excludeRestrictedData = readBooleanQueryParameter(uri,
4066                 Data.FOR_EXPORT_ONLY, false);
4067 
4068         String requestingPackage = uri.getQueryParameter(
4069                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
4070         if (requestingPackage != null) {
4071             excludeRestrictedData = excludeRestrictedData
4072                     || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
4073         }
4074         qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData));
4075         qb.setProjectionMap(sRawContactsEntityProjectionMap);
4076         appendAccountFromParameter(qb, uri);
4077     }
4078 
setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, String[] projection, boolean distinct)4079     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
4080             String[] projection, boolean distinct) {
4081         StringBuilder sb = new StringBuilder();
4082         // Note: currently, "export only" equals to "restricted", but may not in the future.
4083         boolean excludeRestrictedData = readBooleanQueryParameter(uri,
4084                 Data.FOR_EXPORT_ONLY, false);
4085 
4086         String requestingPackage = uri.getQueryParameter(
4087                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
4088         if (requestingPackage != null) {
4089             excludeRestrictedData = excludeRestrictedData
4090                     || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
4091         }
4092 
4093         sb.append(mDbHelper.getDataView(excludeRestrictedData));
4094         sb.append(" data");
4095 
4096         // Include aggregated presence when requested
4097         if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) {
4098             sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
4099                     " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "="
4100                     + RawContacts.CONTACT_ID + ")");
4101         }
4102 
4103         // Include aggregated status updates when requested
4104         if (mDbHelper.isInProjection(projection,
4105                 Data.CONTACT_STATUS,
4106                 Data.CONTACT_STATUS_RES_PACKAGE,
4107                 Data.CONTACT_STATUS_ICON,
4108                 Data.CONTACT_STATUS_LABEL,
4109                 Data.CONTACT_STATUS_TIMESTAMP)) {
4110             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
4111                     + ContactsStatusUpdatesColumns.ALIAS +
4112                     " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
4113                             + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
4114         }
4115 
4116         // Include individual presence when requested
4117         if (mDbHelper.isInProjection(projection, Data.PRESENCE)) {
4118             sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
4119                     " ON (" + StatusUpdates.DATA_ID + "="
4120                     + DataColumns.CONCRETE_ID + ")");
4121         }
4122 
4123         // Include individual status updates when requested
4124         if (mDbHelper.isInProjection(projection,
4125                 Data.STATUS,
4126                 Data.STATUS_RES_PACKAGE,
4127                 Data.STATUS_ICON,
4128                 Data.STATUS_LABEL,
4129                 Data.STATUS_TIMESTAMP)) {
4130             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
4131                     " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
4132                             + DataColumns.CONCRETE_ID + ")");
4133         }
4134 
4135         qb.setTables(sb.toString());
4136         qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap);
4137         appendAccountFromParameter(qb, uri);
4138     }
4139 
setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, String[] projection)4140     private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
4141             String[] projection) {
4142         StringBuilder sb = new StringBuilder();
4143         sb.append(mDbHelper.getDataView());
4144         sb.append(" data");
4145 
4146         if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) {
4147             sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
4148                     " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID
4149                     + "=" + DataColumns.CONCRETE_ID + ")");
4150         }
4151 
4152         if (mDbHelper.isInProjection(projection,
4153                 StatusUpdates.STATUS,
4154                 StatusUpdates.STATUS_RES_PACKAGE,
4155                 StatusUpdates.STATUS_ICON,
4156                 StatusUpdates.STATUS_LABEL,
4157                 StatusUpdates.STATUS_TIMESTAMP)) {
4158             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
4159                     " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID
4160                     + "=" + DataColumns.CONCRETE_ID + ")");
4161         }
4162         qb.setTables(sb.toString());
4163         qb.setProjectionMap(sStatusUpdatesProjectionMap);
4164     }
4165 
appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri)4166     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
4167         final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
4168         final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
4169         if (!TextUtils.isEmpty(accountName)) {
4170             qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
4171                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
4172                     + RawContacts.ACCOUNT_TYPE + "="
4173                     + DatabaseUtils.sqlEscapeString(accountType));
4174         } else {
4175             qb.appendWhere("1");
4176         }
4177     }
4178 
appendAccountToSelection(Uri uri, String selection)4179     private String appendAccountToSelection(Uri uri, String selection) {
4180         final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
4181         final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
4182         if (!TextUtils.isEmpty(accountName)) {
4183             StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
4184                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
4185                     + RawContacts.ACCOUNT_TYPE + "="
4186                     + DatabaseUtils.sqlEscapeString(accountType));
4187             if (!TextUtils.isEmpty(selection)) {
4188                 selectionSb.append(" AND (");
4189                 selectionSb.append(selection);
4190                 selectionSb.append(')');
4191             }
4192             return selectionSb.toString();
4193         } else {
4194             return selection;
4195         }
4196     }
4197 
4198     /**
4199      * Gets the value of the "limit" URI query parameter.
4200      *
4201      * @return A string containing a non-negative integer, or <code>null</code> if
4202      *         the parameter is not set, or is set to an invalid value.
4203      */
getLimit(Uri url)4204     private String getLimit(Uri url) {
4205         String limitParam = url.getQueryParameter("limit");
4206         if (limitParam == null) {
4207             return null;
4208         }
4209         // make sure that the limit is a non-negative integer
4210         try {
4211             int l = Integer.parseInt(limitParam);
4212             if (l < 0) {
4213                 Log.w(TAG, "Invalid limit parameter: " + limitParam);
4214                 return null;
4215             }
4216             return String.valueOf(l);
4217         } catch (NumberFormatException ex) {
4218             Log.w(TAG, "Invalid limit parameter: " + limitParam);
4219             return null;
4220         }
4221     }
4222 
4223     /**
4224      * Returns true if all the characters are meaningful as digits
4225      * in a phone number -- letters, digits, and a few punctuation marks.
4226      */
isPhoneNumber(CharSequence cons)4227     private boolean isPhoneNumber(CharSequence cons) {
4228         int len = cons.length();
4229 
4230         for (int i = 0; i < len; i++) {
4231             char c = cons.charAt(i);
4232 
4233             if ((c >= '0') && (c <= '9')) {
4234                 continue;
4235             }
4236             if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+')
4237                     || (c == '#') || (c == '*')) {
4238                 continue;
4239             }
4240             if ((c >= 'A') && (c <= 'Z')) {
4241                 continue;
4242             }
4243             if ((c >= 'a') && (c <= 'z')) {
4244                 continue;
4245             }
4246 
4247             return false;
4248         }
4249 
4250         return true;
4251     }
4252 
getContactsRestrictions()4253     String getContactsRestrictions() {
4254         if (mDbHelper.hasAccessToRestrictedData()) {
4255             return "1";
4256         } else {
4257             return RawContacts.IS_RESTRICTED + "=0";
4258         }
4259     }
4260 
getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn)4261     public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
4262         if (mDbHelper.hasAccessToRestrictedData()) {
4263             return "1";
4264         } else {
4265             return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
4266                     + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
4267         }
4268     }
4269 
4270     @Override
openAssetFile(Uri uri, String mode)4271     public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
4272         int match = sUriMatcher.match(uri);
4273         switch (match) {
4274             case CONTACTS_PHOTO: {
4275                 if (!"r".equals(mode)) {
4276                     throw new FileNotFoundException("Mode " + mode + " not supported.");
4277                 }
4278 
4279                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
4280 
4281                 String sql =
4282                         "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
4283                         " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID
4284                                 + " AND " + RawContacts.CONTACT_ID + "=" + contactId;
4285                 SQLiteDatabase db = mDbHelper.getReadableDatabase();
4286                 return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null);
4287             }
4288 
4289             case CONTACTS_AS_VCARD: {
4290                 final String lookupKey = uri.getPathSegments().get(2);
4291                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
4292                 final String selection = Contacts._ID + "=" + contactId;
4293 
4294                 // When opening a contact as file, we pass back contents as a
4295                 // vCard-encoded stream. We build into a local buffer first,
4296                 // then pipe into MemoryFile once the exact size is known.
4297                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
4298                 outputRawContactsAsVCard(localStream, selection, null);
4299                 return buildAssetFileDescriptor(localStream);
4300             }
4301 
4302             default:
4303                 throw new FileNotFoundException("No file at: " + uri);
4304         }
4305     }
4306 
4307     private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
4308     private static final String VCARD_TYPE_DEFAULT = "default";
4309 
4310     /**
4311      * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the
4312      * contents of the given {@link ByteArrayOutputStream}.
4313      */
buildAssetFileDescriptor(ByteArrayOutputStream stream)4314     private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
4315         AssetFileDescriptor fd = null;
4316         try {
4317             stream.flush();
4318 
4319             final byte[] byteData = stream.toByteArray();
4320             final int size = byteData.length;
4321 
4322             final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size);
4323             memoryFile.writeBytes(byteData, 0, 0, size);
4324             memoryFile.deactivate();
4325 
4326             fd = AssetFileDescriptor.fromMemoryFile(memoryFile);
4327         } catch (IOException e) {
4328             Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString());
4329         }
4330         return fd;
4331     }
4332 
4333     /**
4334      * Output {@link RawContacts} matching the requested selection in the vCard
4335      * format to the given {@link OutputStream}. This method returns silently if
4336      * any errors encountered.
4337      */
outputRawContactsAsVCard(OutputStream stream, String selection, String[] selectionArgs)4338     private void outputRawContactsAsVCard(OutputStream stream, String selection,
4339             String[] selectionArgs) {
4340         final Context context = this.getContext();
4341         final VCardComposer composer = new VCardComposer(context, VCARD_TYPE_DEFAULT, false);
4342         composer.addHandler(composer.new HandlerForOutputStream(stream));
4343 
4344         // No extra checks since composer always uses restricted views
4345         if (!composer.init(selection, selectionArgs))
4346             return;
4347 
4348         while (!composer.isAfterLast()) {
4349             if (!composer.createOneEntry()) {
4350                 Log.w(TAG, "Failed to output a contact.");
4351             }
4352         }
4353         composer.terminate();
4354     }
4355 
4356 
readAccountFromQueryParams(Uri uri)4357     private static Account readAccountFromQueryParams(Uri uri) {
4358         final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
4359         final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
4360         if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
4361             return null;
4362         }
4363         return new Account(name, type);
4364     }
4365 
4366 
4367     /**
4368      * An implementation of EntityIterator that joins the contacts and data tables
4369      * and consumes all the data rows for a contact in order to build the Entity for a contact.
4370      */
4371     private static class RawContactsEntityIterator implements EntityIterator {
4372         private final Cursor mEntityCursor;
4373         private volatile boolean mIsClosed;
4374 
4375         private static final String[] DATA_KEYS = new String[]{
4376                 Data.DATA1,
4377                 Data.DATA2,
4378                 Data.DATA3,
4379                 Data.DATA4,
4380                 Data.DATA5,
4381                 Data.DATA6,
4382                 Data.DATA7,
4383                 Data.DATA8,
4384                 Data.DATA9,
4385                 Data.DATA10,
4386                 Data.DATA11,
4387                 Data.DATA12,
4388                 Data.DATA13,
4389                 Data.DATA14,
4390                 Data.DATA15,
4391                 Data.SYNC1,
4392                 Data.SYNC2,
4393                 Data.SYNC3,
4394                 Data.SYNC4};
4395 
4396         public static final String[] PROJECTION = new String[]{
4397                 RawContacts.ACCOUNT_NAME,
4398                 RawContacts.ACCOUNT_TYPE,
4399                 RawContacts.SOURCE_ID,
4400                 RawContacts.VERSION,
4401                 RawContacts.DIRTY,
4402                 RawContacts.Entity.DATA_ID,
4403                 Data.RES_PACKAGE,
4404                 Data.MIMETYPE,
4405                 Data.DATA1,
4406                 Data.DATA2,
4407                 Data.DATA3,
4408                 Data.DATA4,
4409                 Data.DATA5,
4410                 Data.DATA6,
4411                 Data.DATA7,
4412                 Data.DATA8,
4413                 Data.DATA9,
4414                 Data.DATA10,
4415                 Data.DATA11,
4416                 Data.DATA12,
4417                 Data.DATA13,
4418                 Data.DATA14,
4419                 Data.DATA15,
4420                 Data.SYNC1,
4421                 Data.SYNC2,
4422                 Data.SYNC3,
4423                 Data.SYNC4,
4424                 RawContacts._ID,
4425                 Data.IS_PRIMARY,
4426                 Data.IS_SUPER_PRIMARY,
4427                 Data.DATA_VERSION,
4428                 GroupMembership.GROUP_SOURCE_ID,
4429                 RawContacts.SYNC1,
4430                 RawContacts.SYNC2,
4431                 RawContacts.SYNC3,
4432                 RawContacts.SYNC4,
4433                 RawContacts.DELETED,
4434                 RawContacts.CONTACT_ID,
4435                 RawContacts.STARRED,
4436                 RawContacts.IS_RESTRICTED};
4437 
4438         private static final int COLUMN_ACCOUNT_NAME = 0;
4439         private static final int COLUMN_ACCOUNT_TYPE = 1;
4440         private static final int COLUMN_SOURCE_ID = 2;
4441         private static final int COLUMN_VERSION = 3;
4442         private static final int COLUMN_DIRTY = 4;
4443         private static final int COLUMN_DATA_ID = 5;
4444         private static final int COLUMN_RES_PACKAGE = 6;
4445         private static final int COLUMN_MIMETYPE = 7;
4446         private static final int COLUMN_DATA1 = 8;
4447         private static final int COLUMN_RAW_CONTACT_ID = 27;
4448         private static final int COLUMN_IS_PRIMARY = 28;
4449         private static final int COLUMN_IS_SUPER_PRIMARY = 29;
4450         private static final int COLUMN_DATA_VERSION = 30;
4451         private static final int COLUMN_GROUP_SOURCE_ID = 31;
4452         private static final int COLUMN_SYNC1 = 32;
4453         private static final int COLUMN_SYNC2 = 33;
4454         private static final int COLUMN_SYNC3 = 34;
4455         private static final int COLUMN_SYNC4 = 35;
4456         private static final int COLUMN_DELETED = 36;
4457         private static final int COLUMN_CONTACT_ID = 37;
4458         private static final int COLUMN_STARRED = 38;
4459         private static final int COLUMN_IS_RESTRICTED = 39;
4460 
RawContactsEntityIterator(ContactsProvider2 provider, Uri entityUri, String contactsIdString, String selection, String[] selectionArgs, String sortOrder)4461         public RawContactsEntityIterator(ContactsProvider2 provider, Uri entityUri,
4462                 String contactsIdString,
4463                 String selection, String[] selectionArgs, String sortOrder) {
4464             mIsClosed = false;
4465             Uri uri;
4466             if (contactsIdString != null) {
4467                 uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, contactsIdString);
4468                 uri = Uri.withAppendedPath(uri, RawContacts.Entity.CONTENT_DIRECTORY);
4469             } else {
4470                 uri = ContactsContract.RawContactsEntity.CONTENT_URI;
4471             }
4472             final Uri.Builder builder = uri.buildUpon();
4473             String query = entityUri.getQuery();
4474             builder.encodedQuery(query);
4475             mEntityCursor = provider.query(builder.build(),
4476                     PROJECTION, selection, selectionArgs, sortOrder);
4477             mEntityCursor.moveToFirst();
4478         }
4479 
reset()4480         public void reset() throws RemoteException {
4481             if (mIsClosed) {
4482                 throw new IllegalStateException("calling reset() when the iterator is closed");
4483             }
4484             mEntityCursor.moveToFirst();
4485         }
4486 
close()4487         public void close() {
4488             if (mIsClosed) {
4489                 throw new IllegalStateException("closing when already closed");
4490             }
4491             mIsClosed = true;
4492             mEntityCursor.close();
4493         }
4494 
hasNext()4495         public boolean hasNext() throws RemoteException {
4496             if (mIsClosed) {
4497                 throw new IllegalStateException("calling hasNext() when the iterator is closed");
4498             }
4499 
4500             return !mEntityCursor.isAfterLast();
4501         }
4502 
next()4503         public Entity next() throws RemoteException {
4504             if (mIsClosed) {
4505                 throw new IllegalStateException("calling next() when the iterator is closed");
4506             }
4507             if (!hasNext()) {
4508                 throw new IllegalStateException("you may only call next() if hasNext() is true");
4509             }
4510 
4511             final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
4512 
4513             final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID);
4514 
4515             // we expect the cursor is already at the row we need to read from
4516             ContentValues contactValues = new ContentValues();
4517             contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
4518             contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
4519             contactValues.put(RawContacts._ID, rawContactId);
4520             contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
4521             contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
4522             contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
4523             contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1));
4524             contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2));
4525             contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3));
4526             contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4));
4527             contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED));
4528             contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID));
4529             contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED));
4530             contactValues.put(RawContacts.IS_RESTRICTED, c.getInt(COLUMN_IS_RESTRICTED));
4531             Entity contact = new Entity(contactValues);
4532 
4533             // read data rows until the contact id changes
4534             do {
4535                 if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) {
4536                     break;
4537                 }
4538 //                if (c.isNull(COLUMN_CONTACT_ID)) {
4539 //                    continue;
4540 //                }
4541                 // add the data to to the contact
4542                 ContentValues dataValues = new ContentValues();
4543                 dataValues.put(Data._ID, c.getLong(COLUMN_DATA_ID));
4544                 dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
4545                 dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
4546                 dataValues.put(Data.IS_PRIMARY, c.getLong(COLUMN_IS_PRIMARY));
4547                 dataValues.put(Data.IS_SUPER_PRIMARY, c.getLong(COLUMN_IS_SUPER_PRIMARY));
4548                 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
4549                 if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
4550                     dataValues.put(GroupMembership.GROUP_SOURCE_ID,
4551                             c.getString(COLUMN_GROUP_SOURCE_ID));
4552                 }
4553                 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
4554                 for (int i = 0; i < DATA_KEYS.length; i++) {
4555                     final int columnIndex = i + COLUMN_DATA1;
4556                     String key = DATA_KEYS[i];
4557                     if (c.isNull(columnIndex)) {
4558                         // don't put anything
4559                     } else if (c.isLong(columnIndex)) {
4560                         dataValues.put(key, c.getLong(columnIndex));
4561                     } else if (c.isFloat(columnIndex)) {
4562                         dataValues.put(key, c.getFloat(columnIndex));
4563                     } else if (c.isString(columnIndex)) {
4564                         dataValues.put(key, c.getString(columnIndex));
4565                     } else if (c.isBlob(columnIndex)) {
4566                         dataValues.put(key, c.getBlob(columnIndex));
4567                     }
4568                 }
4569                 contact.addSubValue(Data.CONTENT_URI, dataValues);
4570             } while (mEntityCursor.moveToNext());
4571 
4572             return contact;
4573         }
4574     }
4575 
4576     /**
4577      * An implementation of EntityIterator that joins the contacts and data tables
4578      * and consumes all the data rows for a contact in order to build the Entity for a contact.
4579      */
4580     private static class GroupsEntityIterator implements EntityIterator {
4581         private final Cursor mEntityCursor;
4582         private volatile boolean mIsClosed;
4583 
4584         private static final String[] PROJECTION = new String[]{
4585                 Groups._ID,
4586                 Groups.ACCOUNT_NAME,
4587                 Groups.ACCOUNT_TYPE,
4588                 Groups.SOURCE_ID,
4589                 Groups.DIRTY,
4590                 Groups.VERSION,
4591                 Groups.RES_PACKAGE,
4592                 Groups.TITLE,
4593                 Groups.TITLE_RES,
4594                 Groups.GROUP_VISIBLE,
4595                 Groups.SYNC1,
4596                 Groups.SYNC2,
4597                 Groups.SYNC3,
4598                 Groups.SYNC4,
4599                 Groups.SYSTEM_ID,
4600                 Groups.NOTES,
4601                 Groups.DELETED,
4602                 Groups.SHOULD_SYNC};
4603 
4604         private static final int COLUMN_ID = 0;
4605         private static final int COLUMN_ACCOUNT_NAME = 1;
4606         private static final int COLUMN_ACCOUNT_TYPE = 2;
4607         private static final int COLUMN_SOURCE_ID = 3;
4608         private static final int COLUMN_DIRTY = 4;
4609         private static final int COLUMN_VERSION = 5;
4610         private static final int COLUMN_RES_PACKAGE = 6;
4611         private static final int COLUMN_TITLE = 7;
4612         private static final int COLUMN_TITLE_RES = 8;
4613         private static final int COLUMN_GROUP_VISIBLE = 9;
4614         private static final int COLUMN_SYNC1 = 10;
4615         private static final int COLUMN_SYNC2 = 11;
4616         private static final int COLUMN_SYNC3 = 12;
4617         private static final int COLUMN_SYNC4 = 13;
4618         private static final int COLUMN_SYSTEM_ID = 14;
4619         private static final int COLUMN_NOTES = 15;
4620         private static final int COLUMN_DELETED = 16;
4621         private static final int COLUMN_SHOULD_SYNC = 17;
4622 
GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri, String selection, String[] selectionArgs, String sortOrder)4623         public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
4624                 String selection, String[] selectionArgs, String sortOrder) {
4625             mIsClosed = false;
4626 
4627             final String updatedSortOrder = (sortOrder == null)
4628                     ? Groups._ID
4629                     : (Groups._ID + "," + sortOrder);
4630 
4631             final SQLiteDatabase db = provider.mDbHelper.getReadableDatabase();
4632             final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
4633             qb.setTables(provider.mDbHelper.getGroupView());
4634             qb.setProjectionMap(sGroupsProjectionMap);
4635             if (groupIdString != null) {
4636                 qb.appendWhere(Groups._ID + "=" + groupIdString);
4637             }
4638             final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
4639             final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
4640             if (!TextUtils.isEmpty(accountName)) {
4641                 qb.appendWhere(Groups.ACCOUNT_NAME + "="
4642                         + DatabaseUtils.sqlEscapeString(accountName) + " AND "
4643                         + Groups.ACCOUNT_TYPE + "="
4644                         + DatabaseUtils.sqlEscapeString(accountType));
4645             }
4646             mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
4647                     null, null, updatedSortOrder);
4648             mEntityCursor.moveToFirst();
4649         }
4650 
close()4651         public void close() {
4652             if (mIsClosed) {
4653                 throw new IllegalStateException("closing when already closed");
4654             }
4655             mIsClosed = true;
4656             mEntityCursor.close();
4657         }
4658 
hasNext()4659         public boolean hasNext() throws RemoteException {
4660             if (mIsClosed) {
4661                 throw new IllegalStateException("calling hasNext() when the iterator is closed");
4662             }
4663 
4664             return !mEntityCursor.isAfterLast();
4665         }
4666 
reset()4667         public void reset() throws RemoteException {
4668             if (mIsClosed) {
4669                 throw new IllegalStateException("calling reset() when the iterator is closed");
4670             }
4671             mEntityCursor.moveToFirst();
4672         }
4673 
next()4674         public Entity next() throws RemoteException {
4675             if (mIsClosed) {
4676                 throw new IllegalStateException("calling next() when the iterator is closed");
4677             }
4678             if (!hasNext()) {
4679                 throw new IllegalStateException("you may only call next() if hasNext() is true");
4680             }
4681 
4682             final SQLiteCursor c = (SQLiteCursor) mEntityCursor;
4683 
4684             final long groupId = c.getLong(COLUMN_ID);
4685 
4686             // we expect the cursor is already at the row we need to read from
4687             ContentValues groupValues = new ContentValues();
4688             groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
4689             groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
4690             groupValues.put(Groups._ID, groupId);
4691             groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
4692             groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
4693             groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
4694             groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
4695             groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
4696             groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
4697             groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
4698             groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1));
4699             groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2));
4700             groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3));
4701             groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4));
4702             groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID));
4703             groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED));
4704             groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES));
4705             groupValues.put(Groups.SHOULD_SYNC, c.getString(COLUMN_SHOULD_SYNC));
4706             Entity group = new Entity(groupValues);
4707 
4708             mEntityCursor.moveToNext();
4709 
4710             return group;
4711         }
4712     }
4713 
4714     @Override
queryEntities(Uri uri, String selection, String[] selectionArgs, String sortOrder)4715     public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
4716             String sortOrder) {
4717         waitForAccess();
4718 
4719         final int match = sUriMatcher.match(uri);
4720         switch (match) {
4721             case RAW_CONTACTS:
4722             case RAW_CONTACTS_ID:
4723                 String contactsIdString = null;
4724                 if (match == RAW_CONTACTS_ID) {
4725                     contactsIdString = uri.getPathSegments().get(1);
4726                 }
4727 
4728                 return new RawContactsEntityIterator(this, uri, contactsIdString,
4729                         selection, selectionArgs, sortOrder);
4730             case GROUPS:
4731             case GROUPS_ID:
4732                 String idString = null;
4733                 if (match == GROUPS_ID) {
4734                     idString = uri.getPathSegments().get(1);
4735                 }
4736 
4737                 return new GroupsEntityIterator(this, idString,
4738                         uri, selection, selectionArgs, sortOrder);
4739             default:
4740                 throw new UnsupportedOperationException("Unknown uri: " + uri);
4741         }
4742     }
4743 
4744     @Override
getType(Uri uri)4745     public String getType(Uri uri) {
4746         final int match = sUriMatcher.match(uri);
4747         switch (match) {
4748             case CONTACTS:
4749             case CONTACTS_LOOKUP:
4750                 return Contacts.CONTENT_TYPE;
4751             case CONTACTS_ID:
4752             case CONTACTS_LOOKUP_ID:
4753                 return Contacts.CONTENT_ITEM_TYPE;
4754             case CONTACTS_AS_VCARD:
4755                 return Contacts.CONTENT_VCARD_TYPE;
4756             case RAW_CONTACTS:
4757                 return RawContacts.CONTENT_TYPE;
4758             case RAW_CONTACTS_ID:
4759                 return RawContacts.CONTENT_ITEM_TYPE;
4760             case DATA_ID:
4761                 return mDbHelper.getDataMimeType(ContentUris.parseId(uri));
4762             case PHONES:
4763                 return Phone.CONTENT_TYPE;
4764             case PHONES_ID:
4765                 return Phone.CONTENT_ITEM_TYPE;
4766             case EMAILS:
4767                 return Email.CONTENT_TYPE;
4768             case EMAILS_ID:
4769                 return Email.CONTENT_ITEM_TYPE;
4770             case POSTALS:
4771                 return StructuredPostal.CONTENT_TYPE;
4772             case POSTALS_ID:
4773                 return StructuredPostal.CONTENT_ITEM_TYPE;
4774             case AGGREGATION_EXCEPTIONS:
4775                 return AggregationExceptions.CONTENT_TYPE;
4776             case AGGREGATION_EXCEPTION_ID:
4777                 return AggregationExceptions.CONTENT_ITEM_TYPE;
4778             case SETTINGS:
4779                 return Settings.CONTENT_TYPE;
4780             case AGGREGATION_SUGGESTIONS:
4781                 return Contacts.CONTENT_TYPE;
4782             case SEARCH_SUGGESTIONS:
4783                 return SearchManager.SUGGEST_MIME_TYPE;
4784             case SEARCH_SHORTCUT:
4785                 return SearchManager.SHORTCUT_MIME_TYPE;
4786             default:
4787                 return mLegacyApiSupport.getType(uri);
4788         }
4789     }
4790 
setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource)4791     private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) {
4792         if (displayName != null) {
4793             mRawContactDisplayNameUpdate.bindString(1, displayName);
4794         } else {
4795             mRawContactDisplayNameUpdate.bindNull(1);
4796         }
4797         mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource);
4798         mRawContactDisplayNameUpdate.bindLong(3, rawContactId);
4799         mRawContactDisplayNameUpdate.execute();
4800     }
4801 
4802     /**
4803      * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
4804      */
setRawContactDirty(long rawContactId)4805     private void setRawContactDirty(long rawContactId) {
4806         mRawContactDirtyUpdate.bindLong(1, rawContactId);
4807         mRawContactDirtyUpdate.execute();
4808     }
4809 
4810     /*
4811      * Sets the given dataId record in the "data" table to primary, and resets all data records of
4812      * the same mimetype and under the same contact to not be primary.
4813      *
4814      * @param dataId the id of the data record to be set to primary.
4815      */
setIsPrimary(long rawContactId, long dataId, long mimeTypeId)4816     private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
4817         mSetPrimaryStatement.bindLong(1, dataId);
4818         mSetPrimaryStatement.bindLong(2, mimeTypeId);
4819         mSetPrimaryStatement.bindLong(3, rawContactId);
4820         mSetPrimaryStatement.execute();
4821     }
4822 
4823     /*
4824      * Sets the given dataId record in the "data" table to "super primary", and resets all data
4825      * records of the same mimetype and under the same aggregate to not be "super primary".
4826      *
4827      * @param dataId the id of the data record to be set to primary.
4828      */
setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId)4829     private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
4830         mSetSuperPrimaryStatement.bindLong(1, dataId);
4831         mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
4832         mSetSuperPrimaryStatement.bindLong(3, rawContactId);
4833         mSetSuperPrimaryStatement.execute();
4834     }
4835 
insertNameLookupForEmail(long rawContactId, long dataId, String email)4836     public void insertNameLookupForEmail(long rawContactId, long dataId, String email) {
4837         if (TextUtils.isEmpty(email)) {
4838             return;
4839         }
4840 
4841         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
4842         if (tokens.length == 0) {
4843             return;
4844         }
4845 
4846         String address = tokens[0].getAddress();
4847         int at = address.indexOf('@');
4848         if (at != -1) {
4849             address = address.substring(0, at);
4850         }
4851 
4852         insertNameLookup(rawContactId, dataId,
4853                 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address));
4854     }
4855 
4856     /**
4857      * Normalizes the nickname and inserts it in the name lookup table.
4858      */
insertNameLookupForNickname(long rawContactId, long dataId, String nickname)4859     public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) {
4860         if (TextUtils.isEmpty(nickname)) {
4861             return;
4862         }
4863 
4864         insertNameLookup(rawContactId, dataId,
4865                 NameLookupType.NICKNAME, NameNormalizer.normalize(nickname));
4866     }
4867 
insertNameLookupForOrganization(long rawContactId, long dataId, String company, String title)4868     public void insertNameLookupForOrganization(long rawContactId, long dataId, String company,
4869             String title) {
4870         if (!TextUtils.isEmpty(company)) {
4871             insertNameLookup(rawContactId, dataId,
4872                     NameLookupType.ORGANIZATION, NameNormalizer.normalize(company));
4873         }
4874         if (!TextUtils.isEmpty(title)) {
4875             insertNameLookup(rawContactId, dataId,
4876                     NameLookupType.ORGANIZATION, NameNormalizer.normalize(title));
4877         }
4878     }
4879 
insertNameLookupForStructuredName(long rawContactId, long dataId, String name)4880     public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name) {
4881         mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name);
4882     }
4883 
4884     /**
4885      * Returns nickname cluster IDs or null. Maintains cache.
4886      */
getCommonNicknameClusters(String normalizedName)4887     protected String[] getCommonNicknameClusters(String normalizedName) {
4888         SoftReference<String[]> ref;
4889         String[] clusters = null;
4890         synchronized (mNicknameClusterCache) {
4891             if (mNicknameClusterCache.containsKey(normalizedName)) {
4892                 ref = mNicknameClusterCache.get(normalizedName);
4893                 if (ref == null) {
4894                     return null;
4895                 }
4896                 clusters = ref.get();
4897             }
4898         }
4899 
4900         if (clusters == null) {
4901             clusters = loadNicknameClusters(normalizedName);
4902             ref = clusters == null ? null : new SoftReference<String[]>(clusters);
4903             synchronized (mNicknameClusterCache) {
4904                 mNicknameClusterCache.put(normalizedName, ref);
4905             }
4906         }
4907         return clusters;
4908     }
4909 
loadNicknameClusters(String normalizedName)4910     protected String[] loadNicknameClusters(String normalizedName) {
4911         SQLiteDatabase db = mDbHelper.getReadableDatabase();
4912         String[] clusters = null;
4913         Cursor cursor = db.query(NicknameLookupQuery.TABLE, NicknameLookupQuery.COLUMNS,
4914                 NicknameLookupColumns.NAME + "=?", new String[] { normalizedName },
4915                 null, null, null);
4916         try {
4917             int count = cursor.getCount();
4918             if (count > 0) {
4919                 clusters = new String[count];
4920                 for (int i = 0; i < count; i++) {
4921                     cursor.moveToNext();
4922                     clusters[i] = cursor.getString(NicknameLookupQuery.CLUSTER);
4923                 }
4924             }
4925         } finally {
4926             cursor.close();
4927         }
4928         return clusters;
4929     }
4930 
4931     private class StructuredNameLookupBuilder extends NameLookupBuilder {
4932 
StructuredNameLookupBuilder(NameSplitter splitter)4933         public StructuredNameLookupBuilder(NameSplitter splitter) {
4934             super(splitter);
4935         }
4936 
4937         @Override
insertNameLookup(long rawContactId, long dataId, int lookupType, String name)4938         protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
4939                 String name) {
4940             ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name);
4941         }
4942 
4943         @Override
getCommonNicknameClusters(String normalizedName)4944         protected String[] getCommonNicknameClusters(String normalizedName) {
4945             return ContactsProvider2.this.getCommonNicknameClusters(normalizedName);
4946         }
4947     }
4948 
4949     /**
4950      * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
4951      */
insertNameLookup(long rawContactId, long dataId, int lookupType, String name)4952     public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
4953         DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 1, rawContactId);
4954         DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 2, dataId);
4955         DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 3, lookupType);
4956         DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 4, name);
4957         mNameLookupInsert.executeInsert();
4958     }
4959 
4960     /**
4961      * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
4962      */
deleteNameLookup(long dataId)4963     public void deleteNameLookup(long dataId) {
4964         DatabaseUtils.bindObjectToProgram(mNameLookupDelete, 1, dataId);
4965         mNameLookupDelete.execute();
4966     }
4967 
appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam)4968     public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
4969         sb.append("(" +
4970                 "SELECT DISTINCT " + RawContacts.CONTACT_ID +
4971                 " FROM " + Tables.RAW_CONTACTS +
4972                 " JOIN " + Tables.NAME_LOOKUP +
4973                 " ON(" + RawContactsColumns.CONCRETE_ID + "="
4974                         + NameLookupColumns.RAW_CONTACT_ID + ")" +
4975                 " WHERE normalized_name GLOB '");
4976         sb.append(NameNormalizer.normalize(filterParam));
4977         sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN("
4978                 + NameLookupType.NAME_COLLATION_KEY + ","
4979                 + NameLookupType.EMAIL_BASED_NICKNAME + ","
4980                 + NameLookupType.NICKNAME + ","
4981                 + NameLookupType.ORGANIZATION + "))");
4982     }
4983 
getRawContactsByFilterAsNestedQuery(String filterParam)4984     public String getRawContactsByFilterAsNestedQuery(String filterParam) {
4985         StringBuilder sb = new StringBuilder();
4986         appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
4987         return sb.toString();
4988     }
4989 
appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam, String limit)4990     public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
4991             String limit) {
4992         appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit,
4993                 true);
4994     }
4995 
appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, String limit, boolean allowEmailMatch)4996     private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
4997             String limit, boolean allowEmailMatch) {
4998         sb.append("(" +
4999                 "SELECT DISTINCT " + NameLookupColumns.RAW_CONTACT_ID +
5000                 " FROM " + Tables.NAME_LOOKUP +
5001                 " WHERE " + NameLookupColumns.NORMALIZED_NAME +
5002                 " GLOB '");
5003         sb.append(normalizedName);
5004         sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
5005                 + NameLookupType.NAME_COLLATION_KEY + ","
5006                 + NameLookupType.NICKNAME + ","
5007                 + NameLookupType.ORGANIZATION);
5008         if (allowEmailMatch) {
5009             sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
5010         }
5011         sb.append(")");
5012 
5013         if (limit != null) {
5014             sb.append(" LIMIT ").append(limit);
5015         }
5016         sb.append(")");
5017     }
5018 
5019     /**
5020      * Inserts an argument at the beginning of the selection arg list.
5021      */
insertSelectionArg(String[] selectionArgs, String arg)5022     private String[] insertSelectionArg(String[] selectionArgs, String arg) {
5023         if (selectionArgs == null) {
5024             return new String[] {arg};
5025         } else {
5026             int newLength = selectionArgs.length + 1;
5027             String[] newSelectionArgs = new String[newLength];
5028             newSelectionArgs[0] = arg;
5029             System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
5030             return newSelectionArgs;
5031         }
5032     }
5033 
appendProjectionArg(String[] projection, String arg)5034     private String[] appendProjectionArg(String[] projection, String arg) {
5035         if (projection == null) {
5036             return null;
5037         }
5038         final int length = projection.length;
5039         String[] newProjection = new String[length + 1];
5040         System.arraycopy(projection, 0, newProjection, 0, length);
5041         newProjection[length] = arg;
5042         return newProjection;
5043     }
5044 
getDefaultAccount()5045     protected Account getDefaultAccount() {
5046         AccountManager accountManager = AccountManager.get(getContext());
5047         try {
5048             Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
5049                     new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
5050             if (accounts != null && accounts.length > 0) {
5051                 return accounts[0];
5052             }
5053         } catch (Throwable e) {
5054             Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
5055         }
5056         return null;
5057     }
5058 
isWritableAccount(Account account)5059     protected boolean isWritableAccount(Account account) {
5060         IContentService contentService = ContentResolver.getContentService();
5061         try {
5062             for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
5063                 if (ContactsContract.AUTHORITY.equals(sync.authority) &&
5064                         account.type.equals(sync.accountType)) {
5065                     return sync.supportsUploading();
5066                 }
5067             }
5068         } catch (RemoteException e) {
5069             Log.e(TAG, "Could not acquire sync adapter types");
5070         }
5071 
5072         return false;
5073     }
5074 }
5075