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