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