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