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