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