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