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