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