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