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