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