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