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