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