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