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