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