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