ContactsProvider2.java revision b0812f94fa50c54d06978cdd65651a487c717dff
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.internal.content.SyncStateContentProviderHelper; 20import com.android.providers.contacts.ContactLookupKey.LookupKeySegment; 21import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; 22import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns; 23import com.android.providers.contacts.ContactsDatabaseHelper.Clauses; 24import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 25import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns; 26import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; 27import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns; 28import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; 29import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; 30import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; 31import com.android.providers.contacts.ContactsDatabaseHelper.NicknameLookupColumns; 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.google.android.collect.Lists; 40import com.google.android.collect.Maps; 41import com.google.android.collect.Sets; 42 43import android.accounts.Account; 44import android.accounts.AccountManager; 45import android.accounts.OnAccountsUpdateListener; 46import android.app.SearchManager; 47import android.content.ContentProviderOperation; 48import android.content.ContentProviderResult; 49import android.content.ContentResolver; 50import android.content.ContentUris; 51import android.content.ContentValues; 52import android.content.Context; 53import android.content.IContentService; 54import android.content.OperationApplicationException; 55import android.content.SharedPreferences; 56import android.content.SyncAdapterType; 57import android.content.UriMatcher; 58import android.content.SharedPreferences.Editor; 59import android.content.res.AssetFileDescriptor; 60import android.database.CharArrayBuffer; 61import android.database.Cursor; 62import android.database.DatabaseUtils; 63import android.database.sqlite.SQLiteConstraintException; 64import android.database.sqlite.SQLiteContentHelper; 65import android.database.sqlite.SQLiteDatabase; 66import android.database.sqlite.SQLiteQueryBuilder; 67import android.database.sqlite.SQLiteStatement; 68import android.net.Uri; 69import android.os.Bundle; 70import android.os.MemoryFile; 71import android.os.RemoteException; 72import android.os.SystemProperties; 73import android.pim.vcard.VCardComposer; 74import android.preference.PreferenceManager; 75import android.provider.BaseColumns; 76import android.provider.ContactsContract; 77import android.provider.LiveFolders; 78import android.provider.OpenableColumns; 79import android.provider.SyncStateContract; 80import android.provider.ContactsContract.AggregationExceptions; 81import android.provider.ContactsContract.Contacts; 82import android.provider.ContactsContract.Data; 83import android.provider.ContactsContract.DisplayNameSources; 84import android.provider.ContactsContract.FullNameStyle; 85import android.provider.ContactsContract.Groups; 86import android.provider.ContactsContract.PhoneLookup; 87import android.provider.ContactsContract.PhoneticNameStyle; 88import android.provider.ContactsContract.RawContacts; 89import android.provider.ContactsContract.Settings; 90import android.provider.ContactsContract.StatusUpdates; 91import android.provider.ContactsContract.CommonDataKinds.BaseTypes; 92import android.provider.ContactsContract.CommonDataKinds.Email; 93import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 94import android.provider.ContactsContract.CommonDataKinds.Im; 95import android.provider.ContactsContract.CommonDataKinds.Nickname; 96import android.provider.ContactsContract.CommonDataKinds.Organization; 97import android.provider.ContactsContract.CommonDataKinds.Phone; 98import android.provider.ContactsContract.CommonDataKinds.Photo; 99import android.provider.ContactsContract.CommonDataKinds.StructuredName; 100import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 101import android.telephony.PhoneNumberUtils; 102import android.text.TextUtils; 103import android.text.util.Rfc822Token; 104import android.text.util.Rfc822Tokenizer; 105import android.util.Log; 106 107import java.io.ByteArrayOutputStream; 108import java.io.FileNotFoundException; 109import java.io.IOException; 110import java.io.OutputStream; 111import java.lang.ref.SoftReference; 112import java.util.ArrayList; 113import java.util.BitSet; 114import java.util.Collections; 115import java.util.HashMap; 116import java.util.HashSet; 117import java.util.List; 118import java.util.Locale; 119import java.util.Map; 120import java.util.Set; 121import java.util.concurrent.CountDownLatch; 122 123/** 124 * Contacts content provider. The contract between this provider and applications 125 * is defined in {@link ContactsContract}. 126 */ 127public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener { 128 129 private static final String TAG = "ContactsProvider"; 130 131 private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 132 133 // TODO: carefully prevent all incoming nested queries; they can be gaping security holes 134 // TODO: check for restricted flag during insert(), update(), and delete() calls 135 136 /** Default for the maximum number of returned aggregation suggestions. */ 137 private static final int DEFAULT_MAX_SUGGESTIONS = 5; 138 139 /** 140 * Shared preference key for the legacy contact import version. The need for a version 141 * as opposed to a boolean flag is that if we discover bugs in the contact import process, 142 * we can trigger re-import by incrementing the import version. 143 */ 144 private static final String PREF_CONTACTS_IMPORTED = "contacts_imported_v1"; 145 private static final int PREF_CONTACTS_IMPORT_VERSION = 1; 146 147 private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate"; 148 149 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 150 151 private static final String TIMES_CONTACED_SORT_COLUMN = "times_contacted_sort"; 152 153 private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, " 154 + TIMES_CONTACED_SORT_COLUMN + " DESC, " 155 + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 156 private static final String STREQUENT_LIMIT = 157 "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE " 158 + Contacts.STARRED + "=1) + 25"; 159 160 /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE = 161 "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" + 162 " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " + 163 " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?"; 164 165 /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE = 166 "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" + 167 " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " + 168 " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?"; 169 170 private static final int CONTACTS = 1000; 171 private static final int CONTACTS_ID = 1001; 172 private static final int CONTACTS_LOOKUP = 1002; 173 private static final int CONTACTS_LOOKUP_ID = 1003; 174 private static final int CONTACTS_DATA = 1004; 175 private static final int CONTACTS_FILTER = 1005; 176 private static final int CONTACTS_STREQUENT = 1006; 177 private static final int CONTACTS_STREQUENT_FILTER = 1007; 178 private static final int CONTACTS_GROUP = 1008; 179 private static final int CONTACTS_PHOTO = 1009; 180 private static final int CONTACTS_AS_VCARD = 1010; 181 182 private static final int RAW_CONTACTS = 2002; 183 private static final int RAW_CONTACTS_ID = 2003; 184 private static final int RAW_CONTACTS_DATA = 2004; 185 private static final int RAW_CONTACT_ENTITY_ID = 2005; 186 187 private static final int DATA = 3000; 188 private static final int DATA_ID = 3001; 189 private static final int PHONES = 3002; 190 private static final int PHONES_ID = 3003; 191 private static final int PHONES_FILTER = 3004; 192 private static final int EMAILS = 3005; 193 private static final int EMAILS_ID = 3006; 194 private static final int EMAILS_LOOKUP = 3007; 195 private static final int EMAILS_FILTER = 3008; 196 private static final int POSTALS = 3009; 197 private static final int POSTALS_ID = 3010; 198 199 private static final int PHONE_LOOKUP = 4000; 200 201 private static final int AGGREGATION_EXCEPTIONS = 6000; 202 private static final int AGGREGATION_EXCEPTION_ID = 6001; 203 204 private static final int STATUS_UPDATES = 7000; 205 private static final int STATUS_UPDATES_ID = 7001; 206 207 private static final int AGGREGATION_SUGGESTIONS = 8000; 208 209 private static final int SETTINGS = 9000; 210 211 private static final int GROUPS = 10000; 212 private static final int GROUPS_ID = 10001; 213 private static final int GROUPS_SUMMARY = 10003; 214 215 private static final int SYNCSTATE = 11000; 216 private static final int SYNCSTATE_ID = 11001; 217 218 private static final int SEARCH_SUGGESTIONS = 12001; 219 private static final int SEARCH_SHORTCUT = 12002; 220 221 private static final int LIVE_FOLDERS_CONTACTS = 14000; 222 private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001; 223 private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002; 224 private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003; 225 226 private static final int RAW_CONTACT_ENTITIES = 15001; 227 228 private interface DataContactsQuery { 229 public static final String TABLE = "data " 230 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) " 231 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; 232 233 public static final String[] PROJECTION = new String[] { 234 RawContactsColumns.CONCRETE_ID, 235 DataColumns.CONCRETE_ID, 236 ContactsColumns.CONCRETE_ID 237 }; 238 239 public static final int RAW_CONTACT_ID = 0; 240 public static final int DATA_ID = 1; 241 public static final int CONTACT_ID = 2; 242 } 243 244 private interface DataDeleteQuery { 245 public static final String TABLE = Tables.DATA_JOIN_MIMETYPES; 246 247 public static final String[] CONCRETE_COLUMNS = new String[] { 248 DataColumns.CONCRETE_ID, 249 MimetypesColumns.MIMETYPE, 250 Data.RAW_CONTACT_ID, 251 Data.IS_PRIMARY, 252 Data.DATA1, 253 }; 254 255 public static final String[] COLUMNS = new String[] { 256 Data._ID, 257 MimetypesColumns.MIMETYPE, 258 Data.RAW_CONTACT_ID, 259 Data.IS_PRIMARY, 260 Data.DATA1, 261 }; 262 263 public static final int _ID = 0; 264 public static final int MIMETYPE = 1; 265 public static final int RAW_CONTACT_ID = 2; 266 public static final int IS_PRIMARY = 3; 267 public static final int DATA1 = 4; 268 } 269 270 private interface DataUpdateQuery { 271 String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE }; 272 273 int _ID = 0; 274 int RAW_CONTACT_ID = 1; 275 int MIMETYPE = 2; 276 } 277 278 279 private interface RawContactsQuery { 280 String TABLE = Tables.RAW_CONTACTS; 281 282 String[] COLUMNS = new String[] { 283 RawContacts.DELETED, 284 RawContacts.ACCOUNT_TYPE, 285 RawContacts.ACCOUNT_NAME, 286 }; 287 288 int DELETED = 0; 289 int ACCOUNT_TYPE = 1; 290 int ACCOUNT_NAME = 2; 291 } 292 293 public static final String DEFAULT_ACCOUNT_TYPE = "com.google"; 294 public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google"; 295 296 /** Sql where statement for filtering on groups. */ 297 private static final String CONTACTS_IN_GROUP_SELECT = 298 Contacts._ID + " IN " 299 + "(SELECT " + RawContacts.CONTACT_ID 300 + " FROM " + Tables.RAW_CONTACTS 301 + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN " 302 + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID 303 + " FROM " + Tables.DATA_JOIN_MIMETYPES 304 + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 305 + "' AND " + GroupMembership.GROUP_ROW_ID + "=" 306 + "(SELECT " + Tables.GROUPS + "." + Groups._ID 307 + " FROM " + Tables.GROUPS 308 + " WHERE " + Groups.TITLE + "=?)))"; 309 310 /** Sql for updating DIRTY flag on multiple raw contacts */ 311 private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL = 312 "UPDATE " + Tables.RAW_CONTACTS + 313 " SET " + RawContacts.DIRTY + "=1" + 314 " WHERE " + RawContacts._ID + " IN ("; 315 316 /** Sql for updating VERSION on multiple raw contacts */ 317 private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL = 318 "UPDATE " + Tables.RAW_CONTACTS + 319 " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" + 320 " WHERE " + RawContacts._ID + " IN ("; 321 322 /** Contains just BaseColumns._COUNT */ 323 private static final HashMap<String, String> sCountProjectionMap; 324 /** Contains just the contacts columns */ 325 private static final HashMap<String, String> sContactsProjectionMap; 326 /** Used for pushing starred contacts to the top of a times contacted list **/ 327 private static final HashMap<String, String> sStrequentStarredProjectionMap; 328 private static final HashMap<String, String> sStrequentFrequentProjectionMap; 329 /** Contains just the contacts vCard columns */ 330 private static final HashMap<String, String> sContactsVCardProjectionMap; 331 /** Contains just the raw contacts columns */ 332 private static final HashMap<String, String> sRawContactsProjectionMap; 333 /** Contains the columns from the raw contacts entity view*/ 334 private static final HashMap<String, String> sRawContactsEntityProjectionMap; 335 /** Contains columns from the data view */ 336 private static final HashMap<String, String> sDataProjectionMap; 337 /** Contains columns from the data view */ 338 private static final HashMap<String, String> sDistinctDataProjectionMap; 339 /** Contains the data and contacts columns, for joined tables */ 340 private static final HashMap<String, String> sPhoneLookupProjectionMap; 341 /** Contains the just the {@link Groups} columns */ 342 private static final HashMap<String, String> sGroupsProjectionMap; 343 /** Contains {@link Groups} columns along with summary details */ 344 private static final HashMap<String, String> sGroupsSummaryProjectionMap; 345 /** Contains the agg_exceptions columns */ 346 private static final HashMap<String, String> sAggregationExceptionsProjectionMap; 347 /** Contains the agg_exceptions columns */ 348 private static final HashMap<String, String> sSettingsProjectionMap; 349 /** Contains StatusUpdates columns */ 350 private static final HashMap<String, String> sStatusUpdatesProjectionMap; 351 /** Contains Live Folders columns */ 352 private static final HashMap<String, String> sLiveFoldersProjectionMap; 353 354 // where clause to update the status_updates table 355 private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE = 356 StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID + 357 " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE + 358 " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE "; 359 360 /** Precompiled sql statement for setting a data record to the primary. */ 361 private SQLiteStatement mSetPrimaryStatement; 362 /** Precompiled sql statement for setting a data record to the super primary. */ 363 private SQLiteStatement mSetSuperPrimaryStatement; 364 /** Precompiled sql statement for updating a contact display name */ 365 private SQLiteStatement mRawContactDisplayNameUpdate; 366 /** Precompiled sql statement for updating an aggregated status update */ 367 private SQLiteStatement mLastStatusUpdate; 368 private SQLiteStatement mNameLookupInsert; 369 private SQLiteStatement mNameLookupDelete; 370 private SQLiteStatement mStatusUpdateAutoTimestamp; 371 private SQLiteStatement mStatusUpdateInsert; 372 private SQLiteStatement mStatusUpdateReplace; 373 private SQLiteStatement mStatusAttributionUpdate; 374 private SQLiteStatement mStatusUpdateDelete; 375 376 private long mMimeTypeIdEmail; 377 private long mMimeTypeIdIm; 378 private long mMimeTypeIdStructuredName; 379 private long mMimeTypeIdOrganization; 380 private long mMimeTypeIdNickname; 381 private long mMimeTypeIdPhone; 382 private StringBuilder mSb = new StringBuilder(); 383 private String[] mSelectionArgs1 = new String[1]; 384 private String[] mSelectionArgs2 = new String[2]; 385 private String[] mSelectionArgs3 = new String[3]; 386 private Account mAccount; 387 388 static { 389 // Contacts URI matching table 390 final UriMatcher matcher = sUriMatcher; 391 matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS); 392 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 393 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA); 394 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", 395 AGGREGATION_SUGGESTIONS); 396 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*", 397 AGGREGATION_SUGGESTIONS); 398 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO); 399 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER); 400 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP); 401 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID); 402 matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD); 403 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT); 404 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", 405 CONTACTS_STREQUENT_FILTER); 406 matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP); 407 408 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS); 409 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID); 410 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA); 411 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID); 412 413 matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES); 414 415 matcher.addURI(ContactsContract.AUTHORITY, "data", DATA); 416 matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID); 417 matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES); 418 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID); 419 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER); 420 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER); 421 matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS); 422 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID); 423 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP); 424 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER); 425 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER); 426 matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS); 427 matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID); 428 429 matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS); 430 matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID); 431 matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY); 432 433 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE); 434 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#", 435 SYNCSTATE_ID); 436 437 matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP); 438 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", 439 AGGREGATION_EXCEPTIONS); 440 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", 441 AGGREGATION_EXCEPTION_ID); 442 443 matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS); 444 445 matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES); 446 matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID); 447 448 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, 449 SEARCH_SUGGESTIONS); 450 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 451 SEARCH_SUGGESTIONS); 452 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#", 453 SEARCH_SHORTCUT); 454 455 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts", 456 LIVE_FOLDERS_CONTACTS); 457 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*", 458 LIVE_FOLDERS_CONTACTS_GROUP_NAME); 459 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones", 460 LIVE_FOLDERS_CONTACTS_WITH_PHONES); 461 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites", 462 LIVE_FOLDERS_CONTACTS_FAVORITES); 463 } 464 465 static { 466 sCountProjectionMap = new HashMap<String, String>(); 467 sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)"); 468 469 sContactsProjectionMap = new HashMap<String, String>(); 470 sContactsProjectionMap.put(Contacts._ID, Contacts._ID); 471 sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY); 472 sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, 473 Contacts.DISPLAY_NAME_ALTERNATIVE); 474 sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); 475 sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); 476 sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); 477 sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); 478 sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE); 479 sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 480 sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 481 sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 482 sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); 483 sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 484 sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 485 sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER); 486 sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 487 sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 488 489 // Handle projections for Contacts-level statuses 490 addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE, 491 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); 492 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS, 493 ContactsStatusUpdatesColumns.CONCRETE_STATUS); 494 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, 495 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 496 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, 497 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 498 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL, 499 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); 500 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON, 501 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); 502 503 sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap); 504 sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, 505 Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN); 506 507 sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap); 508 sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, 509 Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN); 510 511 sContactsVCardProjectionMap = Maps.newHashMap(); 512 sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME 513 + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME); 514 sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "0 AS " + OpenableColumns.SIZE); 515 516 sRawContactsProjectionMap = new HashMap<String, String>(); 517 sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID); 518 sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 519 sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 520 sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 521 sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 522 sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); 523 sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); 524 sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED); 525 sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY, 526 RawContacts.DISPLAY_NAME_PRIMARY); 527 sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, 528 RawContacts.DISPLAY_NAME_ALTERNATIVE); 529 sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE, 530 RawContacts.DISPLAY_NAME_SOURCE); 531 sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME, 532 RawContacts.PHONETIC_NAME); 533 sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE, 534 RawContacts.PHONETIC_NAME_STYLE); 535 sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY, 536 RawContacts.SORT_KEY_PRIMARY); 537 sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE, 538 RawContacts.SORT_KEY_ALTERNATIVE); 539 sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED); 540 sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED, 541 RawContacts.LAST_TIME_CONTACTED); 542 sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE); 543 sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL); 544 sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED); 545 sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE); 546 sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1); 547 sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2); 548 sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3); 549 sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4); 550 551 sDataProjectionMap = new HashMap<String, String>(); 552 sDataProjectionMap.put(Data._ID, Data._ID); 553 sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID); 554 sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); 555 sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 556 sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 557 sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 558 sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); 559 sDataProjectionMap.put(Data.DATA1, Data.DATA1); 560 sDataProjectionMap.put(Data.DATA2, Data.DATA2); 561 sDataProjectionMap.put(Data.DATA3, Data.DATA3); 562 sDataProjectionMap.put(Data.DATA4, Data.DATA4); 563 sDataProjectionMap.put(Data.DATA5, Data.DATA5); 564 sDataProjectionMap.put(Data.DATA6, Data.DATA6); 565 sDataProjectionMap.put(Data.DATA7, Data.DATA7); 566 sDataProjectionMap.put(Data.DATA8, Data.DATA8); 567 sDataProjectionMap.put(Data.DATA9, Data.DATA9); 568 sDataProjectionMap.put(Data.DATA10, Data.DATA10); 569 sDataProjectionMap.put(Data.DATA11, Data.DATA11); 570 sDataProjectionMap.put(Data.DATA12, Data.DATA12); 571 sDataProjectionMap.put(Data.DATA13, Data.DATA13); 572 sDataProjectionMap.put(Data.DATA14, Data.DATA14); 573 sDataProjectionMap.put(Data.DATA15, Data.DATA15); 574 sDataProjectionMap.put(Data.SYNC1, Data.SYNC1); 575 sDataProjectionMap.put(Data.SYNC2, Data.SYNC2); 576 sDataProjectionMap.put(Data.SYNC3, Data.SYNC3); 577 sDataProjectionMap.put(Data.SYNC4, Data.SYNC4); 578 sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID); 579 sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 580 sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 581 sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 582 sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); 583 sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); 584 sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 585 sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 586 sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, 587 Contacts.DISPLAY_NAME_ALTERNATIVE); 588 sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); 589 sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); 590 sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); 591 sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); 592 sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE); 593 sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 594 sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 595 sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 596 sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 597 sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 598 sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 599 sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); 600 sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID); 601 602 HashMap<String, String> columns; 603 columns = new HashMap<String, String>(); 604 columns.put(RawContacts._ID, RawContacts._ID); 605 columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 606 columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 607 columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 608 columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 609 columns.put(RawContacts.VERSION, RawContacts.VERSION); 610 columns.put(RawContacts.DIRTY, RawContacts.DIRTY); 611 columns.put(RawContacts.DELETED, RawContacts.DELETED); 612 columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED); 613 columns.put(RawContacts.SYNC1, RawContacts.SYNC1); 614 columns.put(RawContacts.SYNC2, RawContacts.SYNC2); 615 columns.put(RawContacts.SYNC3, RawContacts.SYNC3); 616 columns.put(RawContacts.SYNC4, RawContacts.SYNC4); 617 columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 618 columns.put(Data.MIMETYPE, Data.MIMETYPE); 619 columns.put(Data.DATA1, Data.DATA1); 620 columns.put(Data.DATA2, Data.DATA2); 621 columns.put(Data.DATA3, Data.DATA3); 622 columns.put(Data.DATA4, Data.DATA4); 623 columns.put(Data.DATA5, Data.DATA5); 624 columns.put(Data.DATA6, Data.DATA6); 625 columns.put(Data.DATA7, Data.DATA7); 626 columns.put(Data.DATA8, Data.DATA8); 627 columns.put(Data.DATA9, Data.DATA9); 628 columns.put(Data.DATA10, Data.DATA10); 629 columns.put(Data.DATA11, Data.DATA11); 630 columns.put(Data.DATA12, Data.DATA12); 631 columns.put(Data.DATA13, Data.DATA13); 632 columns.put(Data.DATA14, Data.DATA14); 633 columns.put(Data.DATA15, Data.DATA15); 634 columns.put(Data.SYNC1, Data.SYNC1); 635 columns.put(Data.SYNC2, Data.SYNC2); 636 columns.put(Data.SYNC3, Data.SYNC3); 637 columns.put(Data.SYNC4, Data.SYNC4); 638 columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID); 639 columns.put(Data.STARRED, Data.STARRED); 640 columns.put(Data.DATA_VERSION, Data.DATA_VERSION); 641 columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 642 columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 643 columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID); 644 sRawContactsEntityProjectionMap = columns; 645 646 // Handle projections for Contacts-level statuses 647 addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE, 648 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); 649 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS, 650 ContactsStatusUpdatesColumns.CONCRETE_STATUS); 651 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, 652 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 653 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, 654 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 655 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, 656 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); 657 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON, 658 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); 659 660 // Handle projections for Data-level statuses 661 addProjection(sDataProjectionMap, Data.PRESENCE, 662 Tables.PRESENCE + "." + StatusUpdates.PRESENCE); 663 addProjection(sDataProjectionMap, Data.STATUS, 664 StatusUpdatesColumns.CONCRETE_STATUS); 665 addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP, 666 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 667 addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE, 668 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 669 addProjection(sDataProjectionMap, Data.STATUS_LABEL, 670 StatusUpdatesColumns.CONCRETE_STATUS_LABEL); 671 addProjection(sDataProjectionMap, Data.STATUS_ICON, 672 StatusUpdatesColumns.CONCRETE_STATUS_ICON); 673 674 // Projection map for data grouped by contact (not raw contact) and some data field(s) 675 sDistinctDataProjectionMap = new HashMap<String, String>(); 676 sDistinctDataProjectionMap.put(Data._ID, 677 "MIN(" + Data._ID + ") AS " + Data._ID); 678 sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); 679 sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 680 sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 681 sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 682 sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); 683 sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1); 684 sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2); 685 sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3); 686 sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4); 687 sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5); 688 sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6); 689 sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7); 690 sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8); 691 sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9); 692 sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10); 693 sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11); 694 sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12); 695 sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13); 696 sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14); 697 sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15); 698 sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1); 699 sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2); 700 sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3); 701 sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4); 702 sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 703 sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 704 sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 705 sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, 706 Contacts.DISPLAY_NAME_ALTERNATIVE); 707 sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); 708 sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); 709 sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); 710 sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); 711 sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, 712 Contacts.SORT_KEY_ALTERNATIVE); 713 sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 714 sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 715 sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 716 sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 717 sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 718 sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 719 sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); 720 sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, 721 GroupMembership.GROUP_SOURCE_ID); 722 723 // Handle projections for Contacts-level statuses 724 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE, 725 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); 726 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS, 727 ContactsStatusUpdatesColumns.CONCRETE_STATUS); 728 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, 729 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 730 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, 731 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 732 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, 733 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); 734 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON, 735 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); 736 737 // Handle projections for Data-level statuses 738 addProjection(sDistinctDataProjectionMap, Data.PRESENCE, 739 Tables.PRESENCE + "." + StatusUpdates.PRESENCE); 740 addProjection(sDistinctDataProjectionMap, Data.STATUS, 741 StatusUpdatesColumns.CONCRETE_STATUS); 742 addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP, 743 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 744 addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE, 745 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 746 addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL, 747 StatusUpdatesColumns.CONCRETE_STATUS_LABEL); 748 addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON, 749 StatusUpdatesColumns.CONCRETE_STATUS_ICON); 750 751 sPhoneLookupProjectionMap = new HashMap<String, String>(); 752 sPhoneLookupProjectionMap.put(PhoneLookup._ID, 753 "contacts_view." + Contacts._ID 754 + " AS " + PhoneLookup._ID); 755 sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY, 756 "contacts_view." + Contacts.LOOKUP_KEY 757 + " AS " + PhoneLookup.LOOKUP_KEY); 758 sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME, 759 "contacts_view." + Contacts.DISPLAY_NAME 760 + " AS " + PhoneLookup.DISPLAY_NAME); 761 sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED, 762 "contacts_view." + Contacts.LAST_TIME_CONTACTED 763 + " AS " + PhoneLookup.LAST_TIME_CONTACTED); 764 sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED, 765 "contacts_view." + Contacts.TIMES_CONTACTED 766 + " AS " + PhoneLookup.TIMES_CONTACTED); 767 sPhoneLookupProjectionMap.put(PhoneLookup.STARRED, 768 "contacts_view." + Contacts.STARRED 769 + " AS " + PhoneLookup.STARRED); 770 sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP, 771 "contacts_view." + Contacts.IN_VISIBLE_GROUP 772 + " AS " + PhoneLookup.IN_VISIBLE_GROUP); 773 sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID, 774 "contacts_view." + Contacts.PHOTO_ID 775 + " AS " + PhoneLookup.PHOTO_ID); 776 sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE, 777 "contacts_view." + Contacts.CUSTOM_RINGTONE 778 + " AS " + PhoneLookup.CUSTOM_RINGTONE); 779 sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER, 780 "contacts_view." + Contacts.HAS_PHONE_NUMBER 781 + " AS " + PhoneLookup.HAS_PHONE_NUMBER); 782 sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL, 783 "contacts_view." + Contacts.SEND_TO_VOICEMAIL 784 + " AS " + PhoneLookup.SEND_TO_VOICEMAIL); 785 sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER, 786 Phone.NUMBER + " AS " + PhoneLookup.NUMBER); 787 sPhoneLookupProjectionMap.put(PhoneLookup.TYPE, 788 Phone.TYPE + " AS " + PhoneLookup.TYPE); 789 sPhoneLookupProjectionMap.put(PhoneLookup.LABEL, 790 Phone.LABEL + " AS " + PhoneLookup.LABEL); 791 792 // Groups projection map 793 columns = new HashMap<String, String>(); 794 columns.put(Groups._ID, Groups._ID); 795 columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME); 796 columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE); 797 columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID); 798 columns.put(Groups.DIRTY, Groups.DIRTY); 799 columns.put(Groups.VERSION, Groups.VERSION); 800 columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE); 801 columns.put(Groups.TITLE, Groups.TITLE); 802 columns.put(Groups.TITLE_RES, Groups.TITLE_RES); 803 columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE); 804 columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID); 805 columns.put(Groups.DELETED, Groups.DELETED); 806 columns.put(Groups.NOTES, Groups.NOTES); 807 columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC); 808 columns.put(Groups.SYNC1, Groups.SYNC1); 809 columns.put(Groups.SYNC2, Groups.SYNC2); 810 columns.put(Groups.SYNC3, Groups.SYNC3); 811 columns.put(Groups.SYNC4, Groups.SYNC4); 812 sGroupsProjectionMap = columns; 813 814 // RawContacts and groups projection map 815 columns = new HashMap<String, String>(); 816 columns.putAll(sGroupsProjectionMap); 817 columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID 818 + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " 819 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 820 + ") AS " + Groups.SUMMARY_COUNT); 821 columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT " 822 + ContactsColumns.CONCRETE_ID + ") FROM " 823 + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " 824 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 825 + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES); 826 sGroupsSummaryProjectionMap = columns; 827 828 // Aggregate exception projection map 829 columns = new HashMap<String, String>(); 830 columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id"); 831 columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE); 832 columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1); 833 columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2); 834 sAggregationExceptionsProjectionMap = columns; 835 836 // Settings projection map 837 columns = new HashMap<String, String>(); 838 columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME); 839 columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE); 840 columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE); 841 columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC); 842 columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC 843 + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN(" 844 + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE " 845 + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME 846 + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" 847 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS " 848 + Settings.ANY_UNSYNCED); 849 columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM " 850 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY " 851 + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS 852 + ")) AS " + Settings.UNGROUPED_COUNT); 853 columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM " 854 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE " 855 + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID 856 + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS " 857 + Settings.UNGROUPED_WITH_PHONES); 858 sSettingsProjectionMap = columns; 859 860 columns = new HashMap<String, String>(); 861 columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID); 862 columns.put(StatusUpdates.DATA_ID, 863 DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID); 864 columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT); 865 columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE); 866 columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL); 867 // We cannot allow a null in the custom protocol field, because SQLite3 does not 868 // properly enforce uniqueness of null values 869 columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL 870 + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS " 871 + StatusUpdates.CUSTOM_PROTOCOL); 872 columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE); 873 columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS); 874 columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP); 875 columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE); 876 columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON); 877 columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL); 878 sStatusUpdatesProjectionMap = columns; 879 880 // Live folder projection 881 sLiveFoldersProjectionMap = new HashMap<String, String>(); 882 sLiveFoldersProjectionMap.put(LiveFolders._ID, 883 Contacts._ID + " AS " + LiveFolders._ID); 884 sLiveFoldersProjectionMap.put(LiveFolders.NAME, 885 Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME); 886 887 // TODO: Put contact photo back when we have a way to display a default icon 888 // for contacts without a photo 889 // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP, 890 // Photos.DATA + " AS " + LiveFolders.ICON_BITMAP); 891 } 892 893 private static void addProjection(HashMap<String, String> map, String toField, String fromField) { 894 map.put(toField, fromField + " AS " + toField); 895 } 896 897 /** 898 * Handles inserts and update for a specific Data type. 899 */ 900 private abstract class DataRowHandler { 901 902 protected final String mMimetype; 903 protected long mMimetypeId; 904 905 @SuppressWarnings("all") 906 public DataRowHandler(String mimetype) { 907 mMimetype = mimetype; 908 909 // To ensure the data column position. This is dead code if properly configured. 910 if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1 911 || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1 912 || Email.DATA != Data.DATA1) { 913 throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary" 914 + " data is not in DATA1 column"); 915 } 916 } 917 918 protected long getMimeTypeId() { 919 if (mMimetypeId == 0) { 920 mMimetypeId = mDbHelper.getMimeTypeId(mMimetype); 921 } 922 return mMimetypeId; 923 } 924 925 /** 926 * Inserts a row into the {@link Data} table. 927 */ 928 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 929 final long dataId = db.insert(Tables.DATA, null, values); 930 931 Integer primary = values.getAsInteger(Data.IS_PRIMARY); 932 if (primary != null && primary != 0) { 933 setIsPrimary(rawContactId, dataId, getMimeTypeId()); 934 } 935 936 return dataId; 937 } 938 939 /** 940 * Validates data and updates a {@link Data} row using the cursor, which contains 941 * the current data. 942 */ 943 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 944 boolean callerIsSyncAdapter) { 945 long dataId = c.getLong(DataUpdateQuery._ID); 946 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 947 948 if (values.containsKey(Data.IS_SUPER_PRIMARY)) { 949 long mimeTypeId = getMimeTypeId(); 950 setIsSuperPrimary(rawContactId, dataId, mimeTypeId); 951 setIsPrimary(rawContactId, dataId, mimeTypeId); 952 953 // Now that we've taken care of setting these, remove them from "values". 954 values.remove(Data.IS_SUPER_PRIMARY); 955 values.remove(Data.IS_PRIMARY); 956 } else if (values.containsKey(Data.IS_PRIMARY)) { 957 setIsPrimary(rawContactId, dataId, getMimeTypeId()); 958 959 // Now that we've taken care of setting this, remove it from "values". 960 values.remove(Data.IS_PRIMARY); 961 } 962 963 if (values.size() > 0) { 964 mSelectionArgs1[0] = String.valueOf(dataId); 965 mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1); 966 } 967 968 if (!callerIsSyncAdapter) { 969 setRawContactDirty(rawContactId); 970 } 971 } 972 973 public int delete(SQLiteDatabase db, Cursor c) { 974 long dataId = c.getLong(DataDeleteQuery._ID); 975 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 976 boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0; 977 mSelectionArgs1[0] = String.valueOf(dataId); 978 int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1); 979 mSelectionArgs1[0] = String.valueOf(rawContactId); 980 db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1); 981 if (count != 0 && primary) { 982 fixPrimary(db, rawContactId); 983 } 984 return count; 985 } 986 987 private void fixPrimary(SQLiteDatabase db, long rawContactId) { 988 long mimeTypeId = getMimeTypeId(); 989 long primaryId = -1; 990 int primaryType = -1; 991 mSelectionArgs1[0] = String.valueOf(rawContactId); 992 Cursor c = db.query(DataDeleteQuery.TABLE, 993 DataDeleteQuery.CONCRETE_COLUMNS, 994 Data.RAW_CONTACT_ID + "=?" + 995 " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId, 996 mSelectionArgs1, null, null, null); 997 try { 998 while (c.moveToNext()) { 999 long dataId = c.getLong(DataDeleteQuery._ID); 1000 int type = c.getInt(DataDeleteQuery.DATA1); 1001 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) { 1002 primaryId = dataId; 1003 primaryType = type; 1004 } 1005 } 1006 } finally { 1007 c.close(); 1008 } 1009 if (primaryId != -1) { 1010 setIsPrimary(rawContactId, primaryId, mimeTypeId); 1011 } 1012 } 1013 1014 /** 1015 * Returns the rank of a specific record type to be used in determining the primary 1016 * row. Lower number represents higher priority. 1017 */ 1018 protected int getTypeRank(int type) { 1019 return 0; 1020 } 1021 1022 protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 1023 if (!isNewRawContact(rawContactId)) { 1024 updateRawContactDisplayName(db, rawContactId); 1025 mContactAggregator.updateDisplayNameForRawContact(db, rawContactId); 1026 } 1027 } 1028 1029 public boolean isAggregationRequired() { 1030 return true; 1031 } 1032 1033 /** 1034 * Return set of values, using current values at given {@link Data#_ID} 1035 * as baseline, but augmented with any updates. 1036 */ 1037 public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId, 1038 ContentValues update) { 1039 final ContentValues values = new ContentValues(); 1040 mSelectionArgs1[0] = String.valueOf(dataId); 1041 final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?", 1042 mSelectionArgs1, null, null, null); 1043 try { 1044 if (cursor.moveToFirst()) { 1045 for (int i = 0; i < cursor.getColumnCount(); i++) { 1046 final String key = cursor.getColumnName(i); 1047 values.put(key, cursor.getString(i)); 1048 } 1049 } 1050 } finally { 1051 cursor.close(); 1052 } 1053 values.putAll(update); 1054 return values; 1055 } 1056 } 1057 1058 public class CustomDataRowHandler extends DataRowHandler { 1059 1060 public CustomDataRowHandler(String mimetype) { 1061 super(mimetype); 1062 } 1063 } 1064 1065 public class StructuredNameRowHandler extends DataRowHandler { 1066 private final NameSplitter mSplitter; 1067 1068 public StructuredNameRowHandler(NameSplitter splitter) { 1069 super(StructuredName.CONTENT_ITEM_TYPE); 1070 mSplitter = splitter; 1071 } 1072 1073 @Override 1074 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1075 fixStructuredNameComponents(values, values); 1076 1077 long dataId = super.insert(db, rawContactId, values); 1078 1079 String name = values.getAsString(StructuredName.DISPLAY_NAME); 1080 insertNameLookupForStructuredName(rawContactId, dataId, name); 1081 fixRawContactDisplayName(db, rawContactId); 1082 return dataId; 1083 } 1084 1085 @Override 1086 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1087 boolean callerIsSyncAdapter) { 1088 final long dataId = c.getLong(DataUpdateQuery._ID); 1089 final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1090 1091 final ContentValues augmented = getAugmentedValues(db, dataId, values); 1092 fixStructuredNameComponents(augmented, values); 1093 1094 super.update(db, values, c, callerIsSyncAdapter); 1095 1096 if (values.containsKey(StructuredName.DISPLAY_NAME)) { 1097 String name = values.getAsString(StructuredName.DISPLAY_NAME); 1098 deleteNameLookup(dataId); 1099 insertNameLookupForStructuredName(rawContactId, dataId, name); 1100 } 1101 fixRawContactDisplayName(db, rawContactId); 1102 } 1103 1104 @Override 1105 public int delete(SQLiteDatabase db, Cursor c) { 1106 long dataId = c.getLong(DataDeleteQuery._ID); 1107 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1108 1109 int count = super.delete(db, c); 1110 1111 deleteNameLookup(dataId); 1112 fixRawContactDisplayName(db, rawContactId); 1113 return count; 1114 } 1115 1116 /** 1117 * Specific list of structured fields. 1118 */ 1119 private final String[] STRUCTURED_FIELDS = new String[] { 1120 StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME, 1121 StructuredName.FAMILY_NAME, StructuredName.SUFFIX 1122 }; 1123 1124 /** 1125 * Parses the supplied display name, but only if the incoming values do 1126 * not already contain structured name parts. Also, if the display name 1127 * is not provided, generate one by concatenating first name and last 1128 * name. 1129 */ 1130 private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) { 1131 final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME); 1132 1133 final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); 1134 final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); 1135 1136 if (touchedUnstruct && !touchedStruct) { 1137 NameSplitter.Name name = new NameSplitter.Name(); 1138 mSplitter.split(name, unstruct); 1139 name.toValues(update); 1140 } else if (!touchedUnstruct 1141 && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) { 1142 // We need to update the display name when any structured components 1143 // are specified, even when they are null, which is why we are checking 1144 // areAnySpecified. The touchedStruct in the condition is an optimization: 1145 // if there are non-null values, we know for a fact that some values are present. 1146 NameSplitter.Name name = new NameSplitter.Name(); 1147 name.fromValues(augmented); 1148 if (name.fullNameStyle == FullNameStyle.UNDEFINED) { 1149 mSplitter.guessNameStyle(name); 1150 } 1151 1152 final String joined = mSplitter.join(name, true); 1153 update.put(StructuredName.DISPLAY_NAME, joined); 1154 1155 update.put(StructuredName.FULL_NAME_STYLE, name.fullNameStyle); 1156 update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle); 1157 } 1158 } 1159 } 1160 1161 public class StructuredPostalRowHandler extends DataRowHandler { 1162 private PostalSplitter mSplitter; 1163 1164 public StructuredPostalRowHandler(PostalSplitter splitter) { 1165 super(StructuredPostal.CONTENT_ITEM_TYPE); 1166 mSplitter = splitter; 1167 } 1168 1169 @Override 1170 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1171 fixStructuredPostalComponents(values, values); 1172 return super.insert(db, rawContactId, values); 1173 } 1174 1175 @Override 1176 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1177 boolean callerIsSyncAdapter) { 1178 final long dataId = c.getLong(DataUpdateQuery._ID); 1179 final ContentValues augmented = getAugmentedValues(db, dataId, values); 1180 fixStructuredPostalComponents(augmented, values); 1181 super.update(db, values, c, callerIsSyncAdapter); 1182 } 1183 1184 /** 1185 * Specific list of structured fields. 1186 */ 1187 private final String[] STRUCTURED_FIELDS = new String[] { 1188 StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD, 1189 StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE, 1190 StructuredPostal.COUNTRY, 1191 }; 1192 1193 /** 1194 * Prepares the given {@link StructuredPostal} row, building 1195 * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured 1196 * values when missing. When structured components are missing, the 1197 * unstructured value is assigned to {@link StructuredPostal#STREET}. 1198 */ 1199 private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) { 1200 final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS); 1201 1202 final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); 1203 final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); 1204 1205 final PostalSplitter.Postal postal = new PostalSplitter.Postal(); 1206 1207 if (touchedUnstruct && !touchedStruct) { 1208 mSplitter.split(postal, unstruct); 1209 postal.toValues(update); 1210 } else if (!touchedUnstruct 1211 && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) { 1212 // See comment in 1213 postal.fromValues(augmented); 1214 final String joined = mSplitter.join(postal); 1215 update.put(StructuredPostal.FORMATTED_ADDRESS, joined); 1216 } 1217 } 1218 } 1219 1220 public class CommonDataRowHandler extends DataRowHandler { 1221 1222 private final String mTypeColumn; 1223 private final String mLabelColumn; 1224 1225 public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) { 1226 super(mimetype); 1227 mTypeColumn = typeColumn; 1228 mLabelColumn = labelColumn; 1229 } 1230 1231 @Override 1232 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1233 enforceTypeAndLabel(values, values); 1234 return super.insert(db, rawContactId, values); 1235 } 1236 1237 @Override 1238 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1239 boolean callerIsSyncAdapter) { 1240 final long dataId = c.getLong(DataUpdateQuery._ID); 1241 final ContentValues augmented = getAugmentedValues(db, dataId, values); 1242 enforceTypeAndLabel(augmented, values); 1243 super.update(db, values, c, callerIsSyncAdapter); 1244 } 1245 1246 /** 1247 * If the given {@link ContentValues} defines {@link #mTypeColumn}, 1248 * enforce that {@link #mLabelColumn} only appears when type is 1249 * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise. 1250 */ 1251 private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) { 1252 final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn)); 1253 final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn)); 1254 1255 if (hasLabel && !hasType) { 1256 // When label exists, assert that some type is defined 1257 throw new IllegalArgumentException(mTypeColumn + " must be specified when " 1258 + mLabelColumn + " is defined."); 1259 } 1260 } 1261 } 1262 1263 public class OrganizationDataRowHandler extends CommonDataRowHandler { 1264 1265 public OrganizationDataRowHandler() { 1266 super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL); 1267 } 1268 1269 @Override 1270 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1271 String company = values.getAsString(Organization.COMPANY); 1272 String title = values.getAsString(Organization.TITLE); 1273 1274 long dataId = super.insert(db, rawContactId, values); 1275 1276 fixRawContactDisplayName(db, rawContactId); 1277 insertNameLookupForOrganization(rawContactId, dataId, company, title); 1278 return dataId; 1279 } 1280 1281 @Override 1282 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1283 boolean callerIsSyncAdapter) { 1284 String company = values.getAsString(Organization.COMPANY); 1285 String title = values.getAsString(Organization.TITLE); 1286 long dataId = c.getLong(DataUpdateQuery._ID); 1287 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1288 1289 super.update(db, values, c, callerIsSyncAdapter); 1290 1291 fixRawContactDisplayName(db, rawContactId); 1292 deleteNameLookup(dataId); 1293 insertNameLookupForOrganization(rawContactId, dataId, company, title); 1294 } 1295 1296 @Override 1297 public int delete(SQLiteDatabase db, Cursor c) { 1298 long dataId = c.getLong(DataUpdateQuery._ID); 1299 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1300 1301 int count = super.delete(db, c); 1302 fixRawContactDisplayName(db, rawContactId); 1303 deleteNameLookup(dataId); 1304 return count; 1305 } 1306 1307 @Override 1308 protected int getTypeRank(int type) { 1309 switch (type) { 1310 case Organization.TYPE_WORK: return 0; 1311 case Organization.TYPE_CUSTOM: return 1; 1312 case Organization.TYPE_OTHER: return 2; 1313 default: return 1000; 1314 } 1315 } 1316 1317 @Override 1318 public boolean isAggregationRequired() { 1319 return false; 1320 } 1321 } 1322 1323 public class EmailDataRowHandler extends CommonDataRowHandler { 1324 1325 public EmailDataRowHandler() { 1326 super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL); 1327 } 1328 1329 @Override 1330 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1331 String address = values.getAsString(Email.DATA); 1332 1333 long dataId = super.insert(db, rawContactId, values); 1334 1335 fixRawContactDisplayName(db, rawContactId); 1336 insertNameLookupForEmail(rawContactId, dataId, address); 1337 return dataId; 1338 } 1339 1340 @Override 1341 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1342 boolean callerIsSyncAdapter) { 1343 long dataId = c.getLong(DataUpdateQuery._ID); 1344 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1345 String address = values.getAsString(Email.DATA); 1346 1347 super.update(db, values, c, callerIsSyncAdapter); 1348 1349 deleteNameLookup(dataId); 1350 insertNameLookupForEmail(rawContactId, dataId, address); 1351 fixRawContactDisplayName(db, rawContactId); 1352 } 1353 1354 @Override 1355 public int delete(SQLiteDatabase db, Cursor c) { 1356 long dataId = c.getLong(DataDeleteQuery._ID); 1357 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1358 1359 int count = super.delete(db, c); 1360 1361 deleteNameLookup(dataId); 1362 fixRawContactDisplayName(db, rawContactId); 1363 return count; 1364 } 1365 1366 @Override 1367 protected int getTypeRank(int type) { 1368 switch (type) { 1369 case Email.TYPE_HOME: return 0; 1370 case Email.TYPE_WORK: return 1; 1371 case Email.TYPE_CUSTOM: return 2; 1372 case Email.TYPE_OTHER: return 3; 1373 default: return 1000; 1374 } 1375 } 1376 } 1377 1378 public class NicknameDataRowHandler extends CommonDataRowHandler { 1379 1380 public NicknameDataRowHandler() { 1381 super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL); 1382 } 1383 1384 @Override 1385 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1386 String nickname = values.getAsString(Nickname.NAME); 1387 1388 long dataId = super.insert(db, rawContactId, values); 1389 1390 fixRawContactDisplayName(db, rawContactId); 1391 insertNameLookupForNickname(rawContactId, dataId, nickname); 1392 return dataId; 1393 } 1394 1395 @Override 1396 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1397 boolean callerIsSyncAdapter) { 1398 long dataId = c.getLong(DataUpdateQuery._ID); 1399 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1400 String nickname = values.getAsString(Nickname.NAME); 1401 1402 super.update(db, values, c, callerIsSyncAdapter); 1403 1404 deleteNameLookup(dataId); 1405 insertNameLookupForNickname(rawContactId, dataId, nickname); 1406 fixRawContactDisplayName(db, rawContactId); 1407 } 1408 1409 @Override 1410 public int delete(SQLiteDatabase db, Cursor c) { 1411 long dataId = c.getLong(DataDeleteQuery._ID); 1412 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1413 1414 int count = super.delete(db, c); 1415 1416 deleteNameLookup(dataId); 1417 fixRawContactDisplayName(db, rawContactId); 1418 return count; 1419 } 1420 } 1421 1422 public class PhoneDataRowHandler extends CommonDataRowHandler { 1423 1424 public PhoneDataRowHandler() { 1425 super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL); 1426 } 1427 1428 @Override 1429 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1430 long dataId; 1431 if (values.containsKey(Phone.NUMBER)) { 1432 String number = values.getAsString(Phone.NUMBER); 1433 String normalizedNumber = computeNormalizedNumber(number, values); 1434 1435 dataId = super.insert(db, rawContactId, values); 1436 1437 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); 1438 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1439 fixRawContactDisplayName(db, rawContactId); 1440 } else { 1441 dataId = super.insert(db, rawContactId, values); 1442 } 1443 return dataId; 1444 } 1445 1446 @Override 1447 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1448 boolean callerIsSyncAdapter) { 1449 long dataId = c.getLong(DataUpdateQuery._ID); 1450 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1451 if (values.containsKey(Phone.NUMBER)) { 1452 String number = values.getAsString(Phone.NUMBER); 1453 String normalizedNumber = computeNormalizedNumber(number, values); 1454 1455 super.update(db, values, c, callerIsSyncAdapter); 1456 1457 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); 1458 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1459 fixRawContactDisplayName(db, rawContactId); 1460 } else { 1461 super.update(db, values, c, callerIsSyncAdapter); 1462 } 1463 } 1464 1465 @Override 1466 public int delete(SQLiteDatabase db, Cursor c) { 1467 long dataId = c.getLong(DataDeleteQuery._ID); 1468 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1469 1470 int count = super.delete(db, c); 1471 1472 updatePhoneLookup(db, rawContactId, dataId, null, null); 1473 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1474 fixRawContactDisplayName(db, rawContactId); 1475 return count; 1476 } 1477 1478 private String computeNormalizedNumber(String number, ContentValues values) { 1479 String normalizedNumber = null; 1480 if (number != null) { 1481 normalizedNumber = PhoneNumberUtils.getStrippedReversed(number); 1482 } 1483 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber); 1484 return normalizedNumber; 1485 } 1486 1487 private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId, 1488 String number, String normalizedNumber) { 1489 if (number != null) { 1490 ContentValues phoneValues = new ContentValues(); 1491 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId); 1492 phoneValues.put(PhoneLookupColumns.DATA_ID, dataId); 1493 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber); 1494 phoneValues.put(PhoneLookupColumns.MIN_MATCH, 1495 PhoneNumberUtils.toCallerIDMinMatch(number)); 1496 1497 db.replace(Tables.PHONE_LOOKUP, null, phoneValues); 1498 } else { 1499 mSelectionArgs1[0] = String.valueOf(dataId); 1500 db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1); 1501 } 1502 } 1503 1504 @Override 1505 protected int getTypeRank(int type) { 1506 switch (type) { 1507 case Phone.TYPE_MOBILE: return 0; 1508 case Phone.TYPE_WORK: return 1; 1509 case Phone.TYPE_HOME: return 2; 1510 case Phone.TYPE_PAGER: return 3; 1511 case Phone.TYPE_CUSTOM: return 4; 1512 case Phone.TYPE_OTHER: return 5; 1513 case Phone.TYPE_FAX_WORK: return 6; 1514 case Phone.TYPE_FAX_HOME: return 7; 1515 default: return 1000; 1516 } 1517 } 1518 } 1519 1520 public class GroupMembershipRowHandler extends DataRowHandler { 1521 1522 public GroupMembershipRowHandler() { 1523 super(GroupMembership.CONTENT_ITEM_TYPE); 1524 } 1525 1526 @Override 1527 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1528 resolveGroupSourceIdInValues(rawContactId, db, values, true); 1529 long dataId = super.insert(db, rawContactId, values); 1530 updateVisibility(rawContactId); 1531 return dataId; 1532 } 1533 1534 @Override 1535 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1536 boolean callerIsSyncAdapter) { 1537 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1538 resolveGroupSourceIdInValues(rawContactId, db, values, false); 1539 super.update(db, values, c, callerIsSyncAdapter); 1540 updateVisibility(rawContactId); 1541 } 1542 1543 @Override 1544 public int delete(SQLiteDatabase db, Cursor c) { 1545 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1546 int count = super.delete(db, c); 1547 updateVisibility(rawContactId); 1548 return count; 1549 } 1550 1551 private void updateVisibility(long rawContactId) { 1552 long contactId = mDbHelper.getContactId(rawContactId); 1553 if (contactId != 0) { 1554 mDbHelper.updateContactVisible(contactId); 1555 } 1556 } 1557 1558 private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db, 1559 ContentValues values, boolean isInsert) { 1560 boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID); 1561 boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID); 1562 if (containsGroupSourceId && containsGroupId) { 1563 throw new IllegalArgumentException( 1564 "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID " 1565 + "and GroupMembership.GROUP_ROW_ID"); 1566 } 1567 1568 if (!containsGroupSourceId && !containsGroupId) { 1569 if (isInsert) { 1570 throw new IllegalArgumentException( 1571 "you must set exactly one of GroupMembership.GROUP_SOURCE_ID " 1572 + "and GroupMembership.GROUP_ROW_ID"); 1573 } else { 1574 return; 1575 } 1576 } 1577 1578 if (containsGroupSourceId) { 1579 final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID); 1580 final long groupId = getOrMakeGroup(db, rawContactId, sourceId, 1581 mInsertedRawContacts.get(rawContactId)); 1582 values.remove(GroupMembership.GROUP_SOURCE_ID); 1583 values.put(GroupMembership.GROUP_ROW_ID, groupId); 1584 } 1585 } 1586 1587 @Override 1588 public boolean isAggregationRequired() { 1589 return false; 1590 } 1591 } 1592 1593 public class PhotoDataRowHandler extends DataRowHandler { 1594 1595 public PhotoDataRowHandler() { 1596 super(Photo.CONTENT_ITEM_TYPE); 1597 } 1598 1599 @Override 1600 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1601 long dataId = super.insert(db, rawContactId, values); 1602 if (!isNewRawContact(rawContactId)) { 1603 mContactAggregator.updatePhotoId(db, rawContactId); 1604 } 1605 return dataId; 1606 } 1607 1608 @Override 1609 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1610 boolean callerIsSyncAdapter) { 1611 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1612 super.update(db, values, c, callerIsSyncAdapter); 1613 mContactAggregator.updatePhotoId(db, rawContactId); 1614 } 1615 1616 @Override 1617 public int delete(SQLiteDatabase db, Cursor c) { 1618 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1619 int count = super.delete(db, c); 1620 mContactAggregator.updatePhotoId(db, rawContactId); 1621 return count; 1622 } 1623 1624 @Override 1625 public boolean isAggregationRequired() { 1626 return false; 1627 } 1628 } 1629 1630 /** 1631 * An entry in group id cache. It maps the combination of (account type, account name 1632 * and source id) to group row id. 1633 */ 1634 public class GroupIdCacheEntry { 1635 String accountType; 1636 String accountName; 1637 String sourceId; 1638 long groupId; 1639 } 1640 1641 private HashMap<String, DataRowHandler> mDataRowHandlers; 1642 private ContactsDatabaseHelper mDbHelper; 1643 1644 private NameSplitter mNameSplitter; 1645 private NameLookupBuilder mNameLookupBuilder; 1646 1647 // We will use this much memory (in bits) to optimize the nickname cluster lookup 1648 private static final int NICKNAME_BLOOM_FILTER_SIZE = 0x1FFF; // =long[128] 1649 private BitSet mNicknameBloomFilter; 1650 1651 private HashMap<String, SoftReference<String[]>> mNicknameClusterCache = Maps.newHashMap(); 1652 1653 private PostalSplitter mPostalSplitter; 1654 1655 // We don't need a soft cache for groups - the assumption is that there will only 1656 // be a small number of contact groups. The cache is keyed off source id. The value 1657 // is a list of groups with this group id. 1658 private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap(); 1659 1660 private ContactAggregator mContactAggregator; 1661 private LegacyApiSupport mLegacyApiSupport; 1662 private GlobalSearchSupport mGlobalSearchSupport; 1663 1664 private ContentValues mValues = new ContentValues(); 1665 private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128); 1666 private NameSplitter.Name mName = new NameSplitter.Name(); 1667 1668 private volatile CountDownLatch mAccessLatch; 1669 1670 private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap(); 1671 private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet(); 1672 private HashSet<Long> mDirtyRawContacts = Sets.newHashSet(); 1673 private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap(); 1674 1675 private boolean mVisibleTouched = false; 1676 1677 private boolean mSyncToNetwork; 1678 1679 @Override 1680 public boolean onCreate() { 1681 super.onCreate(); 1682 try { 1683 return initialize(); 1684 } catch (RuntimeException e) { 1685 Log.e(TAG, "Cannot start provider", e); 1686 return false; 1687 } 1688 } 1689 1690 private boolean initialize() { 1691 final Context context = getContext(); 1692 mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper(); 1693 mGlobalSearchSupport = new GlobalSearchSupport(this); 1694 mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport); 1695 mContactAggregator = new ContactAggregator(this, mDbHelper); 1696 mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); 1697 1698 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 1699 1700 mSetPrimaryStatement = db.compileStatement( 1701 "UPDATE " + Tables.DATA + 1702 " SET " + Data.IS_PRIMARY + "=(_id=?)" + 1703 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1704 " AND " + Data.RAW_CONTACT_ID + "=?"); 1705 1706 mSetSuperPrimaryStatement = db.compileStatement( 1707 "UPDATE " + Tables.DATA + 1708 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" + 1709 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1710 " AND " + Data.RAW_CONTACT_ID + " IN (" + 1711 "SELECT " + RawContacts._ID + 1712 " FROM " + Tables.RAW_CONTACTS + 1713 " WHERE " + RawContacts.CONTACT_ID + " =(" + 1714 "SELECT " + RawContacts.CONTACT_ID + 1715 " FROM " + Tables.RAW_CONTACTS + 1716 " WHERE " + RawContacts._ID + "=?))"); 1717 1718 mRawContactDisplayNameUpdate = db.compileStatement( 1719 "UPDATE " + Tables.RAW_CONTACTS + 1720 " SET " + 1721 RawContacts.DISPLAY_NAME_SOURCE + "=?," + 1722 RawContacts.DISPLAY_NAME_PRIMARY + "=?," + 1723 RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," + 1724 RawContacts.PHONETIC_NAME + "=?," + 1725 RawContacts.PHONETIC_NAME_STYLE + "=?," + 1726 RawContacts.SORT_KEY_PRIMARY + "=?," + 1727 RawContacts.SORT_KEY_ALTERNATIVE + "=?" + 1728 " WHERE " + RawContacts._ID + "=?"); 1729 1730 mLastStatusUpdate = db.compileStatement( 1731 "UPDATE " + Tables.CONTACTS + 1732 " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" + 1733 "(SELECT " + DataColumns.CONCRETE_ID + 1734 " FROM " + Tables.STATUS_UPDATES + 1735 " JOIN " + Tables.DATA + 1736 " ON (" + StatusUpdatesColumns.DATA_ID + "=" 1737 + DataColumns.CONCRETE_ID + ")" + 1738 " JOIN " + Tables.RAW_CONTACTS + 1739 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" 1740 + RawContactsColumns.CONCRETE_ID + ")" + 1741 " WHERE " + RawContacts.CONTACT_ID + "=?" + 1742 " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC," 1743 + StatusUpdates.STATUS + 1744 " LIMIT 1)" + 1745 " WHERE " + ContactsColumns.CONCRETE_ID + "=?"); 1746 1747 final Locale locale = getLocale(); 1748 mNameSplitter = new NameSplitter( 1749 context.getString(com.android.internal.R.string.common_name_prefixes), 1750 context.getString(com.android.internal.R.string.common_last_name_prefixes), 1751 context.getString(com.android.internal.R.string.common_name_suffixes), 1752 context.getString(com.android.internal.R.string.common_name_conjunctions), 1753 locale); 1754 mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter); 1755 mPostalSplitter = new PostalSplitter(locale); 1756 1757 mNameLookupInsert = db.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "(" 1758 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + "," 1759 + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME 1760 + ") VALUES (?,?,?,?)"); 1761 mNameLookupDelete = db.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE " 1762 + NameLookupColumns.DATA_ID + "=?"); 1763 1764 mStatusUpdateInsert = db.compileStatement( 1765 "INSERT INTO " + Tables.STATUS_UPDATES + "(" 1766 + StatusUpdatesColumns.DATA_ID + ", " 1767 + StatusUpdates.STATUS + "," 1768 + StatusUpdates.STATUS_RES_PACKAGE + "," 1769 + StatusUpdates.STATUS_ICON + "," 1770 + StatusUpdates.STATUS_LABEL + ")" + 1771 " VALUES (?,?,?,?,?)"); 1772 1773 mStatusUpdateReplace = db.compileStatement( 1774 "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "(" 1775 + StatusUpdatesColumns.DATA_ID + ", " 1776 + StatusUpdates.STATUS_TIMESTAMP + "," 1777 + StatusUpdates.STATUS + "," 1778 + StatusUpdates.STATUS_RES_PACKAGE + "," 1779 + StatusUpdates.STATUS_ICON + "," 1780 + StatusUpdates.STATUS_LABEL + ")" + 1781 " VALUES (?,?,?,?,?,?)"); 1782 1783 mStatusUpdateAutoTimestamp = db.compileStatement( 1784 "UPDATE " + Tables.STATUS_UPDATES + 1785 " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?," 1786 + StatusUpdates.STATUS + "=?" + 1787 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?" 1788 + " AND " + StatusUpdates.STATUS + "!=?"); 1789 1790 mStatusAttributionUpdate = db.compileStatement( 1791 "UPDATE " + Tables.STATUS_UPDATES + 1792 " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?," 1793 + StatusUpdates.STATUS_ICON + "=?," 1794 + StatusUpdates.STATUS_LABEL + "=?" + 1795 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); 1796 1797 mStatusUpdateDelete = db.compileStatement( 1798 "DELETE FROM " + Tables.STATUS_UPDATES + 1799 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); 1800 1801 mDataRowHandlers = new HashMap<String, DataRowHandler>(); 1802 1803 mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler()); 1804 mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE, 1805 new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL)); 1806 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler( 1807 StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL)); 1808 mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler()); 1809 mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler()); 1810 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler()); 1811 mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE, 1812 new StructuredNameRowHandler(mNameSplitter)); 1813 mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, 1814 new StructuredPostalRowHandler(mPostalSplitter)); 1815 mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler()); 1816 mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler()); 1817 1818 if (isLegacyContactImportNeeded()) { 1819 importLegacyContactsAsync(); 1820 } 1821 1822 verifyAccounts(); 1823 1824 mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); 1825 mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE); 1826 mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE); 1827 mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE); 1828 mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE); 1829 mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); 1830 preloadNicknameBloomFilter(); 1831 return (db != null); 1832 } 1833 1834 protected void verifyAccounts() { 1835 AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false); 1836 onAccountsUpdated(AccountManager.get(getContext()).getAccounts()); 1837 } 1838 1839 /* Visible for testing */ 1840 @Override 1841 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 1842 return ContactsDatabaseHelper.getInstance(context); 1843 } 1844 1845 /* package */ NameSplitter getNameSplitter() { 1846 return mNameSplitter; 1847 } 1848 1849 /* Visible for testing */ 1850 protected Locale getLocale() { 1851 return Locale.getDefault(); 1852 } 1853 1854 protected boolean isLegacyContactImportNeeded() { 1855 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1856 return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION; 1857 } 1858 1859 protected LegacyContactImporter getLegacyContactImporter() { 1860 return new LegacyContactImporter(getContext(), this); 1861 } 1862 1863 /** 1864 * Imports legacy contacts in a separate thread. As long as the import process is running 1865 * all other access to the contacts is blocked. 1866 */ 1867 private void importLegacyContactsAsync() { 1868 mAccessLatch = new CountDownLatch(1); 1869 1870 Thread importThread = new Thread("LegacyContactImport") { 1871 @Override 1872 public void run() { 1873 if (importLegacyContacts()) { 1874 // TODO aggregate all newly added raw contacts 1875 1876 /* 1877 * When the import process is done, we can unlock the provider and 1878 * start aggregating the imported contacts asynchronously. 1879 */ 1880 mAccessLatch.countDown(); 1881 mAccessLatch = null; 1882 } 1883 } 1884 }; 1885 1886 importThread.start(); 1887 } 1888 1889 private boolean importLegacyContacts() { 1890 LegacyContactImporter importer = getLegacyContactImporter(); 1891 if (importLegacyContacts(importer)) { 1892 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1893 Editor editor = prefs.edit(); 1894 editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION); 1895 editor.commit(); 1896 return true; 1897 } else { 1898 return false; 1899 } 1900 } 1901 1902 /* Visible for testing */ 1903 /* package */ boolean importLegacyContacts(LegacyContactImporter importer) { 1904 boolean aggregatorEnabled = mContactAggregator.isEnabled(); 1905 mContactAggregator.setEnabled(false); 1906 try { 1907 importer.importContacts(); 1908 mContactAggregator.setEnabled(aggregatorEnabled); 1909 return true; 1910 } catch (Throwable e) { 1911 Log.e(TAG, "Legacy contact import failed", e); 1912 return false; 1913 } 1914 } 1915 1916 /** 1917 * Wipes all data from the contacts database. 1918 */ 1919 /* package */ void wipeData() { 1920 mDbHelper.wipeData(); 1921 } 1922 1923 /** 1924 * While importing and aggregating contacts, this content provider will 1925 * block all attempts to change contacts data. In particular, it will hold 1926 * up all contact syncs. As soon as the import process is complete, all 1927 * processes waiting to write to the provider are unblocked and can proceed 1928 * to compete for the database transaction monitor. 1929 */ 1930 private void waitForAccess() { 1931 CountDownLatch latch = mAccessLatch; 1932 if (latch != null) { 1933 while (true) { 1934 try { 1935 latch.await(); 1936 mAccessLatch = null; 1937 return; 1938 } catch (InterruptedException e) { 1939 Thread.currentThread().interrupt(); 1940 } 1941 } 1942 } 1943 } 1944 1945 @Override 1946 public Uri insert(Uri uri, ContentValues values) { 1947 waitForAccess(); 1948 return super.insert(uri, values); 1949 } 1950 1951 @Override 1952 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1953 waitForAccess(); 1954 return super.update(uri, values, selection, selectionArgs); 1955 } 1956 1957 @Override 1958 public int delete(Uri uri, String selection, String[] selectionArgs) { 1959 waitForAccess(); 1960 return super.delete(uri, selection, selectionArgs); 1961 } 1962 1963 @Override 1964 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1965 throws OperationApplicationException { 1966 waitForAccess(); 1967 return super.applyBatch(operations); 1968 } 1969 1970 @Override 1971 protected void onBeginTransaction() { 1972 if (VERBOSE_LOGGING) { 1973 Log.v(TAG, "onBeginTransaction"); 1974 } 1975 super.onBeginTransaction(); 1976 mContactAggregator.clearPendingAggregations(); 1977 clearTransactionalChanges(); 1978 } 1979 1980 private void clearTransactionalChanges() { 1981 mInsertedRawContacts.clear(); 1982 mUpdatedRawContacts.clear(); 1983 mUpdatedSyncStates.clear(); 1984 mDirtyRawContacts.clear(); 1985 } 1986 1987 @Override 1988 protected void beforeTransactionCommit() { 1989 1990 if (VERBOSE_LOGGING) { 1991 Log.v(TAG, "beforeTransactionCommit"); 1992 } 1993 super.beforeTransactionCommit(); 1994 flushTransactionalChanges(); 1995 mContactAggregator.aggregateInTransaction(mDb); 1996 if (mVisibleTouched) { 1997 mVisibleTouched = false; 1998 mDbHelper.updateAllVisible(); 1999 } 2000 } 2001 2002 private void flushTransactionalChanges() { 2003 if (VERBOSE_LOGGING) { 2004 Log.v(TAG, "flushTransactionChanges"); 2005 } 2006 2007 for (long rawContactId : mInsertedRawContacts.keySet()) { 2008 updateRawContactDisplayName(mDb, rawContactId); 2009 mContactAggregator.onRawContactInsert(mDb, rawContactId); 2010 } 2011 2012 if (!mDirtyRawContacts.isEmpty()) { 2013 mSb.setLength(0); 2014 mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL); 2015 appendIds(mSb, mDirtyRawContacts); 2016 mSb.append(")"); 2017 mDb.execSQL(mSb.toString()); 2018 } 2019 2020 if (!mUpdatedRawContacts.isEmpty()) { 2021 mSb.setLength(0); 2022 mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL); 2023 appendIds(mSb, mUpdatedRawContacts); 2024 mSb.append(")"); 2025 mDb.execSQL(mSb.toString()); 2026 } 2027 2028 for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) { 2029 long id = entry.getKey(); 2030 mDbHelper.getSyncState().update(mDb, id, entry.getValue()); 2031 } 2032 2033 clearTransactionalChanges(); 2034 } 2035 2036 /** 2037 * Appends comma separated ids. 2038 * @param ids Should not be empty 2039 */ 2040 private void appendIds(StringBuilder sb, HashSet<Long> ids) { 2041 for (long id : ids) { 2042 sb.append(id).append(','); 2043 } 2044 2045 sb.setLength(sb.length() - 1); // Yank the last comma 2046 } 2047 2048 @Override 2049 protected void notifyChange() { 2050 notifyChange(mSyncToNetwork); 2051 mSyncToNetwork = false; 2052 } 2053 2054 protected void notifyChange(boolean syncToNetwork) { 2055 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, 2056 syncToNetwork); 2057 } 2058 2059 private boolean isNewRawContact(long rawContactId) { 2060 return mInsertedRawContacts.containsKey(rawContactId); 2061 } 2062 2063 private DataRowHandler getDataRowHandler(final String mimeType) { 2064 DataRowHandler handler = mDataRowHandlers.get(mimeType); 2065 if (handler == null) { 2066 handler = new CustomDataRowHandler(mimeType); 2067 mDataRowHandlers.put(mimeType, handler); 2068 } 2069 return handler; 2070 } 2071 2072 @Override 2073 protected Uri insertInTransaction(Uri uri, ContentValues values) { 2074 if (VERBOSE_LOGGING) { 2075 Log.v(TAG, "insertInTransaction: " + uri + " " + values); 2076 } 2077 2078 final boolean callerIsSyncAdapter = 2079 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2080 2081 final int match = sUriMatcher.match(uri); 2082 long id = 0; 2083 2084 switch (match) { 2085 case SYNCSTATE: 2086 id = mDbHelper.getSyncState().insert(mDb, values); 2087 break; 2088 2089 case CONTACTS: { 2090 insertContact(values); 2091 break; 2092 } 2093 2094 case RAW_CONTACTS: { 2095 id = insertRawContact(uri, values); 2096 mSyncToNetwork |= !callerIsSyncAdapter; 2097 break; 2098 } 2099 2100 case RAW_CONTACTS_DATA: { 2101 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 2102 id = insertData(values, callerIsSyncAdapter); 2103 mSyncToNetwork |= !callerIsSyncAdapter; 2104 break; 2105 } 2106 2107 case DATA: { 2108 id = insertData(values, callerIsSyncAdapter); 2109 mSyncToNetwork |= !callerIsSyncAdapter; 2110 break; 2111 } 2112 2113 case GROUPS: { 2114 id = insertGroup(uri, values, callerIsSyncAdapter); 2115 mSyncToNetwork |= !callerIsSyncAdapter; 2116 break; 2117 } 2118 2119 case SETTINGS: { 2120 id = insertSettings(uri, values); 2121 mSyncToNetwork |= !callerIsSyncAdapter; 2122 break; 2123 } 2124 2125 case STATUS_UPDATES: { 2126 id = insertStatusUpdate(values); 2127 break; 2128 } 2129 2130 default: 2131 mSyncToNetwork = true; 2132 return mLegacyApiSupport.insert(uri, values); 2133 } 2134 2135 if (id < 0) { 2136 return null; 2137 } 2138 2139 return ContentUris.withAppendedId(uri, id); 2140 } 2141 2142 /** 2143 * If account is non-null then store it in the values. If the account is already 2144 * specified in the values then it must be consistent with the account, if it is non-null. 2145 * @param uri the ContentValues to read from and update 2146 * @param values the explicitly provided Account 2147 * @return false if the parameters are inconsistent 2148 */ 2149 private boolean resolveAccount(Uri uri, ContentValues values) { 2150 String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 2151 String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 2152 2153 if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) { 2154 accountName = null; 2155 accountType = null; 2156 } 2157 2158 String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME); 2159 String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 2160 2161 if (TextUtils.isEmpty(valueAccountName) && TextUtils.isEmpty(valueAccountType)) { 2162 values.put(RawContacts.ACCOUNT_NAME, accountName); 2163 values.put(RawContacts.ACCOUNT_TYPE, accountType); 2164 } else { 2165 if (accountName != null && !accountName.equals(valueAccountName)) { 2166 return false; 2167 } 2168 2169 if (accountType != null && !accountType.equals(valueAccountType)) { 2170 return false; 2171 } 2172 2173 accountName = valueAccountName; 2174 accountType = valueAccountType; 2175 } 2176 2177 if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) { 2178 mAccount = null; 2179 return true; 2180 } 2181 2182 if (mAccount == null 2183 || !mAccount.name.equals(accountName) 2184 || !mAccount.type.equals(accountType)) { 2185 mAccount = new Account(accountName, accountType); 2186 } 2187 2188 return true; 2189 } 2190 2191 /** 2192 * Inserts an item in the contacts table 2193 * 2194 * @param values the values for the new row 2195 * @return the row ID of the newly created row 2196 */ 2197 private long insertContact(ContentValues values) { 2198 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 2199 } 2200 2201 /** 2202 * Inserts an item in the contacts table 2203 * 2204 * @param uri the values for the new row 2205 * @param values the account this contact should be associated with. may be null. 2206 * @return the row ID of the newly created row 2207 */ 2208 private long insertRawContact(Uri uri, ContentValues values) { 2209 mValues.clear(); 2210 mValues.putAll(values); 2211 mValues.putNull(RawContacts.CONTACT_ID); 2212 2213 if (!resolveAccount(uri, mValues)) { 2214 return -1; 2215 } 2216 2217 if (values.containsKey(RawContacts.DELETED) 2218 && values.getAsInteger(RawContacts.DELETED) != 0) { 2219 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2220 } 2221 2222 long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues); 2223 mContactAggregator.markNewForAggregation(rawContactId); 2224 2225 // Trigger creation of a Contact based on this RawContact at the end of transaction 2226 mInsertedRawContacts.put(rawContactId, mAccount); 2227 2228 return rawContactId; 2229 } 2230 2231 /** 2232 * Inserts an item in the data table 2233 * 2234 * @param values the values for the new row 2235 * @return the row ID of the newly created row 2236 */ 2237 private long insertData(ContentValues values, boolean callerIsSyncAdapter) { 2238 long id = 0; 2239 mValues.clear(); 2240 mValues.putAll(values); 2241 2242 long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 2243 2244 // Replace package with internal mapping 2245 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 2246 if (packageName != null) { 2247 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2248 } 2249 mValues.remove(Data.RES_PACKAGE); 2250 2251 // Replace mimetype with internal mapping 2252 final String mimeType = mValues.getAsString(Data.MIMETYPE); 2253 if (TextUtils.isEmpty(mimeType)) { 2254 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 2255 } 2256 2257 mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType)); 2258 mValues.remove(Data.MIMETYPE); 2259 2260 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2261 id = rowHandler.insert(mDb, rawContactId, mValues); 2262 if (!callerIsSyncAdapter) { 2263 setRawContactDirty(rawContactId); 2264 } 2265 mUpdatedRawContacts.add(rawContactId); 2266 2267 if (rowHandler.isAggregationRequired()) { 2268 triggerAggregation(rawContactId); 2269 } 2270 return id; 2271 } 2272 2273 private void triggerAggregation(long rawContactId) { 2274 if (!mContactAggregator.isEnabled()) { 2275 return; 2276 } 2277 2278 int aggregationMode = mDbHelper.getAggregationMode(rawContactId); 2279 switch (aggregationMode) { 2280 case RawContacts.AGGREGATION_MODE_DISABLED: 2281 break; 2282 2283 case RawContacts.AGGREGATION_MODE_DEFAULT: { 2284 mContactAggregator.markForAggregation(rawContactId); 2285 break; 2286 } 2287 2288 case RawContacts.AGGREGATION_MODE_SUSPENDED: { 2289 long contactId = mDbHelper.getContactId(rawContactId); 2290 2291 if (contactId != 0) { 2292 mContactAggregator.updateAggregateData(contactId); 2293 } 2294 break; 2295 } 2296 2297 case RawContacts.AGGREGATION_MODE_IMMEDIATE: { 2298 long contactId = mDbHelper.getContactId(rawContactId); 2299 mContactAggregator.aggregateContact(mDb, rawContactId, contactId); 2300 break; 2301 } 2302 } 2303 } 2304 2305 /** 2306 * Returns the group id of the group with sourceId and the same account as rawContactId. 2307 * If the group doesn't already exist then it is first created, 2308 * @param db SQLiteDatabase to use for this operation 2309 * @param rawContactId the contact this group is associated with 2310 * @param sourceId the sourceIf of the group to query or create 2311 * @return the group id of the existing or created group 2312 * @throws IllegalArgumentException if the contact is not associated with an account 2313 * @throws IllegalStateException if a group needs to be created but the creation failed 2314 */ 2315 private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId, 2316 Account account) { 2317 2318 if (account == null) { 2319 mSelectionArgs1[0] = String.valueOf(rawContactId); 2320 Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, 2321 RawContacts._ID + "=?", mSelectionArgs1, null, null, null); 2322 try { 2323 if (c.moveToFirst()) { 2324 String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME); 2325 String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); 2326 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 2327 account = new Account(accountName, accountType); 2328 } 2329 } 2330 } finally { 2331 c.close(); 2332 } 2333 } 2334 2335 if (account == null) { 2336 throw new IllegalArgumentException("if the groupmembership only " 2337 + "has a sourceid the the contact must be associated with " 2338 + "an account"); 2339 } 2340 2341 ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId); 2342 if (entries == null) { 2343 entries = new ArrayList<GroupIdCacheEntry>(1); 2344 mGroupIdCache.put(sourceId, entries); 2345 } 2346 2347 int count = entries.size(); 2348 for (int i = 0; i < count; i++) { 2349 GroupIdCacheEntry entry = entries.get(i); 2350 if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) { 2351 return entry.groupId; 2352 } 2353 } 2354 2355 GroupIdCacheEntry entry = new GroupIdCacheEntry(); 2356 entry.accountName = account.name; 2357 entry.accountType = account.type; 2358 entry.sourceId = sourceId; 2359 entries.add(0, entry); 2360 2361 // look up the group that contains this sourceId and has the same account name and type 2362 // as the contact refered to by rawContactId 2363 Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID}, 2364 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID, 2365 new String[]{sourceId, account.name, account.type}, null, null, null); 2366 try { 2367 if (c.moveToFirst()) { 2368 entry.groupId = c.getLong(0); 2369 } else { 2370 ContentValues groupValues = new ContentValues(); 2371 groupValues.put(Groups.ACCOUNT_NAME, account.name); 2372 groupValues.put(Groups.ACCOUNT_TYPE, account.type); 2373 groupValues.put(Groups.SOURCE_ID, sourceId); 2374 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues); 2375 if (groupId < 0) { 2376 throw new IllegalStateException("unable to create a new group with " 2377 + "this sourceid: " + groupValues); 2378 } 2379 entry.groupId = groupId; 2380 } 2381 } finally { 2382 c.close(); 2383 } 2384 2385 return entry.groupId; 2386 } 2387 2388 private interface DisplayNameQuery { 2389 public static final String RAW_SQL = 2390 "SELECT " 2391 + DataColumns.MIMETYPE_ID + "," 2392 + Data.IS_PRIMARY + "," 2393 + Data.DATA1 + "," 2394 + Data.DATA2 + "," 2395 + Data.DATA3 + "," 2396 + Data.DATA4 + "," 2397 + Data.DATA5 + "," 2398 + Data.DATA6 + "," 2399 + Data.DATA7 + "," 2400 + Data.DATA8 + "," 2401 + Data.DATA9 + "," 2402 + Data.DATA10 + "," 2403 + Data.DATA11 + 2404 " FROM " + Tables.DATA + 2405 " WHERE " + Data.RAW_CONTACT_ID + "=?" + 2406 " AND (" + Data.DATA1 + " NOT NULL OR " + 2407 Organization.TITLE + " NOT NULL)"; 2408 2409 public static final int MIMETYPE = 0; 2410 public static final int IS_PRIMARY = 1; 2411 public static final int DATA1 = 2; 2412 public static final int GIVEN_NAME = 3; // data2 2413 public static final int FAMILY_NAME = 4; // data3 2414 public static final int PREFIX = 5; // data4 2415 public static final int TITLE = 5; // data4 2416 public static final int MIDDLE_NAME = 6; // data5 2417 public static final int SUFFIX = 7; // data6 2418 public static final int PHONETIC_GIVEN_NAME = 8; // data7 2419 public static final int PHONETIC_MIDDLE_NAME = 9; // data8 2420 public static final int ORGANIZATION_PHONETIC_NAME = 9; // data8 2421 public static final int PHONETIC_FAMILY_NAME = 10; // data9 2422 public static final int FULL_NAME_STYLE = 11; // data10 2423 public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11; // data10 2424 public static final int PHONETIC_NAME_STYLE = 12; // data11 2425 } 2426 2427 /** 2428 * Updates a raw contact display name based on data rows, e.g. structured name, 2429 * organization, email etc. 2430 */ 2431 private void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 2432 int bestDisplayNameSource = DisplayNameSources.UNDEFINED; 2433 NameSplitter.Name bestName = null; 2434 String bestDisplayName = null; 2435 String bestPhoneticName = null; 2436 int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2437 2438 mSelectionArgs1[0] = String.valueOf(rawContactId); 2439 Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1); 2440 try { 2441 while (c.moveToNext()) { 2442 int mimeType = c.getInt(DisplayNameQuery.MIMETYPE); 2443 int source = getDisplayNameSource(mimeType); 2444 if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) { 2445 continue; 2446 } 2447 2448 if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) { 2449 continue; 2450 } 2451 2452 if (mimeType == mMimeTypeIdStructuredName) { 2453 NameSplitter.Name name; 2454 if (bestName != null) { 2455 name = new NameSplitter.Name(); 2456 } else { 2457 name = mName; 2458 name.clear(); 2459 } 2460 name.prefix = c.getString(DisplayNameQuery.PREFIX); 2461 name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME); 2462 name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME); 2463 name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME); 2464 name.suffix = c.getString(DisplayNameQuery.SUFFIX); 2465 name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE) 2466 ? FullNameStyle.UNDEFINED 2467 : c.getInt(DisplayNameQuery.FULL_NAME_STYLE); 2468 name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME); 2469 name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME); 2470 name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME); 2471 name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE) 2472 ? PhoneticNameStyle.UNDEFINED 2473 : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE); 2474 if (!name.isEmpty()) { 2475 bestDisplayNameSource = source; 2476 bestName = name; 2477 } 2478 } else if (mimeType == mMimeTypeIdOrganization) { 2479 mCharArrayBuffer.sizeCopied = 0; 2480 c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer); 2481 if (mCharArrayBuffer.sizeCopied != 0) { 2482 bestDisplayNameSource = source; 2483 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2484 mCharArrayBuffer.sizeCopied); 2485 bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME); 2486 bestPhoneticNameStyle = 2487 c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE) 2488 ? PhoneticNameStyle.UNDEFINED 2489 : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE); 2490 } else { 2491 c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer); 2492 if (mCharArrayBuffer.sizeCopied != 0) { 2493 bestDisplayNameSource = source; 2494 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2495 mCharArrayBuffer.sizeCopied); 2496 bestPhoneticName = null; 2497 bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2498 } 2499 } 2500 } else { 2501 // Display name is at DATA1 in all other types. 2502 // This is ensured in the constructor. 2503 2504 mCharArrayBuffer.sizeCopied = 0; 2505 c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer); 2506 if (mCharArrayBuffer.sizeCopied != 0) { 2507 bestDisplayNameSource = source; 2508 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2509 mCharArrayBuffer.sizeCopied); 2510 bestPhoneticName = null; 2511 bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2512 } 2513 } 2514 } 2515 2516 } finally { 2517 c.close(); 2518 } 2519 2520 String displayNamePrimary; 2521 String displayNameAlternative; 2522 String sortKeyPrimary = null; 2523 String sortKeyAlternative = null; 2524 int displayNameStyle = FullNameStyle.UNDEFINED; 2525 2526 if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) { 2527 displayNameStyle = bestName.fullNameStyle; 2528 if (displayNameStyle == FullNameStyle.CJK 2529 || displayNameStyle == FullNameStyle.UNDEFINED) { 2530 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle); 2531 bestName.fullNameStyle = displayNameStyle; 2532 } 2533 2534 displayNamePrimary = mNameSplitter.join(bestName, true); 2535 displayNameAlternative = mNameSplitter.join(bestName, false); 2536 2537 bestPhoneticName = mNameSplitter.joinPhoneticName(bestName); 2538 bestPhoneticNameStyle = bestName.phoneticNameStyle; 2539 } else { 2540 displayNamePrimary = displayNameAlternative = bestDisplayName; 2541 } 2542 2543 if (bestPhoneticName != null) { 2544 sortKeyPrimary = sortKeyAlternative = bestPhoneticName; 2545 if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) { 2546 bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName); 2547 } 2548 } else { 2549 if (displayNameStyle == FullNameStyle.UNDEFINED) { 2550 displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName); 2551 if (displayNameStyle == FullNameStyle.UNDEFINED 2552 || displayNameStyle == FullNameStyle.CJK) { 2553 displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle( 2554 displayNameStyle, bestPhoneticNameStyle); 2555 } 2556 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle); 2557 } 2558 if (displayNameStyle == FullNameStyle.CHINESE) { 2559 sortKeyPrimary = sortKeyAlternative = 2560 mNameSplitter.convertHanziToPinyin(displayNamePrimary); 2561 } 2562 } 2563 2564 if (sortKeyPrimary == null) { 2565 sortKeyPrimary = displayNamePrimary; 2566 sortKeyAlternative = displayNameAlternative; 2567 } 2568 2569 setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary, 2570 displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle, 2571 sortKeyPrimary, sortKeyAlternative); 2572 } 2573 2574 private int getDisplayNameSource(int mimeTypeId) { 2575 if (mimeTypeId == mMimeTypeIdStructuredName) { 2576 return DisplayNameSources.STRUCTURED_NAME; 2577 } else if (mimeTypeId == mMimeTypeIdEmail) { 2578 return DisplayNameSources.EMAIL; 2579 } else if (mimeTypeId == mMimeTypeIdPhone) { 2580 return DisplayNameSources.PHONE; 2581 } else if (mimeTypeId == mMimeTypeIdOrganization) { 2582 return DisplayNameSources.ORGANIZATION; 2583 } else if (mimeTypeId == mMimeTypeIdNickname) { 2584 return DisplayNameSources.NICKNAME; 2585 } else { 2586 return DisplayNameSources.UNDEFINED; 2587 } 2588 } 2589 2590 /** 2591 * Delete data row by row so that fixing of primaries etc work correctly. 2592 */ 2593 private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { 2594 int count = 0; 2595 2596 // Note that the query will return data according to the access restrictions, 2597 // so we don't need to worry about deleting data we don't have permission to read. 2598 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null); 2599 try { 2600 while(c.moveToNext()) { 2601 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 2602 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 2603 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2604 count += rowHandler.delete(mDb, c); 2605 if (!callerIsSyncAdapter) { 2606 setRawContactDirty(rawContactId); 2607 if (rowHandler.isAggregationRequired()) { 2608 triggerAggregation(rawContactId); 2609 } 2610 } 2611 } 2612 } finally { 2613 c.close(); 2614 } 2615 2616 return count; 2617 } 2618 2619 /** 2620 * Delete a data row provided that it is one of the allowed mime types. 2621 */ 2622 public int deleteData(long dataId, String[] allowedMimeTypes) { 2623 2624 // Note that the query will return data according to the access restrictions, 2625 // so we don't need to worry about deleting data we don't have permission to read. 2626 mSelectionArgs1[0] = String.valueOf(dataId); 2627 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?", 2628 mSelectionArgs1, null); 2629 2630 try { 2631 if (!c.moveToFirst()) { 2632 return 0; 2633 } 2634 2635 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 2636 boolean valid = false; 2637 for (int i = 0; i < allowedMimeTypes.length; i++) { 2638 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 2639 valid = true; 2640 break; 2641 } 2642 } 2643 2644 if (!valid) { 2645 throw new IllegalArgumentException("Data type mismatch: expected " 2646 + Lists.newArrayList(allowedMimeTypes)); 2647 } 2648 2649 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2650 int count = rowHandler.delete(mDb, c); 2651 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 2652 if (rowHandler.isAggregationRequired()) { 2653 triggerAggregation(rawContactId); 2654 } 2655 return count; 2656 } finally { 2657 c.close(); 2658 } 2659 } 2660 2661 /** 2662 * Inserts an item in the groups table 2663 */ 2664 private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 2665 mValues.clear(); 2666 mValues.putAll(values); 2667 2668 if (!resolveAccount(uri, mValues)) { 2669 return -1; 2670 } 2671 2672 // Replace package with internal mapping 2673 final String packageName = mValues.getAsString(Groups.RES_PACKAGE); 2674 if (packageName != null) { 2675 mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2676 } 2677 mValues.remove(Groups.RES_PACKAGE); 2678 2679 if (!callerIsSyncAdapter) { 2680 mValues.put(Groups.DIRTY, 1); 2681 } 2682 2683 long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues); 2684 2685 if (mValues.containsKey(Groups.GROUP_VISIBLE)) { 2686 mVisibleTouched = true; 2687 } 2688 2689 return result; 2690 } 2691 2692 private long insertSettings(Uri uri, ContentValues values) { 2693 final long id = mDb.insert(Tables.SETTINGS, null, values); 2694 2695 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2696 mVisibleTouched = true; 2697 } 2698 2699 return id; 2700 } 2701 2702 /** 2703 * Inserts a status update. 2704 */ 2705 public long insertStatusUpdate(ContentValues values) { 2706 final String handle = values.getAsString(StatusUpdates.IM_HANDLE); 2707 final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL); 2708 String customProtocol = null; 2709 2710 if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) { 2711 customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL); 2712 if (TextUtils.isEmpty(customProtocol)) { 2713 throw new IllegalArgumentException( 2714 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 2715 } 2716 } 2717 2718 long rawContactId = -1; 2719 long contactId = -1; 2720 Long dataId = values.getAsLong(StatusUpdates.DATA_ID); 2721 mSb.setLength(0); 2722 if (dataId != null) { 2723 // Lookup the contact info for the given data row. 2724 2725 mSb.append(Tables.DATA + "." + Data._ID + "="); 2726 mSb.append(dataId); 2727 } else { 2728 // Lookup the data row to attach this presence update to 2729 2730 if (TextUtils.isEmpty(handle) || protocol == null) { 2731 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 2732 } 2733 2734 // TODO: generalize to allow other providers to match against email 2735 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 2736 2737 if (matchEmail) { 2738 2739 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise 2740 // the "OR" conjunction confuses it and it switches to a full scan of 2741 // the raw_contacts table. 2742 2743 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same 2744 // column - Data.DATA1 2745 mSb.append(DataColumns.MIMETYPE_ID + " IN (") 2746 .append(mMimeTypeIdEmail) 2747 .append(",") 2748 .append(mMimeTypeIdIm) 2749 .append(")" + " AND " + Data.DATA1 + "="); 2750 DatabaseUtils.appendEscapedSQLString(mSb, handle); 2751 mSb.append(" AND ((" + DataColumns.MIMETYPE_ID + "=") 2752 .append(mMimeTypeIdIm) 2753 .append(" AND " + Im.PROTOCOL + "=") 2754 .append(protocol); 2755 if (customProtocol != null) { 2756 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "="); 2757 DatabaseUtils.appendEscapedSQLString(mSb, customProtocol); 2758 } 2759 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=") 2760 .append(mMimeTypeIdEmail) 2761 .append("))"); 2762 } else { 2763 mSb.append(DataColumns.MIMETYPE_ID + "=") 2764 .append(mMimeTypeIdIm) 2765 .append(" AND " + Im.PROTOCOL + "=") 2766 .append(protocol) 2767 .append(" AND " + Im.DATA + "="); 2768 DatabaseUtils.appendEscapedSQLString(mSb, handle); 2769 if (customProtocol != null) { 2770 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "="); 2771 DatabaseUtils.appendEscapedSQLString(mSb, customProtocol); 2772 } 2773 } 2774 2775 if (values.containsKey(StatusUpdates.DATA_ID)) { 2776 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=") 2777 .append(values.getAsLong(StatusUpdates.DATA_ID)); 2778 } 2779 } 2780 mSb.append(" AND ").append(getContactsRestrictions()); 2781 2782 Cursor cursor = null; 2783 try { 2784 cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 2785 mSb.toString(), null, null, null, 2786 Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID); 2787 if (cursor.moveToFirst()) { 2788 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 2789 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 2790 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 2791 } else { 2792 // No contact found, return a null URI 2793 return -1; 2794 } 2795 } finally { 2796 if (cursor != null) { 2797 cursor.close(); 2798 } 2799 } 2800 2801 if (values.containsKey(StatusUpdates.PRESENCE)) { 2802 if (customProtocol == null) { 2803 // We cannot allow a null in the custom protocol field, because SQLite3 does not 2804 // properly enforce uniqueness of null values 2805 customProtocol = ""; 2806 } 2807 2808 mValues.clear(); 2809 mValues.put(StatusUpdates.DATA_ID, dataId); 2810 mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 2811 mValues.put(PresenceColumns.CONTACT_ID, contactId); 2812 mValues.put(StatusUpdates.PROTOCOL, protocol); 2813 mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol); 2814 mValues.put(StatusUpdates.IM_HANDLE, handle); 2815 if (values.containsKey(StatusUpdates.IM_ACCOUNT)) { 2816 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT)); 2817 } 2818 mValues.put(StatusUpdates.PRESENCE, 2819 values.getAsString(StatusUpdates.PRESENCE)); 2820 2821 // Insert the presence update 2822 mDb.replace(Tables.PRESENCE, null, mValues); 2823 } 2824 2825 2826 if (values.containsKey(StatusUpdates.STATUS)) { 2827 String status = values.getAsString(StatusUpdates.STATUS); 2828 String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE); 2829 Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL); 2830 2831 if (TextUtils.isEmpty(resPackage) 2832 && (labelResource == null || labelResource == 0) 2833 && protocol != null) { 2834 labelResource = Im.getProtocolLabelResource(protocol); 2835 } 2836 2837 Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON); 2838 // TODO compute the default icon based on the protocol 2839 2840 if (TextUtils.isEmpty(status)) { 2841 mStatusUpdateDelete.bindLong(1, dataId); 2842 mStatusUpdateDelete.execute(); 2843 } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) { 2844 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP); 2845 mStatusUpdateReplace.bindLong(1, dataId); 2846 mStatusUpdateReplace.bindLong(2, timestamp); 2847 bindString(mStatusUpdateReplace, 3, status); 2848 bindString(mStatusUpdateReplace, 4, resPackage); 2849 bindLong(mStatusUpdateReplace, 5, iconResource); 2850 bindLong(mStatusUpdateReplace, 6, labelResource); 2851 mStatusUpdateReplace.execute(); 2852 } else { 2853 2854 try { 2855 mStatusUpdateInsert.bindLong(1, dataId); 2856 bindString(mStatusUpdateInsert, 2, status); 2857 bindString(mStatusUpdateInsert, 3, resPackage); 2858 bindLong(mStatusUpdateInsert, 4, iconResource); 2859 bindLong(mStatusUpdateInsert, 5, labelResource); 2860 mStatusUpdateInsert.executeInsert(); 2861 } catch (SQLiteConstraintException e) { 2862 // The row already exists - update it 2863 long timestamp = System.currentTimeMillis(); 2864 mStatusUpdateAutoTimestamp.bindLong(1, timestamp); 2865 bindString(mStatusUpdateAutoTimestamp, 2, status); 2866 mStatusUpdateAutoTimestamp.bindLong(3, dataId); 2867 bindString(mStatusUpdateAutoTimestamp, 4, status); 2868 mStatusUpdateAutoTimestamp.execute(); 2869 2870 bindString(mStatusAttributionUpdate, 1, resPackage); 2871 bindLong(mStatusAttributionUpdate, 2, iconResource); 2872 bindLong(mStatusAttributionUpdate, 3, labelResource); 2873 mStatusAttributionUpdate.bindLong(4, dataId); 2874 mStatusAttributionUpdate.execute(); 2875 } 2876 } 2877 } 2878 2879 if (contactId != -1) { 2880 mLastStatusUpdate.bindLong(1, contactId); 2881 mLastStatusUpdate.bindLong(2, contactId); 2882 mLastStatusUpdate.execute(); 2883 } 2884 2885 return dataId; 2886 } 2887 2888 @Override 2889 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 2890 if (VERBOSE_LOGGING) { 2891 Log.v(TAG, "deleteInTransaction: " + uri); 2892 } 2893 flushTransactionalChanges(); 2894 final boolean callerIsSyncAdapter = 2895 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2896 final int match = sUriMatcher.match(uri); 2897 switch (match) { 2898 case SYNCSTATE: 2899 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 2900 2901 case SYNCSTATE_ID: 2902 String selectionWithId = 2903 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 2904 + (selection == null ? "" : " AND (" + selection + ")"); 2905 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); 2906 2907 case CONTACTS: { 2908 // TODO 2909 return 0; 2910 } 2911 2912 case CONTACTS_ID: { 2913 long contactId = ContentUris.parseId(uri); 2914 return deleteContact(contactId); 2915 } 2916 2917 case CONTACTS_LOOKUP: 2918 case CONTACTS_LOOKUP_ID: { 2919 final List<String> pathSegments = uri.getPathSegments(); 2920 final int segmentCount = pathSegments.size(); 2921 if (segmentCount < 3) { 2922 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 2923 } 2924 final String lookupKey = pathSegments.get(2); 2925 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 2926 return deleteContact(contactId); 2927 } 2928 2929 case RAW_CONTACTS: { 2930 int numDeletes = 0; 2931 Cursor c = mDb.query(Tables.RAW_CONTACTS, 2932 new String[]{RawContacts._ID, RawContacts.CONTACT_ID}, 2933 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2934 try { 2935 while (c.moveToNext()) { 2936 final long rawContactId = c.getLong(0); 2937 long contactId = c.getLong(1); 2938 numDeletes += deleteRawContact(rawContactId, contactId, 2939 callerIsSyncAdapter); 2940 } 2941 } finally { 2942 c.close(); 2943 } 2944 return numDeletes; 2945 } 2946 2947 case RAW_CONTACTS_ID: { 2948 final long rawContactId = ContentUris.parseId(uri); 2949 return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId), 2950 callerIsSyncAdapter); 2951 } 2952 2953 case DATA: { 2954 mSyncToNetwork |= !callerIsSyncAdapter; 2955 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 2956 callerIsSyncAdapter); 2957 } 2958 2959 case DATA_ID: 2960 case PHONES_ID: 2961 case EMAILS_ID: 2962 case POSTALS_ID: { 2963 long dataId = ContentUris.parseId(uri); 2964 mSyncToNetwork |= !callerIsSyncAdapter; 2965 mSelectionArgs1[0] = String.valueOf(dataId); 2966 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); 2967 } 2968 2969 case GROUPS_ID: { 2970 mSyncToNetwork |= !callerIsSyncAdapter; 2971 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter); 2972 } 2973 2974 case GROUPS: { 2975 int numDeletes = 0; 2976 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 2977 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2978 try { 2979 while (c.moveToNext()) { 2980 numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter); 2981 } 2982 } finally { 2983 c.close(); 2984 } 2985 if (numDeletes > 0) { 2986 mSyncToNetwork |= !callerIsSyncAdapter; 2987 } 2988 return numDeletes; 2989 } 2990 2991 case SETTINGS: { 2992 mSyncToNetwork |= !callerIsSyncAdapter; 2993 return deleteSettings(uri, selection, selectionArgs); 2994 } 2995 2996 case STATUS_UPDATES: { 2997 return deleteStatusUpdates(selection, selectionArgs); 2998 } 2999 3000 default: { 3001 mSyncToNetwork = true; 3002 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 3003 } 3004 } 3005 } 3006 3007 public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) { 3008 mGroupIdCache.clear(); 3009 final long groupMembershipMimetypeId = mDbHelper 3010 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 3011 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 3012 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 3013 + groupId, null); 3014 3015 try { 3016 if (callerIsSyncAdapter) { 3017 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 3018 } else { 3019 mValues.clear(); 3020 mValues.put(Groups.DELETED, 1); 3021 mValues.put(Groups.DIRTY, 1); 3022 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 3023 } 3024 } finally { 3025 mVisibleTouched = true; 3026 } 3027 } 3028 3029 private int deleteSettings(Uri uri, String selection, String[] selectionArgs) { 3030 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 3031 mVisibleTouched = true; 3032 return count; 3033 } 3034 3035 private int deleteContact(long contactId) { 3036 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 3037 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null); 3038 try { 3039 while (c.moveToNext()) { 3040 long rawContactId = c.getLong(0); 3041 markRawContactAsDeleted(rawContactId); 3042 } 3043 } finally { 3044 c.close(); 3045 } 3046 3047 return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); 3048 } 3049 3050 public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) { 3051 mContactAggregator.invalidateAggregationExceptionCache(); 3052 if (callerIsSyncAdapter) { 3053 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 3054 int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 3055 mContactAggregator.updateDisplayNameForContact(mDb, contactId); 3056 return count; 3057 } else { 3058 mDbHelper.removeContactIfSingleton(rawContactId); 3059 return markRawContactAsDeleted(rawContactId); 3060 } 3061 } 3062 3063 private int deleteStatusUpdates(String selection, String[] selectionArgs) { 3064 // delete from both tables: presence and status_updates 3065 // TODO should account type/name be appended to the where clause? 3066 if (VERBOSE_LOGGING) { 3067 Log.v(TAG, "deleting data from status_updates for " + selection); 3068 } 3069 mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection), 3070 selectionArgs); 3071 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 3072 } 3073 3074 private int markRawContactAsDeleted(long rawContactId) { 3075 mSyncToNetwork = true; 3076 3077 mValues.clear(); 3078 mValues.put(RawContacts.DELETED, 1); 3079 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 3080 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 3081 mValues.putNull(RawContacts.CONTACT_ID); 3082 mValues.put(RawContacts.DIRTY, 1); 3083 return updateRawContact(rawContactId, mValues); 3084 } 3085 3086 @Override 3087 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 3088 String[] selectionArgs) { 3089 if (VERBOSE_LOGGING) { 3090 Log.v(TAG, "updateInTransaction: " + uri); 3091 } 3092 3093 int count = 0; 3094 3095 final int match = sUriMatcher.match(uri); 3096 if (match == SYNCSTATE_ID && selection == null) { 3097 long rowId = ContentUris.parseId(uri); 3098 Object data = values.get(ContactsContract.SyncState.DATA); 3099 mUpdatedSyncStates.put(rowId, data); 3100 return 1; 3101 } 3102 flushTransactionalChanges(); 3103 final boolean callerIsSyncAdapter = 3104 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3105 switch(match) { 3106 case SYNCSTATE: 3107 return mDbHelper.getSyncState().update(mDb, values, 3108 appendAccountToSelection(uri, selection), selectionArgs); 3109 3110 case SYNCSTATE_ID: { 3111 selection = appendAccountToSelection(uri, selection); 3112 String selectionWithId = 3113 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3114 + (selection == null ? "" : " AND (" + selection + ")"); 3115 return mDbHelper.getSyncState().update(mDb, values, 3116 selectionWithId, selectionArgs); 3117 } 3118 3119 case CONTACTS: { 3120 count = updateContactOptions(values, selection, selectionArgs); 3121 break; 3122 } 3123 3124 case CONTACTS_ID: { 3125 count = updateContactOptions(ContentUris.parseId(uri), values); 3126 break; 3127 } 3128 3129 case CONTACTS_LOOKUP: 3130 case CONTACTS_LOOKUP_ID: { 3131 final List<String> pathSegments = uri.getPathSegments(); 3132 final int segmentCount = pathSegments.size(); 3133 if (segmentCount < 3) { 3134 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 3135 } 3136 final String lookupKey = pathSegments.get(2); 3137 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 3138 count = updateContactOptions(contactId, values); 3139 break; 3140 } 3141 3142 case RAW_CONTACTS_DATA: { 3143 final String rawContactId = uri.getPathSegments().get(1); 3144 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") 3145 + (selection == null ? "" : " AND " + selection); 3146 3147 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); 3148 3149 break; 3150 } 3151 3152 case DATA: { 3153 count = updateData(uri, values, appendAccountToSelection(uri, selection), 3154 selectionArgs, callerIsSyncAdapter); 3155 if (count > 0) { 3156 mSyncToNetwork |= !callerIsSyncAdapter; 3157 } 3158 break; 3159 } 3160 3161 case DATA_ID: 3162 case PHONES_ID: 3163 case EMAILS_ID: 3164 case POSTALS_ID: { 3165 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); 3166 if (count > 0) { 3167 mSyncToNetwork |= !callerIsSyncAdapter; 3168 } 3169 break; 3170 } 3171 3172 case RAW_CONTACTS: { 3173 selection = appendAccountToSelection(uri, selection); 3174 count = updateRawContacts(values, selection, selectionArgs); 3175 break; 3176 } 3177 3178 case RAW_CONTACTS_ID: { 3179 long rawContactId = ContentUris.parseId(uri); 3180 if (selection != null) { 3181 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3182 count = updateRawContacts(values, RawContacts._ID + "=?" 3183 + " AND(" + selection + ")", selectionArgs); 3184 } else { 3185 mSelectionArgs1[0] = String.valueOf(rawContactId); 3186 count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1); 3187 } 3188 break; 3189 } 3190 3191 case GROUPS: { 3192 count = updateGroups(uri, values, appendAccountToSelection(uri, selection), 3193 selectionArgs, callerIsSyncAdapter); 3194 if (count > 0) { 3195 mSyncToNetwork |= !callerIsSyncAdapter; 3196 } 3197 break; 3198 } 3199 3200 case GROUPS_ID: { 3201 long groupId = ContentUris.parseId(uri); 3202 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); 3203 String selectionWithId = Groups._ID + "=? " 3204 + (selection == null ? "" : " AND " + selection); 3205 count = updateGroups(uri, values, selectionWithId, selectionArgs, 3206 callerIsSyncAdapter); 3207 if (count > 0) { 3208 mSyncToNetwork |= !callerIsSyncAdapter; 3209 } 3210 break; 3211 } 3212 3213 case AGGREGATION_EXCEPTIONS: { 3214 count = updateAggregationException(mDb, values); 3215 break; 3216 } 3217 3218 case SETTINGS: { 3219 count = updateSettings(uri, values, selection, selectionArgs); 3220 mSyncToNetwork |= !callerIsSyncAdapter; 3221 break; 3222 } 3223 3224 case STATUS_UPDATES: { 3225 count = updateStatusUpdate(uri, values, selection, selectionArgs); 3226 break; 3227 } 3228 3229 default: { 3230 mSyncToNetwork = true; 3231 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 3232 } 3233 } 3234 3235 return count; 3236 } 3237 3238 private int updateStatusUpdate(Uri uri, ContentValues values, String selection, 3239 String[] selectionArgs) { 3240 // update status_updates table, if status is provided 3241 // TODO should account type/name be appended to the where clause? 3242 int updateCount = 0; 3243 ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values); 3244 if (settableValues.size() > 0) { 3245 updateCount = mDb.update(Tables.STATUS_UPDATES, 3246 settableValues, 3247 getWhereClauseForStatusUpdatesTable(selection), 3248 selectionArgs); 3249 } 3250 3251 // now update the Presence table 3252 settableValues = getSettableColumnsForPresenceTable(values); 3253 if (settableValues.size() > 0) { 3254 updateCount = mDb.update(Tables.PRESENCE, settableValues, 3255 selection, selectionArgs); 3256 } 3257 // TODO updateCount is not entirely a valid count of updated rows because 2 tables could 3258 // potentially get updated in this method. 3259 return updateCount; 3260 } 3261 3262 /** 3263 * Build a where clause to select the rows to be updated in status_updates table. 3264 */ 3265 private String getWhereClauseForStatusUpdatesTable(String selection) { 3266 mSb.setLength(0); 3267 mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE); 3268 mSb.append(selection); 3269 mSb.append(")"); 3270 return mSb.toString(); 3271 } 3272 3273 private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) { 3274 mValues.clear(); 3275 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values, 3276 StatusUpdates.STATUS); 3277 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values, 3278 StatusUpdates.STATUS_TIMESTAMP); 3279 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values, 3280 StatusUpdates.STATUS_RES_PACKAGE); 3281 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values, 3282 StatusUpdates.STATUS_LABEL); 3283 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values, 3284 StatusUpdates.STATUS_ICON); 3285 return mValues; 3286 } 3287 3288 private ContentValues getSettableColumnsForPresenceTable(ContentValues values) { 3289 mValues.clear(); 3290 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values, 3291 StatusUpdates.PRESENCE); 3292 return mValues; 3293 } 3294 3295 private int updateGroups(Uri uri, ContentValues values, String selectionWithId, 3296 String[] selectionArgs, boolean callerIsSyncAdapter) { 3297 3298 mGroupIdCache.clear(); 3299 3300 ContentValues updatedValues; 3301 if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) { 3302 updatedValues = mValues; 3303 updatedValues.clear(); 3304 updatedValues.putAll(values); 3305 updatedValues.put(Groups.DIRTY, 1); 3306 } else { 3307 updatedValues = values; 3308 } 3309 3310 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 3311 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 3312 mVisibleTouched = true; 3313 } 3314 if (updatedValues.containsKey(Groups.SHOULD_SYNC) 3315 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) { 3316 final long groupId = ContentUris.parseId(uri); 3317 mSelectionArgs1[0] = String.valueOf(groupId); 3318 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME, 3319 Groups.ACCOUNT_TYPE}, Groups._ID + "=?", mSelectionArgs1, null, 3320 null, null); 3321 String accountName; 3322 String accountType; 3323 try { 3324 while (c.moveToNext()) { 3325 accountName = c.getString(0); 3326 accountType = c.getString(1); 3327 if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 3328 Account account = new Account(accountName, accountType); 3329 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, 3330 new Bundle()); 3331 break; 3332 } 3333 } 3334 } finally { 3335 c.close(); 3336 } 3337 } 3338 return count; 3339 } 3340 3341 private int updateSettings(Uri uri, ContentValues values, String selection, 3342 String[] selectionArgs) { 3343 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 3344 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 3345 mVisibleTouched = true; 3346 } 3347 return count; 3348 } 3349 3350 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) { 3351 if (values.containsKey(RawContacts.CONTACT_ID)) { 3352 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 3353 "in content values. Contact IDs are assigned automatically"); 3354 } 3355 3356 int count = 0; 3357 Cursor cursor = mDb.query(mDbHelper.getRawContactView(), 3358 new String[] { RawContacts._ID }, selection, 3359 selectionArgs, null, null, null); 3360 try { 3361 while (cursor.moveToNext()) { 3362 long rawContactId = cursor.getLong(0); 3363 updateRawContact(rawContactId, values); 3364 count++; 3365 } 3366 } finally { 3367 cursor.close(); 3368 } 3369 3370 return count; 3371 } 3372 3373 private int updateRawContact(long rawContactId, ContentValues values) { 3374 final String selection = RawContacts._ID + " = " + rawContactId; 3375 final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED) 3376 && values.getAsInteger(RawContacts.DELETED) == 0); 3377 int previousDeleted = 0; 3378 String accountType = null; 3379 String accountName = null; 3380 if (requestUndoDelete) { 3381 Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection, 3382 null, null, null, null); 3383 try { 3384 if (cursor.moveToFirst()) { 3385 previousDeleted = cursor.getInt(RawContactsQuery.DELETED); 3386 accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE); 3387 accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME); 3388 } 3389 } finally { 3390 cursor.close(); 3391 } 3392 values.put(ContactsContract.RawContacts.AGGREGATION_MODE, 3393 ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT); 3394 } 3395 int count = mDb.update(Tables.RAW_CONTACTS, values, selection, null); 3396 if (count != 0) { 3397 if (values.containsKey(RawContacts.STARRED)) { 3398 mContactAggregator.updateStarred(rawContactId); 3399 } 3400 if (values.containsKey(RawContacts.SOURCE_ID)) { 3401 mContactAggregator.updateLookupKey(mDb, rawContactId); 3402 } 3403 if (requestUndoDelete && previousDeleted == 1) { 3404 // undo delete, needs aggregation again. 3405 mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType)); 3406 } 3407 } 3408 return count; 3409 } 3410 3411 private int updateData(Uri uri, ContentValues values, String selection, 3412 String[] selectionArgs, boolean callerIsSyncAdapter) { 3413 mValues.clear(); 3414 mValues.putAll(values); 3415 mValues.remove(Data._ID); 3416 mValues.remove(Data.RAW_CONTACT_ID); 3417 mValues.remove(Data.MIMETYPE); 3418 3419 String packageName = values.getAsString(Data.RES_PACKAGE); 3420 if (packageName != null) { 3421 mValues.remove(Data.RES_PACKAGE); 3422 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 3423 } 3424 3425 boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY); 3426 boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY); 3427 3428 // Remove primary or super primary values being set to 0. This is disallowed by the 3429 // content provider. 3430 if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { 3431 containsIsSuperPrimary = false; 3432 mValues.remove(Data.IS_SUPER_PRIMARY); 3433 } 3434 if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) { 3435 containsIsPrimary = false; 3436 mValues.remove(Data.IS_PRIMARY); 3437 } 3438 3439 int count = 0; 3440 3441 // Note that the query will return data according to the access restrictions, 3442 // so we don't need to worry about updating data we don't have permission to read. 3443 Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null); 3444 try { 3445 while(c.moveToNext()) { 3446 count += updateData(mValues, c, callerIsSyncAdapter); 3447 } 3448 } finally { 3449 c.close(); 3450 } 3451 3452 return count; 3453 } 3454 3455 private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { 3456 if (values.size() == 0) { 3457 return 0; 3458 } 3459 3460 final String mimeType = c.getString(DataUpdateQuery.MIMETYPE); 3461 DataRowHandler rowHandler = getDataRowHandler(mimeType); 3462 rowHandler.update(mDb, values, c, callerIsSyncAdapter); 3463 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 3464 if (rowHandler.isAggregationRequired()) { 3465 triggerAggregation(rawContactId); 3466 } 3467 3468 return 1; 3469 } 3470 3471 private int updateContactOptions(ContentValues values, String selection, 3472 String[] selectionArgs) { 3473 int count = 0; 3474 Cursor cursor = mDb.query(mDbHelper.getContactView(), 3475 new String[] { Contacts._ID }, selection, 3476 selectionArgs, null, null, null); 3477 try { 3478 while (cursor.moveToNext()) { 3479 long contactId = cursor.getLong(0); 3480 updateContactOptions(contactId, values); 3481 count++; 3482 } 3483 } finally { 3484 cursor.close(); 3485 } 3486 3487 return count; 3488 } 3489 3490 private int updateContactOptions(long contactId, ContentValues values) { 3491 3492 mValues.clear(); 3493 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 3494 values, Contacts.CUSTOM_RINGTONE); 3495 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 3496 values, Contacts.SEND_TO_VOICEMAIL); 3497 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 3498 values, Contacts.LAST_TIME_CONTACTED); 3499 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 3500 values, Contacts.TIMES_CONTACTED); 3501 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 3502 values, Contacts.STARRED); 3503 3504 // Nothing to update - just return 3505 if (mValues.size() == 0) { 3506 return 0; 3507 } 3508 3509 if (mValues.containsKey(RawContacts.STARRED)) { 3510 // Mark dirty when changing starred to trigger sync 3511 mValues.put(RawContacts.DIRTY, 1); 3512 } 3513 3514 mSelectionArgs1[0] = String.valueOf(contactId); 3515 mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1); 3516 3517 // Copy changeable values to prevent automatically managed fields from 3518 // being explicitly updated by clients. 3519 mValues.clear(); 3520 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 3521 values, Contacts.CUSTOM_RINGTONE); 3522 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 3523 values, Contacts.SEND_TO_VOICEMAIL); 3524 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 3525 values, Contacts.LAST_TIME_CONTACTED); 3526 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 3527 values, Contacts.TIMES_CONTACTED); 3528 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 3529 values, Contacts.STARRED); 3530 3531 int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1); 3532 3533 if (values.containsKey(Contacts.LAST_TIME_CONTACTED) && 3534 !values.containsKey(Contacts.TIMES_CONTACTED)) { 3535 mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1); 3536 mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1); 3537 } 3538 return rslt; 3539 } 3540 3541 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 3542 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 3543 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 3544 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 3545 3546 long rawContactId1, rawContactId2; 3547 if (rcId1 < rcId2) { 3548 rawContactId1 = rcId1; 3549 rawContactId2 = rcId2; 3550 } else { 3551 rawContactId2 = rcId1; 3552 rawContactId1 = rcId2; 3553 } 3554 3555 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 3556 mSelectionArgs2[0] = String.valueOf(rawContactId1); 3557 mSelectionArgs2[1] = String.valueOf(rawContactId2); 3558 db.delete(Tables.AGGREGATION_EXCEPTIONS, 3559 AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " 3560 + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2); 3561 } else { 3562 ContentValues exceptionValues = new ContentValues(3); 3563 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 3564 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 3565 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 3566 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 3567 exceptionValues); 3568 } 3569 3570 mContactAggregator.invalidateAggregationExceptionCache(); 3571 mContactAggregator.markForAggregation(rawContactId1); 3572 mContactAggregator.markForAggregation(rawContactId2); 3573 3574 long contactId1 = mDbHelper.getContactId(rawContactId1); 3575 mContactAggregator.aggregateContact(db, rawContactId1, contactId1); 3576 3577 long contactId2 = mDbHelper.getContactId(rawContactId2); 3578 mContactAggregator.aggregateContact(db, rawContactId2, contactId2); 3579 3580 // The return value is fake - we just confirm that we made a change, not count actual 3581 // rows changed. 3582 return 1; 3583 } 3584 3585 public void onAccountsUpdated(Account[] accounts) { 3586 mDb = mDbHelper.getWritableDatabase(); 3587 if (mDb == null) return; 3588 3589 HashSet<Account> existingAccounts = new HashSet<Account>(); 3590 boolean hasUnassignedContacts[] = new boolean[]{false}; 3591 mDb.beginTransaction(); 3592 try { 3593 findValidAccounts(existingAccounts, hasUnassignedContacts, 3594 Tables.RAW_CONTACTS, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE); 3595 findValidAccounts(existingAccounts, hasUnassignedContacts, 3596 Tables.GROUPS, Groups.ACCOUNT_NAME, Groups.ACCOUNT_TYPE); 3597 findValidAccounts(existingAccounts, hasUnassignedContacts, 3598 Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE); 3599 3600 // Remove all valid accounts from the existing account set. What is left 3601 // in the existingAccounts set will be extra accounts whose data must be deleted. 3602 HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts); 3603 for (Account account : accounts) { 3604 accountsToDelete.remove(account); 3605 } 3606 3607 for (Account account : accountsToDelete) { 3608 Log.d(TAG, "removing data for removed account " + account); 3609 String[] params = new String[] {account.name, account.type}; 3610 mDb.execSQL( 3611 "DELETE FROM " + Tables.GROUPS + 3612 " WHERE " + Groups.ACCOUNT_NAME + " = ?" + 3613 " AND " + Groups.ACCOUNT_TYPE + " = ?", params); 3614 mDb.execSQL( 3615 "DELETE FROM " + Tables.PRESENCE + 3616 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" + 3617 "SELECT " + RawContacts._ID + 3618 " FROM " + Tables.RAW_CONTACTS + 3619 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 3620 " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params); 3621 mDb.execSQL( 3622 "DELETE FROM " + Tables.RAW_CONTACTS + 3623 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 3624 " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params); 3625 mDb.execSQL( 3626 "DELETE FROM " + Tables.SETTINGS + 3627 " WHERE " + Settings.ACCOUNT_NAME + " = ?" + 3628 " AND " + Settings.ACCOUNT_TYPE + " = ?", params); 3629 } 3630 3631 if (hasUnassignedContacts[0]) { 3632 3633 Account primaryAccount = null; 3634 for (Account account : accounts) { 3635 if (isWritableAccount(account)) { 3636 primaryAccount = account; 3637 break; 3638 } 3639 } 3640 3641 if (primaryAccount != null) { 3642 String[] params = new String[] {primaryAccount.name, primaryAccount.type}; 3643 3644 mDb.execSQL( 3645 "UPDATE " + Tables.RAW_CONTACTS + 3646 " SET " + RawContacts.ACCOUNT_NAME + "=?," 3647 + RawContacts.ACCOUNT_TYPE + "=?" + 3648 " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" + 3649 " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params); 3650 3651 // We don't currently support groups for unsynced accounts, so this is for 3652 // the future 3653 mDb.execSQL( 3654 "UPDATE " + Tables.GROUPS + 3655 " SET " + Groups.ACCOUNT_NAME + "=?," 3656 + Groups.ACCOUNT_TYPE + "=?" + 3657 " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" + 3658 " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params); 3659 } 3660 } 3661 3662 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 3663 mDb.setTransactionSuccessful(); 3664 } finally { 3665 mDb.endTransaction(); 3666 } 3667 } 3668 3669 /** 3670 * Finds all distinct accounts present in the specified table. 3671 */ 3672 private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts, 3673 String table, String accountNameColumn, String accountTypeColumn) { 3674 Cursor c = mDb.rawQuery("SELECT DISTINCT " + accountNameColumn + "," + accountTypeColumn 3675 + " FROM " + table, null); 3676 try { 3677 while (c.moveToNext()) { 3678 if (c.isNull(0) && c.isNull(1)) { 3679 hasUnassignedContacts[0] = true; 3680 } else { 3681 validAccounts.add(new Account(c.getString(0), c.getString(1))); 3682 } 3683 } 3684 } finally { 3685 c.close(); 3686 } 3687 } 3688 3689 /** 3690 * Test all against {@link TextUtils#isEmpty(CharSequence)}. 3691 */ 3692 private static boolean areAllEmpty(ContentValues values, String[] keys) { 3693 for (String key : keys) { 3694 if (!TextUtils.isEmpty(values.getAsString(key))) { 3695 return false; 3696 } 3697 } 3698 return true; 3699 } 3700 3701 /** 3702 * Returns true if a value (possibly null) is specified for at least one of the supplied keys. 3703 */ 3704 private static boolean areAnySpecified(ContentValues values, String[] keys) { 3705 for (String key : keys) { 3706 if (values.containsKey(key)) { 3707 return true; 3708 } 3709 } 3710 return false; 3711 } 3712 3713 @Override 3714 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 3715 String sortOrder) { 3716 if (VERBOSE_LOGGING) { 3717 Log.v(TAG, "query: " + uri); 3718 } 3719 3720 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 3721 3722 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3723 String groupBy = null; 3724 String limit = getLimit(uri); 3725 3726 // TODO: Consider writing a test case for RestrictionExceptions when you 3727 // write a new query() block to make sure it protects restricted data. 3728 final int match = sUriMatcher.match(uri); 3729 switch (match) { 3730 case SYNCSTATE: 3731 return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, 3732 sortOrder); 3733 3734 case CONTACTS: { 3735 setTablesAndProjectionMapForContacts(qb, uri, projection); 3736 break; 3737 } 3738 3739 case CONTACTS_ID: { 3740 long contactId = ContentUris.parseId(uri); 3741 setTablesAndProjectionMapForContacts(qb, uri, projection); 3742 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3743 qb.appendWhere(Contacts._ID + "=?"); 3744 break; 3745 } 3746 3747 case CONTACTS_LOOKUP: 3748 case CONTACTS_LOOKUP_ID: { 3749 List<String> pathSegments = uri.getPathSegments(); 3750 int segmentCount = pathSegments.size(); 3751 if (segmentCount < 3) { 3752 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 3753 } 3754 String lookupKey = pathSegments.get(2); 3755 if (segmentCount == 4) { 3756 long contactId = Long.parseLong(pathSegments.get(3)); 3757 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3758 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 3759 String[] args; 3760 if (selectionArgs == null) { 3761 args = new String[2]; 3762 } else { 3763 args = new String[selectionArgs.length + 2]; 3764 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 3765 } 3766 args[0] = String.valueOf(contactId); 3767 args[1] = lookupKey; 3768 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); 3769 Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, 3770 groupBy, limit); 3771 if (c.getCount() != 0) { 3772 return c; 3773 } 3774 3775 c.close(); 3776 } 3777 3778 setTablesAndProjectionMapForContacts(qb, uri, projection); 3779 selectionArgs = insertSelectionArg(selectionArgs, 3780 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 3781 qb.appendWhere(Contacts._ID + "=?"); 3782 break; 3783 } 3784 3785 case CONTACTS_AS_VCARD: { 3786 // When reading as vCard always use restricted view 3787 final String lookupKey = uri.getPathSegments().get(2); 3788 qb.setTables(mDbHelper.getContactView(true /* require restricted */)); 3789 qb.setProjectionMap(sContactsVCardProjectionMap); 3790 selectionArgs = insertSelectionArg(selectionArgs, 3791 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 3792 qb.appendWhere(Contacts._ID + "=?"); 3793 break; 3794 } 3795 3796 case CONTACTS_FILTER: { 3797 setTablesAndProjectionMapForContacts(qb, uri, projection); 3798 if (uri.getPathSegments().size() > 2) { 3799 String filterParam = uri.getLastPathSegment(); 3800 StringBuilder sb = new StringBuilder(); 3801 sb.append(Contacts._ID + " IN "); 3802 appendContactFilterAsNestedQuery(sb, filterParam); 3803 qb.appendWhere(sb.toString()); 3804 } 3805 break; 3806 } 3807 3808 case CONTACTS_STREQUENT_FILTER: 3809 case CONTACTS_STREQUENT: { 3810 String filterSql = null; 3811 if (match == CONTACTS_STREQUENT_FILTER 3812 && uri.getPathSegments().size() > 3) { 3813 String filterParam = uri.getLastPathSegment(); 3814 StringBuilder sb = new StringBuilder(); 3815 sb.append(Contacts._ID + " IN "); 3816 appendContactFilterAsNestedQuery(sb, filterParam); 3817 filterSql = sb.toString(); 3818 } 3819 3820 setTablesAndProjectionMapForContacts(qb, uri, projection); 3821 3822 String[] starredProjection = null; 3823 String[] frequentProjection = null; 3824 if (projection != null) { 3825 starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN); 3826 frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN); 3827 } 3828 3829 // Build the first query for starred 3830 if (filterSql != null) { 3831 qb.appendWhere(filterSql); 3832 } 3833 qb.setProjectionMap(sStrequentStarredProjectionMap); 3834 final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1", 3835 null, Contacts._ID, null, null, null); 3836 3837 // Build the second query for frequent 3838 qb = new SQLiteQueryBuilder(); 3839 setTablesAndProjectionMapForContacts(qb, uri, projection); 3840 if (filterSql != null) { 3841 qb.appendWhere(filterSql); 3842 } 3843 qb.setProjectionMap(sStrequentFrequentProjectionMap); 3844 final String frequentQuery = qb.buildQuery(frequentProjection, 3845 Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED 3846 + " = 0 OR " + Contacts.STARRED + " IS NULL)", 3847 null, Contacts._ID, null, null, null); 3848 3849 // Put them together 3850 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 3851 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 3852 Cursor c = db.rawQuery(query, null); 3853 if (c != null) { 3854 c.setNotificationUri(getContext().getContentResolver(), 3855 ContactsContract.AUTHORITY_URI); 3856 } 3857 return c; 3858 } 3859 3860 case CONTACTS_GROUP: { 3861 setTablesAndProjectionMapForContacts(qb, uri, projection); 3862 if (uri.getPathSegments().size() > 2) { 3863 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 3864 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3865 } 3866 break; 3867 } 3868 3869 case CONTACTS_DATA: { 3870 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3871 setTablesAndProjectionMapForData(qb, uri, projection, false); 3872 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3873 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 3874 break; 3875 } 3876 3877 case CONTACTS_PHOTO: { 3878 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3879 setTablesAndProjectionMapForData(qb, uri, projection, false); 3880 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3881 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 3882 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 3883 break; 3884 } 3885 3886 case PHONES: { 3887 setTablesAndProjectionMapForData(qb, uri, projection, false); 3888 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3889 break; 3890 } 3891 3892 case PHONES_ID: { 3893 setTablesAndProjectionMapForData(qb, uri, projection, false); 3894 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3895 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3896 qb.appendWhere(" AND " + Data._ID + "=?"); 3897 break; 3898 } 3899 3900 case PHONES_FILTER: { 3901 setTablesAndProjectionMapForData(qb, uri, projection, true); 3902 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3903 if (uri.getPathSegments().size() > 2) { 3904 String filterParam = uri.getLastPathSegment(); 3905 StringBuilder sb = new StringBuilder(); 3906 sb.append(" AND ("); 3907 3908 boolean orNeeded = false; 3909 String normalizedName = NameNormalizer.normalize(filterParam); 3910 if (normalizedName.length() > 0) { 3911 sb.append(Data.RAW_CONTACT_ID + " IN "); 3912 appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); 3913 orNeeded = true; 3914 } 3915 3916 if (isPhoneNumber(filterParam)) { 3917 if (orNeeded) { 3918 sb.append(" OR "); 3919 } 3920 String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam); 3921 String reversed = PhoneNumberUtils.getStrippedReversed(number); 3922 sb.append(Data._ID + 3923 " IN (SELECT " + PhoneLookupColumns.DATA_ID 3924 + " FROM " + Tables.PHONE_LOOKUP 3925 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%"); 3926 sb.append(reversed); 3927 sb.append("')"); 3928 } 3929 sb.append(")"); 3930 qb.appendWhere(sb); 3931 } 3932 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 3933 if (sortOrder == null) { 3934 sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; 3935 } 3936 break; 3937 } 3938 3939 case EMAILS: { 3940 setTablesAndProjectionMapForData(qb, uri, projection, false); 3941 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3942 break; 3943 } 3944 3945 case EMAILS_ID: { 3946 setTablesAndProjectionMapForData(qb, uri, projection, false); 3947 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3948 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'" 3949 + " AND " + Data._ID + "=?"); 3950 break; 3951 } 3952 3953 case EMAILS_LOOKUP: { 3954 setTablesAndProjectionMapForData(qb, uri, projection, false); 3955 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3956 if (uri.getPathSegments().size() > 2) { 3957 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3958 qb.appendWhere(" AND " + Email.DATA + "=?"); 3959 } 3960 break; 3961 } 3962 3963 case EMAILS_FILTER: { 3964 setTablesAndProjectionMapForData(qb, uri, projection, true); 3965 String filterParam = null; 3966 if (uri.getPathSegments().size() > 3) { 3967 filterParam = uri.getLastPathSegment(); 3968 if (TextUtils.isEmpty(filterParam)) { 3969 filterParam = null; 3970 } 3971 } 3972 3973 if (filterParam == null) { 3974 // If the filter is unspecified, return nothing 3975 qb.appendWhere(" AND 0"); 3976 } else { 3977 StringBuilder sb = new StringBuilder(); 3978 sb.append(" AND " + Data._ID + " IN ("); 3979 sb.append( 3980 "SELECT " + Data._ID + 3981 " FROM " + Tables.DATA + 3982 " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail + 3983 " AND " + Data.DATA1 + " LIKE "); 3984 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); 3985 if (!filterParam.contains("@")) { 3986 String normalizedName = NameNormalizer.normalize(filterParam); 3987 if (normalizedName.length() > 0) { 3988 3989 /* 3990 * Using a UNION instead of an "OR" to make SQLite use the right 3991 * indexes. We need it to use the (mimetype,data1) index for the 3992 * email lookup (see above), but not for the name lookup. 3993 * SQLite is not smart enough to use the index on one side of an OR 3994 * but not on the other. Using two separate nested queries 3995 * and a UNION between them does the job. 3996 */ 3997 sb.append( 3998 " UNION SELECT " + Data._ID + 3999 " FROM " + Tables.DATA + 4000 " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail + 4001 " AND " + Data.RAW_CONTACT_ID + " IN "); 4002 appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); 4003 } 4004 } 4005 sb.append(")"); 4006 qb.appendWhere(sb); 4007 } 4008 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 4009 if (sortOrder == null) { 4010 sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; 4011 } 4012 break; 4013 } 4014 4015 case POSTALS: { 4016 setTablesAndProjectionMapForData(qb, uri, projection, false); 4017 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 4018 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 4019 break; 4020 } 4021 4022 case POSTALS_ID: { 4023 setTablesAndProjectionMapForData(qb, uri, projection, false); 4024 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4025 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 4026 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 4027 qb.appendWhere(" AND " + Data._ID + "=?"); 4028 break; 4029 } 4030 4031 case RAW_CONTACTS: { 4032 setTablesAndProjectionMapForRawContacts(qb, uri); 4033 break; 4034 } 4035 4036 case RAW_CONTACTS_ID: { 4037 long rawContactId = ContentUris.parseId(uri); 4038 setTablesAndProjectionMapForRawContacts(qb, uri); 4039 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 4040 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 4041 break; 4042 } 4043 4044 case RAW_CONTACTS_DATA: { 4045 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 4046 setTablesAndProjectionMapForData(qb, uri, projection, false); 4047 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 4048 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?"); 4049 break; 4050 } 4051 4052 case DATA: { 4053 setTablesAndProjectionMapForData(qb, uri, projection, false); 4054 break; 4055 } 4056 4057 case DATA_ID: { 4058 setTablesAndProjectionMapForData(qb, uri, projection, false); 4059 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4060 qb.appendWhere(" AND " + Data._ID + "=?"); 4061 break; 4062 } 4063 4064 case PHONE_LOOKUP: { 4065 4066 if (TextUtils.isEmpty(sortOrder)) { 4067 // Default the sort order to something reasonable so we get consistent 4068 // results when callers don't request an ordering 4069 sortOrder = RawContactsColumns.CONCRETE_ID; 4070 } 4071 4072 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 4073 mDbHelper.buildPhoneLookupAndContactQuery(qb, number); 4074 qb.setProjectionMap(sPhoneLookupProjectionMap); 4075 4076 // Phone lookup cannot be combined with a selection 4077 selection = null; 4078 selectionArgs = null; 4079 break; 4080 } 4081 4082 case GROUPS: { 4083 qb.setTables(mDbHelper.getGroupView()); 4084 qb.setProjectionMap(sGroupsProjectionMap); 4085 appendAccountFromParameter(qb, uri); 4086 break; 4087 } 4088 4089 case GROUPS_ID: { 4090 qb.setTables(mDbHelper.getGroupView()); 4091 qb.setProjectionMap(sGroupsProjectionMap); 4092 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4093 qb.appendWhere(Groups._ID + "=?"); 4094 break; 4095 } 4096 4097 case GROUPS_SUMMARY: { 4098 qb.setTables(mDbHelper.getGroupView() + " AS groups"); 4099 qb.setProjectionMap(sGroupsSummaryProjectionMap); 4100 appendAccountFromParameter(qb, uri); 4101 groupBy = Groups._ID; 4102 break; 4103 } 4104 4105 case AGGREGATION_EXCEPTIONS: { 4106 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 4107 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 4108 break; 4109 } 4110 4111 case AGGREGATION_SUGGESTIONS: { 4112 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4113 String filter = null; 4114 if (uri.getPathSegments().size() > 3) { 4115 filter = uri.getPathSegments().get(3); 4116 } 4117 final int maxSuggestions; 4118 if (limit != null) { 4119 maxSuggestions = Integer.parseInt(limit); 4120 } else { 4121 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 4122 } 4123 4124 setTablesAndProjectionMapForContacts(qb, uri, projection); 4125 4126 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, 4127 maxSuggestions, filter); 4128 } 4129 4130 case SETTINGS: { 4131 qb.setTables(Tables.SETTINGS); 4132 qb.setProjectionMap(sSettingsProjectionMap); 4133 appendAccountFromParameter(qb, uri); 4134 4135 // When requesting specific columns, this query requires 4136 // late-binding of the GroupMembership MIME-type. 4137 final String groupMembershipMimetypeId = Long.toString(mDbHelper 4138 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 4139 if (projection != null && projection.length != 0 && 4140 mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 4141 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 4142 } 4143 if (projection != null && projection.length != 0 && 4144 mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 4145 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 4146 } 4147 4148 break; 4149 } 4150 4151 case STATUS_UPDATES: { 4152 setTableAndProjectionMapForStatusUpdates(qb, projection); 4153 break; 4154 } 4155 4156 case STATUS_UPDATES_ID: { 4157 setTableAndProjectionMapForStatusUpdates(qb, projection); 4158 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4159 qb.appendWhere(DataColumns.CONCRETE_ID + "=?"); 4160 break; 4161 } 4162 4163 case SEARCH_SUGGESTIONS: { 4164 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); 4165 } 4166 4167 case SEARCH_SHORTCUT: { 4168 long contactId = ContentUris.parseId(uri); 4169 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection); 4170 } 4171 4172 case LIVE_FOLDERS_CONTACTS: 4173 qb.setTables(mDbHelper.getContactView()); 4174 qb.setProjectionMap(sLiveFoldersProjectionMap); 4175 break; 4176 4177 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 4178 qb.setTables(mDbHelper.getContactView()); 4179 qb.setProjectionMap(sLiveFoldersProjectionMap); 4180 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 4181 break; 4182 4183 case LIVE_FOLDERS_CONTACTS_FAVORITES: 4184 qb.setTables(mDbHelper.getContactView()); 4185 qb.setProjectionMap(sLiveFoldersProjectionMap); 4186 qb.appendWhere(Contacts.STARRED + "=1"); 4187 break; 4188 4189 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 4190 qb.setTables(mDbHelper.getContactView()); 4191 qb.setProjectionMap(sLiveFoldersProjectionMap); 4192 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 4193 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4194 break; 4195 4196 case RAW_CONTACT_ENTITIES: { 4197 setTablesAndProjectionMapForRawContactsEntities(qb, uri); 4198 break; 4199 } 4200 4201 case RAW_CONTACT_ENTITY_ID: { 4202 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 4203 setTablesAndProjectionMapForRawContactsEntities(qb, uri); 4204 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 4205 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 4206 break; 4207 } 4208 4209 default: 4210 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 4211 sortOrder, limit); 4212 } 4213 4214 return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 4215 } 4216 4217 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 4218 String selection, String[] selectionArgs, String sortOrder, String groupBy, 4219 String limit) { 4220 if (projection != null && projection.length == 1 4221 && BaseColumns._COUNT.equals(projection[0])) { 4222 qb.setProjectionMap(sCountProjectionMap); 4223 } 4224 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 4225 sortOrder, limit); 4226 if (c != null) { 4227 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 4228 } 4229 return c; 4230 } 4231 4232 private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 4233 ContactLookupKey key = new ContactLookupKey(); 4234 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 4235 4236 long contactId = lookupContactIdBySourceIds(db, segments); 4237 if (contactId == -1) { 4238 contactId = lookupContactIdByDisplayNames(db, segments); 4239 } 4240 4241 return contactId; 4242 } 4243 4244 private interface LookupBySourceIdQuery { 4245 String TABLE = Tables.RAW_CONTACTS; 4246 4247 String COLUMNS[] = { 4248 RawContacts.CONTACT_ID, 4249 RawContacts.ACCOUNT_TYPE, 4250 RawContacts.ACCOUNT_NAME, 4251 RawContacts.SOURCE_ID 4252 }; 4253 4254 int CONTACT_ID = 0; 4255 int ACCOUNT_TYPE = 1; 4256 int ACCOUNT_NAME = 2; 4257 int SOURCE_ID = 3; 4258 } 4259 4260 private long lookupContactIdBySourceIds(SQLiteDatabase db, 4261 ArrayList<LookupKeySegment> segments) { 4262 int sourceIdCount = 0; 4263 for (int i = 0; i < segments.size(); i++) { 4264 LookupKeySegment segment = segments.get(i); 4265 if (segment.sourceIdLookup) { 4266 sourceIdCount++; 4267 } 4268 } 4269 4270 if (sourceIdCount == 0) { 4271 return -1; 4272 } 4273 4274 // First try sync ids 4275 StringBuilder sb = new StringBuilder(); 4276 sb.append(RawContacts.SOURCE_ID + " IN ("); 4277 for (int i = 0; i < segments.size(); i++) { 4278 LookupKeySegment segment = segments.get(i); 4279 if (segment.sourceIdLookup) { 4280 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 4281 sb.append(","); 4282 } 4283 } 4284 sb.setLength(sb.length() - 1); // Last comma 4285 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 4286 4287 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 4288 sb.toString(), null, null, null, null); 4289 try { 4290 while (c.moveToNext()) { 4291 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); 4292 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 4293 int accountHashCode = 4294 ContactLookupKey.getAccountHashCode(accountType, accountName); 4295 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 4296 for (int i = 0; i < segments.size(); i++) { 4297 LookupKeySegment segment = segments.get(i); 4298 if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode 4299 && segment.key.equals(sourceId)) { 4300 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 4301 break; 4302 } 4303 } 4304 } 4305 } finally { 4306 c.close(); 4307 } 4308 4309 return getMostReferencedContactId(segments); 4310 } 4311 4312 private interface LookupByDisplayNameQuery { 4313 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 4314 4315 String COLUMNS[] = { 4316 RawContacts.CONTACT_ID, 4317 RawContacts.ACCOUNT_TYPE, 4318 RawContacts.ACCOUNT_NAME, 4319 NameLookupColumns.NORMALIZED_NAME 4320 }; 4321 4322 int CONTACT_ID = 0; 4323 int ACCOUNT_TYPE = 1; 4324 int ACCOUNT_NAME = 2; 4325 int NORMALIZED_NAME = 3; 4326 } 4327 4328 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 4329 ArrayList<LookupKeySegment> segments) { 4330 int displayNameCount = 0; 4331 for (int i = 0; i < segments.size(); i++) { 4332 LookupKeySegment segment = segments.get(i); 4333 if (!segment.sourceIdLookup) { 4334 displayNameCount++; 4335 } 4336 } 4337 4338 if (displayNameCount == 0) { 4339 return -1; 4340 } 4341 4342 // First try sync ids 4343 StringBuilder sb = new StringBuilder(); 4344 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 4345 for (int i = 0; i < segments.size(); i++) { 4346 LookupKeySegment segment = segments.get(i); 4347 if (!segment.sourceIdLookup) { 4348 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 4349 sb.append(","); 4350 } 4351 } 4352 sb.setLength(sb.length() - 1); // Last comma 4353 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 4354 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 4355 4356 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 4357 sb.toString(), null, null, null, null); 4358 try { 4359 while (c.moveToNext()) { 4360 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); 4361 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 4362 int accountHashCode = 4363 ContactLookupKey.getAccountHashCode(accountType, accountName); 4364 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 4365 for (int i = 0; i < segments.size(); i++) { 4366 LookupKeySegment segment = segments.get(i); 4367 if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode 4368 && segment.key.equals(name)) { 4369 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 4370 break; 4371 } 4372 } 4373 } 4374 } finally { 4375 c.close(); 4376 } 4377 4378 return getMostReferencedContactId(segments); 4379 } 4380 4381 /** 4382 * Returns the contact ID that is mentioned the highest number of times. 4383 */ 4384 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 4385 Collections.sort(segments); 4386 4387 long bestContactId = -1; 4388 int bestRefCount = 0; 4389 4390 long contactId = -1; 4391 int count = 0; 4392 4393 int segmentCount = segments.size(); 4394 for (int i = 0; i < segmentCount; i++) { 4395 LookupKeySegment segment = segments.get(i); 4396 if (segment.contactId != -1) { 4397 if (segment.contactId == contactId) { 4398 count++; 4399 } else { 4400 if (count > bestRefCount) { 4401 bestContactId = contactId; 4402 bestRefCount = count; 4403 } 4404 contactId = segment.contactId; 4405 count = 1; 4406 } 4407 } 4408 } 4409 if (count > bestRefCount) { 4410 return contactId; 4411 } else { 4412 return bestContactId; 4413 } 4414 } 4415 4416 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 4417 String[] projection) { 4418 StringBuilder sb = new StringBuilder(); 4419 boolean excludeRestrictedData = false; 4420 String requestingPackage = getQueryParameter(uri, 4421 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4422 if (requestingPackage != null) { 4423 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4424 } 4425 sb.append(mDbHelper.getContactView(excludeRestrictedData)); 4426 if (mDbHelper.isInProjection(projection, 4427 Contacts.CONTACT_PRESENCE)) { 4428 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 4429 " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")"); 4430 } 4431 if (mDbHelper.isInProjection(projection, 4432 Contacts.CONTACT_STATUS, 4433 Contacts.CONTACT_STATUS_RES_PACKAGE, 4434 Contacts.CONTACT_STATUS_ICON, 4435 Contacts.CONTACT_STATUS_LABEL, 4436 Contacts.CONTACT_STATUS_TIMESTAMP)) { 4437 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 4438 + ContactsStatusUpdatesColumns.ALIAS + 4439 " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" 4440 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 4441 } 4442 qb.setTables(sb.toString()); 4443 qb.setProjectionMap(sContactsProjectionMap); 4444 } 4445 4446 private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) { 4447 StringBuilder sb = new StringBuilder(); 4448 boolean excludeRestrictedData = false; 4449 String requestingPackage = getQueryParameter(uri, 4450 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4451 if (requestingPackage != null) { 4452 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4453 } 4454 sb.append(mDbHelper.getRawContactView(excludeRestrictedData)); 4455 qb.setTables(sb.toString()); 4456 qb.setProjectionMap(sRawContactsProjectionMap); 4457 appendAccountFromParameter(qb, uri); 4458 } 4459 4460 private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) { 4461 // Note: currently, "export only" equals to "restricted", but may not in the future. 4462 boolean excludeRestrictedData = readBooleanQueryParameter(uri, 4463 Data.FOR_EXPORT_ONLY, false); 4464 4465 String requestingPackage = getQueryParameter(uri, 4466 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4467 if (requestingPackage != null) { 4468 excludeRestrictedData = excludeRestrictedData 4469 || !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4470 } 4471 qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData)); 4472 qb.setProjectionMap(sRawContactsEntityProjectionMap); 4473 appendAccountFromParameter(qb, uri); 4474 } 4475 4476 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 4477 String[] projection, boolean distinct) { 4478 StringBuilder sb = new StringBuilder(); 4479 // Note: currently, "export only" equals to "restricted", but may not in the future. 4480 boolean excludeRestrictedData = readBooleanQueryParameter(uri, 4481 Data.FOR_EXPORT_ONLY, false); 4482 4483 String requestingPackage = getQueryParameter(uri, 4484 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4485 if (requestingPackage != null) { 4486 excludeRestrictedData = excludeRestrictedData 4487 || !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4488 } 4489 4490 sb.append(mDbHelper.getDataView(excludeRestrictedData)); 4491 sb.append(" data"); 4492 4493 // Include aggregated presence when requested 4494 if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) { 4495 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 4496 " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "=" 4497 + RawContacts.CONTACT_ID + ")"); 4498 } 4499 4500 // Include aggregated status updates when requested 4501 if (mDbHelper.isInProjection(projection, 4502 Data.CONTACT_STATUS, 4503 Data.CONTACT_STATUS_RES_PACKAGE, 4504 Data.CONTACT_STATUS_ICON, 4505 Data.CONTACT_STATUS_LABEL, 4506 Data.CONTACT_STATUS_TIMESTAMP)) { 4507 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 4508 + ContactsStatusUpdatesColumns.ALIAS + 4509 " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" 4510 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 4511 } 4512 4513 // Include individual presence when requested 4514 if (mDbHelper.isInProjection(projection, Data.PRESENCE)) { 4515 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 4516 " ON (" + StatusUpdates.DATA_ID + "=" 4517 + DataColumns.CONCRETE_ID + ")"); 4518 } 4519 4520 // Include individual status updates when requested 4521 if (mDbHelper.isInProjection(projection, 4522 Data.STATUS, 4523 Data.STATUS_RES_PACKAGE, 4524 Data.STATUS_ICON, 4525 Data.STATUS_LABEL, 4526 Data.STATUS_TIMESTAMP)) { 4527 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 4528 " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" 4529 + DataColumns.CONCRETE_ID + ")"); 4530 } 4531 4532 qb.setTables(sb.toString()); 4533 qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap); 4534 appendAccountFromParameter(qb, uri); 4535 } 4536 4537 private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, 4538 String[] projection) { 4539 StringBuilder sb = new StringBuilder(); 4540 sb.append(mDbHelper.getDataView()); 4541 sb.append(" data"); 4542 4543 if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) { 4544 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 4545 " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID 4546 + "=" + DataColumns.CONCRETE_ID + ")"); 4547 } 4548 4549 if (mDbHelper.isInProjection(projection, 4550 StatusUpdates.STATUS, 4551 StatusUpdates.STATUS_RES_PACKAGE, 4552 StatusUpdates.STATUS_ICON, 4553 StatusUpdates.STATUS_LABEL, 4554 StatusUpdates.STATUS_TIMESTAMP)) { 4555 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 4556 " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID 4557 + "=" + DataColumns.CONCRETE_ID + ")"); 4558 } 4559 qb.setTables(sb.toString()); 4560 qb.setProjectionMap(sStatusUpdatesProjectionMap); 4561 } 4562 4563 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 4564 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 4565 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 4566 if (!TextUtils.isEmpty(accountName)) { 4567 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 4568 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 4569 + RawContacts.ACCOUNT_TYPE + "=" 4570 + DatabaseUtils.sqlEscapeString(accountType)); 4571 } else { 4572 qb.appendWhere("1"); 4573 } 4574 } 4575 4576 private String appendAccountToSelection(Uri uri, String selection) { 4577 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 4578 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 4579 if (!TextUtils.isEmpty(accountName)) { 4580 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 4581 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 4582 + RawContacts.ACCOUNT_TYPE + "=" 4583 + DatabaseUtils.sqlEscapeString(accountType)); 4584 if (!TextUtils.isEmpty(selection)) { 4585 selectionSb.append(" AND ("); 4586 selectionSb.append(selection); 4587 selectionSb.append(')'); 4588 } 4589 return selectionSb.toString(); 4590 } else { 4591 return selection; 4592 } 4593 } 4594 4595 /** 4596 * Gets the value of the "limit" URI query parameter. 4597 * 4598 * @return A string containing a non-negative integer, or <code>null</code> if 4599 * the parameter is not set, or is set to an invalid value. 4600 */ 4601 private String getLimit(Uri uri) { 4602 String limitParam = getQueryParameter(uri, "limit"); 4603 if (limitParam == null) { 4604 return null; 4605 } 4606 // make sure that the limit is a non-negative integer 4607 try { 4608 int l = Integer.parseInt(limitParam); 4609 if (l < 0) { 4610 Log.w(TAG, "Invalid limit parameter: " + limitParam); 4611 return null; 4612 } 4613 return String.valueOf(l); 4614 } catch (NumberFormatException ex) { 4615 Log.w(TAG, "Invalid limit parameter: " + limitParam); 4616 return null; 4617 } 4618 } 4619 4620 /** 4621 * Returns true if all the characters are meaningful as digits 4622 * in a phone number -- letters, digits, and a few punctuation marks. 4623 */ 4624 private boolean isPhoneNumber(CharSequence cons) { 4625 int len = cons.length(); 4626 4627 for (int i = 0; i < len; i++) { 4628 char c = cons.charAt(i); 4629 4630 if ((c >= '0') && (c <= '9')) { 4631 continue; 4632 } 4633 if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+') 4634 || (c == '#') || (c == '*')) { 4635 continue; 4636 } 4637 if ((c >= 'A') && (c <= 'Z')) { 4638 continue; 4639 } 4640 if ((c >= 'a') && (c <= 'z')) { 4641 continue; 4642 } 4643 4644 return false; 4645 } 4646 4647 return true; 4648 } 4649 4650 String getContactsRestrictions() { 4651 if (mDbHelper.hasAccessToRestrictedData()) { 4652 return "1"; 4653 } else { 4654 return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0"; 4655 } 4656 } 4657 4658 public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { 4659 if (mDbHelper.hasAccessToRestrictedData()) { 4660 return "1"; 4661 } else { 4662 return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS 4663 + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; 4664 } 4665 } 4666 4667 @Override 4668 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 4669 int match = sUriMatcher.match(uri); 4670 switch (match) { 4671 case CONTACTS_PHOTO: { 4672 if (!"r".equals(mode)) { 4673 throw new FileNotFoundException("Mode " + mode + " not supported."); 4674 } 4675 4676 String sql = 4677 "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() + 4678 " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID 4679 + " AND " + RawContacts.CONTACT_ID + "=?"; 4680 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4681 return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, 4682 new String[]{uri.getPathSegments().get(1)}); 4683 } 4684 4685 case CONTACTS_AS_VCARD: { 4686 final String lookupKey = uri.getPathSegments().get(2); 4687 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 4688 final String selection = Contacts._ID + "=" + contactId; 4689 4690 // When opening a contact as file, we pass back contents as a 4691 // vCard-encoded stream. We build into a local buffer first, 4692 // then pipe into MemoryFile once the exact size is known. 4693 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 4694 outputRawContactsAsVCard(localStream, selection, null); 4695 return buildAssetFileDescriptor(localStream); 4696 } 4697 4698 default: 4699 throw new FileNotFoundException("No file at: " + uri); 4700 } 4701 } 4702 4703 private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; 4704 private static final String VCARD_TYPE_DEFAULT = "default"; 4705 4706 /** 4707 * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the 4708 * contents of the given {@link ByteArrayOutputStream}. 4709 */ 4710 private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { 4711 AssetFileDescriptor fd = null; 4712 try { 4713 stream.flush(); 4714 4715 final byte[] byteData = stream.toByteArray(); 4716 final int size = byteData.length; 4717 4718 final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size); 4719 memoryFile.writeBytes(byteData, 0, 0, size); 4720 memoryFile.deactivate(); 4721 4722 fd = AssetFileDescriptor.fromMemoryFile(memoryFile); 4723 } catch (IOException e) { 4724 Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString()); 4725 } 4726 return fd; 4727 } 4728 4729 /** 4730 * Output {@link RawContacts} matching the requested selection in the vCard 4731 * format to the given {@link OutputStream}. This method returns silently if 4732 * any errors encountered. 4733 */ 4734 private void outputRawContactsAsVCard(OutputStream stream, String selection, 4735 String[] selectionArgs) { 4736 final Context context = this.getContext(); 4737 final VCardComposer composer = new VCardComposer(context, VCARD_TYPE_DEFAULT, false); 4738 composer.addHandler(composer.new HandlerForOutputStream(stream)); 4739 4740 // No extra checks since composer always uses restricted views 4741 if (!composer.init(selection, selectionArgs)) 4742 return; 4743 4744 while (!composer.isAfterLast()) { 4745 if (!composer.createOneEntry()) { 4746 Log.w(TAG, "Failed to output a contact."); 4747 } 4748 } 4749 composer.terminate(); 4750 } 4751 4752 @Override 4753 public String getType(Uri uri) { 4754 final int match = sUriMatcher.match(uri); 4755 switch (match) { 4756 case CONTACTS: 4757 case CONTACTS_LOOKUP: 4758 return Contacts.CONTENT_TYPE; 4759 case CONTACTS_ID: 4760 case CONTACTS_LOOKUP_ID: 4761 return Contacts.CONTENT_ITEM_TYPE; 4762 case CONTACTS_AS_VCARD: 4763 return Contacts.CONTENT_VCARD_TYPE; 4764 case RAW_CONTACTS: 4765 return RawContacts.CONTENT_TYPE; 4766 case RAW_CONTACTS_ID: 4767 return RawContacts.CONTENT_ITEM_TYPE; 4768 case DATA_ID: 4769 return mDbHelper.getDataMimeType(ContentUris.parseId(uri)); 4770 case PHONES: 4771 return Phone.CONTENT_TYPE; 4772 case PHONES_ID: 4773 return Phone.CONTENT_ITEM_TYPE; 4774 case EMAILS: 4775 return Email.CONTENT_TYPE; 4776 case EMAILS_ID: 4777 return Email.CONTENT_ITEM_TYPE; 4778 case POSTALS: 4779 return StructuredPostal.CONTENT_TYPE; 4780 case POSTALS_ID: 4781 return StructuredPostal.CONTENT_ITEM_TYPE; 4782 case AGGREGATION_EXCEPTIONS: 4783 return AggregationExceptions.CONTENT_TYPE; 4784 case AGGREGATION_EXCEPTION_ID: 4785 return AggregationExceptions.CONTENT_ITEM_TYPE; 4786 case SETTINGS: 4787 return Settings.CONTENT_TYPE; 4788 case AGGREGATION_SUGGESTIONS: 4789 return Contacts.CONTENT_TYPE; 4790 case SEARCH_SUGGESTIONS: 4791 return SearchManager.SUGGEST_MIME_TYPE; 4792 case SEARCH_SHORTCUT: 4793 return SearchManager.SHORTCUT_MIME_TYPE; 4794 default: 4795 return mLegacyApiSupport.getType(uri); 4796 } 4797 } 4798 4799 private void setDisplayName(long rawContactId, int displayNameSource, 4800 String displayNamePrimary, String displayNameAlternative, String phoneticName, 4801 int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) { 4802 mRawContactDisplayNameUpdate.bindLong(1, displayNameSource); 4803 bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary); 4804 bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative); 4805 bindString(mRawContactDisplayNameUpdate, 4, phoneticName); 4806 mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle); 4807 bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary); 4808 bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative); 4809 mRawContactDisplayNameUpdate.bindLong(8, rawContactId); 4810 mRawContactDisplayNameUpdate.execute(); 4811 } 4812 4813 /** 4814 * Sets the {@link RawContacts#DIRTY} for the specified raw contact. 4815 */ 4816 private void setRawContactDirty(long rawContactId) { 4817 mDirtyRawContacts.add(rawContactId); 4818 } 4819 4820 /* 4821 * Sets the given dataId record in the "data" table to primary, and resets all data records of 4822 * the same mimetype and under the same contact to not be primary. 4823 * 4824 * @param dataId the id of the data record to be set to primary. 4825 */ 4826 private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) { 4827 mSetPrimaryStatement.bindLong(1, dataId); 4828 mSetPrimaryStatement.bindLong(2, mimeTypeId); 4829 mSetPrimaryStatement.bindLong(3, rawContactId); 4830 mSetPrimaryStatement.execute(); 4831 } 4832 4833 /* 4834 * Sets the given dataId record in the "data" table to "super primary", and resets all data 4835 * records of the same mimetype and under the same aggregate to not be "super primary". 4836 * 4837 * @param dataId the id of the data record to be set to primary. 4838 */ 4839 private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) { 4840 mSetSuperPrimaryStatement.bindLong(1, dataId); 4841 mSetSuperPrimaryStatement.bindLong(2, mimeTypeId); 4842 mSetSuperPrimaryStatement.bindLong(3, rawContactId); 4843 mSetSuperPrimaryStatement.execute(); 4844 } 4845 4846 public void insertNameLookupForEmail(long rawContactId, long dataId, String email) { 4847 if (TextUtils.isEmpty(email)) { 4848 return; 4849 } 4850 4851 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email); 4852 if (tokens.length == 0) { 4853 return; 4854 } 4855 4856 String address = tokens[0].getAddress(); 4857 int at = address.indexOf('@'); 4858 if (at != -1) { 4859 address = address.substring(0, at); 4860 } 4861 4862 insertNameLookup(rawContactId, dataId, 4863 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address)); 4864 } 4865 4866 /** 4867 * Normalizes the nickname and inserts it in the name lookup table. 4868 */ 4869 public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) { 4870 if (TextUtils.isEmpty(nickname)) { 4871 return; 4872 } 4873 4874 insertNameLookup(rawContactId, dataId, 4875 NameLookupType.NICKNAME, NameNormalizer.normalize(nickname)); 4876 } 4877 4878 public void insertNameLookupForOrganization(long rawContactId, long dataId, String company, 4879 String title) { 4880 if (!TextUtils.isEmpty(company)) { 4881 insertNameLookup(rawContactId, dataId, 4882 NameLookupType.ORGANIZATION, NameNormalizer.normalize(company)); 4883 } 4884 if (!TextUtils.isEmpty(title)) { 4885 insertNameLookup(rawContactId, dataId, 4886 NameLookupType.ORGANIZATION, NameNormalizer.normalize(title)); 4887 } 4888 } 4889 4890 public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name) { 4891 mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name); 4892 } 4893 4894 private interface NicknameLookupPreloadQuery { 4895 String TABLE = Tables.NICKNAME_LOOKUP; 4896 4897 String[] COLUMNS = new String[] { 4898 NicknameLookupColumns.NAME 4899 }; 4900 4901 int NAME = 0; 4902 } 4903 4904 /** 4905 * Read all known common nicknames from the database and populate a Bloom 4906 * filter using the corresponding hash codes. The idea is to eliminate most 4907 * of unnecessary database lookups for nicknames. Given a name, we will take 4908 * its hash code and see if it is set in the Bloom filter. If not, we will know 4909 * that the name is not in the database. If it is, we still need to run a 4910 * query. 4911 * <p> 4912 * Given the size of the filter and the expected size of the nickname table, 4913 * we should expect the combination of the Bloom filter and cache will 4914 * prevent around 98-99% of unnecessary queries from running. 4915 */ 4916 private void preloadNicknameBloomFilter() { 4917 mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1); 4918 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4919 Cursor cursor = db.query(NicknameLookupPreloadQuery.TABLE, 4920 NicknameLookupPreloadQuery.COLUMNS, 4921 null, null, null, null, null); 4922 try { 4923 int count = cursor.getCount(); 4924 for (int i = 0; i < count; i++) { 4925 cursor.moveToNext(); 4926 String normalizedName = cursor.getString(NicknameLookupPreloadQuery.NAME); 4927 int hashCode = normalizedName.hashCode(); 4928 mNicknameBloomFilter.set(hashCode & NICKNAME_BLOOM_FILTER_SIZE); 4929 } 4930 } finally { 4931 cursor.close(); 4932 } 4933 } 4934 4935 4936 /** 4937 * Returns nickname cluster IDs or null. Maintains cache. 4938 */ 4939 protected String[] getCommonNicknameClusters(String normalizedName) { 4940 int hashCode = normalizedName.hashCode(); 4941 if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) { 4942 return null; 4943 } 4944 4945 SoftReference<String[]> ref; 4946 String[] clusters = null; 4947 synchronized (mNicknameClusterCache) { 4948 if (mNicknameClusterCache.containsKey(normalizedName)) { 4949 ref = mNicknameClusterCache.get(normalizedName); 4950 if (ref == null) { 4951 return null; 4952 } 4953 clusters = ref.get(); 4954 } 4955 } 4956 4957 if (clusters == null) { 4958 clusters = loadNicknameClusters(normalizedName); 4959 ref = clusters == null ? null : new SoftReference<String[]>(clusters); 4960 synchronized (mNicknameClusterCache) { 4961 mNicknameClusterCache.put(normalizedName, ref); 4962 } 4963 } 4964 return clusters; 4965 } 4966 4967 private interface NicknameLookupQuery { 4968 String TABLE = Tables.NICKNAME_LOOKUP; 4969 4970 String[] COLUMNS = new String[] { 4971 NicknameLookupColumns.CLUSTER 4972 }; 4973 4974 int CLUSTER = 0; 4975 } 4976 4977 protected String[] loadNicknameClusters(String normalizedName) { 4978 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4979 String[] clusters = null; 4980 Cursor cursor = db.query(NicknameLookupQuery.TABLE, NicknameLookupQuery.COLUMNS, 4981 NicknameLookupColumns.NAME + "=?", new String[] { normalizedName }, 4982 null, null, null); 4983 try { 4984 int count = cursor.getCount(); 4985 if (count > 0) { 4986 clusters = new String[count]; 4987 for (int i = 0; i < count; i++) { 4988 cursor.moveToNext(); 4989 clusters[i] = cursor.getString(NicknameLookupQuery.CLUSTER); 4990 } 4991 } 4992 } finally { 4993 cursor.close(); 4994 } 4995 return clusters; 4996 } 4997 4998 private class StructuredNameLookupBuilder extends NameLookupBuilder { 4999 5000 public StructuredNameLookupBuilder(NameSplitter splitter) { 5001 super(splitter); 5002 } 5003 5004 @Override 5005 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 5006 String name) { 5007 ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name); 5008 } 5009 5010 @Override 5011 protected String[] getCommonNicknameClusters(String normalizedName) { 5012 return ContactsProvider2.this.getCommonNicknameClusters(normalizedName); 5013 } 5014 } 5015 5016 /** 5017 * Inserts a record in the {@link Tables#NAME_LOOKUP} table. 5018 */ 5019 public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) { 5020 mNameLookupInsert.bindLong(1, rawContactId); 5021 mNameLookupInsert.bindLong(2, dataId); 5022 mNameLookupInsert.bindLong(3, lookupType); 5023 bindString(mNameLookupInsert, 4, name); 5024 mNameLookupInsert.executeInsert(); 5025 } 5026 5027 /** 5028 * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element. 5029 */ 5030 public void deleteNameLookup(long dataId) { 5031 mNameLookupDelete.bindLong(1, dataId); 5032 mNameLookupDelete.execute(); 5033 } 5034 5035 public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 5036 sb.append("(" + 5037 "SELECT DISTINCT " + RawContacts.CONTACT_ID + 5038 " FROM " + Tables.RAW_CONTACTS + 5039 " JOIN " + Tables.NAME_LOOKUP + 5040 " ON(" + RawContactsColumns.CONCRETE_ID + "=" 5041 + NameLookupColumns.RAW_CONTACT_ID + ")" + 5042 " WHERE normalized_name GLOB '"); 5043 sb.append(NameNormalizer.normalize(filterParam)); 5044 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN(" 5045 + NameLookupType.NAME_COLLATION_KEY + "," 5046 + NameLookupType.EMAIL_BASED_NICKNAME + "," 5047 + NameLookupType.NICKNAME + "," 5048 + NameLookupType.ORGANIZATION + "))"); 5049 } 5050 5051 public String getRawContactsByFilterAsNestedQuery(String filterParam) { 5052 StringBuilder sb = new StringBuilder(); 5053 appendRawContactsByFilterAsNestedQuery(sb, filterParam); 5054 return sb.toString(); 5055 } 5056 5057 public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) { 5058 appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true); 5059 } 5060 5061 private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, 5062 boolean allowEmailMatch) { 5063 sb.append("(" + 5064 "SELECT " + NameLookupColumns.RAW_CONTACT_ID + 5065 " FROM " + Tables.NAME_LOOKUP + 5066 " WHERE " + NameLookupColumns.NORMALIZED_NAME + 5067 " GLOB '"); 5068 sb.append(normalizedName); 5069 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN (" 5070 + NameLookupType.NAME_COLLATION_KEY + "," 5071 + NameLookupType.NICKNAME + "," 5072 + NameLookupType.ORGANIZATION); 5073 if (allowEmailMatch) { 5074 sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME); 5075 } 5076 sb.append("))"); 5077 } 5078 5079 /** 5080 * Inserts an argument at the beginning of the selection arg list. 5081 */ 5082 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 5083 if (selectionArgs == null) { 5084 return new String[] {arg}; 5085 } else { 5086 int newLength = selectionArgs.length + 1; 5087 String[] newSelectionArgs = new String[newLength]; 5088 newSelectionArgs[0] = arg; 5089 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 5090 return newSelectionArgs; 5091 } 5092 } 5093 5094 private String[] appendProjectionArg(String[] projection, String arg) { 5095 if (projection == null) { 5096 return null; 5097 } 5098 final int length = projection.length; 5099 String[] newProjection = new String[length + 1]; 5100 System.arraycopy(projection, 0, newProjection, 0, length); 5101 newProjection[length] = arg; 5102 return newProjection; 5103 } 5104 5105 protected Account getDefaultAccount() { 5106 AccountManager accountManager = AccountManager.get(getContext()); 5107 try { 5108 Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE, 5109 new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult(); 5110 if (accounts != null && accounts.length > 0) { 5111 return accounts[0]; 5112 } 5113 } catch (Throwable e) { 5114 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 5115 } 5116 return null; 5117 } 5118 5119 protected boolean isWritableAccount(Account account) { 5120 IContentService contentService = ContentResolver.getContentService(); 5121 try { 5122 for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) { 5123 if (ContactsContract.AUTHORITY.equals(sync.authority) && 5124 account.type.equals(sync.accountType)) { 5125 return sync.supportsUploading(); 5126 } 5127 } 5128 } catch (RemoteException e) { 5129 Log.e(TAG, "Could not acquire sync adapter types"); 5130 } 5131 return false; 5132 } 5133 5134 /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter, 5135 boolean defaultValue) { 5136 5137 // Manually parse the query, which is much faster than calling uri.getQueryParameter 5138 String query = uri.getEncodedQuery(); 5139 if (query == null) { 5140 return defaultValue; 5141 } 5142 5143 int index = query.indexOf(parameter); 5144 if (index == -1) { 5145 return defaultValue; 5146 } 5147 5148 index += parameter.length(); 5149 5150 return !matchQueryParameter(query, index, "=0", false) 5151 && !matchQueryParameter(query, index, "=false", true); 5152 } 5153 5154 private static boolean matchQueryParameter(String query, int index, String value, 5155 boolean ignoreCase) { 5156 int length = value.length(); 5157 return query.regionMatches(ignoreCase, index, value, 0, length) 5158 && (query.length() == index + length || query.charAt(index + length) == '&'); 5159 } 5160 5161 /** 5162 * A fast re-implementation of {@link Uri#getQueryParameter} 5163 */ 5164 /* package */ static String getQueryParameter(Uri uri, String parameter) { 5165 String query = uri.getEncodedQuery(); 5166 if (query == null) { 5167 return null; 5168 } 5169 5170 int queryLength = query.length(); 5171 int parameterLength = parameter.length(); 5172 5173 String value; 5174 int index = 0; 5175 while (true) { 5176 index = query.indexOf(parameter, index); 5177 if (index == -1) { 5178 return null; 5179 } 5180 5181 index += parameterLength; 5182 5183 if (queryLength == index) { 5184 return null; 5185 } 5186 5187 if (query.charAt(index) == '=') { 5188 index++; 5189 break; 5190 } 5191 } 5192 5193 int ampIndex = query.indexOf('&', index); 5194 if (ampIndex == -1) { 5195 value = query.substring(index); 5196 } else { 5197 value = query.substring(index, ampIndex); 5198 } 5199 5200 return Uri.decode(value); 5201 } 5202 5203 private void bindString(SQLiteStatement stmt, int index, String value) { 5204 if (value == null) { 5205 stmt.bindNull(index); 5206 } else { 5207 stmt.bindString(index, value); 5208 } 5209 } 5210 5211 private void bindLong(SQLiteStatement stmt, int index, Number value) { 5212 if (value == null) { 5213 stmt.bindNull(index); 5214 } else { 5215 stmt.bindLong(index, value.longValue()); 5216 } 5217 } 5218} 5219