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