ContactsProvider2.java revision bd9abbb6b03b4ec1e28ad3fa2fcba5d1eb8609ea
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(mTransactionContext, mDb); 1476 if (mVisibleTouched) { 1477 mVisibleTouched = false; 1478 mDbHelper.updateAllVisible(); 1479 } 1480 1481 updateSearchIndexInTransaction(); 1482 1483 if (mProviderStatusUpdateNeeded) { 1484 updateProviderStatus(); 1485 mProviderStatusUpdateNeeded = false; 1486 } 1487 } 1488 1489 private void updateSearchIndexInTransaction() { 1490 Set<Long> staleContacts = mTransactionContext.getStaleSearchIndexContactIds(); 1491 Set<Long> staleRawContacts = mTransactionContext.getStaleSearchIndexRawContactIds(); 1492 if (!staleContacts.isEmpty() || !staleRawContacts.isEmpty()) { 1493 mSearchIndexManager.updateIndexForRawContacts(staleContacts, staleRawContacts); 1494 mTransactionContext.clearSearchIndexUpdates(); 1495 } 1496 } 1497 1498 private void flushTransactionalChanges() { 1499 if (VERBOSE_LOGGING) { 1500 Log.v(TAG, "flushTransactionChanges"); 1501 } 1502 1503 for (long rawContactId : mTransactionContext.getInsertedRawContactIds()) { 1504 mContactAggregator.updateRawContactDisplayName(mDb, rawContactId); 1505 mContactAggregator.onRawContactInsert(mTransactionContext, mDb, rawContactId); 1506 } 1507 1508 Set<Long> dirtyRawContacts = mTransactionContext.getDirtyRawContactIds(); 1509 if (!dirtyRawContacts.isEmpty()) { 1510 mSb.setLength(0); 1511 mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL); 1512 appendIds(mSb, dirtyRawContacts); 1513 mSb.append(")"); 1514 mDb.execSQL(mSb.toString()); 1515 } 1516 1517 Set<Long> updatedRawContacts = mTransactionContext.getUpdatedRawContactIds(); 1518 if (!updatedRawContacts.isEmpty()) { 1519 mSb.setLength(0); 1520 mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL); 1521 appendIds(mSb, updatedRawContacts); 1522 mSb.append(")"); 1523 mDb.execSQL(mSb.toString()); 1524 } 1525 1526 for (Map.Entry<Long, Object> entry : mTransactionContext.getUpdatedSyncStates()) { 1527 long id = entry.getKey(); 1528 if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) { 1529 throw new IllegalStateException( 1530 "unable to update sync state, does it still exist?"); 1531 } 1532 } 1533 1534 mTransactionContext.clear(); 1535 } 1536 1537 /** 1538 * Appends comma separated ids. 1539 * @param ids Should not be empty 1540 */ 1541 private void appendIds(StringBuilder sb, Set<Long> ids) { 1542 for (long id : ids) { 1543 sb.append(id).append(','); 1544 } 1545 1546 sb.setLength(sb.length() - 1); // Yank the last comma 1547 } 1548 1549 @Override 1550 protected void notifyChange() { 1551 notifyChange(mSyncToNetwork); 1552 mSyncToNetwork = false; 1553 } 1554 1555 protected void notifyChange(boolean syncToNetwork) { 1556 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, 1557 syncToNetwork); 1558 } 1559 1560 protected void setProviderStatus(int status) { 1561 if (mProviderStatus != status) { 1562 mProviderStatus = status; 1563 getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false); 1564 } 1565 } 1566 1567 public DataRowHandler getDataRowHandler(final String mimeType) { 1568 DataRowHandler handler = mDataRowHandlers.get(mimeType); 1569 if (handler == null) { 1570 handler = new DataRowHandlerForCustomMimetype( 1571 getContext(), mDbHelper, mContactAggregator, mimeType); 1572 mDataRowHandlers.put(mimeType, handler); 1573 } 1574 return handler; 1575 } 1576 1577 @Override 1578 protected Uri insertInTransaction(Uri uri, ContentValues values) { 1579 if (VERBOSE_LOGGING) { 1580 Log.v(TAG, "insertInTransaction: " + uri + " " + values); 1581 } 1582 1583 final boolean callerIsSyncAdapter = 1584 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 1585 1586 final int match = sUriMatcher.match(uri); 1587 long id = 0; 1588 1589 switch (match) { 1590 case SYNCSTATE: 1591 id = mDbHelper.getSyncState().insert(mDb, values); 1592 break; 1593 1594 case CONTACTS: { 1595 insertContact(values); 1596 break; 1597 } 1598 1599 case RAW_CONTACTS: { 1600 id = insertRawContact(uri, values, callerIsSyncAdapter); 1601 mSyncToNetwork |= !callerIsSyncAdapter; 1602 break; 1603 } 1604 1605 case RAW_CONTACTS_DATA: { 1606 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 1607 id = insertData(values, callerIsSyncAdapter); 1608 mSyncToNetwork |= !callerIsSyncAdapter; 1609 break; 1610 } 1611 1612 case DATA: { 1613 id = insertData(values, callerIsSyncAdapter); 1614 mSyncToNetwork |= !callerIsSyncAdapter; 1615 break; 1616 } 1617 1618 case GROUPS: { 1619 id = insertGroup(uri, values, callerIsSyncAdapter); 1620 mSyncToNetwork |= !callerIsSyncAdapter; 1621 break; 1622 } 1623 1624 case SETTINGS: { 1625 id = insertSettings(uri, values); 1626 mSyncToNetwork |= !callerIsSyncAdapter; 1627 break; 1628 } 1629 1630 case STATUS_UPDATES: { 1631 id = insertStatusUpdate(values); 1632 break; 1633 } 1634 1635 default: 1636 mSyncToNetwork = true; 1637 return mLegacyApiSupport.insert(uri, values); 1638 } 1639 1640 if (id < 0) { 1641 return null; 1642 } 1643 1644 return ContentUris.withAppendedId(uri, id); 1645 } 1646 1647 /** 1648 * If account is non-null then store it in the values. If the account is 1649 * already specified in the values then it must be consistent with the 1650 * account, if it is non-null. 1651 * 1652 * @param uri Current {@link Uri} being operated on. 1653 * @param values {@link ContentValues} to read and possibly update. 1654 * @throws IllegalArgumentException when only one of 1655 * {@link RawContacts#ACCOUNT_NAME} or 1656 * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the 1657 * other undefined. 1658 * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME} 1659 * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between 1660 * the given {@link Uri} and {@link ContentValues}. 1661 */ 1662 private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException { 1663 String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 1664 String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 1665 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 1666 1667 String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME); 1668 String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 1669 final boolean partialValues = TextUtils.isEmpty(valueAccountName) 1670 ^ TextUtils.isEmpty(valueAccountType); 1671 1672 if (partialUri || partialValues) { 1673 // Throw when either account is incomplete 1674 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 1675 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 1676 } 1677 1678 // Accounts are valid by only checking one parameter, since we've 1679 // already ruled out partial accounts. 1680 final boolean validUri = !TextUtils.isEmpty(accountName); 1681 final boolean validValues = !TextUtils.isEmpty(valueAccountName); 1682 1683 if (validValues && validUri) { 1684 // Check that accounts match when both present 1685 final boolean accountMatch = TextUtils.equals(accountName, valueAccountName) 1686 && TextUtils.equals(accountType, valueAccountType); 1687 if (!accountMatch) { 1688 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 1689 "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri)); 1690 } 1691 } else if (validUri) { 1692 // Fill values from Uri when not present 1693 values.put(RawContacts.ACCOUNT_NAME, accountName); 1694 values.put(RawContacts.ACCOUNT_TYPE, accountType); 1695 } else if (validValues) { 1696 accountName = valueAccountName; 1697 accountType = valueAccountType; 1698 } else { 1699 return null; 1700 } 1701 1702 // Use cached Account object when matches, otherwise create 1703 if (mAccount == null 1704 || !mAccount.name.equals(accountName) 1705 || !mAccount.type.equals(accountType)) { 1706 mAccount = new Account(accountName, accountType); 1707 } 1708 1709 return mAccount; 1710 } 1711 1712 /** 1713 * Inserts an item in the contacts table 1714 * 1715 * @param values the values for the new row 1716 * @return the row ID of the newly created row 1717 */ 1718 private long insertContact(ContentValues values) { 1719 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 1720 } 1721 1722 /** 1723 * Inserts an item in the contacts table 1724 * 1725 * @param uri the values for the new row 1726 * @param values the account this contact should be associated with. may be null. 1727 * @param callerIsSyncAdapter 1728 * @return the row ID of the newly created row 1729 */ 1730 private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 1731 mValues.clear(); 1732 mValues.putAll(values); 1733 mValues.putNull(RawContacts.CONTACT_ID); 1734 1735 final Account account = resolveAccount(uri, mValues); 1736 1737 if (values.containsKey(RawContacts.DELETED) 1738 && values.getAsInteger(RawContacts.DELETED) != 0) { 1739 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 1740 } 1741 1742 long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues); 1743 int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; 1744 if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) { 1745 aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE); 1746 } 1747 mContactAggregator.markNewForAggregation(rawContactId, aggregationMode); 1748 1749 // Trigger creation of a Contact based on this RawContact at the end of transaction 1750 mTransactionContext.rawContactInserted(rawContactId, account); 1751 1752 if (!callerIsSyncAdapter) { 1753 addAutoAddMembership(rawContactId); 1754 final Long starred = values.getAsLong(RawContacts.STARRED); 1755 if (starred != null && starred != 0) { 1756 updateFavoritesMembership(rawContactId, starred != 0); 1757 } 1758 } 1759 1760 mProviderStatusUpdateNeeded = true; 1761 return rawContactId; 1762 } 1763 1764 private void addAutoAddMembership(long rawContactId) { 1765 final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID, 1766 rawContactId); 1767 if (groupId != null) { 1768 insertDataGroupMembership(rawContactId, groupId); 1769 } 1770 } 1771 1772 private Long findGroupByRawContactId(String selection, long rawContactId) { 1773 Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID, 1774 selection, 1775 new String[]{Long.toString(rawContactId)}, 1776 null /* groupBy */, null /* having */, null /* orderBy */); 1777 try { 1778 while (c.moveToNext()) { 1779 return c.getLong(0); 1780 } 1781 return null; 1782 } finally { 1783 c.close(); 1784 } 1785 } 1786 1787 private void updateFavoritesMembership(long rawContactId, boolean isStarred) { 1788 final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID, 1789 rawContactId); 1790 if (groupId != null) { 1791 if (isStarred) { 1792 insertDataGroupMembership(rawContactId, groupId); 1793 } else { 1794 deleteDataGroupMembership(rawContactId, groupId); 1795 } 1796 } 1797 } 1798 1799 private void insertDataGroupMembership(long rawContactId, long groupId) { 1800 ContentValues groupMembershipValues = new ContentValues(); 1801 groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId); 1802 groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId); 1803 groupMembershipValues.put(DataColumns.MIMETYPE_ID, 1804 mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 1805 mDb.insert(Tables.DATA, null, groupMembershipValues); 1806 } 1807 1808 private void deleteDataGroupMembership(long rawContactId, long groupId) { 1809 final String[] selectionArgs = { 1810 Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)), 1811 Long.toString(groupId), 1812 Long.toString(rawContactId)}; 1813 mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs); 1814 } 1815 1816 /** 1817 * Inserts an item in the data table 1818 * 1819 * @param values the values for the new row 1820 * @return the row ID of the newly created row 1821 */ 1822 private long insertData(ContentValues values, boolean callerIsSyncAdapter) { 1823 long id = 0; 1824 mValues.clear(); 1825 mValues.putAll(values); 1826 1827 long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 1828 1829 // Replace package with internal mapping 1830 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 1831 if (packageName != null) { 1832 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 1833 } 1834 mValues.remove(Data.RES_PACKAGE); 1835 1836 // Replace mimetype with internal mapping 1837 final String mimeType = mValues.getAsString(Data.MIMETYPE); 1838 if (TextUtils.isEmpty(mimeType)) { 1839 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 1840 } 1841 1842 mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType)); 1843 mValues.remove(Data.MIMETYPE); 1844 1845 DataRowHandler rowHandler = getDataRowHandler(mimeType); 1846 id = rowHandler.insert(mDb, mTransactionContext, rawContactId, mValues); 1847 if (!callerIsSyncAdapter) { 1848 mTransactionContext.markRawContactDirty(rawContactId); 1849 } 1850 mTransactionContext.rawContactUpdated(rawContactId); 1851 return id; 1852 } 1853 1854 public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 1855 mContactAggregator.updateRawContactDisplayName(db, rawContactId); 1856 } 1857 1858 /** 1859 * Delete data row by row so that fixing of primaries etc work correctly. 1860 */ 1861 private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { 1862 int count = 0; 1863 1864 // Note that the query will return data according to the access restrictions, 1865 // so we don't need to worry about deleting data we don't have permission to read. 1866 Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, 1867 selection, selectionArgs, null); 1868 try { 1869 while(c.moveToNext()) { 1870 long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID); 1871 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE); 1872 DataRowHandler rowHandler = getDataRowHandler(mimeType); 1873 count += rowHandler.delete(mDb, mTransactionContext, c); 1874 if (!callerIsSyncAdapter) { 1875 mTransactionContext.markRawContactDirty(rawContactId); 1876 } 1877 } 1878 } finally { 1879 c.close(); 1880 } 1881 1882 return count; 1883 } 1884 1885 /** 1886 * Delete a data row provided that it is one of the allowed mime types. 1887 */ 1888 public int deleteData(long dataId, String[] allowedMimeTypes) { 1889 1890 // Note that the query will return data according to the access restrictions, 1891 // so we don't need to worry about deleting data we don't have permission to read. 1892 mSelectionArgs1[0] = String.valueOf(dataId); 1893 Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, Data._ID + "=?", 1894 mSelectionArgs1, null); 1895 1896 try { 1897 if (!c.moveToFirst()) { 1898 return 0; 1899 } 1900 1901 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE); 1902 boolean valid = false; 1903 for (int i = 0; i < allowedMimeTypes.length; i++) { 1904 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 1905 valid = true; 1906 break; 1907 } 1908 } 1909 1910 if (!valid) { 1911 throw new IllegalArgumentException("Data type mismatch: expected " 1912 + Lists.newArrayList(allowedMimeTypes)); 1913 } 1914 1915 DataRowHandler rowHandler = getDataRowHandler(mimeType); 1916 return rowHandler.delete(mDb, mTransactionContext, c); 1917 } finally { 1918 c.close(); 1919 } 1920 } 1921 1922 /** 1923 * Inserts an item in the groups table 1924 */ 1925 private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 1926 mValues.clear(); 1927 mValues.putAll(values); 1928 1929 final Account account = resolveAccount(uri, mValues); 1930 1931 // Replace package with internal mapping 1932 final String packageName = mValues.getAsString(Groups.RES_PACKAGE); 1933 if (packageName != null) { 1934 mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 1935 } 1936 mValues.remove(Groups.RES_PACKAGE); 1937 1938 final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null 1939 ? mValues.getAsLong(Groups.FAVORITES) != 0 1940 : false; 1941 1942 if (!callerIsSyncAdapter) { 1943 mValues.put(Groups.DIRTY, 1); 1944 } 1945 1946 long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues); 1947 1948 if (!callerIsSyncAdapter && isFavoritesGroup) { 1949 // add all starred raw contacts to this group 1950 String selection; 1951 String[] selectionArgs; 1952 if (account == null) { 1953 selection = RawContacts.ACCOUNT_NAME + " IS NULL AND " 1954 + RawContacts.ACCOUNT_TYPE + " IS NULL"; 1955 selectionArgs = null; 1956 } else { 1957 selection = RawContacts.ACCOUNT_NAME + "=? AND " 1958 + RawContacts.ACCOUNT_TYPE + "=?"; 1959 selectionArgs = new String[]{account.name, account.type}; 1960 } 1961 Cursor c = mDb.query(Tables.RAW_CONTACTS, 1962 new String[]{RawContacts._ID, RawContacts.STARRED}, 1963 selection, selectionArgs, null, null, null); 1964 try { 1965 while (c.moveToNext()) { 1966 if (c.getLong(1) != 0) { 1967 final long rawContactId = c.getLong(0); 1968 insertDataGroupMembership(rawContactId, result); 1969 mTransactionContext.markRawContactDirty(rawContactId); 1970 } 1971 } 1972 } finally { 1973 c.close(); 1974 } 1975 } 1976 1977 if (mValues.containsKey(Groups.GROUP_VISIBLE)) { 1978 mVisibleTouched = true; 1979 } 1980 1981 return result; 1982 } 1983 1984 private long insertSettings(Uri uri, ContentValues values) { 1985 final long id = mDb.insert(Tables.SETTINGS, null, values); 1986 1987 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 1988 mVisibleTouched = true; 1989 } 1990 1991 return id; 1992 } 1993 1994 /** 1995 * Inserts a status update. 1996 */ 1997 public long insertStatusUpdate(ContentValues values) { 1998 final String handle = values.getAsString(StatusUpdates.IM_HANDLE); 1999 final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL); 2000 String customProtocol = null; 2001 2002 if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) { 2003 customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL); 2004 if (TextUtils.isEmpty(customProtocol)) { 2005 throw new IllegalArgumentException( 2006 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 2007 } 2008 } 2009 2010 long rawContactId = -1; 2011 long contactId = -1; 2012 Long dataId = values.getAsLong(StatusUpdates.DATA_ID); 2013 mSb.setLength(0); 2014 mSelectionArgs.clear(); 2015 if (dataId != null) { 2016 // Lookup the contact info for the given data row. 2017 2018 mSb.append(Tables.DATA + "." + Data._ID + "=?"); 2019 mSelectionArgs.add(String.valueOf(dataId)); 2020 } else { 2021 // Lookup the data row to attach this presence update to 2022 2023 if (TextUtils.isEmpty(handle) || protocol == null) { 2024 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 2025 } 2026 2027 // TODO: generalize to allow other providers to match against email 2028 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 2029 2030 String mimeTypeIdIm = String.valueOf(mDbHelper.getMimeTypeIdForIm()); 2031 if (matchEmail) { 2032 String mimeTypeIdEmail = String.valueOf(mDbHelper.getMimeTypeIdForEmail()); 2033 2034 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise 2035 // the "OR" conjunction confuses it and it switches to a full scan of 2036 // the raw_contacts table. 2037 2038 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same 2039 // column - Data.DATA1 2040 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" + 2041 " AND " + Data.DATA1 + "=?" + 2042 " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?"); 2043 mSelectionArgs.add(mimeTypeIdEmail); 2044 mSelectionArgs.add(mimeTypeIdIm); 2045 mSelectionArgs.add(handle); 2046 mSelectionArgs.add(mimeTypeIdIm); 2047 mSelectionArgs.add(String.valueOf(protocol)); 2048 if (customProtocol != null) { 2049 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 2050 mSelectionArgs.add(customProtocol); 2051 } 2052 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))"); 2053 mSelectionArgs.add(mimeTypeIdEmail); 2054 } else { 2055 mSb.append(DataColumns.MIMETYPE_ID + "=?" + 2056 " AND " + Im.PROTOCOL + "=?" + 2057 " AND " + Im.DATA + "=?"); 2058 mSelectionArgs.add(mimeTypeIdIm); 2059 mSelectionArgs.add(String.valueOf(protocol)); 2060 mSelectionArgs.add(handle); 2061 if (customProtocol != null) { 2062 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 2063 mSelectionArgs.add(customProtocol); 2064 } 2065 } 2066 2067 if (values.containsKey(StatusUpdates.DATA_ID)) { 2068 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?"); 2069 mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID)); 2070 } 2071 } 2072 mSb.append(" AND ").append(getContactsRestrictions()); 2073 2074 Cursor cursor = null; 2075 try { 2076 cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 2077 mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null, 2078 Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID); 2079 if (cursor.moveToFirst()) { 2080 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 2081 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 2082 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 2083 } else { 2084 // No contact found, return a null URI 2085 return -1; 2086 } 2087 } finally { 2088 if (cursor != null) { 2089 cursor.close(); 2090 } 2091 } 2092 2093 if (values.containsKey(StatusUpdates.PRESENCE)) { 2094 if (customProtocol == null) { 2095 // We cannot allow a null in the custom protocol field, because SQLite3 does not 2096 // properly enforce uniqueness of null values 2097 customProtocol = ""; 2098 } 2099 2100 mValues.clear(); 2101 mValues.put(StatusUpdates.DATA_ID, dataId); 2102 mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 2103 mValues.put(PresenceColumns.CONTACT_ID, contactId); 2104 mValues.put(StatusUpdates.PROTOCOL, protocol); 2105 mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol); 2106 mValues.put(StatusUpdates.IM_HANDLE, handle); 2107 if (values.containsKey(StatusUpdates.IM_ACCOUNT)) { 2108 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT)); 2109 } 2110 mValues.put(StatusUpdates.PRESENCE, 2111 values.getAsString(StatusUpdates.PRESENCE)); 2112 mValues.put(StatusUpdates.CHAT_CAPABILITY, 2113 values.getAsString(StatusUpdates.CHAT_CAPABILITY)); 2114 2115 // Insert the presence update 2116 mDb.replace(Tables.PRESENCE, null, mValues); 2117 } 2118 2119 2120 if (values.containsKey(StatusUpdates.STATUS)) { 2121 String status = values.getAsString(StatusUpdates.STATUS); 2122 String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE); 2123 Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL); 2124 2125 if (TextUtils.isEmpty(resPackage) 2126 && (labelResource == null || labelResource == 0) 2127 && protocol != null) { 2128 labelResource = Im.getProtocolLabelResource(protocol); 2129 } 2130 2131 Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON); 2132 // TODO compute the default icon based on the protocol 2133 2134 if (TextUtils.isEmpty(status)) { 2135 mDbHelper.deleteStatusUpdate(dataId); 2136 } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) { 2137 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP); 2138 mDbHelper.replaceStatusUpdate(dataId, timestamp, status, resPackage, iconResource, 2139 labelResource); 2140 } else { 2141 mDbHelper.insertStatusUpdate(dataId, status, resPackage, iconResource, 2142 labelResource); 2143 } 2144 } 2145 2146 if (contactId != -1) { 2147 mContactAggregator.updateLastStatusUpdateId(contactId); 2148 } 2149 2150 return dataId; 2151 } 2152 2153 @Override 2154 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 2155 if (VERBOSE_LOGGING) { 2156 Log.v(TAG, "deleteInTransaction: " + uri); 2157 } 2158 flushTransactionalChanges(); 2159 final boolean callerIsSyncAdapter = 2160 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2161 final int match = sUriMatcher.match(uri); 2162 switch (match) { 2163 case SYNCSTATE: 2164 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 2165 2166 case SYNCSTATE_ID: 2167 String selectionWithId = 2168 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 2169 + (selection == null ? "" : " AND (" + selection + ")"); 2170 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); 2171 2172 case CONTACTS: { 2173 // TODO 2174 return 0; 2175 } 2176 2177 case CONTACTS_ID: { 2178 long contactId = ContentUris.parseId(uri); 2179 return deleteContact(contactId, callerIsSyncAdapter); 2180 } 2181 2182 case CONTACTS_LOOKUP: { 2183 final List<String> pathSegments = uri.getPathSegments(); 2184 final int segmentCount = pathSegments.size(); 2185 if (segmentCount < 3) { 2186 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 2187 "Missing a lookup key", uri)); 2188 } 2189 final String lookupKey = pathSegments.get(2); 2190 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 2191 return deleteContact(contactId, callerIsSyncAdapter); 2192 } 2193 2194 case CONTACTS_LOOKUP_ID: { 2195 // lookup contact by id and lookup key to see if they still match the actual record 2196 final List<String> pathSegments = uri.getPathSegments(); 2197 final String lookupKey = pathSegments.get(2); 2198 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 2199 setTablesAndProjectionMapForContacts(lookupQb, uri, null); 2200 long contactId = ContentUris.parseId(uri); 2201 String[] args; 2202 if (selectionArgs == null) { 2203 args = new String[2]; 2204 } else { 2205 args = new String[selectionArgs.length + 2]; 2206 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 2207 } 2208 args[0] = String.valueOf(contactId); 2209 args[1] = Uri.encode(lookupKey); 2210 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); 2211 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 2212 Cursor c = query(db, lookupQb, null, selection, args, null, null, null); 2213 try { 2214 if (c.getCount() == 1) { 2215 // contact was unmodified so go ahead and delete it 2216 return deleteContact(contactId, callerIsSyncAdapter); 2217 } else { 2218 // row was changed (e.g. the merging might have changed), we got multiple 2219 // rows or the supplied selection filtered the record out 2220 return 0; 2221 } 2222 } finally { 2223 c.close(); 2224 } 2225 } 2226 2227 case RAW_CONTACTS: { 2228 int numDeletes = 0; 2229 Cursor c = mDb.query(Tables.RAW_CONTACTS, 2230 new String[]{RawContacts._ID, RawContacts.CONTACT_ID}, 2231 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2232 try { 2233 while (c.moveToNext()) { 2234 final long rawContactId = c.getLong(0); 2235 long contactId = c.getLong(1); 2236 numDeletes += deleteRawContact(rawContactId, contactId, 2237 callerIsSyncAdapter); 2238 } 2239 } finally { 2240 c.close(); 2241 } 2242 return numDeletes; 2243 } 2244 2245 case RAW_CONTACTS_ID: { 2246 final long rawContactId = ContentUris.parseId(uri); 2247 return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId), 2248 callerIsSyncAdapter); 2249 } 2250 2251 case DATA: { 2252 mSyncToNetwork |= !callerIsSyncAdapter; 2253 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 2254 callerIsSyncAdapter); 2255 } 2256 2257 case DATA_ID: 2258 case PHONES_ID: 2259 case EMAILS_ID: 2260 case POSTALS_ID: { 2261 long dataId = ContentUris.parseId(uri); 2262 mSyncToNetwork |= !callerIsSyncAdapter; 2263 mSelectionArgs1[0] = String.valueOf(dataId); 2264 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); 2265 } 2266 2267 case GROUPS_ID: { 2268 mSyncToNetwork |= !callerIsSyncAdapter; 2269 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter); 2270 } 2271 2272 case GROUPS: { 2273 int numDeletes = 0; 2274 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 2275 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2276 try { 2277 while (c.moveToNext()) { 2278 numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter); 2279 } 2280 } finally { 2281 c.close(); 2282 } 2283 if (numDeletes > 0) { 2284 mSyncToNetwork |= !callerIsSyncAdapter; 2285 } 2286 return numDeletes; 2287 } 2288 2289 case SETTINGS: { 2290 mSyncToNetwork |= !callerIsSyncAdapter; 2291 return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs); 2292 } 2293 2294 case STATUS_UPDATES: { 2295 return deleteStatusUpdates(selection, selectionArgs); 2296 } 2297 2298 default: { 2299 mSyncToNetwork = true; 2300 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 2301 } 2302 } 2303 } 2304 2305 public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) { 2306 mGroupIdCache.clear(); 2307 final long groupMembershipMimetypeId = mDbHelper 2308 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 2309 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 2310 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 2311 + groupId, null); 2312 2313 try { 2314 if (callerIsSyncAdapter) { 2315 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 2316 } else { 2317 mValues.clear(); 2318 mValues.put(Groups.DELETED, 1); 2319 mValues.put(Groups.DIRTY, 1); 2320 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 2321 } 2322 } finally { 2323 mVisibleTouched = true; 2324 } 2325 } 2326 2327 private int deleteSettings(Uri uri, String selection, String[] selectionArgs) { 2328 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 2329 mVisibleTouched = true; 2330 return count; 2331 } 2332 2333 private int deleteContact(long contactId, boolean callerIsSyncAdapter) { 2334 mSelectionArgs1[0] = Long.toString(contactId); 2335 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2336 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, 2337 null, null, null); 2338 try { 2339 while (c.moveToNext()) { 2340 long rawContactId = c.getLong(0); 2341 markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); 2342 } 2343 } finally { 2344 c.close(); 2345 } 2346 2347 mProviderStatusUpdateNeeded = true; 2348 2349 return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); 2350 } 2351 2352 public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) { 2353 mContactAggregator.invalidateAggregationExceptionCache(); 2354 mProviderStatusUpdateNeeded = true; 2355 2356 if (callerIsSyncAdapter) { 2357 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 2358 int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 2359 mContactAggregator.updateDisplayNameForContact(mDb, contactId); 2360 return count; 2361 } else { 2362 mDbHelper.removeContactIfSingleton(rawContactId); 2363 return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); 2364 } 2365 } 2366 2367 private int deleteStatusUpdates(String selection, String[] selectionArgs) { 2368 // delete from both tables: presence and status_updates 2369 // TODO should account type/name be appended to the where clause? 2370 if (VERBOSE_LOGGING) { 2371 Log.v(TAG, "deleting data from status_updates for " + selection); 2372 } 2373 mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection), 2374 selectionArgs); 2375 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 2376 } 2377 2378 private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) { 2379 mSyncToNetwork = true; 2380 2381 mValues.clear(); 2382 mValues.put(RawContacts.DELETED, 1); 2383 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2384 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 2385 mValues.putNull(RawContacts.CONTACT_ID); 2386 mValues.put(RawContacts.DIRTY, 1); 2387 return updateRawContact(rawContactId, mValues, callerIsSyncAdapter); 2388 } 2389 2390 @Override 2391 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 2392 String[] selectionArgs) { 2393 if (VERBOSE_LOGGING) { 2394 Log.v(TAG, "updateInTransaction: " + uri); 2395 } 2396 2397 int count = 0; 2398 2399 final int match = sUriMatcher.match(uri); 2400 if (match == SYNCSTATE_ID && selection == null) { 2401 long rowId = ContentUris.parseId(uri); 2402 Object data = values.get(ContactsContract.SyncState.DATA); 2403 mTransactionContext.syncStateUpdated(rowId, data); 2404 return 1; 2405 } 2406 flushTransactionalChanges(); 2407 final boolean callerIsSyncAdapter = 2408 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2409 switch(match) { 2410 case SYNCSTATE: 2411 return mDbHelper.getSyncState().update(mDb, values, 2412 appendAccountToSelection(uri, selection), selectionArgs); 2413 2414 case SYNCSTATE_ID: { 2415 selection = appendAccountToSelection(uri, selection); 2416 String selectionWithId = 2417 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 2418 + (selection == null ? "" : " AND (" + selection + ")"); 2419 return mDbHelper.getSyncState().update(mDb, values, 2420 selectionWithId, selectionArgs); 2421 } 2422 2423 case CONTACTS: { 2424 count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter); 2425 break; 2426 } 2427 2428 case CONTACTS_ID: { 2429 count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter); 2430 break; 2431 } 2432 2433 case CONTACTS_LOOKUP: 2434 case CONTACTS_LOOKUP_ID: { 2435 final List<String> pathSegments = uri.getPathSegments(); 2436 final int segmentCount = pathSegments.size(); 2437 if (segmentCount < 3) { 2438 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 2439 "Missing a lookup key", uri)); 2440 } 2441 final String lookupKey = pathSegments.get(2); 2442 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 2443 count = updateContactOptions(contactId, values, callerIsSyncAdapter); 2444 break; 2445 } 2446 2447 case RAW_CONTACTS_DATA: { 2448 final String rawContactId = uri.getPathSegments().get(1); 2449 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") 2450 + (selection == null ? "" : " AND " + selection); 2451 2452 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); 2453 2454 break; 2455 } 2456 2457 case DATA: { 2458 count = updateData(uri, values, appendAccountToSelection(uri, selection), 2459 selectionArgs, callerIsSyncAdapter); 2460 if (count > 0) { 2461 mSyncToNetwork |= !callerIsSyncAdapter; 2462 } 2463 break; 2464 } 2465 2466 case DATA_ID: 2467 case PHONES_ID: 2468 case EMAILS_ID: 2469 case POSTALS_ID: { 2470 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); 2471 if (count > 0) { 2472 mSyncToNetwork |= !callerIsSyncAdapter; 2473 } 2474 break; 2475 } 2476 2477 case RAW_CONTACTS: { 2478 selection = appendAccountToSelection(uri, selection); 2479 count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter); 2480 break; 2481 } 2482 2483 case RAW_CONTACTS_ID: { 2484 long rawContactId = ContentUris.parseId(uri); 2485 if (selection != null) { 2486 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 2487 count = updateRawContacts(values, RawContacts._ID + "=?" 2488 + " AND(" + selection + ")", selectionArgs, 2489 callerIsSyncAdapter); 2490 } else { 2491 mSelectionArgs1[0] = String.valueOf(rawContactId); 2492 count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1, 2493 callerIsSyncAdapter); 2494 } 2495 break; 2496 } 2497 2498 case GROUPS: { 2499 count = updateGroups(uri, values, appendAccountToSelection(uri, selection), 2500 selectionArgs, callerIsSyncAdapter); 2501 if (count > 0) { 2502 mSyncToNetwork |= !callerIsSyncAdapter; 2503 } 2504 break; 2505 } 2506 2507 case GROUPS_ID: { 2508 long groupId = ContentUris.parseId(uri); 2509 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); 2510 String selectionWithId = Groups._ID + "=? " 2511 + (selection == null ? "" : " AND " + selection); 2512 count = updateGroups(uri, values, selectionWithId, selectionArgs, 2513 callerIsSyncAdapter); 2514 if (count > 0) { 2515 mSyncToNetwork |= !callerIsSyncAdapter; 2516 } 2517 break; 2518 } 2519 2520 case AGGREGATION_EXCEPTIONS: { 2521 count = updateAggregationException(mDb, values); 2522 break; 2523 } 2524 2525 case SETTINGS: { 2526 count = updateSettings(uri, values, appendAccountToSelection(uri, selection), 2527 selectionArgs); 2528 mSyncToNetwork |= !callerIsSyncAdapter; 2529 break; 2530 } 2531 2532 case STATUS_UPDATES: { 2533 count = updateStatusUpdate(uri, values, selection, selectionArgs); 2534 break; 2535 } 2536 2537 case DIRECTORIES: { 2538 mContactDirectoryManager.scanPackagesByUid(Binder.getCallingUid()); 2539 count = 1; 2540 break; 2541 } 2542 2543 default: { 2544 mSyncToNetwork = true; 2545 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 2546 } 2547 } 2548 2549 return count; 2550 } 2551 2552 private int updateStatusUpdate(Uri uri, ContentValues values, String selection, 2553 String[] selectionArgs) { 2554 // update status_updates table, if status is provided 2555 // TODO should account type/name be appended to the where clause? 2556 int updateCount = 0; 2557 ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values); 2558 if (settableValues.size() > 0) { 2559 updateCount = mDb.update(Tables.STATUS_UPDATES, 2560 settableValues, 2561 getWhereClauseForStatusUpdatesTable(selection), 2562 selectionArgs); 2563 } 2564 2565 // now update the Presence table 2566 settableValues = getSettableColumnsForPresenceTable(values); 2567 if (settableValues.size() > 0) { 2568 updateCount = mDb.update(Tables.PRESENCE, settableValues, 2569 selection, selectionArgs); 2570 } 2571 // TODO updateCount is not entirely a valid count of updated rows because 2 tables could 2572 // potentially get updated in this method. 2573 return updateCount; 2574 } 2575 2576 /** 2577 * Build a where clause to select the rows to be updated in status_updates table. 2578 */ 2579 private String getWhereClauseForStatusUpdatesTable(String selection) { 2580 mSb.setLength(0); 2581 mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE); 2582 mSb.append(selection); 2583 mSb.append(")"); 2584 return mSb.toString(); 2585 } 2586 2587 private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) { 2588 mValues.clear(); 2589 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values, 2590 StatusUpdates.STATUS); 2591 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values, 2592 StatusUpdates.STATUS_TIMESTAMP); 2593 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values, 2594 StatusUpdates.STATUS_RES_PACKAGE); 2595 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values, 2596 StatusUpdates.STATUS_LABEL); 2597 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values, 2598 StatusUpdates.STATUS_ICON); 2599 return mValues; 2600 } 2601 2602 private ContentValues getSettableColumnsForPresenceTable(ContentValues values) { 2603 mValues.clear(); 2604 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values, 2605 StatusUpdates.PRESENCE); 2606 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values, 2607 StatusUpdates.CHAT_CAPABILITY); 2608 return mValues; 2609 } 2610 2611 private int updateGroups(Uri uri, ContentValues values, String selectionWithId, 2612 String[] selectionArgs, boolean callerIsSyncAdapter) { 2613 2614 mGroupIdCache.clear(); 2615 2616 ContentValues updatedValues; 2617 if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) { 2618 updatedValues = mValues; 2619 updatedValues.clear(); 2620 updatedValues.putAll(values); 2621 updatedValues.put(Groups.DIRTY, 1); 2622 } else { 2623 updatedValues = values; 2624 } 2625 2626 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 2627 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 2628 mVisibleTouched = true; 2629 } 2630 if (updatedValues.containsKey(Groups.SHOULD_SYNC) 2631 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) { 2632 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME, 2633 Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null, 2634 null, null); 2635 String accountName; 2636 String accountType; 2637 try { 2638 while (c.moveToNext()) { 2639 accountName = c.getString(0); 2640 accountType = c.getString(1); 2641 if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 2642 Account account = new Account(accountName, accountType); 2643 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, 2644 new Bundle()); 2645 break; 2646 } 2647 } 2648 } finally { 2649 c.close(); 2650 } 2651 } 2652 return count; 2653 } 2654 2655 private int updateSettings(Uri uri, ContentValues values, String selection, 2656 String[] selectionArgs) { 2657 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 2658 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2659 mVisibleTouched = true; 2660 } 2661 return count; 2662 } 2663 2664 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs, 2665 boolean callerIsSyncAdapter) { 2666 if (values.containsKey(RawContacts.CONTACT_ID)) { 2667 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 2668 "in content values. Contact IDs are assigned automatically"); 2669 } 2670 2671 if (!callerIsSyncAdapter) { 2672 selection = DatabaseUtils.concatenateWhere(selection, 2673 RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0"); 2674 } 2675 2676 int count = 0; 2677 Cursor cursor = mDb.query(mDbHelper.getRawContactView(), 2678 new String[] { RawContacts._ID }, selection, 2679 selectionArgs, null, null, null); 2680 try { 2681 while (cursor.moveToNext()) { 2682 long rawContactId = cursor.getLong(0); 2683 updateRawContact(rawContactId, values, callerIsSyncAdapter); 2684 count++; 2685 } 2686 } finally { 2687 cursor.close(); 2688 } 2689 2690 return count; 2691 } 2692 2693 private int updateRawContact(long rawContactId, ContentValues values, 2694 boolean callerIsSyncAdapter) { 2695 final String selection = RawContacts._ID + " = ?"; 2696 mSelectionArgs1[0] = Long.toString(rawContactId); 2697 final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED) 2698 && values.getAsInteger(RawContacts.DELETED) == 0); 2699 int previousDeleted = 0; 2700 String accountType = null; 2701 String accountName = null; 2702 if (requestUndoDelete) { 2703 Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection, 2704 mSelectionArgs1, null, null, null); 2705 try { 2706 if (cursor.moveToFirst()) { 2707 previousDeleted = cursor.getInt(RawContactsQuery.DELETED); 2708 accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE); 2709 accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME); 2710 } 2711 } finally { 2712 cursor.close(); 2713 } 2714 values.put(ContactsContract.RawContacts.AGGREGATION_MODE, 2715 ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT); 2716 } 2717 2718 int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1); 2719 if (count != 0) { 2720 if (values.containsKey(RawContacts.AGGREGATION_MODE)) { 2721 int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE); 2722 2723 // As per ContactsContract documentation, changing aggregation mode 2724 // to DEFAULT should not trigger aggregation 2725 if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) { 2726 mContactAggregator.markForAggregation(rawContactId, aggregationMode, false); 2727 } 2728 } 2729 if (values.containsKey(RawContacts.STARRED)) { 2730 if (!callerIsSyncAdapter) { 2731 updateFavoritesMembership(rawContactId, 2732 values.getAsLong(RawContacts.STARRED) != 0); 2733 } 2734 mContactAggregator.updateStarred(rawContactId); 2735 } else { 2736 // if this raw contact is being associated with an account, then update the 2737 // favorites group membership based on whether or not this contact is starred. 2738 // If it is starred, add a group membership, if one doesn't already exist 2739 // otherwise delete any matching group memberships. 2740 if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { 2741 boolean starred = 0 != DatabaseUtils.longForQuery(mDb, 2742 SELECTION_STARRED_FROM_RAW_CONTACTS, 2743 new String[]{Long.toString(rawContactId)}); 2744 updateFavoritesMembership(rawContactId, starred); 2745 } 2746 } 2747 2748 // if this raw contact is being associated with an account, then add a 2749 // group membership to the group marked as AutoAdd, if any. 2750 if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { 2751 addAutoAddMembership(rawContactId); 2752 } 2753 2754 if (values.containsKey(RawContacts.SOURCE_ID)) { 2755 mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId); 2756 } 2757 if (values.containsKey(RawContacts.NAME_VERIFIED)) { 2758 2759 // If setting NAME_VERIFIED for this raw contact, reset it for all 2760 // other raw contacts in the same aggregate 2761 if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) { 2762 mDbHelper.resetNameVerifiedForOtherRawContacts(rawContactId); 2763 } 2764 mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId); 2765 } 2766 if (requestUndoDelete && previousDeleted == 1) { 2767 mTransactionContext.rawContactInserted(rawContactId, 2768 new Account(accountName, accountType)); 2769 } 2770 } 2771 return count; 2772 } 2773 2774 private int updateData(Uri uri, ContentValues values, String selection, 2775 String[] selectionArgs, boolean callerIsSyncAdapter) { 2776 mValues.clear(); 2777 mValues.putAll(values); 2778 mValues.remove(Data._ID); 2779 mValues.remove(Data.RAW_CONTACT_ID); 2780 mValues.remove(Data.MIMETYPE); 2781 2782 String packageName = values.getAsString(Data.RES_PACKAGE); 2783 if (packageName != null) { 2784 mValues.remove(Data.RES_PACKAGE); 2785 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2786 } 2787 2788 if (!callerIsSyncAdapter) { 2789 selection = DatabaseUtils.concatenateWhere(selection, 2790 Data.IS_READ_ONLY + "=0"); 2791 } 2792 2793 int count = 0; 2794 2795 // Note that the query will return data according to the access restrictions, 2796 // so we don't need to worry about updating data we don't have permission to read. 2797 Cursor c = query(uri, DataRowHandler.DataUpdateQuery.COLUMNS, 2798 selection, selectionArgs, null); 2799 try { 2800 while(c.moveToNext()) { 2801 count += updateData(mValues, c, callerIsSyncAdapter); 2802 } 2803 } finally { 2804 c.close(); 2805 } 2806 2807 return count; 2808 } 2809 2810 private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { 2811 if (values.size() == 0) { 2812 return 0; 2813 } 2814 2815 final String mimeType = c.getString(DataRowHandler.DataUpdateQuery.MIMETYPE); 2816 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2817 if (rowHandler.update(mDb, mTransactionContext, values, c, callerIsSyncAdapter)) { 2818 return 1; 2819 } else { 2820 return 0; 2821 } 2822 } 2823 2824 private int updateContactOptions(ContentValues values, String selection, 2825 String[] selectionArgs, boolean callerIsSyncAdapter) { 2826 int count = 0; 2827 Cursor cursor = mDb.query(mDbHelper.getContactView(), 2828 new String[] { Contacts._ID }, selection, 2829 selectionArgs, null, null, null); 2830 try { 2831 while (cursor.moveToNext()) { 2832 long contactId = cursor.getLong(0); 2833 updateContactOptions(contactId, values, callerIsSyncAdapter); 2834 count++; 2835 } 2836 } finally { 2837 cursor.close(); 2838 } 2839 2840 return count; 2841 } 2842 2843 private int updateContactOptions(long contactId, ContentValues values, 2844 boolean callerIsSyncAdapter) { 2845 2846 mValues.clear(); 2847 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 2848 values, Contacts.CUSTOM_RINGTONE); 2849 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 2850 values, Contacts.SEND_TO_VOICEMAIL); 2851 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 2852 values, Contacts.LAST_TIME_CONTACTED); 2853 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 2854 values, Contacts.TIMES_CONTACTED); 2855 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 2856 values, Contacts.STARRED); 2857 2858 // Nothing to update - just return 2859 if (mValues.size() == 0) { 2860 return 0; 2861 } 2862 2863 if (mValues.containsKey(RawContacts.STARRED)) { 2864 // Mark dirty when changing starred to trigger sync 2865 mValues.put(RawContacts.DIRTY, 1); 2866 } 2867 2868 mSelectionArgs1[0] = String.valueOf(contactId); 2869 mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?" 2870 + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1); 2871 2872 if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) { 2873 Cursor cursor = mDb.query(mDbHelper.getRawContactView(), 2874 new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?", 2875 mSelectionArgs1, null, null, null); 2876 try { 2877 while (cursor.moveToNext()) { 2878 long rawContactId = cursor.getLong(0); 2879 updateFavoritesMembership(rawContactId, 2880 mValues.getAsLong(RawContacts.STARRED) != 0); 2881 } 2882 } finally { 2883 cursor.close(); 2884 } 2885 } 2886 2887 // Copy changeable values to prevent automatically managed fields from 2888 // being explicitly updated by clients. 2889 mValues.clear(); 2890 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 2891 values, Contacts.CUSTOM_RINGTONE); 2892 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 2893 values, Contacts.SEND_TO_VOICEMAIL); 2894 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 2895 values, Contacts.LAST_TIME_CONTACTED); 2896 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 2897 values, Contacts.TIMES_CONTACTED); 2898 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 2899 values, Contacts.STARRED); 2900 2901 int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1); 2902 2903 if (values.containsKey(Contacts.LAST_TIME_CONTACTED) && 2904 !values.containsKey(Contacts.TIMES_CONTACTED)) { 2905 mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1); 2906 mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1); 2907 } 2908 return rslt; 2909 } 2910 2911 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 2912 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 2913 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 2914 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 2915 2916 long rawContactId1; 2917 long rawContactId2; 2918 if (rcId1 < rcId2) { 2919 rawContactId1 = rcId1; 2920 rawContactId2 = rcId2; 2921 } else { 2922 rawContactId2 = rcId1; 2923 rawContactId1 = rcId2; 2924 } 2925 2926 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 2927 mSelectionArgs2[0] = String.valueOf(rawContactId1); 2928 mSelectionArgs2[1] = String.valueOf(rawContactId2); 2929 db.delete(Tables.AGGREGATION_EXCEPTIONS, 2930 AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " 2931 + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2); 2932 } else { 2933 ContentValues exceptionValues = new ContentValues(3); 2934 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 2935 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 2936 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 2937 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 2938 exceptionValues); 2939 } 2940 2941 mContactAggregator.invalidateAggregationExceptionCache(); 2942 mContactAggregator.markForAggregation(rawContactId1, 2943 RawContacts.AGGREGATION_MODE_DEFAULT, true); 2944 mContactAggregator.markForAggregation(rawContactId2, 2945 RawContacts.AGGREGATION_MODE_DEFAULT, true); 2946 2947 mContactAggregator.aggregateContact(mTransactionContext, db, rawContactId1); 2948 mContactAggregator.aggregateContact(mTransactionContext, db, rawContactId2); 2949 2950 // The return value is fake - we just confirm that we made a change, not count actual 2951 // rows changed. 2952 return 1; 2953 } 2954 2955 public void onAccountsUpdated(Account[] accounts) { 2956 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS); 2957 } 2958 2959 protected boolean updateAccountsInBackground(Account[] accounts) { 2960 // TODO : Check the unit test. 2961 boolean accountsChanged = false; 2962 HashSet<Account> existingAccounts = new HashSet<Account>(); 2963 mDb = mDbHelper.getWritableDatabase(); 2964 mDb.beginTransaction(); 2965 try { 2966 findValidAccounts(existingAccounts); 2967 2968 // Add a row to the ACCOUNTS table for each new account 2969 for (Account account : accounts) { 2970 if (!existingAccounts.contains(account)) { 2971 accountsChanged = true; 2972 mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME 2973 + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)", 2974 new String[] {account.name, account.type}); 2975 } 2976 } 2977 2978 // Remove all valid accounts from the existing account set. What is left 2979 // in the accountsToDelete set will be extra accounts whose data must be deleted. 2980 HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts); 2981 for (Account account : accounts) { 2982 accountsToDelete.remove(account); 2983 } 2984 2985 if (!accountsToDelete.isEmpty()) { 2986 accountsChanged = true; 2987 for (Account account : accountsToDelete) { 2988 Log.d(TAG, "removing data for removed account " + account); 2989 String[] params = new String[] {account.name, account.type}; 2990 mDb.execSQL( 2991 "DELETE FROM " + Tables.GROUPS + 2992 " WHERE " + Groups.ACCOUNT_NAME + " = ?" + 2993 " AND " + Groups.ACCOUNT_TYPE + " = ?", params); 2994 mDb.execSQL( 2995 "DELETE FROM " + Tables.PRESENCE + 2996 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" + 2997 "SELECT " + RawContacts._ID + 2998 " FROM " + Tables.RAW_CONTACTS + 2999 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 3000 " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params); 3001 mDb.execSQL( 3002 "DELETE FROM " + Tables.RAW_CONTACTS + 3003 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 3004 " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params); 3005 mDb.execSQL( 3006 "DELETE FROM " + Tables.SETTINGS + 3007 " WHERE " + Settings.ACCOUNT_NAME + " = ?" + 3008 " AND " + Settings.ACCOUNT_TYPE + " = ?", params); 3009 mDb.execSQL( 3010 "DELETE FROM " + Tables.ACCOUNTS + 3011 " WHERE " + RawContacts.ACCOUNT_NAME + "=?" + 3012 " AND " + RawContacts.ACCOUNT_TYPE + "=?", params); 3013 mDb.execSQL( 3014 "DELETE FROM " + Tables.DIRECTORIES + 3015 " WHERE " + Directory.ACCOUNT_NAME + "=?" + 3016 " AND " + Directory.ACCOUNT_TYPE + "=?", params); 3017 resetDirectoryCache(); 3018 } 3019 3020 // Find all aggregated contacts that used to contain the raw contacts 3021 // we have just deleted and see if they are still referencing the deleted 3022 // names or photos. If so, fix up those contacts. 3023 HashSet<Long> orphanContactIds = Sets.newHashSet(); 3024 Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID + 3025 " FROM " + Tables.CONTACTS + 3026 " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " + 3027 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " + 3028 "(SELECT " + RawContacts._ID + 3029 " FROM " + Tables.RAW_CONTACTS + "))" + 3030 " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " + 3031 Contacts.PHOTO_ID + " NOT IN " + 3032 "(SELECT " + Data._ID + 3033 " FROM " + Tables.DATA + "))", null); 3034 try { 3035 while (cursor.moveToNext()) { 3036 orphanContactIds.add(cursor.getLong(0)); 3037 } 3038 } finally { 3039 cursor.close(); 3040 } 3041 3042 for (Long contactId : orphanContactIds) { 3043 mContactAggregator.updateAggregateData(mTransactionContext, contactId); 3044 } 3045 mDbHelper.updateAllVisible(); 3046 updateSearchIndexInTransaction(); 3047 } 3048 3049 if (accountsChanged) { 3050 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 3051 } 3052 mDb.setTransactionSuccessful(); 3053 } finally { 3054 mDb.endTransaction(); 3055 } 3056 mAccountWritability.clear(); 3057 3058 if (accountsChanged) { 3059 updateContactsAccountCount(accounts); 3060 updateProviderStatus(); 3061 } 3062 3063 return accountsChanged; 3064 } 3065 3066 private void updateContactsAccountCount(Account[] accounts) { 3067 int count = 0; 3068 for (Account account : accounts) { 3069 if (isContactsAccount(account)) { 3070 count++; 3071 } 3072 } 3073 mContactsAccountCount = count; 3074 } 3075 3076 protected boolean isContactsAccount(Account account) { 3077 final IContentService cs = ContentResolver.getContentService(); 3078 try { 3079 return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; 3080 } catch (RemoteException e) { 3081 Log.e(TAG, "Cannot obtain sync flag for account: " + account, e); 3082 return false; 3083 } 3084 } 3085 3086 public void onPackageChanged(String packageName) { 3087 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName); 3088 } 3089 3090 /** 3091 * Finds all distinct accounts present in the specified table. 3092 */ 3093 private void findValidAccounts(Set<Account> validAccounts) { 3094 Cursor c = mDb.rawQuery( 3095 "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE + 3096 " FROM " + Tables.ACCOUNTS, null); 3097 try { 3098 while (c.moveToNext()) { 3099 if (!c.isNull(0) || !c.isNull(1)) { 3100 validAccounts.add(new Account(c.getString(0), c.getString(1))); 3101 } 3102 } 3103 } finally { 3104 c.close(); 3105 } 3106 } 3107 3108 private static class DirectoryCursorWrapper extends CursorWrapper 3109 implements CrossProcessCursor { 3110 private final CrossProcessCursor mCrossProcessCursor; 3111 3112 public DirectoryCursorWrapper(Cursor cursor, CrossProcessCursor crossProcessCursor) { 3113 super(cursor); 3114 mCrossProcessCursor = crossProcessCursor; 3115 } 3116 3117 @Override 3118 public void fillWindow(int pos, CursorWindow window) { 3119 mCrossProcessCursor.fillWindow(pos, window); 3120 } 3121 3122 @Override 3123 public CursorWindow getWindow() { 3124 return mCrossProcessCursor.getWindow(); 3125 } 3126 3127 @Override 3128 public boolean onMove(int oldPosition, int newPosition) { 3129 return mCrossProcessCursor.onMove(oldPosition, newPosition); 3130 } 3131 } 3132 3133 @Override 3134 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 3135 String sortOrder) { 3136 3137 waitForAccess(mReadAccessLatch); 3138 3139 String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY); 3140 if (directory == null) { 3141 return queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1); 3142 } else if (directory.equals("0")) { 3143 return queryLocal(uri, projection, selection, selectionArgs, sortOrder, 3144 Directory.DEFAULT); 3145 } else if (directory.equals("1")) { 3146 return queryLocal(uri, projection, selection, selectionArgs, sortOrder, 3147 Directory.LOCAL_INVISIBLE); 3148 } 3149 3150 DirectoryInfo directoryInfo = getDirectoryAuthority(directory); 3151 if (directoryInfo == null) { 3152 Log.e(TAG, "Invalid directory ID: " + uri); 3153 return null; 3154 } 3155 3156 Builder builder = new Uri.Builder(); 3157 builder.scheme(ContentResolver.SCHEME_CONTENT); 3158 builder.authority(directoryInfo.authority); 3159 builder.encodedPath(uri.getEncodedPath()); 3160 if (directoryInfo.accountName != null) { 3161 builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName); 3162 } 3163 if (directoryInfo.accountType != null) { 3164 builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType); 3165 } 3166 3167 String limit = getLimit(uri); 3168 if (limit != null) { 3169 builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit); 3170 } 3171 3172 Uri directoryUri = builder.build(); 3173 3174 if (projection == null) { 3175 projection = getDefaultProjection(uri); 3176 } 3177 3178 Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection, 3179 selectionArgs, sortOrder); 3180 3181 if (cursor == null) { 3182 return null; 3183 } 3184 3185 CrossProcessCursor crossProcessCursor = getCrossProcessCursor(cursor); 3186 if (crossProcessCursor != null) { 3187 return new DirectoryCursorWrapper(cursor, crossProcessCursor); 3188 } else { 3189 return matrixCursorFromCursor(cursor); 3190 } 3191 } 3192 3193 private CrossProcessCursor getCrossProcessCursor(Cursor cursor) { 3194 Cursor c = cursor; 3195 if (c instanceof CrossProcessCursor) { 3196 return (CrossProcessCursor) c; 3197 } else if (c instanceof CursorWindow) { 3198 return getCrossProcessCursor(((CursorWrapper) c).getWrappedCursor()); 3199 } else { 3200 return null; 3201 } 3202 } 3203 3204 public MatrixCursor matrixCursorFromCursor(Cursor cursor) { 3205 MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames()); 3206 int numColumns = cursor.getColumnCount(); 3207 String data[] = new String[numColumns]; 3208 cursor.moveToPosition(-1); 3209 while (cursor.moveToNext()) { 3210 for (int i = 0; i < numColumns; i++) { 3211 data[i] = cursor.getString(i); 3212 } 3213 newCursor.addRow(data); 3214 } 3215 return newCursor; 3216 } 3217 3218 private static final class DirectoryQuery { 3219 public static final String[] COLUMNS = new String[] { 3220 Directory._ID, 3221 Directory.DIRECTORY_AUTHORITY, 3222 Directory.ACCOUNT_NAME, 3223 Directory.ACCOUNT_TYPE 3224 }; 3225 3226 public static final int DIRECTORY_ID = 0; 3227 public static final int AUTHORITY = 1; 3228 public static final int ACCOUNT_NAME = 2; 3229 public static final int ACCOUNT_TYPE = 3; 3230 } 3231 3232 /** 3233 * Reads and caches directory information for the database. 3234 */ 3235 private DirectoryInfo getDirectoryAuthority(String directoryId) { 3236 synchronized (mDirectoryCache) { 3237 if (!mDirectoryCacheValid) { 3238 mDirectoryCache.clear(); 3239 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 3240 Cursor cursor = db.query(Tables.DIRECTORIES, 3241 DirectoryQuery.COLUMNS, 3242 null, null, null, null, null); 3243 try { 3244 while (cursor.moveToNext()) { 3245 DirectoryInfo info = new DirectoryInfo(); 3246 String id = cursor.getString(DirectoryQuery.DIRECTORY_ID); 3247 info.authority = cursor.getString(DirectoryQuery.AUTHORITY); 3248 info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 3249 info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 3250 mDirectoryCache.put(id, info); 3251 } 3252 } finally { 3253 cursor.close(); 3254 } 3255 mDirectoryCacheValid = true; 3256 } 3257 3258 return mDirectoryCache.get(directoryId); 3259 } 3260 } 3261 3262 public void resetDirectoryCache() { 3263 synchronized(mDirectoryCache) { 3264 mDirectoryCacheValid = false; 3265 } 3266 } 3267 3268 public Cursor queryLocal(Uri uri, String[] projection, String selection, String[] selectionArgs, 3269 String sortOrder, long directoryId) { 3270 if (VERBOSE_LOGGING) { 3271 Log.v(TAG, "query: " + uri); 3272 } 3273 3274 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 3275 3276 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3277 String groupBy = null; 3278 String limit = getLimit(uri); 3279 3280 // TODO: Consider writing a test case for RestrictionExceptions when you 3281 // write a new query() block to make sure it protects restricted data. 3282 final int match = sUriMatcher.match(uri); 3283 switch (match) { 3284 case SYNCSTATE: 3285 return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, 3286 sortOrder); 3287 3288 case CONTACTS: { 3289 setTablesAndProjectionMapForContacts(qb, uri, projection); 3290 appendLocalDirectorySelectionIfNeeded(qb, directoryId); 3291 break; 3292 } 3293 3294 case CONTACTS_ID: { 3295 long contactId = ContentUris.parseId(uri); 3296 setTablesAndProjectionMapForContacts(qb, uri, projection); 3297 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3298 qb.appendWhere(Contacts._ID + "=?"); 3299 break; 3300 } 3301 3302 case CONTACTS_LOOKUP: 3303 case CONTACTS_LOOKUP_ID: { 3304 List<String> pathSegments = uri.getPathSegments(); 3305 int segmentCount = pathSegments.size(); 3306 if (segmentCount < 3) { 3307 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3308 "Missing a lookup key", uri)); 3309 } 3310 3311 String lookupKey = pathSegments.get(2); 3312 if (segmentCount == 4) { 3313 long contactId = Long.parseLong(pathSegments.get(3)); 3314 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3315 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 3316 3317 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 3318 projection, selection, selectionArgs, sortOrder, groupBy, limit, 3319 Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey); 3320 if (c != null) { 3321 return c; 3322 } 3323 } 3324 3325 setTablesAndProjectionMapForContacts(qb, uri, projection); 3326 selectionArgs = insertSelectionArg(selectionArgs, 3327 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 3328 qb.appendWhere(Contacts._ID + "=?"); 3329 break; 3330 } 3331 3332 case CONTACTS_LOOKUP_DATA: 3333 case CONTACTS_LOOKUP_ID_DATA: { 3334 List<String> pathSegments = uri.getPathSegments(); 3335 int segmentCount = pathSegments.size(); 3336 if (segmentCount < 4) { 3337 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3338 "Missing a lookup key", uri)); 3339 } 3340 String lookupKey = pathSegments.get(2); 3341 if (segmentCount == 5) { 3342 long contactId = Long.parseLong(pathSegments.get(3)); 3343 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3344 setTablesAndProjectionMapForData(lookupQb, uri, projection, false); 3345 lookupQb.appendWhere(" AND "); 3346 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 3347 projection, selection, selectionArgs, sortOrder, groupBy, limit, 3348 Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey); 3349 if (c != null) { 3350 return c; 3351 } 3352 3353 // TODO see if the contact exists but has no data rows (rare) 3354 } 3355 3356 setTablesAndProjectionMapForData(qb, uri, projection, false); 3357 selectionArgs = insertSelectionArg(selectionArgs, 3358 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 3359 qb.appendWhere(" AND " + Data.CONTACT_ID + "=?"); 3360 break; 3361 } 3362 3363 case CONTACTS_AS_VCARD: { 3364 // When reading as vCard always use restricted view 3365 final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); 3366 qb.setTables(mDbHelper.getContactView(true /* require restricted */)); 3367 qb.setProjectionMap(sContactsVCardProjectionMap); 3368 selectionArgs = insertSelectionArg(selectionArgs, 3369 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 3370 qb.appendWhere(Contacts._ID + "=?"); 3371 break; 3372 } 3373 3374 case CONTACTS_AS_MULTI_VCARD: { 3375 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); 3376 String currentDateString = dateFormat.format(new Date()).toString(); 3377 return db.rawQuery( 3378 "SELECT" + 3379 " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," + 3380 " NULL AS " + OpenableColumns.SIZE, 3381 new String[] { currentDateString }); 3382 } 3383 3384 case CONTACTS_FILTER: { 3385 String filterParam = ""; 3386 if (uri.getPathSegments().size() > 2) { 3387 filterParam = uri.getLastPathSegment(); 3388 } 3389 setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam); 3390 appendLocalDirectorySelectionIfNeeded(qb, directoryId); 3391 break; 3392 } 3393 3394 case CONTACTS_STREQUENT_FILTER: 3395 case CONTACTS_STREQUENT: { 3396 String filterSql = null; 3397 if (match == CONTACTS_STREQUENT_FILTER 3398 && uri.getPathSegments().size() > 3) { 3399 String filterParam = uri.getLastPathSegment(); 3400 StringBuilder sb = new StringBuilder(); 3401 sb.append(Contacts._ID + " IN "); 3402 appendContactFilterAsNestedQuery(sb, filterParam); 3403 filterSql = sb.toString(); 3404 } 3405 3406 setTablesAndProjectionMapForContacts(qb, uri, projection); 3407 3408 String[] starredProjection = null; 3409 String[] frequentProjection = null; 3410 if (projection != null) { 3411 starredProjection = 3412 appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN); 3413 frequentProjection = 3414 appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN); 3415 } 3416 3417 // Build the first query for starred 3418 if (filterSql != null) { 3419 qb.appendWhere(filterSql); 3420 } 3421 qb.setProjectionMap(sStrequentStarredProjectionMap); 3422 final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1", 3423 null, Contacts._ID, null, null, null); 3424 3425 // Build the second query for frequent 3426 qb = new SQLiteQueryBuilder(); 3427 setTablesAndProjectionMapForContacts(qb, uri, projection); 3428 if (filterSql != null) { 3429 qb.appendWhere(filterSql); 3430 } 3431 qb.setProjectionMap(sStrequentFrequentProjectionMap); 3432 final String frequentQuery = qb.buildQuery(frequentProjection, 3433 Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED 3434 + " = 0 OR " + Contacts.STARRED + " IS NULL)", 3435 null, Contacts._ID, null, null, null); 3436 3437 // Put them together 3438 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 3439 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 3440 Cursor c = db.rawQuery(query, null); 3441 if (c != null) { 3442 c.setNotificationUri(getContext().getContentResolver(), 3443 ContactsContract.AUTHORITY_URI); 3444 } 3445 return c; 3446 } 3447 3448 case CONTACTS_GROUP: { 3449 setTablesAndProjectionMapForContacts(qb, uri, projection); 3450 if (uri.getPathSegments().size() > 2) { 3451 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 3452 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3453 } 3454 break; 3455 } 3456 3457 case CONTACTS_ID_DATA: { 3458 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3459 setTablesAndProjectionMapForData(qb, uri, projection, false); 3460 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3461 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 3462 break; 3463 } 3464 3465 case CONTACTS_ID_PHOTO: { 3466 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3467 setTablesAndProjectionMapForData(qb, uri, projection, false); 3468 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3469 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 3470 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 3471 break; 3472 } 3473 3474 case CONTACTS_ID_ENTITIES: { 3475 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3476 setTablesAndProjectionMapForEntities(qb, uri, projection); 3477 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3478 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 3479 break; 3480 } 3481 3482 case CONTACTS_LOOKUP_ENTITIES: 3483 case CONTACTS_LOOKUP_ID_ENTITIES: { 3484 List<String> pathSegments = uri.getPathSegments(); 3485 int segmentCount = pathSegments.size(); 3486 if (segmentCount < 4) { 3487 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3488 "Missing a lookup key", uri)); 3489 } 3490 String lookupKey = pathSegments.get(2); 3491 if (segmentCount == 5) { 3492 long contactId = Long.parseLong(pathSegments.get(3)); 3493 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3494 setTablesAndProjectionMapForEntities(lookupQb, uri, projection); 3495 lookupQb.appendWhere(" AND "); 3496 3497 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 3498 projection, selection, selectionArgs, sortOrder, groupBy, limit, 3499 Contacts.Entity.CONTACT_ID, contactId, 3500 Contacts.Entity.LOOKUP_KEY, lookupKey); 3501 if (c != null) { 3502 return c; 3503 } 3504 } 3505 3506 setTablesAndProjectionMapForEntities(qb, uri, projection); 3507 selectionArgs = insertSelectionArg(selectionArgs, 3508 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 3509 qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?"); 3510 break; 3511 } 3512 3513 case PHONES: { 3514 setTablesAndProjectionMapForData(qb, uri, projection, false); 3515 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3516 break; 3517 } 3518 3519 case PHONES_ID: { 3520 setTablesAndProjectionMapForData(qb, uri, projection, false); 3521 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3522 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3523 qb.appendWhere(" AND " + Data._ID + "=?"); 3524 break; 3525 } 3526 3527 case PHONES_FILTER: { 3528 setTablesAndProjectionMapForData(qb, uri, projection, true); 3529 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3530 if (uri.getPathSegments().size() > 2) { 3531 String filterParam = uri.getLastPathSegment(); 3532 StringBuilder sb = new StringBuilder(); 3533 sb.append(" AND ("); 3534 3535 boolean hasCondition = false; 3536 boolean orNeeded = false; 3537 String normalizedName = NameNormalizer.normalize(filterParam); 3538 if (normalizedName.length() > 0) { 3539 sb.append(Data.RAW_CONTACT_ID + " IN "); 3540 appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); 3541 orNeeded = true; 3542 hasCondition = true; 3543 } 3544 3545 String number = PhoneNumberUtils.normalizeNumber(filterParam); 3546 if (!TextUtils.isEmpty(number)) { 3547 if (orNeeded) { 3548 sb.append(" OR "); 3549 } 3550 sb.append(Data._ID + 3551 " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID 3552 + " FROM " + Tables.PHONE_LOOKUP 3553 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 3554 sb.append(number); 3555 sb.append("%')"); 3556 hasCondition = true; 3557 } 3558 3559 if (!hasCondition) { 3560 // If it is neither a phone number nor a name, the query should return 3561 // an empty cursor. Let's ensure that. 3562 sb.append("0"); 3563 } 3564 sb.append(")"); 3565 qb.appendWhere(sb); 3566 } 3567 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 3568 if (sortOrder == null) { 3569 sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; 3570 } 3571 break; 3572 } 3573 3574 case EMAILS: { 3575 setTablesAndProjectionMapForData(qb, uri, projection, false); 3576 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3577 break; 3578 } 3579 3580 case EMAILS_ID: { 3581 setTablesAndProjectionMapForData(qb, uri, projection, false); 3582 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3583 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'" 3584 + " AND " + Data._ID + "=?"); 3585 break; 3586 } 3587 3588 case EMAILS_LOOKUP: { 3589 setTablesAndProjectionMapForData(qb, uri, projection, false); 3590 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3591 if (uri.getPathSegments().size() > 2) { 3592 String email = uri.getLastPathSegment(); 3593 String address = mDbHelper.extractAddressFromEmailAddress(email); 3594 selectionArgs = insertSelectionArg(selectionArgs, address); 3595 qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)"); 3596 } 3597 break; 3598 } 3599 3600 case EMAILS_FILTER: { 3601 setTablesAndProjectionMapForData(qb, uri, projection, true); 3602 String filterParam = null; 3603 if (uri.getPathSegments().size() > 3) { 3604 filterParam = uri.getLastPathSegment(); 3605 if (TextUtils.isEmpty(filterParam)) { 3606 filterParam = null; 3607 } 3608 } 3609 3610 if (filterParam == null) { 3611 // If the filter is unspecified, return nothing 3612 qb.appendWhere(" AND 0"); 3613 } else { 3614 StringBuilder sb = new StringBuilder(); 3615 sb.append(" AND " + Data._ID + " IN ("); 3616 sb.append( 3617 "SELECT " + Data._ID + 3618 " FROM " + Tables.DATA + 3619 " WHERE " + DataColumns.MIMETYPE_ID + "="); 3620 sb.append(mDbHelper.getMimeTypeIdForEmail()); 3621 sb.append(" AND " + Data.DATA1 + " LIKE "); 3622 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); 3623 if (!filterParam.contains("@")) { 3624 String normalizedName = NameNormalizer.normalize(filterParam); 3625 if (normalizedName.length() > 0) { 3626 3627 /* 3628 * Using a UNION instead of an "OR" to make SQLite use the right 3629 * indexes. We need it to use the (mimetype,data1) index for the 3630 * email lookup (see above), but not for the name lookup. 3631 * SQLite is not smart enough to use the index on one side of an OR 3632 * but not on the other. Using two separate nested queries 3633 * and a UNION between them does the job. 3634 */ 3635 sb.append( 3636 " UNION SELECT " + Data._ID + 3637 " FROM " + Tables.DATA + 3638 " WHERE +" + DataColumns.MIMETYPE_ID + "="); 3639 sb.append(mDbHelper.getMimeTypeIdForEmail()); 3640 sb.append(" AND " + Data.RAW_CONTACT_ID + " IN "); 3641 appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); 3642 } 3643 } 3644 sb.append(")"); 3645 qb.appendWhere(sb); 3646 } 3647 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 3648 if (sortOrder == null) { 3649 sortOrder = EMAIL_FILTER_SORT_ORDER; 3650 } 3651 break; 3652 } 3653 3654 case POSTALS: { 3655 setTablesAndProjectionMapForData(qb, uri, projection, false); 3656 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 3657 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 3658 break; 3659 } 3660 3661 case POSTALS_ID: { 3662 setTablesAndProjectionMapForData(qb, uri, projection, false); 3663 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3664 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 3665 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 3666 qb.appendWhere(" AND " + Data._ID + "=?"); 3667 break; 3668 } 3669 3670 case RAW_CONTACTS: { 3671 setTablesAndProjectionMapForRawContacts(qb, uri); 3672 break; 3673 } 3674 3675 case RAW_CONTACTS_ID: { 3676 long rawContactId = ContentUris.parseId(uri); 3677 setTablesAndProjectionMapForRawContacts(qb, uri); 3678 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3679 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 3680 break; 3681 } 3682 3683 case RAW_CONTACTS_DATA: { 3684 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 3685 setTablesAndProjectionMapForData(qb, uri, projection, false); 3686 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3687 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?"); 3688 break; 3689 } 3690 3691 case DATA: { 3692 setTablesAndProjectionMapForData(qb, uri, projection, false); 3693 break; 3694 } 3695 3696 case DATA_ID: { 3697 setTablesAndProjectionMapForData(qb, uri, projection, false); 3698 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3699 qb.appendWhere(" AND " + Data._ID + "=?"); 3700 break; 3701 } 3702 3703 case PHONE_LOOKUP: { 3704 3705 if (TextUtils.isEmpty(sortOrder)) { 3706 // Default the sort order to something reasonable so we get consistent 3707 // results when callers don't request an ordering 3708 sortOrder = " length(lookup.normalized_number) DESC"; 3709 } 3710 3711 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 3712 String numberE164 = PhoneNumberUtils.formatNumberToE164(number, 3713 mDbHelper.getCurrentCountryIso()); 3714 String normalizedNumber = 3715 PhoneNumberUtils.normalizeNumber(number); 3716 mDbHelper.buildPhoneLookupAndContactQuery(qb, normalizedNumber, numberE164); 3717 qb.setProjectionMap(sPhoneLookupProjectionMap); 3718 // Phone lookup cannot be combined with a selection 3719 selection = null; 3720 selectionArgs = null; 3721 break; 3722 } 3723 3724 case GROUPS: { 3725 qb.setTables(mDbHelper.getGroupView()); 3726 qb.setProjectionMap(sGroupsProjectionMap); 3727 appendAccountFromParameter(qb, uri); 3728 break; 3729 } 3730 3731 case GROUPS_ID: { 3732 qb.setTables(mDbHelper.getGroupView()); 3733 qb.setProjectionMap(sGroupsProjectionMap); 3734 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3735 qb.appendWhere(Groups._ID + "=?"); 3736 break; 3737 } 3738 3739 case GROUPS_SUMMARY: { 3740 qb.setTables(mDbHelper.getGroupView() + " AS groups"); 3741 qb.setProjectionMap(sGroupsSummaryProjectionMap); 3742 appendAccountFromParameter(qb, uri); 3743 groupBy = Groups._ID; 3744 break; 3745 } 3746 3747 case AGGREGATION_EXCEPTIONS: { 3748 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 3749 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 3750 break; 3751 } 3752 3753 case AGGREGATION_SUGGESTIONS: { 3754 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3755 String filter = null; 3756 if (uri.getPathSegments().size() > 3) { 3757 filter = uri.getPathSegments().get(3); 3758 } 3759 final int maxSuggestions; 3760 if (limit != null) { 3761 maxSuggestions = Integer.parseInt(limit); 3762 } else { 3763 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 3764 } 3765 3766 ArrayList<AggregationSuggestionParameter> parameters = null; 3767 List<String> query = uri.getQueryParameters("query"); 3768 if (query != null && !query.isEmpty()) { 3769 parameters = new ArrayList<AggregationSuggestionParameter>(query.size()); 3770 for (String parameter : query) { 3771 int offset = parameter.indexOf(':'); 3772 parameters.add(offset == -1 3773 ? new AggregationSuggestionParameter( 3774 AggregationSuggestions.PARAMETER_MATCH_NAME, 3775 parameter) 3776 : new AggregationSuggestionParameter( 3777 parameter.substring(0, offset), 3778 parameter.substring(offset + 1))); 3779 } 3780 } 3781 3782 setTablesAndProjectionMapForContacts(qb, uri, projection); 3783 3784 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, 3785 maxSuggestions, filter, parameters); 3786 } 3787 3788 case SETTINGS: { 3789 qb.setTables(Tables.SETTINGS); 3790 qb.setProjectionMap(sSettingsProjectionMap); 3791 appendAccountFromParameter(qb, uri); 3792 3793 // When requesting specific columns, this query requires 3794 // late-binding of the GroupMembership MIME-type. 3795 final String groupMembershipMimetypeId = Long.toString(mDbHelper 3796 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 3797 if (projection != null && projection.length != 0 && 3798 mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 3799 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3800 } 3801 if (projection != null && projection.length != 0 && 3802 mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 3803 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3804 } 3805 3806 break; 3807 } 3808 3809 case STATUS_UPDATES: { 3810 setTableAndProjectionMapForStatusUpdates(qb, projection); 3811 break; 3812 } 3813 3814 case STATUS_UPDATES_ID: { 3815 setTableAndProjectionMapForStatusUpdates(qb, projection); 3816 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3817 qb.appendWhere(DataColumns.CONCRETE_ID + "=?"); 3818 break; 3819 } 3820 3821 case SEARCH_SUGGESTIONS: { 3822 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); 3823 } 3824 3825 case SEARCH_SHORTCUT: { 3826 String lookupKey = uri.getLastPathSegment(); 3827 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection); 3828 } 3829 3830 case LIVE_FOLDERS_CONTACTS: 3831 qb.setTables(mDbHelper.getContactView()); 3832 qb.setProjectionMap(sLiveFoldersProjectionMap); 3833 break; 3834 3835 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 3836 qb.setTables(mDbHelper.getContactView()); 3837 qb.setProjectionMap(sLiveFoldersProjectionMap); 3838 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 3839 break; 3840 3841 case LIVE_FOLDERS_CONTACTS_FAVORITES: 3842 qb.setTables(mDbHelper.getContactView()); 3843 qb.setProjectionMap(sLiveFoldersProjectionMap); 3844 qb.appendWhere(Contacts.STARRED + "=1"); 3845 break; 3846 3847 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 3848 qb.setTables(mDbHelper.getContactView()); 3849 qb.setProjectionMap(sLiveFoldersProjectionMap); 3850 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 3851 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3852 break; 3853 3854 case RAW_CONTACT_ENTITIES: { 3855 setTablesAndProjectionMapForRawEntities(qb, uri); 3856 break; 3857 } 3858 3859 case RAW_CONTACT_ENTITY_ID: { 3860 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 3861 setTablesAndProjectionMapForRawEntities(qb, uri); 3862 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3863 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 3864 break; 3865 } 3866 3867 case PROVIDER_STATUS: { 3868 return queryProviderStatus(uri, projection); 3869 } 3870 3871 case DIRECTORIES : { 3872 qb.setTables(Tables.DIRECTORIES); 3873 qb.setProjectionMap(sDirectoryProjectionMap); 3874 break; 3875 } 3876 3877 case DIRECTORIES_ID : { 3878 long id = ContentUris.parseId(uri); 3879 qb.setTables(Tables.DIRECTORIES); 3880 qb.setProjectionMap(sDirectoryProjectionMap); 3881 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id)); 3882 qb.appendWhere(Directory._ID + "=?"); 3883 break; 3884 } 3885 3886 case COMPLETE_NAME: { 3887 return completeName(uri, projection); 3888 } 3889 3890 default: 3891 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 3892 sortOrder, limit); 3893 } 3894 3895 qb.setStrictProjectionMap(true); 3896 3897 Cursor cursor = 3898 query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 3899 if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) { 3900 cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder); 3901 } 3902 return cursor; 3903 } 3904 3905 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 3906 String selection, String[] selectionArgs, String sortOrder, String groupBy, 3907 String limit) { 3908 if (projection != null && projection.length == 1 3909 && BaseColumns._COUNT.equals(projection[0])) { 3910 qb.setProjectionMap(sCountProjectionMap); 3911 } 3912 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 3913 sortOrder, limit); 3914 if (c != null) { 3915 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 3916 } 3917 return c; 3918 } 3919 3920 /** 3921 * Creates a single-row cursor containing the current status of the provider. 3922 */ 3923 private Cursor queryProviderStatus(Uri uri, String[] projection) { 3924 MatrixCursor cursor = new MatrixCursor(projection); 3925 RowBuilder row = cursor.newRow(); 3926 for (int i = 0; i < projection.length; i++) { 3927 if (ProviderStatus.STATUS.equals(projection[i])) { 3928 row.add(mProviderStatus); 3929 } else if (ProviderStatus.DATA1.equals(projection[i])) { 3930 row.add(mEstimatedStorageRequirement); 3931 } 3932 } 3933 return cursor; 3934 } 3935 3936 /** 3937 * Runs the query with the supplied contact ID and lookup ID. If the query succeeds, 3938 * it returns the resulting cursor, otherwise it returns null and the calling 3939 * method needs to resolve the lookup key and rerun the query. 3940 */ 3941 private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb, 3942 SQLiteDatabase db, Uri uri, 3943 String[] projection, String selection, String[] selectionArgs, 3944 String sortOrder, String groupBy, String limit, 3945 String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) { 3946 String[] args; 3947 if (selectionArgs == null) { 3948 args = new String[2]; 3949 } else { 3950 args = new String[selectionArgs.length + 2]; 3951 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 3952 } 3953 args[0] = String.valueOf(contactId); 3954 args[1] = Uri.encode(lookupKey); 3955 lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?"); 3956 Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, 3957 groupBy, limit); 3958 if (c.getCount() != 0) { 3959 return c; 3960 } 3961 3962 c.close(); 3963 return null; 3964 } 3965 3966 private static final class AddressBookIndexQuery { 3967 public static final String LETTER = "letter"; 3968 public static final String TITLE = "title"; 3969 public static final String COUNT = "count"; 3970 3971 public static final String[] COLUMNS = new String[] { 3972 LETTER, TITLE, COUNT 3973 }; 3974 3975 public static final int COLUMN_LETTER = 0; 3976 public static final int COLUMN_TITLE = 1; 3977 public static final int COLUMN_COUNT = 2; 3978 3979 public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME; 3980 } 3981 3982 /** 3983 * Computes counts by the address book index titles and adds the resulting tally 3984 * to the returned cursor as a bundle of extras. 3985 */ 3986 private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db, 3987 SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) { 3988 String sortKey; 3989 3990 // The sort order suffix could be something like "DESC". 3991 // We want to preserve it in the query even though we will change 3992 // the sort column itself. 3993 String sortOrderSuffix = ""; 3994 if (sortOrder != null) { 3995 int spaceIndex = sortOrder.indexOf(' '); 3996 if (spaceIndex != -1) { 3997 sortKey = sortOrder.substring(0, spaceIndex); 3998 sortOrderSuffix = sortOrder.substring(spaceIndex); 3999 } else { 4000 sortKey = sortOrder; 4001 } 4002 } else { 4003 sortKey = Contacts.SORT_KEY_PRIMARY; 4004 } 4005 4006 String locale = getLocale().toString(); 4007 HashMap<String, String> projectionMap = Maps.newHashMap(); 4008 projectionMap.put(AddressBookIndexQuery.LETTER, 4009 "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER); 4010 4011 /** 4012 * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3, 4013 * to map the first letter of the sort key to a character that is traditionally 4014 * used in phonebooks to represent that letter. For example, in Korean it will 4015 * be the first consonant in the letter; for Japanese it will be Hiragana rather 4016 * than Katakana. 4017 */ 4018 projectionMap.put(AddressBookIndexQuery.TITLE, 4019 "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')" 4020 + " AS " + AddressBookIndexQuery.TITLE); 4021 projectionMap.put(AddressBookIndexQuery.COUNT, 4022 "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT); 4023 qb.setProjectionMap(projectionMap); 4024 4025 Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs, 4026 AddressBookIndexQuery.ORDER_BY, null /* having */, 4027 AddressBookIndexQuery.ORDER_BY + sortOrderSuffix); 4028 4029 try { 4030 int groupCount = indexCursor.getCount(); 4031 String titles[] = new String[groupCount]; 4032 int counts[] = new int[groupCount]; 4033 int indexCount = 0; 4034 String currentTitle = null; 4035 4036 // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up 4037 // with multiple entries for the same title. The following code 4038 // collapses those duplicates. 4039 for (int i = 0; i < groupCount; i++) { 4040 indexCursor.moveToNext(); 4041 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE); 4042 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT); 4043 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) { 4044 titles[indexCount] = currentTitle = title; 4045 counts[indexCount] = count; 4046 indexCount++; 4047 } else { 4048 counts[indexCount - 1] += count; 4049 } 4050 } 4051 4052 if (indexCount < groupCount) { 4053 String[] newTitles = new String[indexCount]; 4054 System.arraycopy(titles, 0, newTitles, 0, indexCount); 4055 titles = newTitles; 4056 4057 int[] newCounts = new int[indexCount]; 4058 System.arraycopy(counts, 0, newCounts, 0, indexCount); 4059 counts = newCounts; 4060 } 4061 4062 return new AddressBookCursor((CrossProcessCursor) cursor, titles, counts); 4063 } finally { 4064 indexCursor.close(); 4065 } 4066 } 4067 4068 /** 4069 * Returns the contact Id for the contact identified by the lookupKey. 4070 * Robust against changes in the lookup key: if the key has changed, will 4071 * look up the contact by the raw contact IDs or name encoded in the lookup 4072 * key. 4073 */ 4074 public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 4075 ContactLookupKey key = new ContactLookupKey(); 4076 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 4077 4078 long contactId = -1; 4079 if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) { 4080 contactId = lookupContactIdBySourceIds(db, segments); 4081 if (contactId != -1) { 4082 return contactId; 4083 } 4084 } 4085 4086 boolean hasRawContactIds = 4087 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID); 4088 if (hasRawContactIds) { 4089 contactId = lookupContactIdByRawContactIds(db, segments); 4090 if (contactId != -1) { 4091 return contactId; 4092 } 4093 } 4094 4095 if (hasRawContactIds 4096 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) { 4097 contactId = lookupContactIdByDisplayNames(db, segments); 4098 } 4099 4100 return contactId; 4101 } 4102 4103 private interface LookupBySourceIdQuery { 4104 String TABLE = Tables.RAW_CONTACTS; 4105 4106 String COLUMNS[] = { 4107 RawContacts.CONTACT_ID, 4108 RawContacts.ACCOUNT_TYPE, 4109 RawContacts.ACCOUNT_NAME, 4110 RawContacts.SOURCE_ID 4111 }; 4112 4113 int CONTACT_ID = 0; 4114 int ACCOUNT_TYPE = 1; 4115 int ACCOUNT_NAME = 2; 4116 int SOURCE_ID = 3; 4117 } 4118 4119 private long lookupContactIdBySourceIds(SQLiteDatabase db, 4120 ArrayList<LookupKeySegment> segments) { 4121 StringBuilder sb = new StringBuilder(); 4122 sb.append(RawContacts.SOURCE_ID + " IN ("); 4123 for (int i = 0; i < segments.size(); i++) { 4124 LookupKeySegment segment = segments.get(i); 4125 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) { 4126 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 4127 sb.append(","); 4128 } 4129 } 4130 sb.setLength(sb.length() - 1); // Last comma 4131 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 4132 4133 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 4134 sb.toString(), null, null, null, null); 4135 try { 4136 while (c.moveToNext()) { 4137 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); 4138 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 4139 int accountHashCode = 4140 ContactLookupKey.getAccountHashCode(accountType, accountName); 4141 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 4142 for (int i = 0; i < segments.size(); i++) { 4143 LookupKeySegment segment = segments.get(i); 4144 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID 4145 && accountHashCode == segment.accountHashCode 4146 && segment.key.equals(sourceId)) { 4147 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 4148 break; 4149 } 4150 } 4151 } 4152 } finally { 4153 c.close(); 4154 } 4155 4156 return getMostReferencedContactId(segments); 4157 } 4158 4159 private interface LookupByRawContactIdQuery { 4160 String TABLE = Tables.RAW_CONTACTS; 4161 4162 String COLUMNS[] = { 4163 RawContacts.CONTACT_ID, 4164 RawContacts.ACCOUNT_TYPE, 4165 RawContacts.ACCOUNT_NAME, 4166 RawContacts._ID, 4167 }; 4168 4169 int CONTACT_ID = 0; 4170 int ACCOUNT_TYPE = 1; 4171 int ACCOUNT_NAME = 2; 4172 int ID = 3; 4173 } 4174 4175 private long lookupContactIdByRawContactIds(SQLiteDatabase db, 4176 ArrayList<LookupKeySegment> segments) { 4177 StringBuilder sb = new StringBuilder(); 4178 sb.append(RawContacts._ID + " IN ("); 4179 for (int i = 0; i < segments.size(); i++) { 4180 LookupKeySegment segment = segments.get(i); 4181 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 4182 sb.append(segment.rawContactId); 4183 sb.append(","); 4184 } 4185 } 4186 sb.setLength(sb.length() - 1); // Last comma 4187 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 4188 4189 Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS, 4190 sb.toString(), null, null, null, null); 4191 try { 4192 while (c.moveToNext()) { 4193 String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE); 4194 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME); 4195 int accountHashCode = 4196 ContactLookupKey.getAccountHashCode(accountType, accountName); 4197 String rawContactId = c.getString(LookupByRawContactIdQuery.ID); 4198 for (int i = 0; i < segments.size(); i++) { 4199 LookupKeySegment segment = segments.get(i); 4200 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID 4201 && accountHashCode == segment.accountHashCode 4202 && segment.rawContactId.equals(rawContactId)) { 4203 segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID); 4204 break; 4205 } 4206 } 4207 } 4208 } finally { 4209 c.close(); 4210 } 4211 4212 return getMostReferencedContactId(segments); 4213 } 4214 4215 private interface LookupByDisplayNameQuery { 4216 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 4217 4218 String COLUMNS[] = { 4219 RawContacts.CONTACT_ID, 4220 RawContacts.ACCOUNT_TYPE, 4221 RawContacts.ACCOUNT_NAME, 4222 NameLookupColumns.NORMALIZED_NAME 4223 }; 4224 4225 int CONTACT_ID = 0; 4226 int ACCOUNT_TYPE = 1; 4227 int ACCOUNT_NAME = 2; 4228 int NORMALIZED_NAME = 3; 4229 } 4230 4231 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 4232 ArrayList<LookupKeySegment> segments) { 4233 StringBuilder sb = new StringBuilder(); 4234 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 4235 for (int i = 0; i < segments.size(); i++) { 4236 LookupKeySegment segment = segments.get(i); 4237 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 4238 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 4239 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 4240 sb.append(","); 4241 } 4242 } 4243 sb.setLength(sb.length() - 1); // Last comma 4244 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 4245 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 4246 4247 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 4248 sb.toString(), null, null, null, null); 4249 try { 4250 while (c.moveToNext()) { 4251 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); 4252 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 4253 int accountHashCode = 4254 ContactLookupKey.getAccountHashCode(accountType, accountName); 4255 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 4256 for (int i = 0; i < segments.size(); i++) { 4257 LookupKeySegment segment = segments.get(i); 4258 if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 4259 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) 4260 && accountHashCode == segment.accountHashCode 4261 && segment.key.equals(name)) { 4262 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 4263 break; 4264 } 4265 } 4266 } 4267 } finally { 4268 c.close(); 4269 } 4270 4271 return getMostReferencedContactId(segments); 4272 } 4273 4274 private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) { 4275 for (int i = 0; i < segments.size(); i++) { 4276 LookupKeySegment segment = segments.get(i); 4277 if (segment.lookupType == lookupType) { 4278 return true; 4279 } 4280 } 4281 4282 return false; 4283 } 4284 4285 public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { 4286 mContactAggregator.updateLookupKeyForRawContact(db, rawContactId); 4287 } 4288 4289 /** 4290 * Returns the contact ID that is mentioned the highest number of times. 4291 */ 4292 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 4293 Collections.sort(segments); 4294 4295 long bestContactId = -1; 4296 int bestRefCount = 0; 4297 4298 long contactId = -1; 4299 int count = 0; 4300 4301 int segmentCount = segments.size(); 4302 for (int i = 0; i < segmentCount; i++) { 4303 LookupKeySegment segment = segments.get(i); 4304 if (segment.contactId != -1) { 4305 if (segment.contactId == contactId) { 4306 count++; 4307 } else { 4308 if (count > bestRefCount) { 4309 bestContactId = contactId; 4310 bestRefCount = count; 4311 } 4312 contactId = segment.contactId; 4313 count = 1; 4314 } 4315 } 4316 } 4317 if (count > bestRefCount) { 4318 return contactId; 4319 } else { 4320 return bestContactId; 4321 } 4322 } 4323 4324 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 4325 String[] projection) { 4326 StringBuilder sb = new StringBuilder(); 4327 appendContactsTables(sb, uri, projection); 4328 qb.setTables(sb.toString()); 4329 qb.setProjectionMap(sContactsProjectionMap); 4330 } 4331 4332 /** 4333 * Finds name lookup records matching the supplied filter, picks one arbitrary match per 4334 * contact and joins that with other contacts tables. 4335 */ 4336 private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri, 4337 String[] projection, String filter) { 4338 4339 if (filter != null) { 4340 filter = filter.trim(); 4341 } 4342 4343 StringBuilder sb = new StringBuilder(); 4344 appendContactsTables(sb, uri, projection); 4345 if (TextUtils.isEmpty(filter)) { 4346 sb.append(" JOIN (SELECT NULL AS " + SearchSnippetColumns.SNIPPET + ") WHERE 0"); 4347 } else { 4348 appendSearchIndexJoin(sb, uri, projection, filter); 4349 } 4350 qb.setTables(sb.toString()); 4351 qb.setProjectionMap(sContactsProjectionWithSnippetMap); 4352 } 4353 4354 private void appendSearchIndexJoin( 4355 StringBuilder sb, Uri uri, String[] projection, String filter) { 4356 sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS snippet_contact_id"); 4357 4358 boolean snippetNeeded = mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET); 4359 boolean isEmailAddress = false; 4360 String emailAddress = null; 4361 boolean isPhoneNumber = false; 4362 String phoneNumber = null; 4363 if (snippetNeeded) { 4364 if (filter.indexOf('@') != -1) { 4365 emailAddress = mDbHelper.extractAddressFromEmailAddress(filter); 4366 isEmailAddress = !TextUtils.isEmpty(emailAddress); 4367 } else { 4368 isPhoneNumber = isPhoneNumber(filter); 4369 } 4370 4371 String[] args = null; 4372 String snippetArgs = 4373 getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY); 4374 if (snippetArgs != null) { 4375 args = snippetArgs.split(","); 4376 } 4377 4378 String startMatch = args != null && args.length > 0 ? args[0] 4379 : DEFAULT_SNIPPET_ARG_START_MATCH; 4380 String endMatch = args != null && args.length > 1 ? args[1] 4381 : DEFAULT_SNIPPET_ARG_END_MATCH; 4382 String ellipsis = args != null && args.length > 2 ? args[2] 4383 : DEFAULT_SNIPPET_ARG_ELLIPSIS; 4384 int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3]) 4385 : DEFAULT_SNIPPET_ARG_MAX_TOKENS; 4386 4387 sb.append(", "); 4388 if (isEmailAddress) { 4389 sb.append("ifnull("); 4390 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 4391 sb.append("||email_address||"); 4392 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 4393 sb.append(","); 4394 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 4395 sb.append(")"); 4396 } else if (isPhoneNumber) { 4397 sb.append("ifnull("); 4398 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 4399 sb.append("||phone_number||"); 4400 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 4401 sb.append(","); 4402 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 4403 sb.append(")"); 4404 } else { 4405 sb.append("(CASE WHEN name_contact_id NOT NULL THEN NULL ELSE "); 4406 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 4407 sb.append(" END)"); 4408 } 4409 sb.append(" AS " + SearchSnippetColumns.SNIPPET); 4410 } 4411 4412 sb.append(" FROM " + Tables.SEARCH_INDEX); 4413 4414 if (isEmailAddress) { 4415 sb.append(" LEFT OUTER JOIN " + 4416 "(SELECT " 4417 + RawContacts.CONTACT_ID + " AS email_contact_id," 4418 + "MIN(" + Email.ADDRESS + ") AS email_address" + 4419 " FROM " + Tables.DATA_JOIN_RAW_CONTACTS + 4420 " WHERE " + Email.ADDRESS + " LIKE "); 4421 DatabaseUtils.appendEscapedSQLString(sb, filter + "%"); 4422 sb.append(") ON (email_contact_id=snippet_contact_id)"); 4423 } else if (isPhoneNumber) { 4424 phoneNumber = PhoneNumberUtils.normalizeNumber(filter); 4425 sb.append(" LEFT OUTER JOIN " + 4426 "(SELECT " 4427 + RawContacts.CONTACT_ID + " AS phone_contact_id," 4428 + "MIN(" + Phone.NUMBER + ") AS phone_number" + 4429 " FROM " + Tables.DATA_JOIN_RAW_CONTACTS + 4430 " JOIN " + Tables.PHONE_LOOKUP + 4431 " ON(" + DataColumns.CONCRETE_ID + "=" 4432 + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID + ")" + 4433 " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 4434 sb.append(phoneNumber); 4435 sb.append("%'"); 4436 4437 String numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber, 4438 mDbHelper.getCountryIso()); 4439 if (!TextUtils.isEmpty(numberE164)) { 4440 sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 4441 sb.append(numberE164); 4442 sb.append("%'"); 4443 } 4444 sb.append(" GROUP BY phone_contact_id"); 4445 sb.append(") ON (phone_contact_id=snippet_contact_id)"); 4446 } else { 4447 sb.append(" LEFT OUTER JOIN " + 4448 "(SELECT DISTINCT " 4449 + RawContacts.CONTACT_ID + " AS name_contact_id" + 4450 " FROM " + Tables.RAW_CONTACTS + 4451 " JOIN " + Tables.NAME_LOOKUP + 4452 " ON(" + RawContactsColumns.CONCRETE_ID + "=" 4453 + NameLookupColumns.RAW_CONTACT_ID + ")"); 4454 4455 String normalizedFilter = NameNormalizer.normalize(filter); 4456 if (!TextUtils.isEmpty(normalizedFilter)) { 4457 sb.append(" WHERE normalized_name GLOB '"); 4458 sb.append(normalizedFilter); 4459 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + 4460 "=" + NameLookupType.NAME_COLLATION_KEY); 4461 } else { 4462 sb.append(" WHERE 0"); 4463 } 4464 sb.append(") ON (name_contact_id=snippet_contact_id)"); 4465 } 4466 4467 sb.append(" WHERE "); 4468 sb.append(Tables.SEARCH_INDEX + " MATCH "); 4469 if (isEmailAddress) { 4470 DatabaseUtils.appendEscapedSQLString(sb, "\"" + filter + "*\""); 4471 } else if (isPhoneNumber) { 4472 DatabaseUtils.appendEscapedSQLString(sb, "\"" + filter + "*\" OR " + phoneNumber + "*"); 4473 } else { 4474 DatabaseUtils.appendEscapedSQLString(sb, filter + "*"); 4475 } 4476 sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)"); 4477 } 4478 4479 private void appendSnippetFunction( 4480 StringBuilder sb, String startMatch, String endMatch, String ellipsis, int maxTokens) { 4481 sb.append("snippet(" + Tables.SEARCH_INDEX + ","); 4482 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 4483 sb.append(","); 4484 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 4485 sb.append(","); 4486 DatabaseUtils.appendEscapedSQLString(sb, ellipsis); 4487 4488 // The index of the column used for the snippet, "content" 4489 sb.append(",1,"); 4490 sb.append(maxTokens); 4491 sb.append(")"); 4492 } 4493 4494 private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) { 4495 boolean excludeRestrictedData = false; 4496 String requestingPackage = getQueryParameter(uri, 4497 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4498 if (requestingPackage != null) { 4499 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4500 } 4501 sb.append(mDbHelper.getContactView(excludeRestrictedData)); 4502 appendContactPresenceJoin(sb, projection, Contacts._ID); 4503 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 4504 } 4505 4506 private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) { 4507 StringBuilder sb = new StringBuilder(); 4508 boolean excludeRestrictedData = false; 4509 String requestingPackage = getQueryParameter(uri, 4510 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4511 if (requestingPackage != null) { 4512 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4513 } 4514 sb.append(mDbHelper.getRawContactView(excludeRestrictedData)); 4515 qb.setTables(sb.toString()); 4516 qb.setProjectionMap(sRawContactsProjectionMap); 4517 appendAccountFromParameter(qb, uri); 4518 } 4519 4520 private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) { 4521 qb.setTables(mDbHelper.getRawEntitiesView(shouldExcludeRestrictedData(uri))); 4522 qb.setProjectionMap(sRawEntityProjectionMap); 4523 appendAccountFromParameter(qb, uri); 4524 } 4525 4526 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 4527 String[] projection, boolean distinct) { 4528 StringBuilder sb = new StringBuilder(); 4529 sb.append(mDbHelper.getDataView(shouldExcludeRestrictedData(uri))); 4530 sb.append(" data"); 4531 4532 appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID); 4533 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 4534 appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); 4535 appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); 4536 4537 qb.setTables(sb.toString()); 4538 4539 boolean useDistinct = distinct 4540 || !mDbHelper.isInProjection(projection, DISTINCT_DATA_PROHIBITING_COLUMNS); 4541 qb.setDistinct(useDistinct); 4542 qb.setProjectionMap(useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap); 4543 appendAccountFromParameter(qb, uri); 4544 } 4545 4546 private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, 4547 String[] projection) { 4548 StringBuilder sb = new StringBuilder(); 4549 sb.append(mDbHelper.getDataView()); 4550 sb.append(" data"); 4551 appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); 4552 appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); 4553 4554 qb.setTables(sb.toString()); 4555 qb.setProjectionMap(sStatusUpdatesProjectionMap); 4556 } 4557 4558 private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri, 4559 String[] projection) { 4560 StringBuilder sb = new StringBuilder(); 4561 sb.append(mDbHelper.getEntitiesView(shouldExcludeRestrictedData(uri))); 4562 sb.append(" data"); 4563 4564 appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID); 4565 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 4566 appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID); 4567 appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID); 4568 4569 qb.setTables(sb.toString()); 4570 qb.setProjectionMap(sEntityProjectionMap); 4571 appendAccountFromParameter(qb, uri); 4572 } 4573 4574 private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection, 4575 String lastStatusUpdateIdColumn) { 4576 if (mDbHelper.isInProjection(projection, 4577 Contacts.CONTACT_STATUS, 4578 Contacts.CONTACT_STATUS_RES_PACKAGE, 4579 Contacts.CONTACT_STATUS_ICON, 4580 Contacts.CONTACT_STATUS_LABEL, 4581 Contacts.CONTACT_STATUS_TIMESTAMP)) { 4582 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 4583 + ContactsStatusUpdatesColumns.ALIAS + 4584 " ON (" + lastStatusUpdateIdColumn + "=" 4585 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 4586 } 4587 } 4588 4589 private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection, 4590 String dataIdColumn) { 4591 if (mDbHelper.isInProjection(projection, 4592 StatusUpdates.STATUS, 4593 StatusUpdates.STATUS_RES_PACKAGE, 4594 StatusUpdates.STATUS_ICON, 4595 StatusUpdates.STATUS_LABEL, 4596 StatusUpdates.STATUS_TIMESTAMP)) { 4597 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 4598 " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" 4599 + dataIdColumn + ")"); 4600 } 4601 } 4602 4603 private void appendContactPresenceJoin(StringBuilder sb, String[] projection, 4604 String contactIdColumn) { 4605 if (mDbHelper.isInProjection(projection, 4606 Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) { 4607 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 4608 " ON (" + contactIdColumn + " = " 4609 + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")"); 4610 } 4611 } 4612 4613 private void appendDataPresenceJoin(StringBuilder sb, String[] projection, 4614 String dataIdColumn) { 4615 if (mDbHelper.isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) { 4616 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 4617 " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")"); 4618 } 4619 } 4620 4621 private void appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) { 4622 if (directoryId == Directory.DEFAULT) { 4623 qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY); 4624 } else if (directoryId == Directory.LOCAL_INVISIBLE){ 4625 qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY); 4626 } 4627 } 4628 4629 private boolean shouldExcludeRestrictedData(Uri uri) { 4630 // Note: currently, "export only" equals to "restricted", but may not in the future. 4631 boolean excludeRestrictedData = readBooleanQueryParameter(uri, 4632 Data.FOR_EXPORT_ONLY, false); 4633 if (excludeRestrictedData) { 4634 return true; 4635 } 4636 4637 String requestingPackage = getQueryParameter(uri, 4638 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4639 if (requestingPackage != null) { 4640 return !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4641 } 4642 4643 return false; 4644 } 4645 4646 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 4647 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 4648 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 4649 4650 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 4651 if (partialUri) { 4652 // Throw when either account is incomplete 4653 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4654 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 4655 } 4656 4657 // Accounts are valid by only checking one parameter, since we've 4658 // already ruled out partial accounts. 4659 final boolean validAccount = !TextUtils.isEmpty(accountName); 4660 if (validAccount) { 4661 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 4662 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 4663 + RawContacts.ACCOUNT_TYPE + "=" 4664 + DatabaseUtils.sqlEscapeString(accountType)); 4665 } else { 4666 qb.appendWhere("1"); 4667 } 4668 } 4669 4670 private String appendAccountToSelection(Uri uri, String selection) { 4671 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 4672 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 4673 4674 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 4675 if (partialUri) { 4676 // Throw when either account is incomplete 4677 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4678 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 4679 } 4680 4681 // Accounts are valid by only checking one parameter, since we've 4682 // already ruled out partial accounts. 4683 final boolean validAccount = !TextUtils.isEmpty(accountName); 4684 if (validAccount) { 4685 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 4686 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 4687 + RawContacts.ACCOUNT_TYPE + "=" 4688 + DatabaseUtils.sqlEscapeString(accountType)); 4689 if (!TextUtils.isEmpty(selection)) { 4690 selectionSb.append(" AND ("); 4691 selectionSb.append(selection); 4692 selectionSb.append(')'); 4693 } 4694 return selectionSb.toString(); 4695 } else { 4696 return selection; 4697 } 4698 } 4699 4700 /** 4701 * Gets the value of the "limit" URI query parameter. 4702 * 4703 * @return A string containing a non-negative integer, or <code>null</code> if 4704 * the parameter is not set, or is set to an invalid value. 4705 */ 4706 private String getLimit(Uri uri) { 4707 String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY); 4708 if (limitParam == null) { 4709 return null; 4710 } 4711 // make sure that the limit is a non-negative integer 4712 try { 4713 int l = Integer.parseInt(limitParam); 4714 if (l < 0) { 4715 Log.w(TAG, "Invalid limit parameter: " + limitParam); 4716 return null; 4717 } 4718 return String.valueOf(l); 4719 } catch (NumberFormatException ex) { 4720 Log.w(TAG, "Invalid limit parameter: " + limitParam); 4721 return null; 4722 } 4723 } 4724 4725 String getContactsRestrictions() { 4726 if (mDbHelper.hasAccessToRestrictedData()) { 4727 return "1"; 4728 } else { 4729 return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0"; 4730 } 4731 } 4732 4733 public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { 4734 if (mDbHelper.hasAccessToRestrictedData()) { 4735 return "1"; 4736 } else { 4737 return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS 4738 + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; 4739 } 4740 } 4741 4742 @Override 4743 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 4744 4745 waitForAccess(mReadAccessLatch); 4746 4747 int match = sUriMatcher.match(uri); 4748 switch (match) { 4749 case CONTACTS_ID_PHOTO: { 4750 return openPhotoAssetFile(uri, mode, 4751 Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?", 4752 new String[]{uri.getPathSegments().get(1)}); 4753 } 4754 4755 case DATA_ID: { 4756 return openPhotoAssetFile(uri, mode, 4757 Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'", 4758 new String[]{uri.getPathSegments().get(1)}); 4759 } 4760 4761 case CONTACTS_AS_VCARD: { 4762 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4763 final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); 4764 mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(db, lookupKey)); 4765 final String selection = Contacts._ID + "=?"; 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, mSelectionArgs1); 4772 return buildAssetFileDescriptor(localStream); 4773 } 4774 4775 case CONTACTS_AS_MULTI_VCARD: { 4776 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4777 final String lookupKeys = uri.getPathSegments().get(2); 4778 final String[] loopupKeyList = lookupKeys.split(":"); 4779 final StringBuilder inBuilder = new StringBuilder(); 4780 int index = 0; 4781 // SQLite has limits on how many parameters can be used 4782 // so the IDs are concatenated to a query string here instead 4783 for (String lookupKey : loopupKeyList) { 4784 if (index == 0) { 4785 inBuilder.append("("); 4786 } else { 4787 inBuilder.append(","); 4788 } 4789 inBuilder.append(lookupContactIdByLookupKey(db, lookupKey)); 4790 index++; 4791 } 4792 inBuilder.append(')'); 4793 final String selection = Contacts._ID + " IN " + inBuilder.toString(); 4794 4795 // When opening a contact as file, we pass back contents as a 4796 // vCard-encoded stream. We build into a local buffer first, 4797 // then pipe into MemoryFile once the exact size is known. 4798 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 4799 outputRawContactsAsVCard(localStream, selection, null); 4800 return buildAssetFileDescriptor(localStream); 4801 } 4802 4803 default: 4804 throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist", 4805 uri)); 4806 } 4807 } 4808 4809 private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection, 4810 String[] selectionArgs) 4811 throws FileNotFoundException { 4812 if (!"r".equals(mode)) { 4813 throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode 4814 + " not supported.", uri)); 4815 } 4816 4817 String sql = 4818 "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() + 4819 " WHERE " + selection; 4820 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4821 try { 4822 return makeAssetFileDescriptor( 4823 DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs)); 4824 } catch (SQLiteDoneException e) { 4825 // this will happen if the DB query returns no rows (i.e. contact does not exist) 4826 throw new FileNotFoundException(uri.toString()); 4827 } 4828 } 4829 4830 private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; 4831 4832 /** 4833 * Returns an {@link AssetFileDescriptor} backed by the 4834 * contents of the given {@link ByteArrayOutputStream}. 4835 */ 4836 private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { 4837 try { 4838 stream.flush(); 4839 4840 final byte[] byteData = stream.toByteArray(); 4841 4842 return makeAssetFileDescriptor( 4843 ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME), 4844 byteData.length); 4845 } catch (IOException e) { 4846 Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString()); 4847 return null; 4848 } 4849 } 4850 4851 private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) { 4852 return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH); 4853 } 4854 4855 private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) { 4856 return fd != null ? new AssetFileDescriptor(fd, 0, length) : null; 4857 } 4858 4859 /** 4860 * Output {@link RawContacts} matching the requested selection in the vCard 4861 * format to the given {@link OutputStream}. This method returns silently if 4862 * any errors encountered. 4863 */ 4864 private void outputRawContactsAsVCard(OutputStream stream, String selection, 4865 String[] selectionArgs) { 4866 final Context context = this.getContext(); 4867 final VCardComposer composer = 4868 new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false); 4869 composer.addHandler(composer.new HandlerForOutputStream(stream)); 4870 4871 // No extra checks since composer always uses restricted views 4872 if (!composer.init(selection, selectionArgs)) { 4873 Log.w(TAG, "Failed to init VCardComposer"); 4874 return; 4875 } 4876 4877 while (!composer.isAfterLast()) { 4878 if (!composer.createOneEntry()) { 4879 Log.w(TAG, "Failed to output a contact."); 4880 } 4881 } 4882 composer.terminate(); 4883 } 4884 4885 @Override 4886 public String getType(Uri uri) { 4887 4888 waitForAccess(mReadAccessLatch); 4889 4890 final int match = sUriMatcher.match(uri); 4891 switch (match) { 4892 case CONTACTS: 4893 return Contacts.CONTENT_TYPE; 4894 case CONTACTS_LOOKUP: 4895 case CONTACTS_ID: 4896 case CONTACTS_LOOKUP_ID: 4897 return Contacts.CONTENT_ITEM_TYPE; 4898 case CONTACTS_AS_VCARD: 4899 case CONTACTS_AS_MULTI_VCARD: 4900 return Contacts.CONTENT_VCARD_TYPE; 4901 case CONTACTS_ID_PHOTO: 4902 return "image/png"; 4903 case RAW_CONTACTS: 4904 return RawContacts.CONTENT_TYPE; 4905 case RAW_CONTACTS_ID: 4906 return RawContacts.CONTENT_ITEM_TYPE; 4907 case DATA: 4908 return Data.CONTENT_TYPE; 4909 case DATA_ID: 4910 return mDbHelper.getDataMimeType(ContentUris.parseId(uri)); 4911 case PHONES: 4912 return Phone.CONTENT_TYPE; 4913 case PHONES_ID: 4914 return Phone.CONTENT_ITEM_TYPE; 4915 case PHONE_LOOKUP: 4916 return PhoneLookup.CONTENT_TYPE; 4917 case EMAILS: 4918 return Email.CONTENT_TYPE; 4919 case EMAILS_ID: 4920 return Email.CONTENT_ITEM_TYPE; 4921 case POSTALS: 4922 return StructuredPostal.CONTENT_TYPE; 4923 case POSTALS_ID: 4924 return StructuredPostal.CONTENT_ITEM_TYPE; 4925 case AGGREGATION_EXCEPTIONS: 4926 return AggregationExceptions.CONTENT_TYPE; 4927 case AGGREGATION_EXCEPTION_ID: 4928 return AggregationExceptions.CONTENT_ITEM_TYPE; 4929 case SETTINGS: 4930 return Settings.CONTENT_TYPE; 4931 case AGGREGATION_SUGGESTIONS: 4932 return Contacts.CONTENT_TYPE; 4933 case SEARCH_SUGGESTIONS: 4934 return SearchManager.SUGGEST_MIME_TYPE; 4935 case SEARCH_SHORTCUT: 4936 return SearchManager.SHORTCUT_MIME_TYPE; 4937 case DIRECTORIES: 4938 return Directory.CONTENT_TYPE; 4939 case DIRECTORIES_ID: 4940 return Directory.CONTENT_ITEM_TYPE; 4941 default: 4942 return mLegacyApiSupport.getType(uri); 4943 } 4944 } 4945 4946 public String[] getDefaultProjection(Uri uri) { 4947 final int match = sUriMatcher.match(uri); 4948 switch (match) { 4949 case CONTACTS: 4950 case CONTACTS_LOOKUP: 4951 case CONTACTS_ID: 4952 case CONTACTS_LOOKUP_ID: 4953 case AGGREGATION_SUGGESTIONS: 4954 return sContactsProjectionMap.getColumnNames(); 4955 4956 case CONTACTS_ID_ENTITIES: 4957 return sEntityProjectionMap.getColumnNames(); 4958 4959 case CONTACTS_AS_VCARD: 4960 case CONTACTS_AS_MULTI_VCARD: 4961 return sContactsVCardProjectionMap.getColumnNames(); 4962 4963 case RAW_CONTACTS: 4964 case RAW_CONTACTS_ID: 4965 return sRawContactsProjectionMap.getColumnNames(); 4966 4967 case DATA_ID: 4968 case PHONES: 4969 case PHONES_ID: 4970 case EMAILS: 4971 case EMAILS_ID: 4972 case POSTALS: 4973 case POSTALS_ID: 4974 return sDataProjectionMap.getColumnNames(); 4975 4976 case PHONE_LOOKUP: 4977 return sPhoneLookupProjectionMap.getColumnNames(); 4978 4979 case AGGREGATION_EXCEPTIONS: 4980 case AGGREGATION_EXCEPTION_ID: 4981 return sAggregationExceptionsProjectionMap.getColumnNames(); 4982 4983 case SETTINGS: 4984 return sSettingsProjectionMap.getColumnNames(); 4985 4986 case DIRECTORIES: 4987 case DIRECTORIES_ID: 4988 return sDirectoryProjectionMap.getColumnNames(); 4989 4990 default: 4991 return null; 4992 } 4993 } 4994 4995 private class StructuredNameLookupBuilder extends NameLookupBuilder { 4996 4997 public StructuredNameLookupBuilder(NameSplitter splitter) { 4998 super(splitter); 4999 } 5000 5001 @Override 5002 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 5003 String name) { 5004 mDbHelper.insertNameLookup(rawContactId, dataId, lookupType, name); 5005 } 5006 5007 @Override 5008 protected String[] getCommonNicknameClusters(String normalizedName) { 5009 return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); 5010 } 5011 } 5012 5013 public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 5014 sb.append("(" + 5015 "SELECT DISTINCT " + RawContacts.CONTACT_ID + 5016 " FROM " + Tables.RAW_CONTACTS + 5017 " JOIN " + Tables.NAME_LOOKUP + 5018 " ON(" + RawContactsColumns.CONCRETE_ID + "=" 5019 + NameLookupColumns.RAW_CONTACT_ID + ")" + 5020 " WHERE normalized_name GLOB '"); 5021 sb.append(NameNormalizer.normalize(filterParam)); 5022 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + 5023 " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))"); 5024 } 5025 5026 public String getRawContactsByFilterAsNestedQuery(String filterParam) { 5027 StringBuilder sb = new StringBuilder(); 5028 appendRawContactsByFilterAsNestedQuery(sb, filterParam); 5029 return sb.toString(); 5030 } 5031 5032 public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) { 5033 appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true); 5034 } 5035 5036 private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, 5037 boolean allowEmailMatch) { 5038 if (TextUtils.isEmpty(normalizedName)) { 5039 // Effectively an empty IN clause - SQL syntax does not allow an actual empty list here 5040 sb.append("(0)"); 5041 } else { 5042 sb.append("(" + 5043 "SELECT " + NameLookupColumns.RAW_CONTACT_ID + 5044 " FROM " + Tables.NAME_LOOKUP + 5045 " WHERE " + NameLookupColumns.NORMALIZED_NAME + 5046 " GLOB '"); 5047 // Should not use a "?" argument placeholder here, because 5048 // that would prevent the SQL optimizer from using the index on NORMALIZED_NAME. 5049 sb.append(normalizedName); 5050 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN (" 5051 + NameLookupType.NAME_COLLATION_KEY + "," 5052 + NameLookupType.NICKNAME + "," 5053 + NameLookupType.NAME_SHORTHAND + "," 5054 + NameLookupType.ORGANIZATION + "," 5055 + NameLookupType.NAME_CONSONANTS); 5056 if (allowEmailMatch) { 5057 sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME); 5058 } 5059 sb.append("))"); 5060 } 5061 } 5062 5063 5064 public boolean appendEmailBasedDataFilter(StringBuilder sb, String filter) { 5065 if (filter.indexOf('@') == -1) { 5066 return false; 5067 } 5068 5069 String address = mDbHelper.extractAddressFromEmailAddress(filter); 5070 if (TextUtils.isEmpty(address)) { 5071 return false; 5072 } 5073 5074 sb.append(DataColumns.MIMETYPE_ID + " IN ("); 5075 sb.append(mDbHelper.getMimeTypeIdForEmail()); 5076 sb.append(","); 5077 sb.append(mDbHelper.getMimeTypeIdForIm()); 5078 sb.append(","); 5079 sb.append(mDbHelper.getMimeTypeIdForSip()); 5080 sb.append(") AND " + Data.DATA1 + " LIKE("); 5081 DatabaseUtils.appendEscapedSQLString(sb, address + '%'); 5082 sb.append(")"); 5083 return true; 5084 } 5085 5086 public boolean appendPhoneNumberBasedDataFilter(StringBuilder sb, String filter) { 5087 if (!isPhoneNumber(filter)) { 5088 return false; 5089 } 5090 5091 String number = PhoneNumberUtils.normalizeNumber(filter); 5092 sb.append(DataColumns.CONCRETE_ID + " IN " + 5093 "(SELECT " + PhoneLookupColumns.DATA_ID 5094 + " FROM " + Tables.PHONE_LOOKUP 5095 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 5096 sb.append(number); 5097 sb.append("%'"); 5098 5099 String numberE164 = PhoneNumberUtils.formatNumberToE164(number, mDbHelper.getCountryIso()); 5100 if (!TextUtils.isEmpty(numberE164)) { 5101 sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 5102 sb.append(numberE164); 5103 sb.append("%'"); 5104 } 5105 sb.append(")"); 5106 5107 String normalizedFilter = NameNormalizer.normalize(filter); 5108 if (TextUtils.isEmpty(normalizedFilter)) { 5109 return true; 5110 } 5111 5112 sb.append(" OR " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN " + 5113 "(SELECT " + NameLookupColumns.RAW_CONTACT_ID + 5114 " FROM " + Tables.NAME_LOOKUP + 5115 " WHERE " + NameLookupColumns.NORMALIZED_NAME + 5116 " GLOB '"); 5117 sb.append(normalizedFilter); 5118 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN (" 5119 + CONTACT_LOOKUP_NAME_TYPES + "))"); 5120 return true; 5121 } 5122 5123 public boolean appendNameBasedRawContactFilter(StringBuilder sb, String filter) { 5124 String normalizedFilter = NameNormalizer.normalize(filter); 5125 if (TextUtils.isEmpty(normalizedFilter)) { 5126 return false; 5127 } 5128 5129 sb.append(DataColumns.CONCRETE_RAW_CONTACT_ID + " IN " + 5130 "(SELECT " + NameLookupColumns.RAW_CONTACT_ID + 5131 " FROM " + Tables.NAME_LOOKUP + 5132 " WHERE " + NameLookupColumns.NORMALIZED_NAME + 5133 " GLOB '"); 5134 // Should not use a "?" argument placeholder here, because 5135 // that would prevent the SQL optimizer from using the index on NORMALIZED_NAME. 5136 sb.append(normalizedFilter); 5137 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN (" 5138 + CONTACT_LOOKUP_NAME_TYPES + "))"); 5139 return true; 5140 } 5141 5142 public boolean isPhoneNumber(String filter) { 5143 boolean atLeastOneDigit = false; 5144 int len = filter.length(); 5145 for (int i = 0; i < len; i++) { 5146 char c = filter.charAt(i); 5147 if (c >= '0' && c <= '9') { 5148 atLeastOneDigit = true; 5149 } else if (c != '*' && c != '#' && c != '+' && c != 'N' && c != '.' && c != ';' 5150 && c != '-' && c != '(' && c != ')' && c != ' ') { 5151 return false; 5152 } 5153 } 5154 return atLeastOneDigit; 5155 } 5156 5157 /** 5158 * Takes components of a name from the query parameters and returns a cursor with those 5159 * components as well as all missing components. There is no database activity involved 5160 * in this so the call can be made on the UI thread. 5161 */ 5162 private Cursor completeName(Uri uri, String[] projection) { 5163 if (projection == null) { 5164 projection = sDataProjectionMap.getColumnNames(); 5165 } 5166 5167 ContentValues values = new ContentValues(); 5168 DataRowHandlerForStructuredName handler = (DataRowHandlerForStructuredName) 5169 getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE); 5170 5171 copyQueryParamsToContentValues(values, uri, 5172 StructuredName.DISPLAY_NAME, 5173 StructuredName.PREFIX, 5174 StructuredName.GIVEN_NAME, 5175 StructuredName.MIDDLE_NAME, 5176 StructuredName.FAMILY_NAME, 5177 StructuredName.SUFFIX, 5178 StructuredName.PHONETIC_NAME, 5179 StructuredName.PHONETIC_FAMILY_NAME, 5180 StructuredName.PHONETIC_MIDDLE_NAME, 5181 StructuredName.PHONETIC_GIVEN_NAME 5182 ); 5183 5184 handler.fixStructuredNameComponents(values, values); 5185 5186 MatrixCursor cursor = new MatrixCursor(projection); 5187 Object[] row = new Object[projection.length]; 5188 for (int i = 0; i < projection.length; i++) { 5189 row[i] = values.get(projection[i]); 5190 } 5191 cursor.addRow(row); 5192 return cursor; 5193 } 5194 5195 private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) { 5196 for (String column : columns) { 5197 String param = uri.getQueryParameter(column); 5198 if (param != null) { 5199 values.put(column, param); 5200 } 5201 } 5202 } 5203 5204 5205 /** 5206 * Inserts an argument at the beginning of the selection arg list. 5207 */ 5208 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 5209 if (selectionArgs == null) { 5210 return new String[] {arg}; 5211 } else { 5212 int newLength = selectionArgs.length + 1; 5213 String[] newSelectionArgs = new String[newLength]; 5214 newSelectionArgs[0] = arg; 5215 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 5216 return newSelectionArgs; 5217 } 5218 } 5219 5220 private String[] appendProjectionArg(String[] projection, String arg) { 5221 if (projection == null) { 5222 return null; 5223 } 5224 final int length = projection.length; 5225 String[] newProjection = new String[length + 1]; 5226 System.arraycopy(projection, 0, newProjection, 0, length); 5227 newProjection[length] = arg; 5228 return newProjection; 5229 } 5230 5231 protected Account getDefaultAccount() { 5232 AccountManager accountManager = AccountManager.get(getContext()); 5233 try { 5234 Account[] accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE); 5235 if (accounts != null && accounts.length > 0) { 5236 return accounts[0]; 5237 } 5238 } catch (Throwable e) { 5239 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 5240 } 5241 return null; 5242 } 5243 5244 /** 5245 * Returns true if the specified account type is writable. 5246 */ 5247 protected boolean isWritableAccount(String accountType) { 5248 if (accountType == null) { 5249 return true; 5250 } 5251 5252 Boolean writable = mAccountWritability.get(accountType); 5253 if (writable != null) { 5254 return writable; 5255 } 5256 5257 IContentService contentService = ContentResolver.getContentService(); 5258 try { 5259 for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) { 5260 if (ContactsContract.AUTHORITY.equals(sync.authority) && 5261 accountType.equals(sync.accountType)) { 5262 writable = sync.supportsUploading(); 5263 break; 5264 } 5265 } 5266 } catch (RemoteException e) { 5267 Log.e(TAG, "Could not acquire sync adapter types"); 5268 } 5269 5270 if (writable == null) { 5271 writable = false; 5272 } 5273 5274 mAccountWritability.put(accountType, writable); 5275 return writable; 5276 } 5277 5278 5279 /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter, 5280 boolean defaultValue) { 5281 5282 // Manually parse the query, which is much faster than calling uri.getQueryParameter 5283 String query = uri.getEncodedQuery(); 5284 if (query == null) { 5285 return defaultValue; 5286 } 5287 5288 int index = query.indexOf(parameter); 5289 if (index == -1) { 5290 return defaultValue; 5291 } 5292 5293 index += parameter.length(); 5294 5295 return !matchQueryParameter(query, index, "=0", false) 5296 && !matchQueryParameter(query, index, "=false", true); 5297 } 5298 5299 private static boolean matchQueryParameter(String query, int index, String value, 5300 boolean ignoreCase) { 5301 int length = value.length(); 5302 return query.regionMatches(ignoreCase, index, value, 0, length) 5303 && (query.length() == index + length || query.charAt(index + length) == '&'); 5304 } 5305 5306 /** 5307 * A fast re-implementation of {@link Uri#getQueryParameter} 5308 */ 5309 /* package */ static String getQueryParameter(Uri uri, String parameter) { 5310 String query = uri.getEncodedQuery(); 5311 if (query == null) { 5312 return null; 5313 } 5314 5315 int queryLength = query.length(); 5316 int parameterLength = parameter.length(); 5317 5318 String value; 5319 int index = 0; 5320 while (true) { 5321 index = query.indexOf(parameter, index); 5322 if (index == -1) { 5323 return null; 5324 } 5325 5326 index += parameterLength; 5327 5328 if (queryLength == index) { 5329 return null; 5330 } 5331 5332 if (query.charAt(index) == '=') { 5333 index++; 5334 break; 5335 } 5336 } 5337 5338 int ampIndex = query.indexOf('&', index); 5339 if (ampIndex == -1) { 5340 value = query.substring(index); 5341 } else { 5342 value = query.substring(index, ampIndex); 5343 } 5344 5345 return Uri.decode(value); 5346 } 5347 5348 protected boolean isAggregationUpgradeNeeded() { 5349 if (!mContactAggregator.isEnabled()) { 5350 return false; 5351 } 5352 5353 int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_AGGREGATION_ALGORITHM, "1")); 5354 return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION; 5355 } 5356 5357 protected void upgradeAggregationAlgorithmInBackground() { 5358 // This upgrade will affect very few contacts, so it can be performed on the 5359 // main thread during the initial boot after an OTA 5360 5361 Log.i(TAG, "Upgrading aggregation algorithm"); 5362 int count = 0; 5363 long start = SystemClock.currentThreadTimeMillis(); 5364 try { 5365 mDb = mDbHelper.getWritableDatabase(); 5366 mDb.beginTransaction(); 5367 Cursor cursor = mDb.query(true, 5368 Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2", 5369 new String[]{"r1." + RawContacts._ID}, 5370 "r1." + RawContacts._ID + "!=r2." + RawContacts._ID + 5371 " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID + 5372 " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME + 5373 " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE, 5374 null, null, null, null, null); 5375 try { 5376 while (cursor.moveToNext()) { 5377 long rawContactId = cursor.getLong(0); 5378 mContactAggregator.markForAggregation(rawContactId, 5379 RawContacts.AGGREGATION_MODE_DEFAULT, true); 5380 count++; 5381 } 5382 } finally { 5383 cursor.close(); 5384 } 5385 mContactAggregator.aggregateInTransaction(mTransactionContext, mDb); 5386 updateSearchIndexInTransaction(); 5387 mDb.setTransactionSuccessful(); 5388 mDbHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM, 5389 String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION)); 5390 } finally { 5391 mDb.endTransaction(); 5392 long end = SystemClock.currentThreadTimeMillis(); 5393 Log.i(TAG, "Aggregation algorithm upgraded for " + count 5394 + " contacts, in " + (end - start) + "ms"); 5395 } 5396 } 5397 5398 /* Visible for testing */ 5399 boolean isPhone() { 5400 if (!sIsPhoneInitialized) { 5401 sIsPhone = new TelephonyManager(getContext()).isVoiceCapable(); 5402 sIsPhoneInitialized = true; 5403 } 5404 return sIsPhone; 5405 } 5406} 5407