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