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