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