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