ContactsProvider2.java revision 3826a44d8de41e9c148dd6a967392ea5af478085
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 boolean mProviderStatusUpdateNeeded; 1892 private long mEstimatedStorageRequirement = 0; 1893 private volatile CountDownLatch mAccessLatch; 1894 1895 private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap(); 1896 private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet(); 1897 private HashSet<Long> mDirtyRawContacts = Sets.newHashSet(); 1898 private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap(); 1899 1900 private boolean mVisibleTouched = false; 1901 1902 private boolean mSyncToNetwork; 1903 1904 private Locale mCurrentLocale; 1905 private int mContactsAccountCount; 1906 1907 private CountryMonitor mCountryMonitor; 1908 1909 @Override 1910 public boolean onCreate() { 1911 super.onCreate(); 1912 try { 1913 return initialize(); 1914 } catch (RuntimeException e) { 1915 Log.e(TAG, "Cannot start provider", e); 1916 return false; 1917 } 1918 } 1919 1920 private boolean initialize() { 1921 final Context context = getContext(); 1922 mCountryMonitor = CountryMonitor.getInstance(context); 1923 mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper(); 1924 mContactDirectoryManager = new ContactDirectoryManager(this); 1925 mGlobalSearchSupport = new GlobalSearchSupport(this); 1926 mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport); 1927 mDb = mDbHelper.getWritableDatabase(); 1928 1929 initForDefaultLocale(); 1930 1931 mSetPrimaryStatement = mDb.compileStatement( 1932 "UPDATE " + Tables.DATA + 1933 " SET " + Data.IS_PRIMARY + "=(_id=?)" + 1934 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1935 " AND " + Data.RAW_CONTACT_ID + "=?"); 1936 1937 mSetSuperPrimaryStatement = mDb.compileStatement( 1938 "UPDATE " + Tables.DATA + 1939 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" + 1940 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1941 " AND " + Data.RAW_CONTACT_ID + " IN (" + 1942 "SELECT " + RawContacts._ID + 1943 " FROM " + Tables.RAW_CONTACTS + 1944 " WHERE " + RawContacts.CONTACT_ID + " =(" + 1945 "SELECT " + RawContacts.CONTACT_ID + 1946 " FROM " + Tables.RAW_CONTACTS + 1947 " WHERE " + RawContacts._ID + "=?))"); 1948 1949 mRawContactDisplayNameUpdate = mDb.compileStatement( 1950 "UPDATE " + Tables.RAW_CONTACTS + 1951 " SET " + 1952 RawContacts.DISPLAY_NAME_SOURCE + "=?," + 1953 RawContacts.DISPLAY_NAME_PRIMARY + "=?," + 1954 RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," + 1955 RawContacts.PHONETIC_NAME + "=?," + 1956 RawContacts.PHONETIC_NAME_STYLE + "=?," + 1957 RawContacts.SORT_KEY_PRIMARY + "=?," + 1958 RawContacts.SORT_KEY_ALTERNATIVE + "=?" + 1959 " WHERE " + RawContacts._ID + "=?"); 1960 1961 mNameLookupInsert = mDb.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "(" 1962 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + "," 1963 + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME 1964 + ") VALUES (?,?,?,?)"); 1965 mNameLookupDelete = mDb.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE " 1966 + NameLookupColumns.DATA_ID + "=?"); 1967 1968 mStatusUpdateInsert = mDb.compileStatement( 1969 "INSERT INTO " + Tables.STATUS_UPDATES + "(" 1970 + StatusUpdatesColumns.DATA_ID + ", " 1971 + StatusUpdates.STATUS + "," 1972 + StatusUpdates.STATUS_RES_PACKAGE + "," 1973 + StatusUpdates.STATUS_ICON + "," 1974 + StatusUpdates.STATUS_LABEL + ")" + 1975 " VALUES (?,?,?,?,?)"); 1976 1977 mStatusUpdateReplace = mDb.compileStatement( 1978 "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "(" 1979 + StatusUpdatesColumns.DATA_ID + ", " 1980 + StatusUpdates.STATUS_TIMESTAMP + "," 1981 + StatusUpdates.STATUS + "," 1982 + StatusUpdates.STATUS_RES_PACKAGE + "," 1983 + StatusUpdates.STATUS_ICON + "," 1984 + StatusUpdates.STATUS_LABEL + ")" + 1985 " VALUES (?,?,?,?,?,?)"); 1986 1987 mStatusUpdateAutoTimestamp = mDb.compileStatement( 1988 "UPDATE " + Tables.STATUS_UPDATES + 1989 " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?," 1990 + StatusUpdates.STATUS + "=?" + 1991 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?" 1992 + " AND " + StatusUpdates.STATUS + "!=?"); 1993 1994 mStatusAttributionUpdate = mDb.compileStatement( 1995 "UPDATE " + Tables.STATUS_UPDATES + 1996 " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?," 1997 + StatusUpdates.STATUS_ICON + "=?," 1998 + StatusUpdates.STATUS_LABEL + "=?" + 1999 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); 2000 2001 mStatusUpdateDelete = mDb.compileStatement( 2002 "DELETE FROM " + Tables.STATUS_UPDATES + 2003 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); 2004 2005 // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0 2006 // on all other raw contacts in the same aggregate 2007 mResetNameVerifiedForOtherRawContacts = mDb.compileStatement( 2008 "UPDATE " + Tables.RAW_CONTACTS + 2009 " SET " + RawContacts.NAME_VERIFIED + "=0" + 2010 " WHERE " + RawContacts.CONTACT_ID + "=(" + 2011 "SELECT " + RawContacts.CONTACT_ID + 2012 " FROM " + Tables.RAW_CONTACTS + 2013 " WHERE " + RawContacts._ID + "=?)" + 2014 " AND " + RawContacts._ID + "!=?"); 2015 2016 mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); 2017 mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE); 2018 mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE); 2019 mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE); 2020 mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE); 2021 mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); 2022 2023 updateAccounts(); 2024 2025 if (isLegacyContactImportNeeded()) { 2026 importLegacyContactsAsync(); 2027 } else { 2028 verifyLocale(); 2029 } 2030 2031 startContactDirectoryManager(); 2032 2033 if (isAggregationUpgradeNeeded()) { 2034 upgradeAggregationAlgorithm(); 2035 } 2036 2037 updateProviderStatus(); 2038 2039 return (mDb != null); 2040 } 2041 2042 protected String getCurrentCountryIso() { 2043 return mCountryMonitor.getCountryIso(); 2044 } 2045 2046 private void initDataRowHandlers() { 2047 mDataRowHandlers = new HashMap<String, DataRowHandler>(); 2048 2049 mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler()); 2050 mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE, 2051 new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL)); 2052 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler( 2053 StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL)); 2054 mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler()); 2055 mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler()); 2056 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler()); 2057 mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE, 2058 new StructuredNameRowHandler(mNameSplitter)); 2059 mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, 2060 new StructuredPostalRowHandler(mPostalSplitter)); 2061 mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler()); 2062 mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler()); 2063 } 2064 2065 /** 2066 * Visible for testing. 2067 */ 2068 /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) { 2069 return new PhotoPriorityResolver(context); 2070 } 2071 2072 /** 2073 * (Re)allocates all locale-sensitive structures. 2074 */ 2075 private void initForDefaultLocale() { 2076 mCurrentLocale = getLocale(); 2077 mNameSplitter = mDbHelper.createNameSplitter(); 2078 mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter); 2079 mPostalSplitter = new PostalSplitter(mCurrentLocale); 2080 mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase()); 2081 ContactLocaleUtils.getIntance().setLocale(mCurrentLocale); 2082 mContactAggregator = new ContactAggregator(this, mDbHelper, 2083 createPhotoPriorityResolver(getContext()), mNameSplitter, mCommonNicknameCache); 2084 mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); 2085 2086 initDataRowHandlers(); 2087 } 2088 2089 public void onLocaleChanged() { 2090 if (mProviderStatus != ProviderStatus.STATUS_NORMAL 2091 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) { 2092 return; 2093 } 2094 2095 initForDefaultLocale(); 2096 verifyLocale(); 2097 } 2098 2099 /** 2100 * Verifies that the contacts database is properly configured for the current locale. 2101 * If not, changes the database locale to the current locale using an asynchronous task. 2102 * This needs to be done asynchronously because the process involves rebuilding 2103 * large data structures (name lookup, sort keys), which can take minutes on 2104 * a large set of contacts. 2105 */ 2106 protected void verifyLocale() { 2107 2108 // The process is already running - postpone the change 2109 if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) { 2110 return; 2111 } 2112 2113 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 2114 final String providerLocale = prefs.getString(PREF_LOCALE, null); 2115 final Locale currentLocale = mCurrentLocale; 2116 if (currentLocale.toString().equals(providerLocale)) { 2117 return; 2118 } 2119 2120 int providerStatus = mProviderStatus; 2121 setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE); 2122 2123 AsyncTask<Integer, Void, Void> task = new AsyncTask<Integer, Void, Void>() { 2124 2125 int savedProviderStatus; 2126 2127 @Override 2128 protected Void doInBackground(Integer... params) { 2129 savedProviderStatus = params[0]; 2130 mDbHelper.setLocale(ContactsProvider2.this, currentLocale); 2131 return null; 2132 } 2133 2134 @Override 2135 protected void onPostExecute(Void result) { 2136 prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply(); 2137 setProviderStatus(savedProviderStatus); 2138 2139 // Recursive invocation, needed to cover the case where locale 2140 // changes once and then changes again before the db upgrade is completed. 2141 verifyLocale(); 2142 } 2143 }; 2144 2145 task.execute(providerStatus); 2146 } 2147 2148 private void updateProviderStatus() { 2149 if (mProviderStatus != ProviderStatus.STATUS_NORMAL 2150 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) { 2151 return; 2152 } 2153 2154 if (mContactsAccountCount == 0 2155 && DatabaseUtils.queryNumEntries(mDb, Tables.CONTACTS, null) == 0) { 2156 setProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS); 2157 } else { 2158 setProviderStatus(ProviderStatus.STATUS_NORMAL); 2159 } 2160 } 2161 2162 /* Visible for testing */ 2163 @Override 2164 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 2165 return ContactsDatabaseHelper.getInstance(context); 2166 } 2167 2168 /* package */ NameSplitter getNameSplitter() { 2169 return mNameSplitter; 2170 } 2171 2172 /* Visible for testing */ 2173 public ContactDirectoryManager getContactDirectoryManager() { 2174 return mContactDirectoryManager; 2175 } 2176 2177 /* Visible for testing */ 2178 protected Locale getLocale() { 2179 return Locale.getDefault(); 2180 } 2181 2182 /* Visible for testing */ 2183 protected void startContactDirectoryManager() { 2184 getContactDirectoryManager().start(); 2185 } 2186 2187 protected boolean isLegacyContactImportNeeded() { 2188 int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0")); 2189 return version < PROPERTY_CONTACTS_IMPORT_VERSION; 2190 } 2191 2192 protected LegacyContactImporter getLegacyContactImporter() { 2193 return new LegacyContactImporter(getContext(), this); 2194 } 2195 2196 /** 2197 * Imports legacy contacts in a separate thread. As long as the import process is running 2198 * all other access to the contacts is blocked. 2199 */ 2200 private void importLegacyContactsAsync() { 2201 Log.v(TAG, "Importing legacy contacts"); 2202 setProviderStatus(ProviderStatus.STATUS_UPGRADING); 2203 if (mAccessLatch == null) { 2204 mAccessLatch = new CountDownLatch(1); 2205 } 2206 2207 Thread importThread = new Thread("LegacyContactImport") { 2208 @Override 2209 public void run() { 2210 final SharedPreferences prefs = 2211 PreferenceManager.getDefaultSharedPreferences(getContext()); 2212 mDbHelper.setLocale(ContactsProvider2.this, mCurrentLocale); 2213 prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit(); 2214 2215 LegacyContactImporter importer = getLegacyContactImporter(); 2216 if (importLegacyContacts(importer)) { 2217 onLegacyContactImportSuccess(); 2218 } else { 2219 onLegacyContactImportFailure(); 2220 } 2221 } 2222 }; 2223 2224 importThread.start(); 2225 } 2226 2227 /** 2228 * Unlocks the provider and declares that the import process is complete. 2229 */ 2230 private void onLegacyContactImportSuccess() { 2231 NotificationManager nm = 2232 (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE); 2233 nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION); 2234 2235 // Store a property in the database indicating that the conversion process succeeded 2236 mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED, 2237 String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION)); 2238 setProviderStatus(ProviderStatus.STATUS_NORMAL); 2239 mAccessLatch.countDown(); 2240 mAccessLatch = null; 2241 Log.v(TAG, "Completed import of legacy contacts"); 2242 } 2243 2244 /** 2245 * Announces the provider status and keeps the provider locked. 2246 */ 2247 private void onLegacyContactImportFailure() { 2248 Context context = getContext(); 2249 NotificationManager nm = 2250 (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); 2251 2252 // Show a notification 2253 Notification n = new Notification(android.R.drawable.stat_notify_error, 2254 context.getString(R.string.upgrade_out_of_memory_notification_ticker), 2255 System.currentTimeMillis()); 2256 n.setLatestEventInfo(context, 2257 context.getString(R.string.upgrade_out_of_memory_notification_title), 2258 context.getString(R.string.upgrade_out_of_memory_notification_text), 2259 PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0)); 2260 n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; 2261 2262 nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n); 2263 2264 setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY); 2265 Log.v(TAG, "Failed to import legacy contacts"); 2266 } 2267 2268 /* Visible for testing */ 2269 /* package */ boolean importLegacyContacts(LegacyContactImporter importer) { 2270 boolean aggregatorEnabled = mContactAggregator.isEnabled(); 2271 mContactAggregator.setEnabled(false); 2272 try { 2273 if (importer.importContacts()) { 2274 2275 // TODO aggregate all newly added raw contacts 2276 mContactAggregator.setEnabled(aggregatorEnabled); 2277 return true; 2278 } 2279 } catch (Throwable e) { 2280 Log.e(TAG, "Legacy contact import failed", e); 2281 } 2282 mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement(); 2283 return false; 2284 } 2285 2286 /** 2287 * Wipes all data from the contacts database. 2288 */ 2289 /* package */ void wipeData() { 2290 mDbHelper.wipeData(); 2291 mProviderStatus = ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS; 2292 } 2293 2294 /** 2295 * While importing and aggregating contacts, this content provider will 2296 * block all attempts to change contacts data. In particular, it will hold 2297 * up all contact syncs. As soon as the import process is complete, all 2298 * processes waiting to write to the provider are unblocked and can proceed 2299 * to compete for the database transaction monitor. 2300 */ 2301 private void waitForAccess() { 2302 CountDownLatch latch = mAccessLatch; 2303 if (latch != null) { 2304 while (true) { 2305 try { 2306 latch.await(); 2307 mAccessLatch = null; 2308 return; 2309 } catch (InterruptedException e) { 2310 Thread.currentThread().interrupt(); 2311 } 2312 } 2313 } 2314 } 2315 2316 @Override 2317 public Uri insert(Uri uri, ContentValues values) { 2318 waitForAccess(); 2319 return super.insert(uri, values); 2320 } 2321 2322 @Override 2323 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 2324 if (mAccessLatch != null) { 2325 // We are stuck trying to upgrade contacts db. The only update request 2326 // allowed in this case is an update of provider status, which will trigger 2327 // an attempt to upgrade contacts again. 2328 int match = sUriMatcher.match(uri); 2329 if (match == PROVIDER_STATUS && isLegacyContactImportNeeded()) { 2330 Integer newStatus = values.getAsInteger(ProviderStatus.STATUS); 2331 if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) { 2332 importLegacyContactsAsync(); 2333 return 1; 2334 } else { 2335 return 0; 2336 } 2337 } 2338 } 2339 waitForAccess(); 2340 return super.update(uri, values, selection, selectionArgs); 2341 } 2342 2343 @Override 2344 public int delete(Uri uri, String selection, String[] selectionArgs) { 2345 waitForAccess(); 2346 return super.delete(uri, selection, selectionArgs); 2347 } 2348 2349 @Override 2350 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 2351 throws OperationApplicationException { 2352 waitForAccess(); 2353 return super.applyBatch(operations); 2354 } 2355 2356 @Override 2357 protected void onBeginTransaction() { 2358 if (VERBOSE_LOGGING) { 2359 Log.v(TAG, "onBeginTransaction"); 2360 } 2361 super.onBeginTransaction(); 2362 mContactAggregator.clearPendingAggregations(); 2363 clearTransactionalChanges(); 2364 } 2365 2366 private void clearTransactionalChanges() { 2367 mInsertedRawContacts.clear(); 2368 mUpdatedRawContacts.clear(); 2369 mUpdatedSyncStates.clear(); 2370 mDirtyRawContacts.clear(); 2371 } 2372 2373 @Override 2374 protected void beforeTransactionCommit() { 2375 2376 if (VERBOSE_LOGGING) { 2377 Log.v(TAG, "beforeTransactionCommit"); 2378 } 2379 super.beforeTransactionCommit(); 2380 flushTransactionalChanges(); 2381 mContactAggregator.aggregateInTransaction(mDb); 2382 if (mVisibleTouched) { 2383 mVisibleTouched = false; 2384 mDbHelper.updateAllVisible(); 2385 } 2386 2387 if (mProviderStatusUpdateNeeded) { 2388 updateProviderStatus(); 2389 mProviderStatusUpdateNeeded = false; 2390 } 2391 } 2392 2393 private void flushTransactionalChanges() { 2394 if (VERBOSE_LOGGING) { 2395 Log.v(TAG, "flushTransactionChanges"); 2396 } 2397 2398 for (long rawContactId : mInsertedRawContacts.keySet()) { 2399 updateRawContactDisplayName(mDb, rawContactId); 2400 mContactAggregator.onRawContactInsert(mDb, rawContactId); 2401 } 2402 2403 if (!mDirtyRawContacts.isEmpty()) { 2404 mSb.setLength(0); 2405 mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL); 2406 appendIds(mSb, mDirtyRawContacts); 2407 mSb.append(")"); 2408 mDb.execSQL(mSb.toString()); 2409 } 2410 2411 if (!mUpdatedRawContacts.isEmpty()) { 2412 mSb.setLength(0); 2413 mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL); 2414 appendIds(mSb, mUpdatedRawContacts); 2415 mSb.append(")"); 2416 mDb.execSQL(mSb.toString()); 2417 } 2418 2419 for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) { 2420 long id = entry.getKey(); 2421 if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) { 2422 throw new IllegalStateException( 2423 "unable to update sync state, does it still exist?"); 2424 } 2425 } 2426 2427 clearTransactionalChanges(); 2428 } 2429 2430 /** 2431 * Appends comma separated ids. 2432 * @param ids Should not be empty 2433 */ 2434 private void appendIds(StringBuilder sb, HashSet<Long> ids) { 2435 for (long id : ids) { 2436 sb.append(id).append(','); 2437 } 2438 2439 sb.setLength(sb.length() - 1); // Yank the last comma 2440 } 2441 2442 @Override 2443 protected void notifyChange() { 2444 notifyChange(mSyncToNetwork); 2445 mSyncToNetwork = false; 2446 } 2447 2448 protected void notifyChange(boolean syncToNetwork) { 2449 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, 2450 syncToNetwork); 2451 } 2452 2453 protected void setProviderStatus(int status) { 2454 if (mProviderStatus != status) { 2455 mProviderStatus = status; 2456 getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false); 2457 } 2458 } 2459 2460 private boolean isNewRawContact(long rawContactId) { 2461 return mInsertedRawContacts.containsKey(rawContactId); 2462 } 2463 2464 private DataRowHandler getDataRowHandler(final String mimeType) { 2465 DataRowHandler handler = mDataRowHandlers.get(mimeType); 2466 if (handler == null) { 2467 handler = new CustomDataRowHandler(mimeType); 2468 mDataRowHandlers.put(mimeType, handler); 2469 } 2470 return handler; 2471 } 2472 2473 @Override 2474 protected Uri insertInTransaction(Uri uri, ContentValues values) { 2475 if (VERBOSE_LOGGING) { 2476 Log.v(TAG, "insertInTransaction: " + uri + " " + values); 2477 } 2478 2479 final boolean callerIsSyncAdapter = 2480 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2481 2482 final int match = sUriMatcher.match(uri); 2483 long id = 0; 2484 2485 switch (match) { 2486 case SYNCSTATE: 2487 id = mDbHelper.getSyncState().insert(mDb, values); 2488 break; 2489 2490 case CONTACTS: { 2491 insertContact(values); 2492 break; 2493 } 2494 2495 case RAW_CONTACTS: { 2496 id = insertRawContact(uri, values, callerIsSyncAdapter); 2497 mSyncToNetwork |= !callerIsSyncAdapter; 2498 break; 2499 } 2500 2501 case RAW_CONTACTS_DATA: { 2502 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 2503 id = insertData(values, callerIsSyncAdapter); 2504 mSyncToNetwork |= !callerIsSyncAdapter; 2505 break; 2506 } 2507 2508 case DATA: { 2509 id = insertData(values, callerIsSyncAdapter); 2510 mSyncToNetwork |= !callerIsSyncAdapter; 2511 break; 2512 } 2513 2514 case GROUPS: { 2515 id = insertGroup(uri, values, callerIsSyncAdapter); 2516 mSyncToNetwork |= !callerIsSyncAdapter; 2517 break; 2518 } 2519 2520 case SETTINGS: { 2521 id = insertSettings(uri, values); 2522 mSyncToNetwork |= !callerIsSyncAdapter; 2523 break; 2524 } 2525 2526 case STATUS_UPDATES: { 2527 id = insertStatusUpdate(values); 2528 break; 2529 } 2530 2531 default: 2532 mSyncToNetwork = true; 2533 return mLegacyApiSupport.insert(uri, values); 2534 } 2535 2536 if (id < 0) { 2537 return null; 2538 } 2539 2540 return ContentUris.withAppendedId(uri, id); 2541 } 2542 2543 /** 2544 * If account is non-null then store it in the values. If the account is 2545 * already specified in the values then it must be consistent with the 2546 * account, if it is non-null. 2547 * 2548 * @param uri Current {@link Uri} being operated on. 2549 * @param values {@link ContentValues} to read and possibly update. 2550 * @throws IllegalArgumentException when only one of 2551 * {@link RawContacts#ACCOUNT_NAME} or 2552 * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the 2553 * other undefined. 2554 * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME} 2555 * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between 2556 * the given {@link Uri} and {@link ContentValues}. 2557 */ 2558 private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException { 2559 String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 2560 String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 2561 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 2562 2563 String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME); 2564 String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 2565 final boolean partialValues = TextUtils.isEmpty(valueAccountName) 2566 ^ TextUtils.isEmpty(valueAccountType); 2567 2568 if (partialUri || partialValues) { 2569 // Throw when either account is incomplete 2570 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 2571 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 2572 } 2573 2574 // Accounts are valid by only checking one parameter, since we've 2575 // already ruled out partial accounts. 2576 final boolean validUri = !TextUtils.isEmpty(accountName); 2577 final boolean validValues = !TextUtils.isEmpty(valueAccountName); 2578 2579 if (validValues && validUri) { 2580 // Check that accounts match when both present 2581 final boolean accountMatch = TextUtils.equals(accountName, valueAccountName) 2582 && TextUtils.equals(accountType, valueAccountType); 2583 if (!accountMatch) { 2584 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 2585 "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri)); 2586 } 2587 } else if (validUri) { 2588 // Fill values from Uri when not present 2589 values.put(RawContacts.ACCOUNT_NAME, accountName); 2590 values.put(RawContacts.ACCOUNT_TYPE, accountType); 2591 } else if (validValues) { 2592 accountName = valueAccountName; 2593 accountType = valueAccountType; 2594 } else { 2595 return null; 2596 } 2597 2598 // Use cached Account object when matches, otherwise create 2599 if (mAccount == null 2600 || !mAccount.name.equals(accountName) 2601 || !mAccount.type.equals(accountType)) { 2602 mAccount = new Account(accountName, accountType); 2603 } 2604 2605 return mAccount; 2606 } 2607 2608 /** 2609 * Inserts an item in the contacts table 2610 * 2611 * @param values the values for the new row 2612 * @return the row ID of the newly created row 2613 */ 2614 private long insertContact(ContentValues values) { 2615 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 2616 } 2617 2618 /** 2619 * Inserts an item in the contacts table 2620 * 2621 * @param uri the values for the new row 2622 * @param values the account this contact should be associated with. may be null. 2623 * @param callerIsSyncAdapter 2624 * @return the row ID of the newly created row 2625 */ 2626 private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 2627 mValues.clear(); 2628 mValues.putAll(values); 2629 mValues.putNull(RawContacts.CONTACT_ID); 2630 2631 final Account account = resolveAccount(uri, mValues); 2632 2633 if (values.containsKey(RawContacts.DELETED) 2634 && values.getAsInteger(RawContacts.DELETED) != 0) { 2635 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2636 } 2637 2638 long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues); 2639 int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; 2640 if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) { 2641 aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE); 2642 } 2643 mContactAggregator.markNewForAggregation(rawContactId, aggregationMode); 2644 2645 // Trigger creation of a Contact based on this RawContact at the end of transaction 2646 mInsertedRawContacts.put(rawContactId, account); 2647 2648 if (!callerIsSyncAdapter) { 2649 addAutoAddMembership(rawContactId); 2650 final Long starred = values.getAsLong(RawContacts.STARRED); 2651 if (starred != null && starred != 0) { 2652 updateFavoritesMembership(rawContactId, starred != 0); 2653 } 2654 } 2655 2656 mProviderStatusUpdateNeeded = true; 2657 return rawContactId; 2658 } 2659 2660 private void addAutoAddMembership(long rawContactId) { 2661 final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID, 2662 rawContactId); 2663 if (groupId != null) { 2664 insertDataGroupMembership(rawContactId, groupId); 2665 } 2666 } 2667 2668 private Long findGroupByRawContactId(String selection, long rawContactId) { 2669 Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID, 2670 selection, 2671 new String[]{Long.toString(rawContactId)}, 2672 null /* groupBy */, null /* having */, null /* orderBy */); 2673 try { 2674 while (c.moveToNext()) { 2675 return c.getLong(0); 2676 } 2677 return null; 2678 } finally { 2679 c.close(); 2680 } 2681 } 2682 2683 private void updateFavoritesMembership(long rawContactId, boolean isStarred) { 2684 final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID, 2685 rawContactId); 2686 if (groupId != null) { 2687 if (isStarred) { 2688 insertDataGroupMembership(rawContactId, groupId); 2689 } else { 2690 deleteDataGroupMembership(rawContactId, groupId); 2691 } 2692 } 2693 } 2694 2695 private void insertDataGroupMembership(long rawContactId, long groupId) { 2696 ContentValues groupMembershipValues = new ContentValues(); 2697 groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId); 2698 groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId); 2699 groupMembershipValues.put(DataColumns.MIMETYPE_ID, 2700 mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 2701 mDb.insert(Tables.DATA, null, groupMembershipValues); 2702 } 2703 2704 private void deleteDataGroupMembership(long rawContactId, long groupId) { 2705 final String[] selectionArgs = { 2706 Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)), 2707 Long.toString(groupId), 2708 Long.toString(rawContactId)}; 2709 mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs); 2710 } 2711 2712 /** 2713 * Inserts an item in the data table 2714 * 2715 * @param values the values for the new row 2716 * @return the row ID of the newly created row 2717 */ 2718 private long insertData(ContentValues values, boolean callerIsSyncAdapter) { 2719 long id = 0; 2720 mValues.clear(); 2721 mValues.putAll(values); 2722 2723 long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 2724 2725 // Replace package with internal mapping 2726 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 2727 if (packageName != null) { 2728 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2729 } 2730 mValues.remove(Data.RES_PACKAGE); 2731 2732 // Replace mimetype with internal mapping 2733 final String mimeType = mValues.getAsString(Data.MIMETYPE); 2734 if (TextUtils.isEmpty(mimeType)) { 2735 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 2736 } 2737 2738 mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType)); 2739 mValues.remove(Data.MIMETYPE); 2740 2741 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2742 id = rowHandler.insert(mDb, rawContactId, mValues); 2743 if (!callerIsSyncAdapter) { 2744 setRawContactDirty(rawContactId); 2745 } 2746 mUpdatedRawContacts.add(rawContactId); 2747 return id; 2748 } 2749 2750 private void triggerAggregation(long rawContactId) { 2751 if (!mContactAggregator.isEnabled()) { 2752 return; 2753 } 2754 2755 int aggregationMode = mDbHelper.getAggregationMode(rawContactId); 2756 switch (aggregationMode) { 2757 case RawContacts.AGGREGATION_MODE_DISABLED: 2758 break; 2759 2760 case RawContacts.AGGREGATION_MODE_DEFAULT: { 2761 mContactAggregator.markForAggregation(rawContactId, aggregationMode, false); 2762 break; 2763 } 2764 2765 case RawContacts.AGGREGATION_MODE_SUSPENDED: { 2766 long contactId = mDbHelper.getContactId(rawContactId); 2767 2768 if (contactId != 0) { 2769 mContactAggregator.updateAggregateData(contactId); 2770 } 2771 break; 2772 } 2773 2774 case RawContacts.AGGREGATION_MODE_IMMEDIATE: { 2775 mContactAggregator.aggregateContact(mDb, rawContactId); 2776 break; 2777 } 2778 } 2779 } 2780 2781 /** 2782 * Returns the group id of the group with sourceId and the same account as rawContactId. 2783 * If the group doesn't already exist then it is first created, 2784 * @param db SQLiteDatabase to use for this operation 2785 * @param rawContactId the contact this group is associated with 2786 * @param sourceId the sourceIf of the group to query or create 2787 * @return the group id of the existing or created group 2788 * @throws IllegalArgumentException if the contact is not associated with an account 2789 * @throws IllegalStateException if a group needs to be created but the creation failed 2790 */ 2791 private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId, 2792 Account account) { 2793 2794 if (account == null) { 2795 mSelectionArgs1[0] = String.valueOf(rawContactId); 2796 Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, 2797 RawContacts._ID + "=?", mSelectionArgs1, null, null, null); 2798 try { 2799 if (c.moveToFirst()) { 2800 String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME); 2801 String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); 2802 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 2803 account = new Account(accountName, accountType); 2804 } 2805 } 2806 } finally { 2807 c.close(); 2808 } 2809 } 2810 2811 if (account == null) { 2812 throw new IllegalArgumentException("if the groupmembership only " 2813 + "has a sourceid the the contact must be associated with " 2814 + "an account"); 2815 } 2816 2817 ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId); 2818 if (entries == null) { 2819 entries = new ArrayList<GroupIdCacheEntry>(1); 2820 mGroupIdCache.put(sourceId, entries); 2821 } 2822 2823 int count = entries.size(); 2824 for (int i = 0; i < count; i++) { 2825 GroupIdCacheEntry entry = entries.get(i); 2826 if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) { 2827 return entry.groupId; 2828 } 2829 } 2830 2831 GroupIdCacheEntry entry = new GroupIdCacheEntry(); 2832 entry.accountName = account.name; 2833 entry.accountType = account.type; 2834 entry.sourceId = sourceId; 2835 entries.add(0, entry); 2836 2837 // look up the group that contains this sourceId and has the same account name and type 2838 // as the contact refered to by rawContactId 2839 Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID}, 2840 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID, 2841 new String[]{sourceId, account.name, account.type}, null, null, null); 2842 try { 2843 if (c.moveToFirst()) { 2844 entry.groupId = c.getLong(0); 2845 } else { 2846 ContentValues groupValues = new ContentValues(); 2847 groupValues.put(Groups.ACCOUNT_NAME, account.name); 2848 groupValues.put(Groups.ACCOUNT_TYPE, account.type); 2849 groupValues.put(Groups.SOURCE_ID, sourceId); 2850 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues); 2851 if (groupId < 0) { 2852 throw new IllegalStateException("unable to create a new group with " 2853 + "this sourceid: " + groupValues); 2854 } 2855 entry.groupId = groupId; 2856 } 2857 } finally { 2858 c.close(); 2859 } 2860 2861 return entry.groupId; 2862 } 2863 2864 private interface DisplayNameQuery { 2865 public static final String RAW_SQL = 2866 "SELECT " 2867 + DataColumns.MIMETYPE_ID + "," 2868 + Data.IS_PRIMARY + "," 2869 + Data.DATA1 + "," 2870 + Data.DATA2 + "," 2871 + Data.DATA3 + "," 2872 + Data.DATA4 + "," 2873 + Data.DATA5 + "," 2874 + Data.DATA6 + "," 2875 + Data.DATA7 + "," 2876 + Data.DATA8 + "," 2877 + Data.DATA9 + "," 2878 + Data.DATA10 + "," 2879 + Data.DATA11 + 2880 " FROM " + Tables.DATA + 2881 " WHERE " + Data.RAW_CONTACT_ID + "=?" + 2882 " AND (" + Data.DATA1 + " NOT NULL OR " + 2883 Organization.TITLE + " NOT NULL)"; 2884 2885 public static final int MIMETYPE = 0; 2886 public static final int IS_PRIMARY = 1; 2887 public static final int DATA1 = 2; 2888 public static final int GIVEN_NAME = 3; // data2 2889 public static final int FAMILY_NAME = 4; // data3 2890 public static final int PREFIX = 5; // data4 2891 public static final int TITLE = 5; // data4 2892 public static final int MIDDLE_NAME = 6; // data5 2893 public static final int SUFFIX = 7; // data6 2894 public static final int PHONETIC_GIVEN_NAME = 8; // data7 2895 public static final int PHONETIC_MIDDLE_NAME = 9; // data8 2896 public static final int ORGANIZATION_PHONETIC_NAME = 9; // data8 2897 public static final int PHONETIC_FAMILY_NAME = 10; // data9 2898 public static final int FULL_NAME_STYLE = 11; // data10 2899 public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11; // data10 2900 public static final int PHONETIC_NAME_STYLE = 12; // data11 2901 } 2902 2903 /** 2904 * Updates a raw contact display name based on data rows, e.g. structured name, 2905 * organization, email etc. 2906 */ 2907 public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 2908 int bestDisplayNameSource = DisplayNameSources.UNDEFINED; 2909 NameSplitter.Name bestName = null; 2910 String bestDisplayName = null; 2911 String bestPhoneticName = null; 2912 int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2913 2914 mSelectionArgs1[0] = String.valueOf(rawContactId); 2915 Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1); 2916 try { 2917 while (c.moveToNext()) { 2918 int mimeType = c.getInt(DisplayNameQuery.MIMETYPE); 2919 int source = getDisplayNameSource(mimeType); 2920 if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) { 2921 continue; 2922 } 2923 2924 if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) { 2925 continue; 2926 } 2927 2928 if (mimeType == mMimeTypeIdStructuredName) { 2929 NameSplitter.Name name; 2930 if (bestName != null) { 2931 name = new NameSplitter.Name(); 2932 } else { 2933 name = mName; 2934 name.clear(); 2935 } 2936 name.prefix = c.getString(DisplayNameQuery.PREFIX); 2937 name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME); 2938 name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME); 2939 name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME); 2940 name.suffix = c.getString(DisplayNameQuery.SUFFIX); 2941 name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE) 2942 ? FullNameStyle.UNDEFINED 2943 : c.getInt(DisplayNameQuery.FULL_NAME_STYLE); 2944 name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME); 2945 name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME); 2946 name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME); 2947 name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE) 2948 ? PhoneticNameStyle.UNDEFINED 2949 : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE); 2950 if (!name.isEmpty()) { 2951 bestDisplayNameSource = source; 2952 bestName = name; 2953 } 2954 } else if (mimeType == mMimeTypeIdOrganization) { 2955 mCharArrayBuffer.sizeCopied = 0; 2956 c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer); 2957 if (mCharArrayBuffer.sizeCopied != 0) { 2958 bestDisplayNameSource = source; 2959 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2960 mCharArrayBuffer.sizeCopied); 2961 bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME); 2962 bestPhoneticNameStyle = 2963 c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE) 2964 ? PhoneticNameStyle.UNDEFINED 2965 : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE); 2966 } else { 2967 c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer); 2968 if (mCharArrayBuffer.sizeCopied != 0) { 2969 bestDisplayNameSource = source; 2970 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2971 mCharArrayBuffer.sizeCopied); 2972 bestPhoneticName = null; 2973 bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2974 } 2975 } 2976 } else { 2977 // Display name is at DATA1 in all other types. 2978 // This is ensured in the constructor. 2979 2980 mCharArrayBuffer.sizeCopied = 0; 2981 c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer); 2982 if (mCharArrayBuffer.sizeCopied != 0) { 2983 bestDisplayNameSource = source; 2984 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2985 mCharArrayBuffer.sizeCopied); 2986 bestPhoneticName = null; 2987 bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2988 } 2989 } 2990 } 2991 2992 } finally { 2993 c.close(); 2994 } 2995 2996 String displayNamePrimary; 2997 String displayNameAlternative; 2998 String sortKeyPrimary = null; 2999 String sortKeyAlternative = null; 3000 int displayNameStyle = FullNameStyle.UNDEFINED; 3001 3002 if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) { 3003 displayNameStyle = bestName.fullNameStyle; 3004 if (displayNameStyle == FullNameStyle.CJK 3005 || displayNameStyle == FullNameStyle.UNDEFINED) { 3006 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle); 3007 bestName.fullNameStyle = displayNameStyle; 3008 } 3009 3010 displayNamePrimary = mNameSplitter.join(bestName, true); 3011 displayNameAlternative = mNameSplitter.join(bestName, false); 3012 3013 bestPhoneticName = mNameSplitter.joinPhoneticName(bestName); 3014 bestPhoneticNameStyle = bestName.phoneticNameStyle; 3015 } else { 3016 displayNamePrimary = displayNameAlternative = bestDisplayName; 3017 } 3018 3019 if (bestPhoneticName != null) { 3020 sortKeyPrimary = sortKeyAlternative = bestPhoneticName; 3021 if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) { 3022 bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName); 3023 } 3024 } else { 3025 if (displayNameStyle == FullNameStyle.UNDEFINED) { 3026 displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName); 3027 if (displayNameStyle == FullNameStyle.UNDEFINED 3028 || displayNameStyle == FullNameStyle.CJK) { 3029 displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle( 3030 displayNameStyle, bestPhoneticNameStyle); 3031 } 3032 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle); 3033 } 3034 if (displayNameStyle == FullNameStyle.CHINESE || 3035 displayNameStyle == FullNameStyle.CJK) { 3036 sortKeyPrimary = sortKeyAlternative = 3037 ContactLocaleUtils.getIntance().getSortKey( 3038 displayNamePrimary, displayNameStyle); 3039 } 3040 } 3041 3042 if (sortKeyPrimary == null) { 3043 sortKeyPrimary = displayNamePrimary; 3044 sortKeyAlternative = displayNameAlternative; 3045 } 3046 3047 setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary, 3048 displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle, 3049 sortKeyPrimary, sortKeyAlternative); 3050 } 3051 3052 private int getDisplayNameSource(int mimeTypeId) { 3053 if (mimeTypeId == mMimeTypeIdStructuredName) { 3054 return DisplayNameSources.STRUCTURED_NAME; 3055 } else if (mimeTypeId == mMimeTypeIdEmail) { 3056 return DisplayNameSources.EMAIL; 3057 } else if (mimeTypeId == mMimeTypeIdPhone) { 3058 return DisplayNameSources.PHONE; 3059 } else if (mimeTypeId == mMimeTypeIdOrganization) { 3060 return DisplayNameSources.ORGANIZATION; 3061 } else if (mimeTypeId == mMimeTypeIdNickname) { 3062 return DisplayNameSources.NICKNAME; 3063 } else { 3064 return DisplayNameSources.UNDEFINED; 3065 } 3066 } 3067 3068 /** 3069 * Delete data row by row so that fixing of primaries etc work correctly. 3070 */ 3071 private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { 3072 int count = 0; 3073 3074 // Note that the query will return data according to the access restrictions, 3075 // so we don't need to worry about deleting data we don't have permission to read. 3076 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null); 3077 try { 3078 while(c.moveToNext()) { 3079 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 3080 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 3081 DataRowHandler rowHandler = getDataRowHandler(mimeType); 3082 count += rowHandler.delete(mDb, c); 3083 if (!callerIsSyncAdapter) { 3084 setRawContactDirty(rawContactId); 3085 } 3086 } 3087 } finally { 3088 c.close(); 3089 } 3090 3091 return count; 3092 } 3093 3094 /** 3095 * Delete a data row provided that it is one of the allowed mime types. 3096 */ 3097 public int deleteData(long dataId, String[] allowedMimeTypes) { 3098 3099 // Note that the query will return data according to the access restrictions, 3100 // so we don't need to worry about deleting data we don't have permission to read. 3101 mSelectionArgs1[0] = String.valueOf(dataId); 3102 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?", 3103 mSelectionArgs1, null); 3104 3105 try { 3106 if (!c.moveToFirst()) { 3107 return 0; 3108 } 3109 3110 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 3111 boolean valid = false; 3112 for (int i = 0; i < allowedMimeTypes.length; i++) { 3113 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 3114 valid = true; 3115 break; 3116 } 3117 } 3118 3119 if (!valid) { 3120 throw new IllegalArgumentException("Data type mismatch: expected " 3121 + Lists.newArrayList(allowedMimeTypes)); 3122 } 3123 3124 DataRowHandler rowHandler = getDataRowHandler(mimeType); 3125 return rowHandler.delete(mDb, c); 3126 } finally { 3127 c.close(); 3128 } 3129 } 3130 3131 /** 3132 * Inserts an item in the groups table 3133 */ 3134 private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 3135 mValues.clear(); 3136 mValues.putAll(values); 3137 3138 final Account account = resolveAccount(uri, mValues); 3139 3140 // Replace package with internal mapping 3141 final String packageName = mValues.getAsString(Groups.RES_PACKAGE); 3142 if (packageName != null) { 3143 mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 3144 } 3145 mValues.remove(Groups.RES_PACKAGE); 3146 3147 final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null 3148 ? mValues.getAsLong(Groups.FAVORITES) != 0 3149 : false; 3150 3151 if (!callerIsSyncAdapter) { 3152 mValues.put(Groups.DIRTY, 1); 3153 } 3154 3155 long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues); 3156 3157 if (!callerIsSyncAdapter && isFavoritesGroup) { 3158 // add all starred raw contacts to this group 3159 String selection; 3160 String[] selectionArgs; 3161 if (account == null) { 3162 selection = RawContacts.ACCOUNT_NAME + " IS NULL AND " 3163 + RawContacts.ACCOUNT_TYPE + " IS NULL"; 3164 selectionArgs = null; 3165 } else { 3166 selection = RawContacts.ACCOUNT_NAME + "=? AND " 3167 + RawContacts.ACCOUNT_TYPE + "=?"; 3168 selectionArgs = new String[]{account.name, account.type}; 3169 } 3170 Cursor c = mDb.query(Tables.RAW_CONTACTS, 3171 new String[]{RawContacts._ID, RawContacts.STARRED}, 3172 selection, selectionArgs, null, null, null); 3173 try { 3174 while (c.moveToNext()) { 3175 if (c.getLong(1) != 0) { 3176 final long rawContactId = c.getLong(0); 3177 insertDataGroupMembership(rawContactId, result); 3178 setRawContactDirty(rawContactId); 3179 } 3180 } 3181 } finally { 3182 c.close(); 3183 } 3184 } 3185 3186 if (mValues.containsKey(Groups.GROUP_VISIBLE)) { 3187 mVisibleTouched = true; 3188 } 3189 3190 return result; 3191 } 3192 3193 private long insertSettings(Uri uri, ContentValues values) { 3194 final long id = mDb.insert(Tables.SETTINGS, null, values); 3195 3196 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 3197 mVisibleTouched = true; 3198 } 3199 3200 return id; 3201 } 3202 3203 /** 3204 * Inserts a status update. 3205 */ 3206 public long insertStatusUpdate(ContentValues values) { 3207 final String handle = values.getAsString(StatusUpdates.IM_HANDLE); 3208 final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL); 3209 String customProtocol = null; 3210 3211 if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) { 3212 customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL); 3213 if (TextUtils.isEmpty(customProtocol)) { 3214 throw new IllegalArgumentException( 3215 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 3216 } 3217 } 3218 3219 long rawContactId = -1; 3220 long contactId = -1; 3221 Long dataId = values.getAsLong(StatusUpdates.DATA_ID); 3222 mSb.setLength(0); 3223 mSelectionArgs.clear(); 3224 if (dataId != null) { 3225 // Lookup the contact info for the given data row. 3226 3227 mSb.append(Tables.DATA + "." + Data._ID + "=?"); 3228 mSelectionArgs.add(String.valueOf(dataId)); 3229 } else { 3230 // Lookup the data row to attach this presence update to 3231 3232 if (TextUtils.isEmpty(handle) || protocol == null) { 3233 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 3234 } 3235 3236 // TODO: generalize to allow other providers to match against email 3237 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 3238 3239 String mimeTypeIdIm = String.valueOf(mMimeTypeIdIm); 3240 if (matchEmail) { 3241 String mimeTypeIdEmail = String.valueOf(mMimeTypeIdEmail); 3242 3243 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise 3244 // the "OR" conjunction confuses it and it switches to a full scan of 3245 // the raw_contacts table. 3246 3247 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same 3248 // column - Data.DATA1 3249 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" + 3250 " AND " + Data.DATA1 + "=?" + 3251 " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?"); 3252 mSelectionArgs.add(mimeTypeIdEmail); 3253 mSelectionArgs.add(mimeTypeIdIm); 3254 mSelectionArgs.add(handle); 3255 mSelectionArgs.add(mimeTypeIdIm); 3256 mSelectionArgs.add(String.valueOf(protocol)); 3257 if (customProtocol != null) { 3258 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 3259 mSelectionArgs.add(customProtocol); 3260 } 3261 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))"); 3262 mSelectionArgs.add(mimeTypeIdEmail); 3263 } else { 3264 mSb.append(DataColumns.MIMETYPE_ID + "=?" + 3265 " AND " + Im.PROTOCOL + "=?" + 3266 " AND " + Im.DATA + "=?"); 3267 mSelectionArgs.add(mimeTypeIdIm); 3268 mSelectionArgs.add(String.valueOf(protocol)); 3269 mSelectionArgs.add(handle); 3270 if (customProtocol != null) { 3271 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 3272 mSelectionArgs.add(customProtocol); 3273 } 3274 } 3275 3276 if (values.containsKey(StatusUpdates.DATA_ID)) { 3277 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?"); 3278 mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID)); 3279 } 3280 } 3281 mSb.append(" AND ").append(getContactsRestrictions()); 3282 3283 Cursor cursor = null; 3284 try { 3285 cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 3286 mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null, 3287 Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID); 3288 if (cursor.moveToFirst()) { 3289 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 3290 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 3291 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 3292 } else { 3293 // No contact found, return a null URI 3294 return -1; 3295 } 3296 } finally { 3297 if (cursor != null) { 3298 cursor.close(); 3299 } 3300 } 3301 3302 if (values.containsKey(StatusUpdates.PRESENCE)) { 3303 if (customProtocol == null) { 3304 // We cannot allow a null in the custom protocol field, because SQLite3 does not 3305 // properly enforce uniqueness of null values 3306 customProtocol = ""; 3307 } 3308 3309 mValues.clear(); 3310 mValues.put(StatusUpdates.DATA_ID, dataId); 3311 mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 3312 mValues.put(PresenceColumns.CONTACT_ID, contactId); 3313 mValues.put(StatusUpdates.PROTOCOL, protocol); 3314 mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol); 3315 mValues.put(StatusUpdates.IM_HANDLE, handle); 3316 if (values.containsKey(StatusUpdates.IM_ACCOUNT)) { 3317 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT)); 3318 } 3319 mValues.put(StatusUpdates.PRESENCE, 3320 values.getAsString(StatusUpdates.PRESENCE)); 3321 mValues.put(StatusUpdates.CHAT_CAPABILITY, 3322 values.getAsString(StatusUpdates.CHAT_CAPABILITY)); 3323 3324 // Insert the presence update 3325 mDb.replace(Tables.PRESENCE, null, mValues); 3326 } 3327 3328 3329 if (values.containsKey(StatusUpdates.STATUS)) { 3330 String status = values.getAsString(StatusUpdates.STATUS); 3331 String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE); 3332 Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL); 3333 3334 if (TextUtils.isEmpty(resPackage) 3335 && (labelResource == null || labelResource == 0) 3336 && protocol != null) { 3337 labelResource = Im.getProtocolLabelResource(protocol); 3338 } 3339 3340 Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON); 3341 // TODO compute the default icon based on the protocol 3342 3343 if (TextUtils.isEmpty(status)) { 3344 mStatusUpdateDelete.bindLong(1, dataId); 3345 mStatusUpdateDelete.execute(); 3346 } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) { 3347 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP); 3348 mStatusUpdateReplace.bindLong(1, dataId); 3349 mStatusUpdateReplace.bindLong(2, timestamp); 3350 bindString(mStatusUpdateReplace, 3, status); 3351 bindString(mStatusUpdateReplace, 4, resPackage); 3352 bindLong(mStatusUpdateReplace, 5, iconResource); 3353 bindLong(mStatusUpdateReplace, 6, labelResource); 3354 mStatusUpdateReplace.execute(); 3355 } else { 3356 3357 try { 3358 mStatusUpdateInsert.bindLong(1, dataId); 3359 bindString(mStatusUpdateInsert, 2, status); 3360 bindString(mStatusUpdateInsert, 3, resPackage); 3361 bindLong(mStatusUpdateInsert, 4, iconResource); 3362 bindLong(mStatusUpdateInsert, 5, labelResource); 3363 mStatusUpdateInsert.executeInsert(); 3364 } catch (SQLiteConstraintException e) { 3365 // The row already exists - update it 3366 long timestamp = System.currentTimeMillis(); 3367 mStatusUpdateAutoTimestamp.bindLong(1, timestamp); 3368 bindString(mStatusUpdateAutoTimestamp, 2, status); 3369 mStatusUpdateAutoTimestamp.bindLong(3, dataId); 3370 bindString(mStatusUpdateAutoTimestamp, 4, status); 3371 mStatusUpdateAutoTimestamp.execute(); 3372 3373 bindString(mStatusAttributionUpdate, 1, resPackage); 3374 bindLong(mStatusAttributionUpdate, 2, iconResource); 3375 bindLong(mStatusAttributionUpdate, 3, labelResource); 3376 mStatusAttributionUpdate.bindLong(4, dataId); 3377 mStatusAttributionUpdate.execute(); 3378 } 3379 } 3380 } 3381 3382 if (contactId != -1) { 3383 mContactAggregator.updateLastStatusUpdateId(contactId); 3384 } 3385 3386 return dataId; 3387 } 3388 3389 @Override 3390 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 3391 if (VERBOSE_LOGGING) { 3392 Log.v(TAG, "deleteInTransaction: " + uri); 3393 } 3394 flushTransactionalChanges(); 3395 final boolean callerIsSyncAdapter = 3396 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3397 final int match = sUriMatcher.match(uri); 3398 switch (match) { 3399 case SYNCSTATE: 3400 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 3401 3402 case SYNCSTATE_ID: 3403 String selectionWithId = 3404 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3405 + (selection == null ? "" : " AND (" + selection + ")"); 3406 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); 3407 3408 case CONTACTS: { 3409 // TODO 3410 return 0; 3411 } 3412 3413 case CONTACTS_ID: { 3414 long contactId = ContentUris.parseId(uri); 3415 return deleteContact(contactId, callerIsSyncAdapter); 3416 } 3417 3418 case CONTACTS_LOOKUP: { 3419 final List<String> pathSegments = uri.getPathSegments(); 3420 final int segmentCount = pathSegments.size(); 3421 if (segmentCount < 3) { 3422 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3423 "Missing a lookup key", uri)); 3424 } 3425 final String lookupKey = pathSegments.get(2); 3426 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 3427 return deleteContact(contactId, callerIsSyncAdapter); 3428 } 3429 3430 case CONTACTS_LOOKUP_ID: { 3431 // lookup contact by id and lookup key to see if they still match the actual record 3432 final List<String> pathSegments = uri.getPathSegments(); 3433 final String lookupKey = pathSegments.get(2); 3434 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3435 setTablesAndProjectionMapForContacts(lookupQb, uri, null); 3436 long contactId = ContentUris.parseId(uri); 3437 String[] args; 3438 if (selectionArgs == null) { 3439 args = new String[2]; 3440 } else { 3441 args = new String[selectionArgs.length + 2]; 3442 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 3443 } 3444 args[0] = String.valueOf(contactId); 3445 args[1] = Uri.encode(lookupKey); 3446 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); 3447 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 3448 Cursor c = query(db, lookupQb, null, selection, args, null, null, null); 3449 try { 3450 if (c.getCount() == 1) { 3451 // contact was unmodified so go ahead and delete it 3452 return deleteContact(contactId, callerIsSyncAdapter); 3453 } else { 3454 // row was changed (e.g. the merging might have changed), we got multiple 3455 // rows or the supplied selection filtered the record out 3456 return 0; 3457 } 3458 } finally { 3459 c.close(); 3460 } 3461 } 3462 3463 case RAW_CONTACTS: { 3464 int numDeletes = 0; 3465 Cursor c = mDb.query(Tables.RAW_CONTACTS, 3466 new String[]{RawContacts._ID, RawContacts.CONTACT_ID}, 3467 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 3468 try { 3469 while (c.moveToNext()) { 3470 final long rawContactId = c.getLong(0); 3471 long contactId = c.getLong(1); 3472 numDeletes += deleteRawContact(rawContactId, contactId, 3473 callerIsSyncAdapter); 3474 } 3475 } finally { 3476 c.close(); 3477 } 3478 return numDeletes; 3479 } 3480 3481 case RAW_CONTACTS_ID: { 3482 final long rawContactId = ContentUris.parseId(uri); 3483 return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId), 3484 callerIsSyncAdapter); 3485 } 3486 3487 case DATA: { 3488 mSyncToNetwork |= !callerIsSyncAdapter; 3489 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 3490 callerIsSyncAdapter); 3491 } 3492 3493 case DATA_ID: 3494 case PHONES_ID: 3495 case EMAILS_ID: 3496 case POSTALS_ID: { 3497 long dataId = ContentUris.parseId(uri); 3498 mSyncToNetwork |= !callerIsSyncAdapter; 3499 mSelectionArgs1[0] = String.valueOf(dataId); 3500 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); 3501 } 3502 3503 case GROUPS_ID: { 3504 mSyncToNetwork |= !callerIsSyncAdapter; 3505 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter); 3506 } 3507 3508 case GROUPS: { 3509 int numDeletes = 0; 3510 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 3511 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 3512 try { 3513 while (c.moveToNext()) { 3514 numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter); 3515 } 3516 } finally { 3517 c.close(); 3518 } 3519 if (numDeletes > 0) { 3520 mSyncToNetwork |= !callerIsSyncAdapter; 3521 } 3522 return numDeletes; 3523 } 3524 3525 case SETTINGS: { 3526 mSyncToNetwork |= !callerIsSyncAdapter; 3527 return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs); 3528 } 3529 3530 case STATUS_UPDATES: { 3531 return deleteStatusUpdates(selection, selectionArgs); 3532 } 3533 3534 default: { 3535 mSyncToNetwork = true; 3536 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 3537 } 3538 } 3539 } 3540 3541 public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) { 3542 mGroupIdCache.clear(); 3543 final long groupMembershipMimetypeId = mDbHelper 3544 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 3545 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 3546 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 3547 + groupId, null); 3548 3549 try { 3550 if (callerIsSyncAdapter) { 3551 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 3552 } else { 3553 mValues.clear(); 3554 mValues.put(Groups.DELETED, 1); 3555 mValues.put(Groups.DIRTY, 1); 3556 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 3557 } 3558 } finally { 3559 mVisibleTouched = true; 3560 } 3561 } 3562 3563 private int deleteSettings(Uri uri, String selection, String[] selectionArgs) { 3564 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 3565 mVisibleTouched = true; 3566 return count; 3567 } 3568 3569 private int deleteContact(long contactId, boolean callerIsSyncAdapter) { 3570 mSelectionArgs1[0] = Long.toString(contactId); 3571 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 3572 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, 3573 null, null, null); 3574 try { 3575 while (c.moveToNext()) { 3576 long rawContactId = c.getLong(0); 3577 markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); 3578 } 3579 } finally { 3580 c.close(); 3581 } 3582 3583 mProviderStatusUpdateNeeded = true; 3584 3585 return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); 3586 } 3587 3588 public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) { 3589 mContactAggregator.invalidateAggregationExceptionCache(); 3590 mProviderStatusUpdateNeeded = true; 3591 3592 if (callerIsSyncAdapter) { 3593 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 3594 int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 3595 mContactAggregator.updateDisplayNameForContact(mDb, contactId); 3596 return count; 3597 } else { 3598 mDbHelper.removeContactIfSingleton(rawContactId); 3599 return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); 3600 } 3601 } 3602 3603 private int deleteStatusUpdates(String selection, String[] selectionArgs) { 3604 // delete from both tables: presence and status_updates 3605 // TODO should account type/name be appended to the where clause? 3606 if (VERBOSE_LOGGING) { 3607 Log.v(TAG, "deleting data from status_updates for " + selection); 3608 } 3609 mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection), 3610 selectionArgs); 3611 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 3612 } 3613 3614 private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) { 3615 mSyncToNetwork = true; 3616 3617 mValues.clear(); 3618 mValues.put(RawContacts.DELETED, 1); 3619 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 3620 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 3621 mValues.putNull(RawContacts.CONTACT_ID); 3622 mValues.put(RawContacts.DIRTY, 1); 3623 return updateRawContact(rawContactId, mValues, callerIsSyncAdapter); 3624 } 3625 3626 @Override 3627 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 3628 String[] selectionArgs) { 3629 if (VERBOSE_LOGGING) { 3630 Log.v(TAG, "updateInTransaction: " + uri); 3631 } 3632 3633 int count = 0; 3634 3635 final int match = sUriMatcher.match(uri); 3636 if (match == SYNCSTATE_ID && selection == null) { 3637 long rowId = ContentUris.parseId(uri); 3638 Object data = values.get(ContactsContract.SyncState.DATA); 3639 mUpdatedSyncStates.put(rowId, data); 3640 return 1; 3641 } 3642 flushTransactionalChanges(); 3643 final boolean callerIsSyncAdapter = 3644 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3645 switch(match) { 3646 case SYNCSTATE: 3647 return mDbHelper.getSyncState().update(mDb, values, 3648 appendAccountToSelection(uri, selection), selectionArgs); 3649 3650 case SYNCSTATE_ID: { 3651 selection = appendAccountToSelection(uri, selection); 3652 String selectionWithId = 3653 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3654 + (selection == null ? "" : " AND (" + selection + ")"); 3655 return mDbHelper.getSyncState().update(mDb, values, 3656 selectionWithId, selectionArgs); 3657 } 3658 3659 case CONTACTS: { 3660 count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter); 3661 break; 3662 } 3663 3664 case CONTACTS_ID: { 3665 count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter); 3666 break; 3667 } 3668 3669 case CONTACTS_LOOKUP: 3670 case CONTACTS_LOOKUP_ID: { 3671 final List<String> pathSegments = uri.getPathSegments(); 3672 final int segmentCount = pathSegments.size(); 3673 if (segmentCount < 3) { 3674 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3675 "Missing a lookup key", uri)); 3676 } 3677 final String lookupKey = pathSegments.get(2); 3678 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 3679 count = updateContactOptions(contactId, values, callerIsSyncAdapter); 3680 break; 3681 } 3682 3683 case RAW_CONTACTS_DATA: { 3684 final String rawContactId = uri.getPathSegments().get(1); 3685 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") 3686 + (selection == null ? "" : " AND " + selection); 3687 3688 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); 3689 3690 break; 3691 } 3692 3693 case DATA: { 3694 count = updateData(uri, values, appendAccountToSelection(uri, selection), 3695 selectionArgs, callerIsSyncAdapter); 3696 if (count > 0) { 3697 mSyncToNetwork |= !callerIsSyncAdapter; 3698 } 3699 break; 3700 } 3701 3702 case DATA_ID: 3703 case PHONES_ID: 3704 case EMAILS_ID: 3705 case POSTALS_ID: { 3706 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); 3707 if (count > 0) { 3708 mSyncToNetwork |= !callerIsSyncAdapter; 3709 } 3710 break; 3711 } 3712 3713 case RAW_CONTACTS: { 3714 selection = appendAccountToSelection(uri, selection); 3715 count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter); 3716 break; 3717 } 3718 3719 case RAW_CONTACTS_ID: { 3720 long rawContactId = ContentUris.parseId(uri); 3721 if (selection != null) { 3722 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3723 count = updateRawContacts(values, RawContacts._ID + "=?" 3724 + " AND(" + selection + ")", selectionArgs, 3725 callerIsSyncAdapter); 3726 } else { 3727 mSelectionArgs1[0] = String.valueOf(rawContactId); 3728 count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1, 3729 callerIsSyncAdapter); 3730 } 3731 break; 3732 } 3733 3734 case GROUPS: { 3735 count = updateGroups(uri, values, appendAccountToSelection(uri, selection), 3736 selectionArgs, callerIsSyncAdapter); 3737 if (count > 0) { 3738 mSyncToNetwork |= !callerIsSyncAdapter; 3739 } 3740 break; 3741 } 3742 3743 case GROUPS_ID: { 3744 long groupId = ContentUris.parseId(uri); 3745 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); 3746 String selectionWithId = Groups._ID + "=? " 3747 + (selection == null ? "" : " AND " + selection); 3748 count = updateGroups(uri, values, selectionWithId, selectionArgs, 3749 callerIsSyncAdapter); 3750 if (count > 0) { 3751 mSyncToNetwork |= !callerIsSyncAdapter; 3752 } 3753 break; 3754 } 3755 3756 case AGGREGATION_EXCEPTIONS: { 3757 count = updateAggregationException(mDb, values); 3758 break; 3759 } 3760 3761 case SETTINGS: { 3762 count = updateSettings(uri, values, appendAccountToSelection(uri, selection), 3763 selectionArgs); 3764 mSyncToNetwork |= !callerIsSyncAdapter; 3765 break; 3766 } 3767 3768 case STATUS_UPDATES: { 3769 count = updateStatusUpdate(uri, values, selection, selectionArgs); 3770 break; 3771 } 3772 3773 case DIRECTORIES: { 3774 mContactDirectoryManager.scheduleDirectoryUpdateForCaller(); 3775 count = 1; 3776 break; 3777 } 3778 3779 default: { 3780 mSyncToNetwork = true; 3781 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 3782 } 3783 } 3784 3785 return count; 3786 } 3787 3788 private int updateStatusUpdate(Uri uri, ContentValues values, String selection, 3789 String[] selectionArgs) { 3790 // update status_updates table, if status is provided 3791 // TODO should account type/name be appended to the where clause? 3792 int updateCount = 0; 3793 ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values); 3794 if (settableValues.size() > 0) { 3795 updateCount = mDb.update(Tables.STATUS_UPDATES, 3796 settableValues, 3797 getWhereClauseForStatusUpdatesTable(selection), 3798 selectionArgs); 3799 } 3800 3801 // now update the Presence table 3802 settableValues = getSettableColumnsForPresenceTable(values); 3803 if (settableValues.size() > 0) { 3804 updateCount = mDb.update(Tables.PRESENCE, settableValues, 3805 selection, selectionArgs); 3806 } 3807 // TODO updateCount is not entirely a valid count of updated rows because 2 tables could 3808 // potentially get updated in this method. 3809 return updateCount; 3810 } 3811 3812 /** 3813 * Build a where clause to select the rows to be updated in status_updates table. 3814 */ 3815 private String getWhereClauseForStatusUpdatesTable(String selection) { 3816 mSb.setLength(0); 3817 mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE); 3818 mSb.append(selection); 3819 mSb.append(")"); 3820 return mSb.toString(); 3821 } 3822 3823 private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) { 3824 mValues.clear(); 3825 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values, 3826 StatusUpdates.STATUS); 3827 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values, 3828 StatusUpdates.STATUS_TIMESTAMP); 3829 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values, 3830 StatusUpdates.STATUS_RES_PACKAGE); 3831 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values, 3832 StatusUpdates.STATUS_LABEL); 3833 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values, 3834 StatusUpdates.STATUS_ICON); 3835 return mValues; 3836 } 3837 3838 private ContentValues getSettableColumnsForPresenceTable(ContentValues values) { 3839 mValues.clear(); 3840 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values, 3841 StatusUpdates.PRESENCE); 3842 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values, 3843 StatusUpdates.CHAT_CAPABILITY); 3844 return mValues; 3845 } 3846 3847 private int updateGroups(Uri uri, ContentValues values, String selectionWithId, 3848 String[] selectionArgs, boolean callerIsSyncAdapter) { 3849 3850 mGroupIdCache.clear(); 3851 3852 ContentValues updatedValues; 3853 if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) { 3854 updatedValues = mValues; 3855 updatedValues.clear(); 3856 updatedValues.putAll(values); 3857 updatedValues.put(Groups.DIRTY, 1); 3858 } else { 3859 updatedValues = values; 3860 } 3861 3862 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 3863 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 3864 mVisibleTouched = true; 3865 } 3866 if (updatedValues.containsKey(Groups.SHOULD_SYNC) 3867 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) { 3868 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME, 3869 Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null, 3870 null, null); 3871 String accountName; 3872 String accountType; 3873 try { 3874 while (c.moveToNext()) { 3875 accountName = c.getString(0); 3876 accountType = c.getString(1); 3877 if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 3878 Account account = new Account(accountName, accountType); 3879 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, 3880 new Bundle()); 3881 break; 3882 } 3883 } 3884 } finally { 3885 c.close(); 3886 } 3887 } 3888 return count; 3889 } 3890 3891 private int updateSettings(Uri uri, ContentValues values, String selection, 3892 String[] selectionArgs) { 3893 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 3894 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 3895 mVisibleTouched = true; 3896 } 3897 return count; 3898 } 3899 3900 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs, 3901 boolean callerIsSyncAdapter) { 3902 if (values.containsKey(RawContacts.CONTACT_ID)) { 3903 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 3904 "in content values. Contact IDs are assigned automatically"); 3905 } 3906 3907 if (!callerIsSyncAdapter) { 3908 selection = DatabaseUtils.concatenateWhere(selection, 3909 RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0"); 3910 } 3911 3912 int count = 0; 3913 Cursor cursor = mDb.query(mDbHelper.getRawContactView(), 3914 new String[] { RawContacts._ID }, selection, 3915 selectionArgs, null, null, null); 3916 try { 3917 while (cursor.moveToNext()) { 3918 long rawContactId = cursor.getLong(0); 3919 updateRawContact(rawContactId, values, callerIsSyncAdapter); 3920 count++; 3921 } 3922 } finally { 3923 cursor.close(); 3924 } 3925 3926 return count; 3927 } 3928 3929 private int updateRawContact(long rawContactId, ContentValues values, 3930 boolean callerIsSyncAdapter) { 3931 final String selection = RawContacts._ID + " = ?"; 3932 mSelectionArgs1[0] = Long.toString(rawContactId); 3933 final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED) 3934 && values.getAsInteger(RawContacts.DELETED) == 0); 3935 int previousDeleted = 0; 3936 String accountType = null; 3937 String accountName = null; 3938 if (requestUndoDelete) { 3939 Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection, 3940 mSelectionArgs1, null, null, null); 3941 try { 3942 if (cursor.moveToFirst()) { 3943 previousDeleted = cursor.getInt(RawContactsQuery.DELETED); 3944 accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE); 3945 accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME); 3946 } 3947 } finally { 3948 cursor.close(); 3949 } 3950 values.put(ContactsContract.RawContacts.AGGREGATION_MODE, 3951 ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT); 3952 } 3953 3954 int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1); 3955 if (count != 0) { 3956 if (values.containsKey(RawContacts.AGGREGATION_MODE)) { 3957 int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE); 3958 3959 // As per ContactsContract documentation, changing aggregation mode 3960 // to DEFAULT should not trigger aggregation 3961 if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) { 3962 mContactAggregator.markForAggregation(rawContactId, aggregationMode, false); 3963 } 3964 } 3965 if (values.containsKey(RawContacts.STARRED)) { 3966 if (!callerIsSyncAdapter) { 3967 updateFavoritesMembership(rawContactId, 3968 values.getAsLong(RawContacts.STARRED) != 0); 3969 } 3970 mContactAggregator.updateStarred(rawContactId); 3971 } else { 3972 // if this raw contact is being associated with an account, then update the 3973 // favorites group membership based on whether or not this contact is starred. 3974 // If it is starred, add a group membership, if one doesn't already exist 3975 // otherwise delete any matching group memberships. 3976 if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { 3977 boolean starred = 0 != DatabaseUtils.longForQuery(mDb, 3978 SELECTION_STARRED_FROM_RAW_CONTACTS, 3979 new String[]{Long.toString(rawContactId)}); 3980 updateFavoritesMembership(rawContactId, starred); 3981 } 3982 } 3983 3984 // if this raw contact is being associated with an account, then add a 3985 // group membership to the group marked as AutoAdd, if any. 3986 if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { 3987 addAutoAddMembership(rawContactId); 3988 } 3989 3990 if (values.containsKey(RawContacts.SOURCE_ID)) { 3991 mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId); 3992 } 3993 if (values.containsKey(RawContacts.NAME_VERIFIED)) { 3994 3995 // If setting NAME_VERIFIED for this raw contact, reset it for all 3996 // other raw contacts in the same aggregate 3997 if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) { 3998 mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId); 3999 mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId); 4000 mResetNameVerifiedForOtherRawContacts.execute(); 4001 } 4002 mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId); 4003 } 4004 if (requestUndoDelete && previousDeleted == 1) { 4005 // undo delete, needs aggregation again. 4006 mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType)); 4007 } 4008 } 4009 return count; 4010 } 4011 4012 private int updateData(Uri uri, ContentValues values, String selection, 4013 String[] selectionArgs, boolean callerIsSyncAdapter) { 4014 mValues.clear(); 4015 mValues.putAll(values); 4016 mValues.remove(Data._ID); 4017 mValues.remove(Data.RAW_CONTACT_ID); 4018 mValues.remove(Data.MIMETYPE); 4019 4020 String packageName = values.getAsString(Data.RES_PACKAGE); 4021 if (packageName != null) { 4022 mValues.remove(Data.RES_PACKAGE); 4023 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 4024 } 4025 4026 boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY); 4027 boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY); 4028 4029 // Remove primary or super primary values being set to 0. This is disallowed by the 4030 // content provider. 4031 if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { 4032 containsIsSuperPrimary = false; 4033 mValues.remove(Data.IS_SUPER_PRIMARY); 4034 } 4035 if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) { 4036 containsIsPrimary = false; 4037 mValues.remove(Data.IS_PRIMARY); 4038 } 4039 4040 if (!callerIsSyncAdapter) { 4041 selection = DatabaseUtils.concatenateWhere(selection, 4042 Data.IS_READ_ONLY + "=0"); 4043 } 4044 4045 int count = 0; 4046 4047 // Note that the query will return data according to the access restrictions, 4048 // so we don't need to worry about updating data we don't have permission to read. 4049 Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null); 4050 try { 4051 while(c.moveToNext()) { 4052 count += updateData(mValues, c, callerIsSyncAdapter); 4053 } 4054 } finally { 4055 c.close(); 4056 } 4057 4058 return count; 4059 } 4060 4061 private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { 4062 if (values.size() == 0) { 4063 return 0; 4064 } 4065 4066 final String mimeType = c.getString(DataUpdateQuery.MIMETYPE); 4067 DataRowHandler rowHandler = getDataRowHandler(mimeType); 4068 if (rowHandler.update(mDb, values, c, callerIsSyncAdapter)) { 4069 return 1; 4070 } else { 4071 return 0; 4072 } 4073 } 4074 4075 private int updateContactOptions(ContentValues values, String selection, 4076 String[] selectionArgs, boolean callerIsSyncAdapter) { 4077 int count = 0; 4078 Cursor cursor = mDb.query(mDbHelper.getContactView(), 4079 new String[] { Contacts._ID }, selection, 4080 selectionArgs, null, null, null); 4081 try { 4082 while (cursor.moveToNext()) { 4083 long contactId = cursor.getLong(0); 4084 updateContactOptions(contactId, values, callerIsSyncAdapter); 4085 count++; 4086 } 4087 } finally { 4088 cursor.close(); 4089 } 4090 4091 return count; 4092 } 4093 4094 private int updateContactOptions(long contactId, ContentValues values, 4095 boolean callerIsSyncAdapter) { 4096 4097 mValues.clear(); 4098 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 4099 values, Contacts.CUSTOM_RINGTONE); 4100 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 4101 values, Contacts.SEND_TO_VOICEMAIL); 4102 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 4103 values, Contacts.LAST_TIME_CONTACTED); 4104 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 4105 values, Contacts.TIMES_CONTACTED); 4106 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 4107 values, Contacts.STARRED); 4108 4109 // Nothing to update - just return 4110 if (mValues.size() == 0) { 4111 return 0; 4112 } 4113 4114 if (mValues.containsKey(RawContacts.STARRED)) { 4115 // Mark dirty when changing starred to trigger sync 4116 mValues.put(RawContacts.DIRTY, 1); 4117 } 4118 4119 mSelectionArgs1[0] = String.valueOf(contactId); 4120 mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?" 4121 + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1); 4122 4123 if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) { 4124 Cursor cursor = mDb.query(mDbHelper.getRawContactView(), 4125 new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?", 4126 mSelectionArgs1, null, null, null); 4127 try { 4128 while (cursor.moveToNext()) { 4129 long rawContactId = cursor.getLong(0); 4130 updateFavoritesMembership(rawContactId, 4131 mValues.getAsLong(RawContacts.STARRED) != 0); 4132 } 4133 } finally { 4134 cursor.close(); 4135 } 4136 } 4137 4138 // Copy changeable values to prevent automatically managed fields from 4139 // being explicitly updated by clients. 4140 mValues.clear(); 4141 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 4142 values, Contacts.CUSTOM_RINGTONE); 4143 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 4144 values, Contacts.SEND_TO_VOICEMAIL); 4145 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 4146 values, Contacts.LAST_TIME_CONTACTED); 4147 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 4148 values, Contacts.TIMES_CONTACTED); 4149 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 4150 values, Contacts.STARRED); 4151 4152 int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1); 4153 4154 if (values.containsKey(Contacts.LAST_TIME_CONTACTED) && 4155 !values.containsKey(Contacts.TIMES_CONTACTED)) { 4156 mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1); 4157 mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1); 4158 } 4159 return rslt; 4160 } 4161 4162 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 4163 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 4164 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 4165 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 4166 4167 long rawContactId1, rawContactId2; 4168 if (rcId1 < rcId2) { 4169 rawContactId1 = rcId1; 4170 rawContactId2 = rcId2; 4171 } else { 4172 rawContactId2 = rcId1; 4173 rawContactId1 = rcId2; 4174 } 4175 4176 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 4177 mSelectionArgs2[0] = String.valueOf(rawContactId1); 4178 mSelectionArgs2[1] = String.valueOf(rawContactId2); 4179 db.delete(Tables.AGGREGATION_EXCEPTIONS, 4180 AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " 4181 + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2); 4182 } else { 4183 ContentValues exceptionValues = new ContentValues(3); 4184 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 4185 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 4186 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 4187 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 4188 exceptionValues); 4189 } 4190 4191 mContactAggregator.invalidateAggregationExceptionCache(); 4192 mContactAggregator.markForAggregation(rawContactId1, 4193 RawContacts.AGGREGATION_MODE_DEFAULT, true); 4194 mContactAggregator.markForAggregation(rawContactId2, 4195 RawContacts.AGGREGATION_MODE_DEFAULT, true); 4196 4197 mContactAggregator.aggregateContact(db, rawContactId1); 4198 mContactAggregator.aggregateContact(db, rawContactId2); 4199 4200 // The return value is fake - we just confirm that we made a change, not count actual 4201 // rows changed. 4202 return 1; 4203 } 4204 4205 public void onAccountsUpdated(Account[] accounts) { 4206 boolean accountsChanged = updateAccounts(accounts); 4207 if (accountsChanged) { 4208 mContactDirectoryManager.scheduleScanAllPackages(true); 4209 } 4210 } 4211 4212 protected void updateAccounts() { 4213 AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false); 4214 Account[] accounts = AccountManager.get(getContext()).getAccounts(); 4215 updateAccounts(accounts); 4216 updateContactsAccountCount(accounts); 4217 } 4218 4219 private boolean updateAccounts(Account[] accounts) { 4220 // TODO : Check the unit test. 4221 boolean accountsChanged = false; 4222 HashSet<Account> existingAccounts = new HashSet<Account>(); 4223 mDb.beginTransaction(); 4224 try { 4225 findValidAccounts(existingAccounts); 4226 4227 // Add a row to the ACCOUNTS table for each new account 4228 for (Account account : accounts) { 4229 if (!existingAccounts.contains(account)) { 4230 accountsChanged = true; 4231 mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME 4232 + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)", 4233 new String[] {account.name, account.type}); 4234 } 4235 } 4236 4237 // Remove all valid accounts from the existing account set. What is left 4238 // in the accountsToDelete set will be extra accounts whose data must be deleted. 4239 HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts); 4240 for (Account account : accounts) { 4241 accountsToDelete.remove(account); 4242 } 4243 4244 if (!accountsToDelete.isEmpty()) { 4245 accountsChanged = true; 4246 for (Account account : accountsToDelete) { 4247 Log.d(TAG, "removing data for removed account " + account); 4248 String[] params = new String[] {account.name, account.type}; 4249 mDb.execSQL( 4250 "DELETE FROM " + Tables.GROUPS + 4251 " WHERE " + Groups.ACCOUNT_NAME + " = ?" + 4252 " AND " + Groups.ACCOUNT_TYPE + " = ?", params); 4253 mDb.execSQL( 4254 "DELETE FROM " + Tables.PRESENCE + 4255 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" + 4256 "SELECT " + RawContacts._ID + 4257 " FROM " + Tables.RAW_CONTACTS + 4258 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 4259 " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params); 4260 mDb.execSQL( 4261 "DELETE FROM " + Tables.RAW_CONTACTS + 4262 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 4263 " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params); 4264 mDb.execSQL( 4265 "DELETE FROM " + Tables.SETTINGS + 4266 " WHERE " + Settings.ACCOUNT_NAME + " = ?" + 4267 " AND " + Settings.ACCOUNT_TYPE + " = ?", params); 4268 mDb.execSQL( 4269 "DELETE FROM " + Tables.ACCOUNTS + 4270 " WHERE " + RawContacts.ACCOUNT_NAME + "=?" + 4271 " AND " + RawContacts.ACCOUNT_TYPE + "=?", params); 4272 mDb.execSQL( 4273 "DELETE FROM " + Tables.DIRECTORIES + 4274 " WHERE " + Directory.ACCOUNT_NAME + "=?" + 4275 " AND " + Directory.ACCOUNT_TYPE + "=?", params); 4276 resetDirectoryCache(); 4277 } 4278 4279 // Find all aggregated contacts that used to contain the raw contacts 4280 // we have just deleted and see if they are still referencing the deleted 4281 // names or photos. If so, fix up those contacts. 4282 HashSet<Long> orphanContactIds = Sets.newHashSet(); 4283 Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID + 4284 " FROM " + Tables.CONTACTS + 4285 " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " + 4286 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " + 4287 "(SELECT " + RawContacts._ID + 4288 " FROM " + Tables.RAW_CONTACTS + "))" + 4289 " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " + 4290 Contacts.PHOTO_ID + " NOT IN " + 4291 "(SELECT " + Data._ID + 4292 " FROM " + Tables.DATA + "))", null); 4293 try { 4294 while (cursor.moveToNext()) { 4295 orphanContactIds.add(cursor.getLong(0)); 4296 } 4297 } finally { 4298 cursor.close(); 4299 } 4300 4301 for (Long contactId : orphanContactIds) { 4302 mContactAggregator.updateAggregateData(contactId); 4303 } 4304 mDbHelper.updateAllVisible(); 4305 } 4306 4307 if (accountsChanged) { 4308 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 4309 } 4310 mDb.setTransactionSuccessful(); 4311 } finally { 4312 mDb.endTransaction(); 4313 } 4314 mAccountWritability.clear(); 4315 4316 if (accountsChanged) { 4317 updateContactsAccountCount(accounts); 4318 updateProviderStatus(); 4319 } 4320 4321 return accountsChanged; 4322 } 4323 4324 private void updateContactsAccountCount(Account[] accounts) { 4325 int count = 0; 4326 for (Account account : accounts) { 4327 if (isContactsAccount(account)) { 4328 count++; 4329 } 4330 } 4331 mContactsAccountCount = count; 4332 } 4333 4334 protected boolean isContactsAccount(Account account) { 4335 final IContentService cs = ContentResolver.getContentService(); 4336 try { 4337 return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; 4338 } catch (RemoteException e) { 4339 Log.e(TAG, "Cannot obtain sync flag for account: " + account, e); 4340 return false; 4341 } 4342 } 4343 4344 public void onPackageChanged(String packageName) { 4345 mContactDirectoryManager.onPackageChanged(packageName); 4346 } 4347 4348 /** 4349 * Finds all distinct accounts present in the specified table. 4350 */ 4351 private void findValidAccounts(Set<Account> validAccounts) { 4352 Cursor c = mDb.rawQuery( 4353 "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE + 4354 " FROM " + Tables.ACCOUNTS, null); 4355 try { 4356 while (c.moveToNext()) { 4357 if (!c.isNull(0) || !c.isNull(1)) { 4358 validAccounts.add(new Account(c.getString(0), c.getString(1))); 4359 } 4360 } 4361 } finally { 4362 c.close(); 4363 } 4364 } 4365 4366 /** 4367 * Test all against {@link TextUtils#isEmpty(CharSequence)}. 4368 */ 4369 private static boolean areAllEmpty(ContentValues values, String[] keys) { 4370 for (String key : keys) { 4371 if (!TextUtils.isEmpty(values.getAsString(key))) { 4372 return false; 4373 } 4374 } 4375 return true; 4376 } 4377 4378 /** 4379 * Returns true if a value (possibly null) is specified for at least one of the supplied keys. 4380 */ 4381 private static boolean areAnySpecified(ContentValues values, String[] keys) { 4382 for (String key : keys) { 4383 if (values.containsKey(key)) { 4384 return true; 4385 } 4386 } 4387 return false; 4388 } 4389 4390 @Override 4391 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 4392 String sortOrder) { 4393 String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY); 4394 if (directory == null) { 4395 return queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1); 4396 } else if (directory.equals("0")) { 4397 return queryLocal(uri, projection, selection, selectionArgs, sortOrder, 4398 Directory.DEFAULT); 4399 } else if (directory.equals("1")) { 4400 return queryLocal(uri, projection, selection, selectionArgs, sortOrder, 4401 Directory.LOCAL_INVISIBLE); 4402 } 4403 4404 DirectoryInfo directoryInfo = getDirectoryAuthority(directory); 4405 if (directoryInfo == null) { 4406 Log.e(TAG, "Invalid directory ID: " + uri); 4407 return null; 4408 } 4409 4410 Builder builder = new Uri.Builder(); 4411 builder.scheme(ContentResolver.SCHEME_CONTENT); 4412 builder.authority(directoryInfo.authority); 4413 builder.encodedPath(uri.getEncodedPath()); 4414 if (directoryInfo.accountName != null) { 4415 builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName); 4416 } 4417 if (directoryInfo.accountType != null) { 4418 builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType); 4419 } 4420 4421 String limit = getLimit(uri); 4422 if (limit != null) { 4423 builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit); 4424 } 4425 4426 Uri directoryUri = builder.build(); 4427 4428 if (projection == null) { 4429 projection = getDefaultProjection(uri); 4430 } 4431 4432 Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection, 4433 selectionArgs, sortOrder); 4434 while (cursor instanceof CursorWrapper) { 4435 cursor = ((CursorWrapper)cursor).getWrappedCursor(); 4436 } 4437 return cursor; 4438 } 4439 4440 private static final class DirectoryQuery { 4441 public static final String[] COLUMNS = new String[] { 4442 Directory._ID, 4443 Directory.DIRECTORY_AUTHORITY, 4444 Directory.ACCOUNT_NAME, 4445 Directory.ACCOUNT_TYPE 4446 }; 4447 4448 public static final int DIRECTORY_ID = 0; 4449 public static final int AUTHORITY = 1; 4450 public static final int ACCOUNT_NAME = 2; 4451 public static final int ACCOUNT_TYPE = 3; 4452 } 4453 4454 /** 4455 * Reads and caches directory information for the database. 4456 */ 4457 private DirectoryInfo getDirectoryAuthority(String directoryId) { 4458 synchronized (mDirectoryCache) { 4459 if (!mDirectoryCacheValid) { 4460 mDirectoryCache.clear(); 4461 Cursor cursor = mDb.query(Tables.DIRECTORIES, 4462 DirectoryQuery.COLUMNS, 4463 null, null, null, null, null); 4464 try { 4465 while (cursor.moveToNext()) { 4466 DirectoryInfo info = new DirectoryInfo(); 4467 String id = cursor.getString(DirectoryQuery.DIRECTORY_ID); 4468 info.authority = cursor.getString(DirectoryQuery.AUTHORITY); 4469 info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 4470 info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 4471 mDirectoryCache.put(id, info); 4472 } 4473 } finally { 4474 cursor.close(); 4475 } 4476 mDirectoryCacheValid = true; 4477 } 4478 4479 return mDirectoryCache.get(directoryId); 4480 } 4481 } 4482 4483 public void resetDirectoryCache() { 4484 synchronized(mDirectoryCache) { 4485 mDirectoryCacheValid = false; 4486 } 4487 } 4488 4489 public Cursor queryLocal(Uri uri, String[] projection, String selection, String[] selectionArgs, 4490 String sortOrder, long directoryId) { 4491 if (VERBOSE_LOGGING) { 4492 Log.v(TAG, "query: " + uri); 4493 } 4494 4495 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4496 4497 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4498 String groupBy = null; 4499 String limit = getLimit(uri); 4500 4501 // TODO: Consider writing a test case for RestrictionExceptions when you 4502 // write a new query() block to make sure it protects restricted data. 4503 final int match = sUriMatcher.match(uri); 4504 switch (match) { 4505 case SYNCSTATE: 4506 return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, 4507 sortOrder); 4508 4509 case CONTACTS: { 4510 setTablesAndProjectionMapForContacts(qb, uri, projection); 4511 appendLocalDirectorySelectionIfNeeded(qb, directoryId); 4512 break; 4513 } 4514 4515 case CONTACTS_ID: { 4516 long contactId = ContentUris.parseId(uri); 4517 setTablesAndProjectionMapForContacts(qb, uri, projection); 4518 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4519 qb.appendWhere(Contacts._ID + "=?"); 4520 break; 4521 } 4522 4523 case CONTACTS_LOOKUP: 4524 case CONTACTS_LOOKUP_ID: { 4525 List<String> pathSegments = uri.getPathSegments(); 4526 int segmentCount = pathSegments.size(); 4527 if (segmentCount < 3) { 4528 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4529 "Missing a lookup key", uri)); 4530 } 4531 4532 String lookupKey = pathSegments.get(2); 4533 if (segmentCount == 4) { 4534 long contactId = Long.parseLong(pathSegments.get(3)); 4535 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 4536 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 4537 4538 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 4539 projection, selection, selectionArgs, sortOrder, groupBy, limit, 4540 Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey); 4541 if (c != null) { 4542 return c; 4543 } 4544 } 4545 4546 setTablesAndProjectionMapForContacts(qb, uri, projection); 4547 selectionArgs = insertSelectionArg(selectionArgs, 4548 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 4549 qb.appendWhere(Contacts._ID + "=?"); 4550 break; 4551 } 4552 4553 case CONTACTS_LOOKUP_DATA: 4554 case CONTACTS_LOOKUP_ID_DATA: { 4555 List<String> pathSegments = uri.getPathSegments(); 4556 int segmentCount = pathSegments.size(); 4557 if (segmentCount < 4) { 4558 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4559 "Missing a lookup key", uri)); 4560 } 4561 String lookupKey = pathSegments.get(2); 4562 if (segmentCount == 5) { 4563 long contactId = Long.parseLong(pathSegments.get(3)); 4564 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 4565 setTablesAndProjectionMapForData(lookupQb, uri, projection, false); 4566 lookupQb.appendWhere(" AND "); 4567 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 4568 projection, selection, selectionArgs, sortOrder, groupBy, limit, 4569 Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey); 4570 if (c != null) { 4571 return c; 4572 } 4573 4574 // TODO see if the contact exists but has no data rows (rare) 4575 } 4576 4577 setTablesAndProjectionMapForData(qb, uri, projection, false); 4578 selectionArgs = insertSelectionArg(selectionArgs, 4579 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 4580 qb.appendWhere(" AND " + Data.CONTACT_ID + "=?"); 4581 break; 4582 } 4583 4584 case CONTACTS_AS_VCARD: { 4585 // When reading as vCard always use restricted view 4586 final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); 4587 qb.setTables(mDbHelper.getContactView(true /* require restricted */)); 4588 qb.setProjectionMap(sContactsVCardProjectionMap); 4589 selectionArgs = insertSelectionArg(selectionArgs, 4590 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 4591 qb.appendWhere(Contacts._ID + "=?"); 4592 break; 4593 } 4594 4595 case CONTACTS_AS_MULTI_VCARD: { 4596 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); 4597 String currentDateString = dateFormat.format(new Date()).toString(); 4598 return db.rawQuery( 4599 "SELECT" + 4600 " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," + 4601 " NULL AS " + OpenableColumns.SIZE, 4602 new String[] { currentDateString }); 4603 } 4604 4605 case CONTACTS_FILTER: { 4606 String filterParam = ""; 4607 if (uri.getPathSegments().size() > 2) { 4608 filterParam = uri.getLastPathSegment(); 4609 } 4610 setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam); 4611 appendLocalDirectorySelectionIfNeeded(qb, directoryId); 4612 break; 4613 } 4614 4615 case CONTACTS_STREQUENT_FILTER: 4616 case CONTACTS_STREQUENT: { 4617 String filterSql = null; 4618 if (match == CONTACTS_STREQUENT_FILTER 4619 && uri.getPathSegments().size() > 3) { 4620 String filterParam = uri.getLastPathSegment(); 4621 StringBuilder sb = new StringBuilder(); 4622 sb.append(Contacts._ID + " IN "); 4623 appendContactFilterAsNestedQuery(sb, filterParam); 4624 filterSql = sb.toString(); 4625 } 4626 4627 setTablesAndProjectionMapForContacts(qb, uri, projection); 4628 4629 String[] starredProjection = null; 4630 String[] frequentProjection = null; 4631 if (projection != null) { 4632 starredProjection = 4633 appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN); 4634 frequentProjection = 4635 appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN); 4636 } 4637 4638 // Build the first query for starred 4639 if (filterSql != null) { 4640 qb.appendWhere(filterSql); 4641 } 4642 qb.setProjectionMap(sStrequentStarredProjectionMap); 4643 final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1", 4644 null, Contacts._ID, null, null, null); 4645 4646 // Build the second query for frequent 4647 qb = new SQLiteQueryBuilder(); 4648 setTablesAndProjectionMapForContacts(qb, uri, projection); 4649 if (filterSql != null) { 4650 qb.appendWhere(filterSql); 4651 } 4652 qb.setProjectionMap(sStrequentFrequentProjectionMap); 4653 final String frequentQuery = qb.buildQuery(frequentProjection, 4654 Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED 4655 + " = 0 OR " + Contacts.STARRED + " IS NULL)", 4656 null, Contacts._ID, null, null, null); 4657 4658 // Put them together 4659 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 4660 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 4661 Cursor c = db.rawQuery(query, null); 4662 if (c != null) { 4663 c.setNotificationUri(getContext().getContentResolver(), 4664 ContactsContract.AUTHORITY_URI); 4665 } 4666 return c; 4667 } 4668 4669 case CONTACTS_GROUP: { 4670 setTablesAndProjectionMapForContacts(qb, uri, projection); 4671 if (uri.getPathSegments().size() > 2) { 4672 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 4673 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4674 } 4675 break; 4676 } 4677 4678 case CONTACTS_ID_DATA: { 4679 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4680 setTablesAndProjectionMapForData(qb, uri, projection, false); 4681 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4682 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 4683 break; 4684 } 4685 4686 case CONTACTS_ID_PHOTO: { 4687 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4688 setTablesAndProjectionMapForData(qb, uri, projection, false); 4689 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4690 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 4691 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 4692 break; 4693 } 4694 4695 case CONTACTS_ID_ENTITIES: { 4696 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4697 setTablesAndProjectionMapForEntities(qb, uri, projection); 4698 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4699 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 4700 break; 4701 } 4702 4703 case CONTACTS_LOOKUP_ENTITIES: 4704 case CONTACTS_LOOKUP_ID_ENTITIES: { 4705 List<String> pathSegments = uri.getPathSegments(); 4706 int segmentCount = pathSegments.size(); 4707 if (segmentCount < 4) { 4708 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4709 "Missing a lookup key", uri)); 4710 } 4711 String lookupKey = pathSegments.get(2); 4712 if (segmentCount == 5) { 4713 long contactId = Long.parseLong(pathSegments.get(3)); 4714 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 4715 setTablesAndProjectionMapForEntities(lookupQb, uri, projection); 4716 lookupQb.appendWhere(" AND "); 4717 4718 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 4719 projection, selection, selectionArgs, sortOrder, groupBy, limit, 4720 Contacts.Entity.CONTACT_ID, contactId, 4721 Contacts.Entity.LOOKUP_KEY, lookupKey); 4722 if (c != null) { 4723 return c; 4724 } 4725 } 4726 4727 setTablesAndProjectionMapForEntities(qb, uri, projection); 4728 selectionArgs = insertSelectionArg(selectionArgs, 4729 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 4730 qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?"); 4731 break; 4732 } 4733 4734 case PHONES: { 4735 setTablesAndProjectionMapForData(qb, uri, projection, false); 4736 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4737 break; 4738 } 4739 4740 case PHONES_ID: { 4741 setTablesAndProjectionMapForData(qb, uri, projection, false); 4742 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4743 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4744 qb.appendWhere(" AND " + Data._ID + "=?"); 4745 break; 4746 } 4747 4748 case PHONES_FILTER: { 4749 setTablesAndProjectionMapForData(qb, uri, projection, true); 4750 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4751 if (uri.getPathSegments().size() > 2) { 4752 String filterParam = uri.getLastPathSegment(); 4753 StringBuilder sb = new StringBuilder(); 4754 sb.append(" AND ("); 4755 4756 boolean hasCondition = false; 4757 boolean orNeeded = false; 4758 String normalizedName = NameNormalizer.normalize(filterParam); 4759 if (normalizedName.length() > 0) { 4760 sb.append(Data.RAW_CONTACT_ID + " IN "); 4761 appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); 4762 orNeeded = true; 4763 hasCondition = true; 4764 } 4765 4766 String number = PhoneNumberUtils.normalizeNumber(filterParam); 4767 if (!TextUtils.isEmpty(number)) { 4768 if (orNeeded) { 4769 sb.append(" OR "); 4770 } 4771 sb.append(Data._ID + 4772 " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID 4773 + " FROM " + Tables.PHONE_LOOKUP 4774 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 4775 sb.append(number); 4776 sb.append("%')"); 4777 hasCondition = true; 4778 } 4779 4780 if (!hasCondition) { 4781 // If it is neither a phone number nor a name, the query should return 4782 // an empty cursor. Let's ensure that. 4783 sb.append("0"); 4784 } 4785 sb.append(")"); 4786 qb.appendWhere(sb); 4787 } 4788 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 4789 if (sortOrder == null) { 4790 sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; 4791 } 4792 break; 4793 } 4794 4795 case EMAILS: { 4796 setTablesAndProjectionMapForData(qb, uri, projection, false); 4797 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 4798 break; 4799 } 4800 4801 case EMAILS_ID: { 4802 setTablesAndProjectionMapForData(qb, uri, projection, false); 4803 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4804 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'" 4805 + " AND " + Data._ID + "=?"); 4806 break; 4807 } 4808 4809 case EMAILS_LOOKUP: { 4810 setTablesAndProjectionMapForData(qb, uri, projection, false); 4811 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 4812 if (uri.getPathSegments().size() > 2) { 4813 String email = uri.getLastPathSegment(); 4814 String address = mDbHelper.extractAddressFromEmailAddress(email); 4815 selectionArgs = insertSelectionArg(selectionArgs, address); 4816 qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)"); 4817 } 4818 break; 4819 } 4820 4821 case EMAILS_FILTER: { 4822 setTablesAndProjectionMapForData(qb, uri, projection, true); 4823 String filterParam = null; 4824 if (uri.getPathSegments().size() > 3) { 4825 filterParam = uri.getLastPathSegment(); 4826 if (TextUtils.isEmpty(filterParam)) { 4827 filterParam = null; 4828 } 4829 } 4830 4831 if (filterParam == null) { 4832 // If the filter is unspecified, return nothing 4833 qb.appendWhere(" AND 0"); 4834 } else { 4835 StringBuilder sb = new StringBuilder(); 4836 sb.append(" AND " + Data._ID + " IN ("); 4837 sb.append( 4838 "SELECT " + Data._ID + 4839 " FROM " + Tables.DATA + 4840 " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail + 4841 " AND " + Data.DATA1 + " LIKE "); 4842 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); 4843 if (!filterParam.contains("@")) { 4844 String normalizedName = NameNormalizer.normalize(filterParam); 4845 if (normalizedName.length() > 0) { 4846 4847 /* 4848 * Using a UNION instead of an "OR" to make SQLite use the right 4849 * indexes. We need it to use the (mimetype,data1) index for the 4850 * email lookup (see above), but not for the name lookup. 4851 * SQLite is not smart enough to use the index on one side of an OR 4852 * but not on the other. Using two separate nested queries 4853 * and a UNION between them does the job. 4854 */ 4855 sb.append( 4856 " UNION SELECT " + Data._ID + 4857 " FROM " + Tables.DATA + 4858 " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail + 4859 " AND " + Data.RAW_CONTACT_ID + " IN "); 4860 appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); 4861 } 4862 } 4863 sb.append(")"); 4864 qb.appendWhere(sb); 4865 } 4866 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 4867 if (sortOrder == null) { 4868 sortOrder = EMAIL_FILTER_SORT_ORDER; 4869 } 4870 break; 4871 } 4872 4873 case POSTALS: { 4874 setTablesAndProjectionMapForData(qb, uri, projection, false); 4875 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 4876 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 4877 break; 4878 } 4879 4880 case POSTALS_ID: { 4881 setTablesAndProjectionMapForData(qb, uri, projection, false); 4882 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4883 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 4884 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 4885 qb.appendWhere(" AND " + Data._ID + "=?"); 4886 break; 4887 } 4888 4889 case RAW_CONTACTS: { 4890 setTablesAndProjectionMapForRawContacts(qb, uri); 4891 break; 4892 } 4893 4894 case RAW_CONTACTS_ID: { 4895 long rawContactId = ContentUris.parseId(uri); 4896 setTablesAndProjectionMapForRawContacts(qb, uri); 4897 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 4898 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 4899 break; 4900 } 4901 4902 case RAW_CONTACTS_DATA: { 4903 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 4904 setTablesAndProjectionMapForData(qb, uri, projection, false); 4905 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 4906 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?"); 4907 break; 4908 } 4909 4910 case DATA: { 4911 setTablesAndProjectionMapForData(qb, uri, projection, false); 4912 break; 4913 } 4914 4915 case DATA_ID: { 4916 setTablesAndProjectionMapForData(qb, uri, projection, false); 4917 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4918 qb.appendWhere(" AND " + Data._ID + "=?"); 4919 break; 4920 } 4921 4922 case PHONE_LOOKUP: { 4923 4924 if (TextUtils.isEmpty(sortOrder)) { 4925 // Default the sort order to something reasonable so we get consistent 4926 // results when callers don't request an ordering 4927 sortOrder = " length(lookup.normalized_number) DESC"; 4928 } 4929 4930 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 4931 String numberE164 = 4932 PhoneNumberUtils.formatNumberToE164(number, getCurrentCountryIso()); 4933 String normalizedNumber = 4934 PhoneNumberUtils.normalizeNumber(number); 4935 mDbHelper.buildPhoneLookupAndContactQuery(qb, normalizedNumber, numberE164); 4936 qb.setProjectionMap(sPhoneLookupProjectionMap); 4937 // Phone lookup cannot be combined with a selection 4938 selection = null; 4939 selectionArgs = null; 4940 break; 4941 } 4942 4943 case GROUPS: { 4944 qb.setTables(mDbHelper.getGroupView()); 4945 qb.setProjectionMap(sGroupsProjectionMap); 4946 appendAccountFromParameter(qb, uri); 4947 break; 4948 } 4949 4950 case GROUPS_ID: { 4951 qb.setTables(mDbHelper.getGroupView()); 4952 qb.setProjectionMap(sGroupsProjectionMap); 4953 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4954 qb.appendWhere(Groups._ID + "=?"); 4955 break; 4956 } 4957 4958 case GROUPS_SUMMARY: { 4959 qb.setTables(mDbHelper.getGroupView() + " AS groups"); 4960 qb.setProjectionMap(sGroupsSummaryProjectionMap); 4961 appendAccountFromParameter(qb, uri); 4962 groupBy = Groups._ID; 4963 break; 4964 } 4965 4966 case AGGREGATION_EXCEPTIONS: { 4967 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 4968 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 4969 break; 4970 } 4971 4972 case AGGREGATION_SUGGESTIONS: { 4973 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4974 String filter = null; 4975 if (uri.getPathSegments().size() > 3) { 4976 filter = uri.getPathSegments().get(3); 4977 } 4978 final int maxSuggestions; 4979 if (limit != null) { 4980 maxSuggestions = Integer.parseInt(limit); 4981 } else { 4982 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 4983 } 4984 4985 ArrayList<AggregationSuggestionParameter> parameters = null; 4986 List<String> query = uri.getQueryParameters("query"); 4987 if (query != null && !query.isEmpty()) { 4988 parameters = new ArrayList<AggregationSuggestionParameter>(query.size()); 4989 for (String parameter : query) { 4990 int offset = parameter.indexOf(':'); 4991 parameters.add(offset == -1 4992 ? new AggregationSuggestionParameter( 4993 AggregationSuggestions.PARAMETER_MATCH_NAME, 4994 parameter) 4995 : new AggregationSuggestionParameter( 4996 parameter.substring(0, offset), 4997 parameter.substring(offset + 1))); 4998 } 4999 } 5000 5001 setTablesAndProjectionMapForContacts(qb, uri, projection); 5002 5003 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, 5004 maxSuggestions, filter, parameters); 5005 } 5006 5007 case SETTINGS: { 5008 qb.setTables(Tables.SETTINGS); 5009 qb.setProjectionMap(sSettingsProjectionMap); 5010 appendAccountFromParameter(qb, uri); 5011 5012 // When requesting specific columns, this query requires 5013 // late-binding of the GroupMembership MIME-type. 5014 final String groupMembershipMimetypeId = Long.toString(mDbHelper 5015 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 5016 if (projection != null && projection.length != 0 && 5017 mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 5018 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 5019 } 5020 if (projection != null && projection.length != 0 && 5021 mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 5022 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 5023 } 5024 5025 break; 5026 } 5027 5028 case STATUS_UPDATES: { 5029 setTableAndProjectionMapForStatusUpdates(qb, projection); 5030 break; 5031 } 5032 5033 case STATUS_UPDATES_ID: { 5034 setTableAndProjectionMapForStatusUpdates(qb, projection); 5035 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5036 qb.appendWhere(DataColumns.CONCRETE_ID + "=?"); 5037 break; 5038 } 5039 5040 case SEARCH_SUGGESTIONS: { 5041 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); 5042 } 5043 5044 case SEARCH_SHORTCUT: { 5045 String lookupKey = uri.getLastPathSegment(); 5046 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection); 5047 } 5048 5049 case LIVE_FOLDERS_CONTACTS: 5050 qb.setTables(mDbHelper.getContactView()); 5051 qb.setProjectionMap(sLiveFoldersProjectionMap); 5052 break; 5053 5054 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 5055 qb.setTables(mDbHelper.getContactView()); 5056 qb.setProjectionMap(sLiveFoldersProjectionMap); 5057 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 5058 break; 5059 5060 case LIVE_FOLDERS_CONTACTS_FAVORITES: 5061 qb.setTables(mDbHelper.getContactView()); 5062 qb.setProjectionMap(sLiveFoldersProjectionMap); 5063 qb.appendWhere(Contacts.STARRED + "=1"); 5064 break; 5065 5066 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 5067 qb.setTables(mDbHelper.getContactView()); 5068 qb.setProjectionMap(sLiveFoldersProjectionMap); 5069 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 5070 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5071 break; 5072 5073 case RAW_CONTACT_ENTITIES: { 5074 setTablesAndProjectionMapForRawEntities(qb, uri); 5075 break; 5076 } 5077 5078 case RAW_CONTACT_ENTITY_ID: { 5079 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 5080 setTablesAndProjectionMapForRawEntities(qb, uri); 5081 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 5082 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 5083 break; 5084 } 5085 5086 case PROVIDER_STATUS: { 5087 return queryProviderStatus(uri, projection); 5088 } 5089 5090 case DIRECTORIES : { 5091 qb.setTables(Tables.DIRECTORIES); 5092 qb.setProjectionMap(sDirectoryProjectionMap); 5093 break; 5094 } 5095 5096 case DIRECTORIES_ID : { 5097 long id = ContentUris.parseId(uri); 5098 qb.setTables(Tables.DIRECTORIES); 5099 qb.setProjectionMap(sDirectoryProjectionMap); 5100 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id)); 5101 qb.appendWhere(Directory._ID + "=?"); 5102 break; 5103 } 5104 5105 case COMPLETE_NAME: { 5106 return completeName(uri, projection); 5107 } 5108 5109 default: 5110 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 5111 sortOrder, limit); 5112 } 5113 5114 qb.setStrictProjectionMap(true); 5115 5116 Cursor cursor = 5117 query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 5118 if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) { 5119 cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder); 5120 } 5121 return cursor; 5122 } 5123 5124 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 5125 String selection, String[] selectionArgs, String sortOrder, String groupBy, 5126 String limit) { 5127 if (projection != null && projection.length == 1 5128 && BaseColumns._COUNT.equals(projection[0])) { 5129 qb.setProjectionMap(sCountProjectionMap); 5130 } 5131 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 5132 sortOrder, limit); 5133 if (c != null) { 5134 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 5135 } 5136 return c; 5137 } 5138 5139 /** 5140 * Creates a single-row cursor containing the current status of the provider. 5141 */ 5142 private Cursor queryProviderStatus(Uri uri, String[] projection) { 5143 MatrixCursor cursor = new MatrixCursor(projection); 5144 RowBuilder row = cursor.newRow(); 5145 for (int i = 0; i < projection.length; i++) { 5146 if (ProviderStatus.STATUS.equals(projection[i])) { 5147 row.add(mProviderStatus); 5148 } else if (ProviderStatus.DATA1.equals(projection[i])) { 5149 row.add(mEstimatedStorageRequirement); 5150 } 5151 } 5152 return cursor; 5153 } 5154 5155 /** 5156 * Runs the query with the supplied contact ID and lookup ID. If the query succeeds, 5157 * it returns the resulting cursor, otherwise it returns null and the calling 5158 * method needs to resolve the lookup key and rerun the query. 5159 */ 5160 private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb, 5161 SQLiteDatabase db, Uri uri, 5162 String[] projection, String selection, String[] selectionArgs, 5163 String sortOrder, String groupBy, String limit, 5164 String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) { 5165 String[] args; 5166 if (selectionArgs == null) { 5167 args = new String[2]; 5168 } else { 5169 args = new String[selectionArgs.length + 2]; 5170 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 5171 } 5172 args[0] = String.valueOf(contactId); 5173 args[1] = Uri.encode(lookupKey); 5174 lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?"); 5175 Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, 5176 groupBy, limit); 5177 if (c.getCount() != 0) { 5178 return c; 5179 } 5180 5181 c.close(); 5182 return null; 5183 } 5184 5185 private static final class AddressBookIndexQuery { 5186 public static final String LETTER = "letter"; 5187 public static final String TITLE = "title"; 5188 public static final String COUNT = "count"; 5189 5190 public static final String[] COLUMNS = new String[] { 5191 LETTER, TITLE, COUNT 5192 }; 5193 5194 public static final int COLUMN_LETTER = 0; 5195 public static final int COLUMN_TITLE = 1; 5196 public static final int COLUMN_COUNT = 2; 5197 5198 public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME; 5199 } 5200 5201 /** 5202 * Computes counts by the address book index titles and adds the resulting tally 5203 * to the returned cursor as a bundle of extras. 5204 */ 5205 private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db, 5206 SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) { 5207 String sortKey; 5208 5209 // The sort order suffix could be something like "DESC". 5210 // We want to preserve it in the query even though we will change 5211 // the sort column itself. 5212 String sortOrderSuffix = ""; 5213 if (sortOrder != null) { 5214 int spaceIndex = sortOrder.indexOf(' '); 5215 if (spaceIndex != -1) { 5216 sortKey = sortOrder.substring(0, spaceIndex); 5217 sortOrderSuffix = sortOrder.substring(spaceIndex); 5218 } else { 5219 sortKey = sortOrder; 5220 } 5221 } else { 5222 sortKey = Contacts.SORT_KEY_PRIMARY; 5223 } 5224 5225 String locale = getLocale().toString(); 5226 HashMap<String, String> projectionMap = Maps.newHashMap(); 5227 projectionMap.put(AddressBookIndexQuery.LETTER, 5228 "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER); 5229 5230 /** 5231 * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3, 5232 * to map the first letter of the sort key to a character that is traditionally 5233 * used in phonebooks to represent that letter. For example, in Korean it will 5234 * be the first consonant in the letter; for Japanese it will be Hiragana rather 5235 * than Katakana. 5236 */ 5237 projectionMap.put(AddressBookIndexQuery.TITLE, 5238 "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')" 5239 + " AS " + AddressBookIndexQuery.TITLE); 5240 projectionMap.put(AddressBookIndexQuery.COUNT, 5241 "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT); 5242 qb.setProjectionMap(projectionMap); 5243 5244 Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs, 5245 AddressBookIndexQuery.ORDER_BY, null /* having */, 5246 AddressBookIndexQuery.ORDER_BY + sortOrderSuffix); 5247 5248 try { 5249 int groupCount = indexCursor.getCount(); 5250 String titles[] = new String[groupCount]; 5251 int counts[] = new int[groupCount]; 5252 int indexCount = 0; 5253 String currentTitle = null; 5254 5255 // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up 5256 // with multiple entries for the same title. The following code 5257 // collapses those duplicates. 5258 for (int i = 0; i < groupCount; i++) { 5259 indexCursor.moveToNext(); 5260 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE); 5261 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT); 5262 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) { 5263 titles[indexCount] = currentTitle = title; 5264 counts[indexCount] = count; 5265 indexCount++; 5266 } else { 5267 counts[indexCount - 1] += count; 5268 } 5269 } 5270 5271 if (indexCount < groupCount) { 5272 String[] newTitles = new String[indexCount]; 5273 System.arraycopy(titles, 0, newTitles, 0, indexCount); 5274 titles = newTitles; 5275 5276 int[] newCounts = new int[indexCount]; 5277 System.arraycopy(counts, 0, newCounts, 0, indexCount); 5278 counts = newCounts; 5279 } 5280 5281 final Bundle bundle = new Bundle(); 5282 bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles); 5283 bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts); 5284 return new CursorWrapper(cursor) { 5285 5286 @Override 5287 public Bundle getExtras() { 5288 return bundle; 5289 } 5290 }; 5291 } finally { 5292 indexCursor.close(); 5293 } 5294 } 5295 5296 /** 5297 * Returns the contact Id for the contact identified by the lookupKey. 5298 * Robust against changes in the lookup key: if the key has changed, will 5299 * look up the contact by the raw contact IDs or name encoded in the lookup 5300 * key. 5301 */ 5302 public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 5303 ContactLookupKey key = new ContactLookupKey(); 5304 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 5305 5306 long contactId = -1; 5307 if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) { 5308 contactId = lookupContactIdBySourceIds(db, segments); 5309 if (contactId != -1) { 5310 return contactId; 5311 } 5312 } 5313 5314 boolean hasRawContactIds = 5315 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID); 5316 if (hasRawContactIds) { 5317 contactId = lookupContactIdByRawContactIds(db, segments); 5318 if (contactId != -1) { 5319 return contactId; 5320 } 5321 } 5322 5323 if (hasRawContactIds 5324 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) { 5325 contactId = lookupContactIdByDisplayNames(db, segments); 5326 } 5327 5328 return contactId; 5329 } 5330 5331 private interface LookupBySourceIdQuery { 5332 String TABLE = Tables.RAW_CONTACTS; 5333 5334 String COLUMNS[] = { 5335 RawContacts.CONTACT_ID, 5336 RawContacts.ACCOUNT_TYPE, 5337 RawContacts.ACCOUNT_NAME, 5338 RawContacts.SOURCE_ID 5339 }; 5340 5341 int CONTACT_ID = 0; 5342 int ACCOUNT_TYPE = 1; 5343 int ACCOUNT_NAME = 2; 5344 int SOURCE_ID = 3; 5345 } 5346 5347 private long lookupContactIdBySourceIds(SQLiteDatabase db, 5348 ArrayList<LookupKeySegment> segments) { 5349 StringBuilder sb = new StringBuilder(); 5350 sb.append(RawContacts.SOURCE_ID + " IN ("); 5351 for (int i = 0; i < segments.size(); i++) { 5352 LookupKeySegment segment = segments.get(i); 5353 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) { 5354 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 5355 sb.append(","); 5356 } 5357 } 5358 sb.setLength(sb.length() - 1); // Last comma 5359 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5360 5361 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 5362 sb.toString(), null, null, null, null); 5363 try { 5364 while (c.moveToNext()) { 5365 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); 5366 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 5367 int accountHashCode = 5368 ContactLookupKey.getAccountHashCode(accountType, accountName); 5369 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 5370 for (int i = 0; i < segments.size(); i++) { 5371 LookupKeySegment segment = segments.get(i); 5372 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID 5373 && accountHashCode == segment.accountHashCode 5374 && segment.key.equals(sourceId)) { 5375 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 5376 break; 5377 } 5378 } 5379 } 5380 } finally { 5381 c.close(); 5382 } 5383 5384 return getMostReferencedContactId(segments); 5385 } 5386 5387 private interface LookupByRawContactIdQuery { 5388 String TABLE = Tables.RAW_CONTACTS; 5389 5390 String COLUMNS[] = { 5391 RawContacts.CONTACT_ID, 5392 RawContacts.ACCOUNT_TYPE, 5393 RawContacts.ACCOUNT_NAME, 5394 RawContacts._ID, 5395 }; 5396 5397 int CONTACT_ID = 0; 5398 int ACCOUNT_TYPE = 1; 5399 int ACCOUNT_NAME = 2; 5400 int ID = 3; 5401 } 5402 5403 private long lookupContactIdByRawContactIds(SQLiteDatabase db, 5404 ArrayList<LookupKeySegment> segments) { 5405 StringBuilder sb = new StringBuilder(); 5406 sb.append(RawContacts._ID + " IN ("); 5407 for (int i = 0; i < segments.size(); i++) { 5408 LookupKeySegment segment = segments.get(i); 5409 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 5410 sb.append(segment.rawContactId); 5411 sb.append(","); 5412 } 5413 } 5414 sb.setLength(sb.length() - 1); // Last comma 5415 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5416 5417 Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS, 5418 sb.toString(), null, null, null, null); 5419 try { 5420 while (c.moveToNext()) { 5421 String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE); 5422 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME); 5423 int accountHashCode = 5424 ContactLookupKey.getAccountHashCode(accountType, accountName); 5425 String rawContactId = c.getString(LookupByRawContactIdQuery.ID); 5426 for (int i = 0; i < segments.size(); i++) { 5427 LookupKeySegment segment = segments.get(i); 5428 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID 5429 && accountHashCode == segment.accountHashCode 5430 && segment.rawContactId.equals(rawContactId)) { 5431 segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID); 5432 break; 5433 } 5434 } 5435 } 5436 } finally { 5437 c.close(); 5438 } 5439 5440 return getMostReferencedContactId(segments); 5441 } 5442 5443 private interface LookupByDisplayNameQuery { 5444 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 5445 5446 String COLUMNS[] = { 5447 RawContacts.CONTACT_ID, 5448 RawContacts.ACCOUNT_TYPE, 5449 RawContacts.ACCOUNT_NAME, 5450 NameLookupColumns.NORMALIZED_NAME 5451 }; 5452 5453 int CONTACT_ID = 0; 5454 int ACCOUNT_TYPE = 1; 5455 int ACCOUNT_NAME = 2; 5456 int NORMALIZED_NAME = 3; 5457 } 5458 5459 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 5460 ArrayList<LookupKeySegment> segments) { 5461 StringBuilder sb = new StringBuilder(); 5462 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 5463 for (int i = 0; i < segments.size(); i++) { 5464 LookupKeySegment segment = segments.get(i); 5465 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 5466 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 5467 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 5468 sb.append(","); 5469 } 5470 } 5471 sb.setLength(sb.length() - 1); // Last comma 5472 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 5473 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5474 5475 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 5476 sb.toString(), null, null, null, null); 5477 try { 5478 while (c.moveToNext()) { 5479 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); 5480 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 5481 int accountHashCode = 5482 ContactLookupKey.getAccountHashCode(accountType, accountName); 5483 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 5484 for (int i = 0; i < segments.size(); i++) { 5485 LookupKeySegment segment = segments.get(i); 5486 if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 5487 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) 5488 && accountHashCode == segment.accountHashCode 5489 && segment.key.equals(name)) { 5490 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 5491 break; 5492 } 5493 } 5494 } 5495 } finally { 5496 c.close(); 5497 } 5498 5499 return getMostReferencedContactId(segments); 5500 } 5501 5502 private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) { 5503 for (int i = 0; i < segments.size(); i++) { 5504 LookupKeySegment segment = segments.get(i); 5505 if (segment.lookupType == lookupType) { 5506 return true; 5507 } 5508 } 5509 5510 return false; 5511 } 5512 5513 public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { 5514 mContactAggregator.updateLookupKeyForRawContact(db, rawContactId); 5515 } 5516 5517 /** 5518 * Returns the contact ID that is mentioned the highest number of times. 5519 */ 5520 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 5521 Collections.sort(segments); 5522 5523 long bestContactId = -1; 5524 int bestRefCount = 0; 5525 5526 long contactId = -1; 5527 int count = 0; 5528 5529 int segmentCount = segments.size(); 5530 for (int i = 0; i < segmentCount; i++) { 5531 LookupKeySegment segment = segments.get(i); 5532 if (segment.contactId != -1) { 5533 if (segment.contactId == contactId) { 5534 count++; 5535 } else { 5536 if (count > bestRefCount) { 5537 bestContactId = contactId; 5538 bestRefCount = count; 5539 } 5540 contactId = segment.contactId; 5541 count = 1; 5542 } 5543 } 5544 } 5545 if (count > bestRefCount) { 5546 return contactId; 5547 } else { 5548 return bestContactId; 5549 } 5550 } 5551 5552 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 5553 String[] projection) { 5554 StringBuilder sb = new StringBuilder(); 5555 appendContactsTables(sb, uri, projection); 5556 qb.setTables(sb.toString()); 5557 qb.setProjectionMap(sContactsProjectionMap); 5558 } 5559 5560 /** 5561 * Finds name lookup records matching the supplied filter, picks one arbitrary match per 5562 * contact and joins that with other contacts tables. 5563 */ 5564 private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri, 5565 String[] projection, String filter) { 5566 5567 StringBuilder sb = new StringBuilder(); 5568 appendContactsTables(sb, uri, projection); 5569 5570 sb.append(" JOIN (SELECT " + 5571 RawContacts.CONTACT_ID + " AS snippet_contact_id"); 5572 5573 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA_ID)) { 5574 sb.append(", " + DataColumns.CONCRETE_ID + " AS " 5575 + SearchSnippetColumns.SNIPPET_DATA_ID); 5576 } 5577 5578 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA1)) { 5579 sb.append(", " + Data.DATA1 + " AS " + SearchSnippetColumns.SNIPPET_DATA1); 5580 } 5581 5582 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA2)) { 5583 sb.append(", " + Data.DATA2 + " AS " + SearchSnippetColumns.SNIPPET_DATA2); 5584 } 5585 5586 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA3)) { 5587 sb.append(", " + Data.DATA3 + " AS " + SearchSnippetColumns.SNIPPET_DATA3); 5588 } 5589 5590 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA4)) { 5591 sb.append(", " + Data.DATA4 + " AS " + SearchSnippetColumns.SNIPPET_DATA4); 5592 } 5593 5594 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_MIMETYPE)) { 5595 sb.append(", (" + 5596 "SELECT " + MimetypesColumns.MIMETYPE + 5597 " FROM " + Tables.MIMETYPES + 5598 " WHERE " + MimetypesColumns._ID + "=" + DataColumns.MIMETYPE_ID + 5599 ") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE); 5600 } 5601 5602 sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS + " WHERE "); 5603 5604 if (!TextUtils.isEmpty(filter)) { 5605 String normalizedFilter = NameNormalizer.normalize(filter); 5606 if (!TextUtils.isEmpty(normalizedFilter)) { 5607 sb.append(DataColumns.CONCRETE_ID + " IN ("); 5608 5609 // Construct a query that gives us exactly one data _id per matching contact. 5610 // MIN stands in for ANY in this context. 5611 sb.append( 5612 "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" + 5613 " FROM " + Tables.NAME_LOOKUP + 5614 " JOIN " + Tables.RAW_CONTACTS + 5615 " ON (" + RawContactsColumns.CONCRETE_ID 5616 + "=" + Tables.NAME_LOOKUP + "." 5617 + NameLookupColumns.RAW_CONTACT_ID + ")" + 5618 " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '"); 5619 sb.append(normalizedFilter); 5620 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + 5621 " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" + 5622 " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID + 5623 ")"); 5624 } else { 5625 sb.append("0"); // Empty filter - return an empty set 5626 } 5627 } else { 5628 sb.append("0"); // Empty filter - return an empty set 5629 } 5630 5631 sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)"); 5632 5633 qb.setTables(sb.toString()); 5634 qb.setProjectionMap(sContactsProjectionWithSnippetMap); 5635 } 5636 5637 private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) { 5638 boolean excludeRestrictedData = false; 5639 String requestingPackage = getQueryParameter(uri, 5640 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 5641 if (requestingPackage != null) { 5642 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 5643 } 5644 sb.append(mDbHelper.getContactView(excludeRestrictedData)); 5645 appendContactPresenceJoin(sb, projection, Contacts._ID); 5646 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 5647 } 5648 5649 private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) { 5650 StringBuilder sb = new StringBuilder(); 5651 boolean excludeRestrictedData = false; 5652 String requestingPackage = getQueryParameter(uri, 5653 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 5654 if (requestingPackage != null) { 5655 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 5656 } 5657 sb.append(mDbHelper.getRawContactView(excludeRestrictedData)); 5658 qb.setTables(sb.toString()); 5659 qb.setProjectionMap(sRawContactsProjectionMap); 5660 appendAccountFromParameter(qb, uri); 5661 } 5662 5663 private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) { 5664 qb.setTables(mDbHelper.getRawEntitiesView(shouldExcludeRestrictedData(uri))); 5665 qb.setProjectionMap(sRawEntityProjectionMap); 5666 appendAccountFromParameter(qb, uri); 5667 } 5668 5669 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 5670 String[] projection, boolean distinct) { 5671 StringBuilder sb = new StringBuilder(); 5672 sb.append(mDbHelper.getDataView(shouldExcludeRestrictedData(uri))); 5673 sb.append(" data"); 5674 5675 appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID); 5676 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 5677 appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); 5678 appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); 5679 5680 qb.setTables(sb.toString()); 5681 5682 boolean useDistinct = distinct 5683 || !mDbHelper.isInProjection(projection, DISTINCT_DATA_PROHIBITING_COLUMNS); 5684 qb.setDistinct(useDistinct); 5685 qb.setProjectionMap(useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap); 5686 appendAccountFromParameter(qb, uri); 5687 } 5688 5689 private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, 5690 String[] projection) { 5691 StringBuilder sb = new StringBuilder(); 5692 sb.append(mDbHelper.getDataView()); 5693 sb.append(" data"); 5694 appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); 5695 appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); 5696 5697 qb.setTables(sb.toString()); 5698 qb.setProjectionMap(sStatusUpdatesProjectionMap); 5699 } 5700 5701 private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri, 5702 String[] projection) { 5703 StringBuilder sb = new StringBuilder(); 5704 sb.append(mDbHelper.getEntitiesView(shouldExcludeRestrictedData(uri))); 5705 sb.append(" data"); 5706 5707 appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID); 5708 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 5709 appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID); 5710 appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID); 5711 5712 qb.setTables(sb.toString()); 5713 qb.setProjectionMap(sEntityProjectionMap); 5714 appendAccountFromParameter(qb, uri); 5715 } 5716 5717 private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection, 5718 String lastStatusUpdateIdColumn) { 5719 if (mDbHelper.isInProjection(projection, 5720 Contacts.CONTACT_STATUS, 5721 Contacts.CONTACT_STATUS_RES_PACKAGE, 5722 Contacts.CONTACT_STATUS_ICON, 5723 Contacts.CONTACT_STATUS_LABEL, 5724 Contacts.CONTACT_STATUS_TIMESTAMP)) { 5725 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 5726 + ContactsStatusUpdatesColumns.ALIAS + 5727 " ON (" + lastStatusUpdateIdColumn + "=" 5728 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 5729 } 5730 } 5731 5732 private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection, 5733 String dataIdColumn) { 5734 if (mDbHelper.isInProjection(projection, 5735 StatusUpdates.STATUS, 5736 StatusUpdates.STATUS_RES_PACKAGE, 5737 StatusUpdates.STATUS_ICON, 5738 StatusUpdates.STATUS_LABEL, 5739 StatusUpdates.STATUS_TIMESTAMP)) { 5740 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 5741 " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" 5742 + dataIdColumn + ")"); 5743 } 5744 } 5745 5746 private void appendContactPresenceJoin(StringBuilder sb, String[] projection, 5747 String contactIdColumn) { 5748 if (mDbHelper.isInProjection(projection, 5749 Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) { 5750 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 5751 " ON (" + contactIdColumn + " = " 5752 + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")"); 5753 } 5754 } 5755 5756 private void appendDataPresenceJoin(StringBuilder sb, String[] projection, 5757 String dataIdColumn) { 5758 if (mDbHelper.isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) { 5759 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 5760 " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")"); 5761 } 5762 } 5763 5764 private void appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) { 5765 if (directoryId == Directory.DEFAULT) { 5766 qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY); 5767 } else if (directoryId == Directory.LOCAL_INVISIBLE){ 5768 qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY); 5769 } 5770 } 5771 5772 private boolean shouldExcludeRestrictedData(Uri uri) { 5773 // Note: currently, "export only" equals to "restricted", but may not in the future. 5774 boolean excludeRestrictedData = readBooleanQueryParameter(uri, 5775 Data.FOR_EXPORT_ONLY, false); 5776 if (excludeRestrictedData) { 5777 return true; 5778 } 5779 5780 String requestingPackage = getQueryParameter(uri, 5781 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 5782 if (requestingPackage != null) { 5783 return !mDbHelper.hasAccessToRestrictedData(requestingPackage); 5784 } 5785 5786 return false; 5787 } 5788 5789 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 5790 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 5791 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 5792 5793 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 5794 if (partialUri) { 5795 // Throw when either account is incomplete 5796 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 5797 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 5798 } 5799 5800 // Accounts are valid by only checking one parameter, since we've 5801 // already ruled out partial accounts. 5802 final boolean validAccount = !TextUtils.isEmpty(accountName); 5803 if (validAccount) { 5804 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 5805 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 5806 + RawContacts.ACCOUNT_TYPE + "=" 5807 + DatabaseUtils.sqlEscapeString(accountType)); 5808 } else { 5809 qb.appendWhere("1"); 5810 } 5811 } 5812 5813 private String appendAccountToSelection(Uri uri, String selection) { 5814 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 5815 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 5816 5817 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 5818 if (partialUri) { 5819 // Throw when either account is incomplete 5820 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 5821 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 5822 } 5823 5824 // Accounts are valid by only checking one parameter, since we've 5825 // already ruled out partial accounts. 5826 final boolean validAccount = !TextUtils.isEmpty(accountName); 5827 if (validAccount) { 5828 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 5829 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 5830 + RawContacts.ACCOUNT_TYPE + "=" 5831 + DatabaseUtils.sqlEscapeString(accountType)); 5832 if (!TextUtils.isEmpty(selection)) { 5833 selectionSb.append(" AND ("); 5834 selectionSb.append(selection); 5835 selectionSb.append(')'); 5836 } 5837 return selectionSb.toString(); 5838 } else { 5839 return selection; 5840 } 5841 } 5842 5843 /** 5844 * Gets the value of the "limit" URI query parameter. 5845 * 5846 * @return A string containing a non-negative integer, or <code>null</code> if 5847 * the parameter is not set, or is set to an invalid value. 5848 */ 5849 private String getLimit(Uri uri) { 5850 String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY); 5851 if (limitParam == null) { 5852 return null; 5853 } 5854 // make sure that the limit is a non-negative integer 5855 try { 5856 int l = Integer.parseInt(limitParam); 5857 if (l < 0) { 5858 Log.w(TAG, "Invalid limit parameter: " + limitParam); 5859 return null; 5860 } 5861 return String.valueOf(l); 5862 } catch (NumberFormatException ex) { 5863 Log.w(TAG, "Invalid limit parameter: " + limitParam); 5864 return null; 5865 } 5866 } 5867 5868 String getContactsRestrictions() { 5869 if (mDbHelper.hasAccessToRestrictedData()) { 5870 return "1"; 5871 } else { 5872 return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0"; 5873 } 5874 } 5875 5876 public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { 5877 if (mDbHelper.hasAccessToRestrictedData()) { 5878 return "1"; 5879 } else { 5880 return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS 5881 + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; 5882 } 5883 } 5884 5885 @Override 5886 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 5887 int match = sUriMatcher.match(uri); 5888 switch (match) { 5889 case CONTACTS_ID_PHOTO: { 5890 return openPhotoAssetFile(uri, mode, 5891 Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?", 5892 new String[]{uri.getPathSegments().get(1)}); 5893 } 5894 5895 case DATA_ID: { 5896 return openPhotoAssetFile(uri, mode, 5897 Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'", 5898 new String[]{uri.getPathSegments().get(1)}); 5899 } 5900 5901 case CONTACTS_AS_VCARD: { 5902 final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); 5903 mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(mDb, lookupKey)); 5904 final String selection = Contacts._ID + "=?"; 5905 5906 // When opening a contact as file, we pass back contents as a 5907 // vCard-encoded stream. We build into a local buffer first, 5908 // then pipe into MemoryFile once the exact size is known. 5909 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 5910 outputRawContactsAsVCard(localStream, selection, mSelectionArgs1); 5911 return buildAssetFileDescriptor(localStream); 5912 } 5913 5914 case CONTACTS_AS_MULTI_VCARD: { 5915 final String lookupKeys = uri.getPathSegments().get(2); 5916 final String[] loopupKeyList = lookupKeys.split(":"); 5917 final StringBuilder inBuilder = new StringBuilder(); 5918 int index = 0; 5919 // SQLite has limits on how many parameters can be used 5920 // so the IDs are concatenated to a query string here instead 5921 for (String lookupKey : loopupKeyList) { 5922 if (index == 0) { 5923 inBuilder.append("("); 5924 } else { 5925 inBuilder.append(","); 5926 } 5927 inBuilder.append(lookupContactIdByLookupKey(mDb, lookupKey)); 5928 index++; 5929 } 5930 inBuilder.append(')'); 5931 final String selection = Contacts._ID + " IN " + inBuilder.toString(); 5932 5933 // When opening a contact as file, we pass back contents as a 5934 // vCard-encoded stream. We build into a local buffer first, 5935 // then pipe into MemoryFile once the exact size is known. 5936 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 5937 outputRawContactsAsVCard(localStream, selection, null); 5938 return buildAssetFileDescriptor(localStream); 5939 } 5940 5941 default: 5942 throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist", 5943 uri)); 5944 } 5945 } 5946 5947 private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection, 5948 String[] selectionArgs) 5949 throws FileNotFoundException { 5950 if (!"r".equals(mode)) { 5951 throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode 5952 + " not supported.", uri)); 5953 } 5954 5955 String sql = 5956 "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() + 5957 " WHERE " + selection; 5958 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 5959 try { 5960 return makeAssetFileDescriptor( 5961 DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs)); 5962 } catch (SQLiteDoneException e) { 5963 // this will happen if the DB query returns no rows (i.e. contact does not exist) 5964 throw new FileNotFoundException(uri.toString()); 5965 } 5966 } 5967 5968 private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; 5969 5970 /** 5971 * Returns an {@link AssetFileDescriptor} backed by the 5972 * contents of the given {@link ByteArrayOutputStream}. 5973 */ 5974 private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { 5975 try { 5976 stream.flush(); 5977 5978 final byte[] byteData = stream.toByteArray(); 5979 5980 return makeAssetFileDescriptor( 5981 ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME), 5982 byteData.length); 5983 } catch (IOException e) { 5984 Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString()); 5985 return null; 5986 } 5987 } 5988 5989 private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) { 5990 return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH); 5991 } 5992 5993 private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) { 5994 return fd != null ? new AssetFileDescriptor(fd, 0, length) : null; 5995 } 5996 5997 /** 5998 * Output {@link RawContacts} matching the requested selection in the vCard 5999 * format to the given {@link OutputStream}. This method returns silently if 6000 * any errors encountered. 6001 */ 6002 private void outputRawContactsAsVCard(OutputStream stream, String selection, 6003 String[] selectionArgs) { 6004 final Context context = this.getContext(); 6005 final VCardComposer composer = 6006 new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false); 6007 composer.addHandler(composer.new HandlerForOutputStream(stream)); 6008 6009 // No extra checks since composer always uses restricted views 6010 if (!composer.init(selection, selectionArgs)) { 6011 Log.w(TAG, "Failed to init VCardComposer"); 6012 return; 6013 } 6014 6015 while (!composer.isAfterLast()) { 6016 if (!composer.createOneEntry()) { 6017 Log.w(TAG, "Failed to output a contact."); 6018 } 6019 } 6020 composer.terminate(); 6021 } 6022 6023 @Override 6024 public String getType(Uri uri) { 6025 final int match = sUriMatcher.match(uri); 6026 switch (match) { 6027 case CONTACTS: 6028 return Contacts.CONTENT_TYPE; 6029 case CONTACTS_LOOKUP: 6030 case CONTACTS_ID: 6031 case CONTACTS_LOOKUP_ID: 6032 return Contacts.CONTENT_ITEM_TYPE; 6033 case CONTACTS_AS_VCARD: 6034 case CONTACTS_AS_MULTI_VCARD: 6035 return Contacts.CONTENT_VCARD_TYPE; 6036 case CONTACTS_ID_PHOTO: 6037 return "image/png"; 6038 case RAW_CONTACTS: 6039 return RawContacts.CONTENT_TYPE; 6040 case RAW_CONTACTS_ID: 6041 return RawContacts.CONTENT_ITEM_TYPE; 6042 case DATA: 6043 return Data.CONTENT_TYPE; 6044 case DATA_ID: 6045 return mDbHelper.getDataMimeType(ContentUris.parseId(uri)); 6046 case PHONES: 6047 return Phone.CONTENT_TYPE; 6048 case PHONES_ID: 6049 return Phone.CONTENT_ITEM_TYPE; 6050 case PHONE_LOOKUP: 6051 return PhoneLookup.CONTENT_TYPE; 6052 case EMAILS: 6053 return Email.CONTENT_TYPE; 6054 case EMAILS_ID: 6055 return Email.CONTENT_ITEM_TYPE; 6056 case POSTALS: 6057 return StructuredPostal.CONTENT_TYPE; 6058 case POSTALS_ID: 6059 return StructuredPostal.CONTENT_ITEM_TYPE; 6060 case AGGREGATION_EXCEPTIONS: 6061 return AggregationExceptions.CONTENT_TYPE; 6062 case AGGREGATION_EXCEPTION_ID: 6063 return AggregationExceptions.CONTENT_ITEM_TYPE; 6064 case SETTINGS: 6065 return Settings.CONTENT_TYPE; 6066 case AGGREGATION_SUGGESTIONS: 6067 return Contacts.CONTENT_TYPE; 6068 case SEARCH_SUGGESTIONS: 6069 return SearchManager.SUGGEST_MIME_TYPE; 6070 case SEARCH_SHORTCUT: 6071 return SearchManager.SHORTCUT_MIME_TYPE; 6072 case DIRECTORIES: 6073 return Directory.CONTENT_TYPE; 6074 case DIRECTORIES_ID: 6075 return Directory.CONTENT_ITEM_TYPE; 6076 default: 6077 return mLegacyApiSupport.getType(uri); 6078 } 6079 } 6080 6081 public String[] getDefaultProjection(Uri uri) { 6082 final int match = sUriMatcher.match(uri); 6083 switch (match) { 6084 case CONTACTS: 6085 case CONTACTS_LOOKUP: 6086 case CONTACTS_ID: 6087 case CONTACTS_LOOKUP_ID: 6088 case AGGREGATION_SUGGESTIONS: 6089 return sContactsProjectionMap.getColumnNames(); 6090 6091 case CONTACTS_ID_ENTITIES: 6092 return sEntityProjectionMap.getColumnNames(); 6093 6094 case CONTACTS_AS_VCARD: 6095 case CONTACTS_AS_MULTI_VCARD: 6096 return sContactsVCardProjectionMap.getColumnNames(); 6097 6098 case RAW_CONTACTS: 6099 case RAW_CONTACTS_ID: 6100 return sRawContactsProjectionMap.getColumnNames(); 6101 6102 case DATA_ID: 6103 case PHONES: 6104 case PHONES_ID: 6105 case EMAILS: 6106 case EMAILS_ID: 6107 case POSTALS: 6108 case POSTALS_ID: 6109 return sDataProjectionMap.getColumnNames(); 6110 6111 case PHONE_LOOKUP: 6112 return sPhoneLookupProjectionMap.getColumnNames(); 6113 6114 case AGGREGATION_EXCEPTIONS: 6115 case AGGREGATION_EXCEPTION_ID: 6116 return sAggregationExceptionsProjectionMap.getColumnNames(); 6117 6118 case SETTINGS: 6119 return sSettingsProjectionMap.getColumnNames(); 6120 6121 case DIRECTORIES: 6122 case DIRECTORIES_ID: 6123 return sDirectoryProjectionMap.getColumnNames(); 6124 6125 default: 6126 return null; 6127 } 6128 } 6129 6130 private void setDisplayName(long rawContactId, int displayNameSource, 6131 String displayNamePrimary, String displayNameAlternative, String phoneticName, 6132 int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) { 6133 mRawContactDisplayNameUpdate.bindLong(1, displayNameSource); 6134 bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary); 6135 bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative); 6136 bindString(mRawContactDisplayNameUpdate, 4, phoneticName); 6137 mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle); 6138 bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary); 6139 bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative); 6140 mRawContactDisplayNameUpdate.bindLong(8, rawContactId); 6141 mRawContactDisplayNameUpdate.execute(); 6142 } 6143 6144 /** 6145 * Sets the {@link RawContacts#DIRTY} for the specified raw contact. 6146 */ 6147 private void setRawContactDirty(long rawContactId) { 6148 mDirtyRawContacts.add(rawContactId); 6149 } 6150 6151 /* 6152 * Sets the given dataId record in the "data" table to primary, and resets all data records of 6153 * the same mimetype and under the same contact to not be primary. 6154 * 6155 * @param dataId the id of the data record to be set to primary. 6156 */ 6157 private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) { 6158 mSetPrimaryStatement.bindLong(1, dataId); 6159 mSetPrimaryStatement.bindLong(2, mimeTypeId); 6160 mSetPrimaryStatement.bindLong(3, rawContactId); 6161 mSetPrimaryStatement.execute(); 6162 } 6163 6164 /* 6165 * Sets the given dataId record in the "data" table to "super primary", and resets all data 6166 * records of the same mimetype and under the same aggregate to not be "super primary". 6167 * 6168 * @param dataId the id of the data record to be set to primary. 6169 */ 6170 private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) { 6171 mSetSuperPrimaryStatement.bindLong(1, dataId); 6172 mSetSuperPrimaryStatement.bindLong(2, mimeTypeId); 6173 mSetSuperPrimaryStatement.bindLong(3, rawContactId); 6174 mSetSuperPrimaryStatement.execute(); 6175 } 6176 6177 public String insertNameLookupForEmail(long rawContactId, long dataId, String email) { 6178 if (TextUtils.isEmpty(email)) { 6179 return null; 6180 } 6181 6182 String address = mDbHelper.extractHandleFromEmailAddress(email); 6183 if (address == null) { 6184 return null; 6185 } 6186 6187 insertNameLookup(rawContactId, dataId, 6188 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address)); 6189 return address; 6190 } 6191 6192 /** 6193 * Normalizes the nickname and inserts it in the name lookup table. 6194 */ 6195 public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) { 6196 if (TextUtils.isEmpty(nickname)) { 6197 return; 6198 } 6199 6200 insertNameLookup(rawContactId, dataId, 6201 NameLookupType.NICKNAME, NameNormalizer.normalize(nickname)); 6202 } 6203 6204 public void insertNameLookupForOrganization(long rawContactId, long dataId, String company, 6205 String title) { 6206 if (!TextUtils.isEmpty(company)) { 6207 insertNameLookup(rawContactId, dataId, 6208 NameLookupType.ORGANIZATION, NameNormalizer.normalize(company)); 6209 } 6210 if (!TextUtils.isEmpty(title)) { 6211 insertNameLookup(rawContactId, dataId, 6212 NameLookupType.ORGANIZATION, NameNormalizer.normalize(title)); 6213 } 6214 } 6215 6216 public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name, 6217 int fullNameStyle) { 6218 mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle); 6219 } 6220 6221 private class StructuredNameLookupBuilder extends NameLookupBuilder { 6222 6223 public StructuredNameLookupBuilder(NameSplitter splitter) { 6224 super(splitter); 6225 } 6226 6227 @Override 6228 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 6229 String name) { 6230 ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name); 6231 } 6232 6233 @Override 6234 protected String[] getCommonNicknameClusters(String normalizedName) { 6235 return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); 6236 } 6237 } 6238 6239 public void insertNameLookupForPhoneticName(long rawContactId, long dataId, 6240 ContentValues values) { 6241 if (values.containsKey(StructuredName.PHONETIC_FAMILY_NAME) 6242 || values.containsKey(StructuredName.PHONETIC_GIVEN_NAME) 6243 || values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME)) { 6244 insertNameLookupForPhoneticName(rawContactId, dataId, 6245 values.getAsString(StructuredName.PHONETIC_FAMILY_NAME), 6246 values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME), 6247 values.getAsString(StructuredName.PHONETIC_GIVEN_NAME)); 6248 } 6249 } 6250 6251 public void insertNameLookupForPhoneticName(long rawContactId, long dataId, String familyName, 6252 String middleName, String givenName) { 6253 mSb.setLength(0); 6254 if (familyName != null) { 6255 mSb.append(familyName.trim()); 6256 } 6257 if (middleName != null) { 6258 mSb.append(middleName.trim()); 6259 } 6260 if (givenName != null) { 6261 mSb.append(givenName.trim()); 6262 } 6263 6264 if (mSb.length() > 0) { 6265 insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY, 6266 NameNormalizer.normalize(mSb.toString())); 6267 } 6268 6269 if (givenName != null) { 6270 // We want the phonetic given name to be used for search, but not for aggregation, 6271 // which is why we are using NAME_SHORTHAND rather than NAME_COLLATION_KEY 6272 insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND, 6273 NameNormalizer.normalize(givenName.trim())); 6274 } 6275 } 6276 6277 /** 6278 * Inserts a record in the {@link Tables#NAME_LOOKUP} table. 6279 */ 6280 public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) { 6281 mNameLookupInsert.bindLong(1, rawContactId); 6282 mNameLookupInsert.bindLong(2, dataId); 6283 mNameLookupInsert.bindLong(3, lookupType); 6284 bindString(mNameLookupInsert, 4, name); 6285 mNameLookupInsert.executeInsert(); 6286 } 6287 6288 /** 6289 * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element. 6290 */ 6291 public void deleteNameLookup(long dataId) { 6292 mNameLookupDelete.bindLong(1, dataId); 6293 mNameLookupDelete.execute(); 6294 } 6295 6296 public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 6297 sb.append("(" + 6298 "SELECT DISTINCT " + RawContacts.CONTACT_ID + 6299 " FROM " + Tables.RAW_CONTACTS + 6300 " JOIN " + Tables.NAME_LOOKUP + 6301 " ON(" + RawContactsColumns.CONCRETE_ID + "=" 6302 + NameLookupColumns.RAW_CONTACT_ID + ")" + 6303 " WHERE normalized_name GLOB '"); 6304 sb.append(NameNormalizer.normalize(filterParam)); 6305 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + 6306 " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))"); 6307 } 6308 6309 public String getRawContactsByFilterAsNestedQuery(String filterParam) { 6310 StringBuilder sb = new StringBuilder(); 6311 appendRawContactsByFilterAsNestedQuery(sb, filterParam); 6312 return sb.toString(); 6313 } 6314 6315 public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) { 6316 appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true); 6317 } 6318 6319 private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, 6320 boolean allowEmailMatch) { 6321 if (TextUtils.isEmpty(normalizedName)) { 6322 // Effectively an empty IN clause - SQL syntax does not allow an actual empty list here 6323 sb.append("(0)"); 6324 } else { 6325 sb.append("(" + 6326 "SELECT " + NameLookupColumns.RAW_CONTACT_ID + 6327 " FROM " + Tables.NAME_LOOKUP + 6328 " WHERE " + NameLookupColumns.NORMALIZED_NAME + 6329 " GLOB '"); 6330 // Should not use a "?" argument placeholder here, because 6331 // that would prevent the SQL optimizer from using the index on NORMALIZED_NAME. 6332 sb.append(normalizedName); 6333 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN (" 6334 + NameLookupType.NAME_COLLATION_KEY + "," 6335 + NameLookupType.NICKNAME + "," 6336 + NameLookupType.NAME_SHORTHAND + "," 6337 + NameLookupType.ORGANIZATION + "," 6338 + NameLookupType.NAME_CONSONANTS); 6339 if (allowEmailMatch) { 6340 sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME); 6341 } 6342 sb.append("))"); 6343 } 6344 } 6345 6346 /** 6347 * Takes components of a name from the query parameters and returns a cursor with those 6348 * components as well as all missing components. There is no database activity involved 6349 * in this so the call can be made on the UI thread. 6350 */ 6351 private Cursor completeName(Uri uri, String[] projection) { 6352 if (projection == null) { 6353 projection = sDataProjectionMap.getColumnNames(); 6354 } 6355 6356 ContentValues values = new ContentValues(); 6357 StructuredNameRowHandler handler = 6358 (StructuredNameRowHandler) getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE); 6359 6360 copyQueryParamsToContentValues(values, uri, 6361 StructuredName.DISPLAY_NAME, 6362 StructuredName.PREFIX, 6363 StructuredName.GIVEN_NAME, 6364 StructuredName.MIDDLE_NAME, 6365 StructuredName.FAMILY_NAME, 6366 StructuredName.SUFFIX, 6367 StructuredName.PHONETIC_NAME, 6368 StructuredName.PHONETIC_FAMILY_NAME, 6369 StructuredName.PHONETIC_MIDDLE_NAME, 6370 StructuredName.PHONETIC_GIVEN_NAME 6371 ); 6372 6373 handler.fixStructuredNameComponents(values, values); 6374 6375 MatrixCursor cursor = new MatrixCursor(projection); 6376 Object[] row = new Object[projection.length]; 6377 for (int i = 0; i < projection.length; i++) { 6378 row[i] = values.get(projection[i]); 6379 } 6380 cursor.addRow(row); 6381 return cursor; 6382 } 6383 6384 private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) { 6385 for (String column : columns) { 6386 String param = uri.getQueryParameter(column); 6387 if (param != null) { 6388 values.put(column, param); 6389 } 6390 } 6391 } 6392 6393 6394 /** 6395 * Inserts an argument at the beginning of the selection arg list. 6396 */ 6397 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 6398 if (selectionArgs == null) { 6399 return new String[] {arg}; 6400 } else { 6401 int newLength = selectionArgs.length + 1; 6402 String[] newSelectionArgs = new String[newLength]; 6403 newSelectionArgs[0] = arg; 6404 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 6405 return newSelectionArgs; 6406 } 6407 } 6408 6409 private String[] appendProjectionArg(String[] projection, String arg) { 6410 if (projection == null) { 6411 return null; 6412 } 6413 final int length = projection.length; 6414 String[] newProjection = new String[length + 1]; 6415 System.arraycopy(projection, 0, newProjection, 0, length); 6416 newProjection[length] = arg; 6417 return newProjection; 6418 } 6419 6420 protected Account getDefaultAccount() { 6421 AccountManager accountManager = AccountManager.get(getContext()); 6422 try { 6423 Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE, 6424 new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult(); 6425 if (accounts != null && accounts.length > 0) { 6426 return accounts[0]; 6427 } 6428 } catch (Throwable e) { 6429 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 6430 } 6431 return null; 6432 } 6433 6434 /** 6435 * Returns true if the specified account type is writable. 6436 */ 6437 protected boolean isWritableAccount(String accountType) { 6438 if (accountType == null) { 6439 return true; 6440 } 6441 6442 Boolean writable = mAccountWritability.get(accountType); 6443 if (writable != null) { 6444 return writable; 6445 } 6446 6447 IContentService contentService = ContentResolver.getContentService(); 6448 try { 6449 for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) { 6450 if (ContactsContract.AUTHORITY.equals(sync.authority) && 6451 accountType.equals(sync.accountType)) { 6452 writable = sync.supportsUploading(); 6453 break; 6454 } 6455 } 6456 } catch (RemoteException e) { 6457 Log.e(TAG, "Could not acquire sync adapter types"); 6458 } 6459 6460 if (writable == null) { 6461 writable = false; 6462 } 6463 6464 mAccountWritability.put(accountType, writable); 6465 return writable; 6466 } 6467 6468 6469 /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter, 6470 boolean defaultValue) { 6471 6472 // Manually parse the query, which is much faster than calling uri.getQueryParameter 6473 String query = uri.getEncodedQuery(); 6474 if (query == null) { 6475 return defaultValue; 6476 } 6477 6478 int index = query.indexOf(parameter); 6479 if (index == -1) { 6480 return defaultValue; 6481 } 6482 6483 index += parameter.length(); 6484 6485 return !matchQueryParameter(query, index, "=0", false) 6486 && !matchQueryParameter(query, index, "=false", true); 6487 } 6488 6489 private static boolean matchQueryParameter(String query, int index, String value, 6490 boolean ignoreCase) { 6491 int length = value.length(); 6492 return query.regionMatches(ignoreCase, index, value, 0, length) 6493 && (query.length() == index + length || query.charAt(index + length) == '&'); 6494 } 6495 6496 /** 6497 * A fast re-implementation of {@link Uri#getQueryParameter} 6498 */ 6499 /* package */ static String getQueryParameter(Uri uri, String parameter) { 6500 String query = uri.getEncodedQuery(); 6501 if (query == null) { 6502 return null; 6503 } 6504 6505 int queryLength = query.length(); 6506 int parameterLength = parameter.length(); 6507 6508 String value; 6509 int index = 0; 6510 while (true) { 6511 index = query.indexOf(parameter, index); 6512 if (index == -1) { 6513 return null; 6514 } 6515 6516 index += parameterLength; 6517 6518 if (queryLength == index) { 6519 return null; 6520 } 6521 6522 if (query.charAt(index) == '=') { 6523 index++; 6524 break; 6525 } 6526 } 6527 6528 int ampIndex = query.indexOf('&', index); 6529 if (ampIndex == -1) { 6530 value = query.substring(index); 6531 } else { 6532 value = query.substring(index, ampIndex); 6533 } 6534 6535 return Uri.decode(value); 6536 } 6537 6538 private void bindString(SQLiteStatement stmt, int index, String value) { 6539 if (value == null) { 6540 stmt.bindNull(index); 6541 } else { 6542 stmt.bindString(index, value); 6543 } 6544 } 6545 6546 private void bindLong(SQLiteStatement stmt, int index, Number value) { 6547 if (value == null) { 6548 stmt.bindNull(index); 6549 } else { 6550 stmt.bindLong(index, value.longValue()); 6551 } 6552 } 6553 6554 protected boolean isAggregationUpgradeNeeded() { 6555 if (!mContactAggregator.isEnabled()) { 6556 return false; 6557 } 6558 6559 int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_AGGREGATION_ALGORITHM, "1")); 6560 return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION; 6561 } 6562 6563 protected void upgradeAggregationAlgorithm() { 6564 // This upgrade will affect very few contacts, so it can be performed on the 6565 // main thread during the initial boot after an OTA 6566 6567 Log.i(TAG, "Upgrading aggregation algorithm"); 6568 int count = 0; 6569 long start = SystemClock.currentThreadTimeMillis(); 6570 try { 6571 mDb.beginTransaction(); 6572 Cursor cursor = mDb.query(true, 6573 Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2", 6574 new String[]{"r1." + RawContacts._ID}, 6575 "r1." + RawContacts._ID + "!=r2." + RawContacts._ID + 6576 " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID + 6577 " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME + 6578 " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE, 6579 null, null, null, null, null); 6580 try { 6581 while (cursor.moveToNext()) { 6582 long rawContactId = cursor.getLong(0); 6583 mContactAggregator.markForAggregation(rawContactId, 6584 RawContacts.AGGREGATION_MODE_DEFAULT, true); 6585 count++; 6586 } 6587 } finally { 6588 cursor.close(); 6589 } 6590 mContactAggregator.aggregateInTransaction(mDb); 6591 mDb.setTransactionSuccessful(); 6592 mDbHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM, 6593 String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION)); 6594 } finally { 6595 mDb.endTransaction(); 6596 long end = SystemClock.currentThreadTimeMillis(); 6597 Log.i(TAG, "Aggregation algorithm upgraded for " + count 6598 + " contacts, in " + (end - start) + "ms"); 6599 } 6600 } 6601} 6602