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