ContactsProvider2.java revision 4da1b8ded4435a3392bac3511c67182015e0953f
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 1623 final Context context = getContext(); 1624 mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper(); 1625 mGlobalSearchSupport = new GlobalSearchSupport(this); 1626 mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport); 1627 mContactAggregator = new ContactAggregator(this, mDbHelper); 1628 mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); 1629 1630 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 1631 1632 mSetPrimaryStatement = db.compileStatement( 1633 "UPDATE " + Tables.DATA + 1634 " SET " + Data.IS_PRIMARY + "=(_id=?)" + 1635 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1636 " AND " + Data.RAW_CONTACT_ID + "=?"); 1637 1638 mSetSuperPrimaryStatement = db.compileStatement( 1639 "UPDATE " + Tables.DATA + 1640 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" + 1641 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1642 " AND " + Data.RAW_CONTACT_ID + " IN (" + 1643 "SELECT " + RawContacts._ID + 1644 " FROM " + Tables.RAW_CONTACTS + 1645 " WHERE " + RawContacts.CONTACT_ID + " =(" + 1646 "SELECT " + RawContacts.CONTACT_ID + 1647 " FROM " + Tables.RAW_CONTACTS + 1648 " WHERE " + RawContacts._ID + "=?))"); 1649 1650 mContactsLastTimeContactedUpdate = db.compileStatement( 1651 "UPDATE " + Tables.CONTACTS + 1652 " SET " + Contacts.LAST_TIME_CONTACTED + "=? " + 1653 "WHERE " + Contacts._ID + "=?"); 1654 1655 mRawContactDisplayNameUpdate = db.compileStatement( 1656 "UPDATE " + Tables.RAW_CONTACTS + 1657 " SET " + RawContactsColumns.DISPLAY_NAME + "=?," 1658 + RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" + 1659 " WHERE " + RawContacts._ID + "=?"); 1660 1661 mLastStatusUpdate = db.compileStatement( 1662 "UPDATE " + Tables.CONTACTS + 1663 " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" + 1664 "(SELECT " + DataColumns.CONCRETE_ID + 1665 " FROM " + Tables.STATUS_UPDATES + 1666 " JOIN " + Tables.DATA + 1667 " ON (" + StatusUpdatesColumns.DATA_ID + "=" 1668 + DataColumns.CONCRETE_ID + ")" + 1669 " JOIN " + Tables.RAW_CONTACTS + 1670 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" 1671 + RawContactsColumns.CONCRETE_ID + ")" + 1672 " WHERE " + RawContacts.CONTACT_ID + "=?" + 1673 " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC," 1674 + StatusUpdates.STATUS + 1675 " LIMIT 1)" + 1676 " WHERE " + ContactsColumns.CONCRETE_ID + "=?"); 1677 1678 final Locale locale = Locale.getDefault(); 1679 mNameSplitter = new NameSplitter( 1680 context.getString(com.android.internal.R.string.common_name_prefixes), 1681 context.getString(com.android.internal.R.string.common_last_name_prefixes), 1682 context.getString(com.android.internal.R.string.common_name_suffixes), 1683 context.getString(com.android.internal.R.string.common_name_conjunctions), 1684 locale); 1685 mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter); 1686 mPostalSplitter = new PostalSplitter(locale); 1687 1688 mNameLookupInsert = db.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "(" 1689 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + "," 1690 + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME 1691 + ") VALUES (?,?,?,?)"); 1692 mNameLookupDelete = db.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE " 1693 + NameLookupColumns.DATA_ID + "=?"); 1694 1695 mStatusUpdateInsert = db.compileStatement( 1696 "INSERT INTO " + Tables.STATUS_UPDATES + "(" 1697 + StatusUpdatesColumns.DATA_ID + ", " 1698 + StatusUpdates.STATUS + "," 1699 + StatusUpdates.STATUS_RES_PACKAGE + "," 1700 + StatusUpdates.STATUS_ICON + "," 1701 + StatusUpdates.STATUS_LABEL + ")" + 1702 " VALUES (?,?,?,?,?)"); 1703 1704 mStatusUpdateReplace = db.compileStatement( 1705 "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "(" 1706 + StatusUpdatesColumns.DATA_ID + ", " 1707 + StatusUpdates.STATUS_TIMESTAMP + "," 1708 + StatusUpdates.STATUS + "," 1709 + StatusUpdates.STATUS_RES_PACKAGE + "," 1710 + StatusUpdates.STATUS_ICON + "," 1711 + StatusUpdates.STATUS_LABEL + ")" + 1712 " VALUES (?,?,?,?,?,?)"); 1713 1714 mStatusUpdateAutoTimestamp = db.compileStatement( 1715 "UPDATE " + Tables.STATUS_UPDATES + 1716 " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?," 1717 + StatusUpdates.STATUS + "=?" + 1718 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?" 1719 + " AND " + StatusUpdates.STATUS + "!=?"); 1720 1721 mStatusAttributionUpdate = db.compileStatement( 1722 "UPDATE " + Tables.STATUS_UPDATES + 1723 " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?," 1724 + StatusUpdates.STATUS_ICON + "=?," 1725 + StatusUpdates.STATUS_LABEL + "=?" + 1726 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); 1727 1728 mStatusUpdateDelete = db.compileStatement( 1729 "DELETE FROM " + Tables.STATUS_UPDATES + 1730 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); 1731 1732 mDataRowHandlers = new HashMap<String, DataRowHandler>(); 1733 1734 mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler()); 1735 mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE, 1736 new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL)); 1737 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler( 1738 StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL)); 1739 mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler()); 1740 mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler()); 1741 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler()); 1742 mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE, 1743 new StructuredNameRowHandler(mNameSplitter)); 1744 mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, 1745 new StructuredPostalRowHandler(mPostalSplitter)); 1746 mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler()); 1747 mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler()); 1748 1749 if (isLegacyContactImportNeeded()) { 1750 importLegacyContactsAsync(); 1751 } 1752 1753 verifyAccounts(); 1754 1755 mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); 1756 mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE); 1757 mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE); 1758 mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE); 1759 mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE); 1760 mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); 1761 preloadNicknameBloomFilter(); 1762 return (db != null); 1763 } 1764 1765 protected void verifyAccounts() { 1766 AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false); 1767 onAccountsUpdated(AccountManager.get(getContext()).getAccounts()); 1768 } 1769 1770 /* Visible for testing */ 1771 @Override 1772 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 1773 return ContactsDatabaseHelper.getInstance(context); 1774 } 1775 1776 /* package */ NameSplitter getNameSplitter() { 1777 return mNameSplitter; 1778 } 1779 1780 protected boolean isLegacyContactImportNeeded() { 1781 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1782 return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION; 1783 } 1784 1785 protected LegacyContactImporter getLegacyContactImporter() { 1786 return new LegacyContactImporter(getContext(), this); 1787 } 1788 1789 /** 1790 * Imports legacy contacts in a separate thread. As long as the import process is running 1791 * all other access to the contacts is blocked. 1792 */ 1793 private void importLegacyContactsAsync() { 1794 mAccessLatch = new CountDownLatch(1); 1795 1796 Thread importThread = new Thread("LegacyContactImport") { 1797 @Override 1798 public void run() { 1799 if (importLegacyContacts()) { 1800 // TODO aggregate all newly added raw contacts 1801 1802 /* 1803 * When the import process is done, we can unlock the provider and 1804 * start aggregating the imported contacts asynchronously. 1805 */ 1806 mAccessLatch.countDown(); 1807 mAccessLatch = null; 1808 } 1809 } 1810 }; 1811 1812 importThread.start(); 1813 } 1814 1815 private boolean importLegacyContacts() { 1816 LegacyContactImporter importer = getLegacyContactImporter(); 1817 if (importLegacyContacts(importer)) { 1818 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1819 Editor editor = prefs.edit(); 1820 editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION); 1821 editor.commit(); 1822 return true; 1823 } else { 1824 return false; 1825 } 1826 } 1827 1828 /* Visible for testing */ 1829 /* package */ boolean importLegacyContacts(LegacyContactImporter importer) { 1830 boolean aggregatorEnabled = mContactAggregator.isEnabled(); 1831 mContactAggregator.setEnabled(false); 1832 try { 1833 importer.importContacts(); 1834 mContactAggregator.setEnabled(aggregatorEnabled); 1835 return true; 1836 } catch (Throwable e) { 1837 Log.e(TAG, "Legacy contact import failed", e); 1838 return false; 1839 } 1840 } 1841 1842 /** 1843 * Wipes all data from the contacts database. 1844 */ 1845 /* package */ void wipeData() { 1846 mDbHelper.wipeData(); 1847 } 1848 1849 /** 1850 * While importing and aggregating contacts, this content provider will 1851 * block all attempts to change contacts data. In particular, it will hold 1852 * up all contact syncs. As soon as the import process is complete, all 1853 * processes waiting to write to the provider are unblocked and can proceed 1854 * to compete for the database transaction monitor. 1855 */ 1856 private void waitForAccess() { 1857 CountDownLatch latch = mAccessLatch; 1858 if (latch != null) { 1859 while (true) { 1860 try { 1861 latch.await(); 1862 mAccessLatch = null; 1863 return; 1864 } catch (InterruptedException e) { 1865 Thread.currentThread().interrupt(); 1866 } 1867 } 1868 } 1869 } 1870 1871 @Override 1872 public Uri insert(Uri uri, ContentValues values) { 1873 waitForAccess(); 1874 return super.insert(uri, values); 1875 } 1876 1877 @Override 1878 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1879 waitForAccess(); 1880 return super.update(uri, values, selection, selectionArgs); 1881 } 1882 1883 @Override 1884 public int delete(Uri uri, String selection, String[] selectionArgs) { 1885 waitForAccess(); 1886 return super.delete(uri, selection, selectionArgs); 1887 } 1888 1889 @Override 1890 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1891 throws OperationApplicationException { 1892 waitForAccess(); 1893 return super.applyBatch(operations); 1894 } 1895 1896 @Override 1897 protected void onBeginTransaction() { 1898 if (VERBOSE_LOGGING) { 1899 Log.v(TAG, "onBeginTransaction"); 1900 } 1901 super.onBeginTransaction(); 1902 mContactAggregator.clearPendingAggregations(); 1903 clearTransactionalChanges(); 1904 } 1905 1906 private void clearTransactionalChanges() { 1907 mInsertedRawContacts.clear(); 1908 mUpdatedRawContacts.clear(); 1909 mUpdatedSyncStates.clear(); 1910 mDirtyRawContacts.clear(); 1911 } 1912 1913 @Override 1914 protected void beforeTransactionCommit() { 1915 1916 if (VERBOSE_LOGGING) { 1917 Log.v(TAG, "beforeTransactionCommit"); 1918 } 1919 super.beforeTransactionCommit(); 1920 flushTransactionalChanges(); 1921 mContactAggregator.aggregateInTransaction(mDb); 1922 if (mVisibleTouched) { 1923 mVisibleTouched = false; 1924 mDbHelper.updateAllVisible(); 1925 } 1926 } 1927 1928 private void flushTransactionalChanges() { 1929 if (VERBOSE_LOGGING) { 1930 Log.v(TAG, "flushTransactionChanges"); 1931 } 1932 1933 for (long rawContactId : mInsertedRawContacts.keySet()) { 1934 updateRawContactDisplayName(mDb, rawContactId); 1935 mContactAggregator.onRawContactInsert(mDb, rawContactId); 1936 } 1937 1938 if (!mDirtyRawContacts.isEmpty()) { 1939 mSb.setLength(0); 1940 mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL); 1941 appendIds(mSb, mDirtyRawContacts); 1942 mSb.append(")"); 1943 mDb.execSQL(mSb.toString()); 1944 } 1945 1946 if (!mUpdatedRawContacts.isEmpty()) { 1947 mSb.setLength(0); 1948 mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL); 1949 appendIds(mSb, mUpdatedRawContacts); 1950 mSb.append(")"); 1951 mDb.execSQL(mSb.toString()); 1952 } 1953 1954 for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) { 1955 long id = entry.getKey(); 1956 mDbHelper.getSyncState().update(mDb, id, entry.getValue()); 1957 } 1958 1959 clearTransactionalChanges(); 1960 } 1961 1962 /** 1963 * Appends comma separated ids. 1964 * @param ids Should not be empty 1965 */ 1966 private void appendIds(StringBuilder sb, HashSet<Long> ids) { 1967 for (long id : ids) { 1968 sb.append(id).append(','); 1969 } 1970 1971 sb.setLength(sb.length() - 1); // Yank the last comma 1972 } 1973 1974 @Override 1975 protected void notifyChange() { 1976 notifyChange(mSyncToNetwork); 1977 mSyncToNetwork = false; 1978 } 1979 1980 protected void notifyChange(boolean syncToNetwork) { 1981 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, 1982 syncToNetwork); 1983 } 1984 1985 private boolean isNewRawContact(long rawContactId) { 1986 return mInsertedRawContacts.containsKey(rawContactId); 1987 } 1988 1989 private DataRowHandler getDataRowHandler(final String mimeType) { 1990 DataRowHandler handler = mDataRowHandlers.get(mimeType); 1991 if (handler == null) { 1992 handler = new CustomDataRowHandler(mimeType); 1993 mDataRowHandlers.put(mimeType, handler); 1994 } 1995 return handler; 1996 } 1997 1998 @Override 1999 protected Uri insertInTransaction(Uri uri, ContentValues values) { 2000 if (VERBOSE_LOGGING) { 2001 Log.v(TAG, "insertInTransaction: " + uri + " " + values); 2002 } 2003 2004 final boolean callerIsSyncAdapter = 2005 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2006 2007 final int match = sUriMatcher.match(uri); 2008 long id = 0; 2009 2010 switch (match) { 2011 case SYNCSTATE: 2012 id = mDbHelper.getSyncState().insert(mDb, values); 2013 break; 2014 2015 case CONTACTS: { 2016 insertContact(values); 2017 break; 2018 } 2019 2020 case RAW_CONTACTS: { 2021 id = insertRawContact(uri, values); 2022 mSyncToNetwork |= !callerIsSyncAdapter; 2023 break; 2024 } 2025 2026 case RAW_CONTACTS_DATA: { 2027 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 2028 id = insertData(values, callerIsSyncAdapter); 2029 mSyncToNetwork |= !callerIsSyncAdapter; 2030 break; 2031 } 2032 2033 case DATA: { 2034 id = insertData(values, callerIsSyncAdapter); 2035 mSyncToNetwork |= !callerIsSyncAdapter; 2036 break; 2037 } 2038 2039 case GROUPS: { 2040 id = insertGroup(uri, values, callerIsSyncAdapter); 2041 mSyncToNetwork |= !callerIsSyncAdapter; 2042 break; 2043 } 2044 2045 case SETTINGS: { 2046 id = insertSettings(uri, values); 2047 mSyncToNetwork |= !callerIsSyncAdapter; 2048 break; 2049 } 2050 2051 case STATUS_UPDATES: { 2052 id = insertStatusUpdate(values); 2053 break; 2054 } 2055 2056 default: 2057 mSyncToNetwork = true; 2058 return mLegacyApiSupport.insert(uri, values); 2059 } 2060 2061 if (id < 0) { 2062 return null; 2063 } 2064 2065 return ContentUris.withAppendedId(uri, id); 2066 } 2067 2068 /** 2069 * If account is non-null then store it in the values. If the account is already 2070 * specified in the values then it must be consistent with the account, if it is non-null. 2071 * @param uri the ContentValues to read from and update 2072 * @param values the explicitly provided Account 2073 * @return false if the parameters are inconsistent 2074 */ 2075 private boolean resolveAccount(Uri uri, ContentValues values) { 2076 String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 2077 String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 2078 2079 if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) { 2080 accountName = null; 2081 accountType = null; 2082 } 2083 2084 String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME); 2085 String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 2086 2087 if (TextUtils.isEmpty(valueAccountName) && TextUtils.isEmpty(valueAccountType)) { 2088 values.put(RawContacts.ACCOUNT_NAME, accountName); 2089 values.put(RawContacts.ACCOUNT_TYPE, accountType); 2090 } else { 2091 if (accountName != null && !accountName.equals(valueAccountName)) { 2092 return false; 2093 } 2094 2095 if (accountType != null && !accountType.equals(valueAccountType)) { 2096 return false; 2097 } 2098 2099 accountName = valueAccountName; 2100 accountType = valueAccountType; 2101 } 2102 2103 if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) { 2104 mAccount = null; 2105 return true; 2106 } 2107 2108 if (mAccount == null 2109 || !mAccount.name.equals(accountName) 2110 || !mAccount.type.equals(accountType)) { 2111 mAccount = new Account(accountName, accountType); 2112 } 2113 2114 return true; 2115 } 2116 2117 /** 2118 * Inserts an item in the contacts table 2119 * 2120 * @param values the values for the new row 2121 * @return the row ID of the newly created row 2122 */ 2123 private long insertContact(ContentValues values) { 2124 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 2125 } 2126 2127 /** 2128 * Inserts an item in the contacts table 2129 * 2130 * @param uri the values for the new row 2131 * @param values the account this contact should be associated with. may be null. 2132 * @return the row ID of the newly created row 2133 */ 2134 private long insertRawContact(Uri uri, ContentValues values) { 2135 mValues.clear(); 2136 mValues.putAll(values); 2137 mValues.putNull(RawContacts.CONTACT_ID); 2138 2139 if (!resolveAccount(uri, mValues)) { 2140 return -1; 2141 } 2142 2143 if (values.containsKey(RawContacts.DELETED) 2144 && values.getAsInteger(RawContacts.DELETED) != 0) { 2145 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2146 } 2147 2148 long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues); 2149 mContactAggregator.markNewForAggregation(rawContactId); 2150 2151 // Trigger creation of a Contact based on this RawContact at the end of transaction 2152 mInsertedRawContacts.put(rawContactId, mAccount); 2153 2154 return rawContactId; 2155 } 2156 2157 /** 2158 * Inserts an item in the data table 2159 * 2160 * @param values the values for the new row 2161 * @return the row ID of the newly created row 2162 */ 2163 private long insertData(ContentValues values, boolean callerIsSyncAdapter) { 2164 long id = 0; 2165 mValues.clear(); 2166 mValues.putAll(values); 2167 2168 long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 2169 2170 // Replace package with internal mapping 2171 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 2172 if (packageName != null) { 2173 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2174 } 2175 mValues.remove(Data.RES_PACKAGE); 2176 2177 // Replace mimetype with internal mapping 2178 final String mimeType = mValues.getAsString(Data.MIMETYPE); 2179 if (TextUtils.isEmpty(mimeType)) { 2180 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 2181 } 2182 2183 mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType)); 2184 mValues.remove(Data.MIMETYPE); 2185 2186 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2187 id = rowHandler.insert(mDb, rawContactId, mValues); 2188 if (!callerIsSyncAdapter) { 2189 setRawContactDirty(rawContactId); 2190 } 2191 mUpdatedRawContacts.add(rawContactId); 2192 2193 if (rowHandler.isAggregationRequired()) { 2194 triggerAggregation(rawContactId); 2195 } 2196 return id; 2197 } 2198 2199 private void triggerAggregation(long rawContactId) { 2200 if (!mContactAggregator.isEnabled()) { 2201 return; 2202 } 2203 2204 int aggregationMode = mDbHelper.getAggregationMode(rawContactId); 2205 switch (aggregationMode) { 2206 case RawContacts.AGGREGATION_MODE_DISABLED: 2207 break; 2208 2209 case RawContacts.AGGREGATION_MODE_DEFAULT: { 2210 mContactAggregator.markForAggregation(rawContactId); 2211 break; 2212 } 2213 2214 case RawContacts.AGGREGATION_MODE_SUSPENDED: { 2215 long contactId = mDbHelper.getContactId(rawContactId); 2216 2217 if (contactId != 0) { 2218 mContactAggregator.updateAggregateData(contactId); 2219 } 2220 break; 2221 } 2222 2223 case RawContacts.AGGREGATION_MODE_IMMEDIATE: { 2224 long contactId = mDbHelper.getContactId(rawContactId); 2225 mContactAggregator.aggregateContact(mDb, rawContactId, contactId); 2226 break; 2227 } 2228 } 2229 } 2230 2231 /** 2232 * Returns the group id of the group with sourceId and the same account as rawContactId. 2233 * If the group doesn't already exist then it is first created, 2234 * @param db SQLiteDatabase to use for this operation 2235 * @param rawContactId the contact this group is associated with 2236 * @param sourceId the sourceIf of the group to query or create 2237 * @return the group id of the existing or created group 2238 * @throws IllegalArgumentException if the contact is not associated with an account 2239 * @throws IllegalStateException if a group needs to be created but the creation failed 2240 */ 2241 private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId, 2242 Account account) { 2243 2244 if (account == null) { 2245 mSelectionArgs1[0] = String.valueOf(rawContactId); 2246 Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, 2247 RawContacts._ID + "=?", mSelectionArgs1, null, null, null); 2248 try { 2249 if (c.moveToFirst()) { 2250 String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME); 2251 String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); 2252 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 2253 account = new Account(accountName, accountType); 2254 } 2255 } 2256 } finally { 2257 c.close(); 2258 } 2259 } 2260 2261 if (account == null) { 2262 throw new IllegalArgumentException("if the groupmembership only " 2263 + "has a sourceid the the contact must be associated with " 2264 + "an account"); 2265 } 2266 2267 ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId); 2268 if (entries == null) { 2269 entries = new ArrayList<GroupIdCacheEntry>(1); 2270 mGroupIdCache.put(sourceId, entries); 2271 } 2272 2273 int count = entries.size(); 2274 for (int i = 0; i < count; i++) { 2275 GroupIdCacheEntry entry = entries.get(i); 2276 if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) { 2277 return entry.groupId; 2278 } 2279 } 2280 2281 GroupIdCacheEntry entry = new GroupIdCacheEntry(); 2282 entry.accountName = account.name; 2283 entry.accountType = account.type; 2284 entry.sourceId = sourceId; 2285 entries.add(0, entry); 2286 2287 // look up the group that contains this sourceId and has the same account name and type 2288 // as the contact refered to by rawContactId 2289 Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID}, 2290 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID, 2291 new String[]{sourceId, account.name, account.type}, null, null, null); 2292 try { 2293 if (c.moveToFirst()) { 2294 entry.groupId = c.getLong(0); 2295 } else { 2296 ContentValues groupValues = new ContentValues(); 2297 groupValues.put(Groups.ACCOUNT_NAME, account.name); 2298 groupValues.put(Groups.ACCOUNT_TYPE, account.type); 2299 groupValues.put(Groups.SOURCE_ID, sourceId); 2300 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues); 2301 if (groupId < 0) { 2302 throw new IllegalStateException("unable to create a new group with " 2303 + "this sourceid: " + groupValues); 2304 } 2305 entry.groupId = groupId; 2306 } 2307 } finally { 2308 c.close(); 2309 } 2310 2311 return entry.groupId; 2312 } 2313 2314 private interface DisplayNameQuery { 2315 public static final String RAW_SQL = 2316 "SELECT " 2317 + DataColumns.MIMETYPE_ID + "," 2318 + Data.IS_PRIMARY + "," 2319 + Data.DATA1 + "," 2320 + Organization.TITLE + 2321 " FROM " + Tables.DATA + 2322 " WHERE " + Data.RAW_CONTACT_ID + "=?" + 2323 " AND (" + Data.DATA1 + " NOT NULL OR " + 2324 Organization.TITLE + " NOT NULL)"; 2325 2326 public static final int MIMETYPE = 0; 2327 public static final int IS_PRIMARY = 1; 2328 public static final int DATA = 2; 2329 public static final int TITLE = 3; 2330 } 2331 2332 /** 2333 * Updates a raw contact display name based on data rows, e.g. structured name, 2334 * organization, email etc. 2335 */ 2336 private void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 2337 String bestDisplayName = null; 2338 int bestDisplayNameSource = DisplayNameSources.UNDEFINED; 2339 2340 mSelectionArgs1[0] = String.valueOf(rawContactId); 2341 Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1); 2342 try { 2343 while (c.moveToNext()) { 2344 int mimeType = c.getInt(DisplayNameQuery.MIMETYPE); 2345 2346 // Display name is at DATA1 in all type. This is ensured in the 2347 // constructor. 2348 mCharArrayBuffer.sizeCopied = 0; 2349 c.copyStringToBuffer(DisplayNameQuery.DATA, mCharArrayBuffer); 2350 if (mimeType == mMimeTypeIdOrganization && mCharArrayBuffer.sizeCopied == 0) { 2351 c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer); 2352 } 2353 2354 if (mCharArrayBuffer.sizeCopied != 0) { 2355 int source = getDisplayNameSource(mimeType); 2356 if (source > bestDisplayNameSource) { 2357 bestDisplayNameSource = source; 2358 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2359 mCharArrayBuffer.sizeCopied); 2360 } else if (source == bestDisplayNameSource 2361 && source != DisplayNameSources.UNDEFINED) { 2362 if (mimeType == mMimeTypeIdStructuredName 2363 || c.getInt(DisplayNameQuery.IS_PRIMARY) != 0) { 2364 bestDisplayNameSource = source; 2365 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2366 mCharArrayBuffer.sizeCopied); 2367 } 2368 } 2369 } 2370 } 2371 2372 } finally { 2373 c.close(); 2374 } 2375 2376 setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource); 2377 } 2378 2379 private int getDisplayNameSource(int mimeTypeId) { 2380 if (mimeTypeId == mMimeTypeIdStructuredName) { 2381 return DisplayNameSources.STRUCTURED_NAME; 2382 } else if (mimeTypeId == mMimeTypeIdEmail) { 2383 return DisplayNameSources.EMAIL; 2384 } else if (mimeTypeId == mMimeTypeIdPhone) { 2385 return DisplayNameSources.PHONE; 2386 } else if (mimeTypeId == mMimeTypeIdOrganization) { 2387 return DisplayNameSources.ORGANIZATION; 2388 } else if (mimeTypeId == mMimeTypeIdNickname) { 2389 return DisplayNameSources.NICKNAME; 2390 } else { 2391 return DisplayNameSources.UNDEFINED; 2392 } 2393 } 2394 2395 /** 2396 * Delete data row by row so that fixing of primaries etc work correctly. 2397 */ 2398 private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { 2399 int count = 0; 2400 2401 // Note that the query will return data according to the access restrictions, 2402 // so we don't need to worry about deleting data we don't have permission to read. 2403 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null); 2404 try { 2405 while(c.moveToNext()) { 2406 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 2407 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 2408 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2409 count += rowHandler.delete(mDb, c); 2410 if (!callerIsSyncAdapter) { 2411 setRawContactDirty(rawContactId); 2412 if (rowHandler.isAggregationRequired()) { 2413 triggerAggregation(rawContactId); 2414 } 2415 } 2416 } 2417 } finally { 2418 c.close(); 2419 } 2420 2421 return count; 2422 } 2423 2424 /** 2425 * Delete a data row provided that it is one of the allowed mime types. 2426 */ 2427 public int deleteData(long dataId, String[] allowedMimeTypes) { 2428 2429 // Note that the query will return data according to the access restrictions, 2430 // so we don't need to worry about deleting data we don't have permission to read. 2431 mSelectionArgs1[0] = String.valueOf(dataId); 2432 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?", 2433 mSelectionArgs1, null); 2434 2435 try { 2436 if (!c.moveToFirst()) { 2437 return 0; 2438 } 2439 2440 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 2441 boolean valid = false; 2442 for (int i = 0; i < allowedMimeTypes.length; i++) { 2443 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 2444 valid = true; 2445 break; 2446 } 2447 } 2448 2449 if (!valid) { 2450 throw new IllegalArgumentException("Data type mismatch: expected " 2451 + Lists.newArrayList(allowedMimeTypes)); 2452 } 2453 2454 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2455 int count = rowHandler.delete(mDb, c); 2456 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 2457 if (rowHandler.isAggregationRequired()) { 2458 triggerAggregation(rawContactId); 2459 } 2460 return count; 2461 } finally { 2462 c.close(); 2463 } 2464 } 2465 2466 /** 2467 * Inserts an item in the groups table 2468 */ 2469 private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 2470 mValues.clear(); 2471 mValues.putAll(values); 2472 2473 if (!resolveAccount(uri, mValues)) { 2474 return -1; 2475 } 2476 2477 // Replace package with internal mapping 2478 final String packageName = mValues.getAsString(Groups.RES_PACKAGE); 2479 if (packageName != null) { 2480 mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2481 } 2482 mValues.remove(Groups.RES_PACKAGE); 2483 2484 if (!callerIsSyncAdapter) { 2485 mValues.put(Groups.DIRTY, 1); 2486 } 2487 2488 long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues); 2489 2490 if (mValues.containsKey(Groups.GROUP_VISIBLE)) { 2491 mVisibleTouched = true; 2492 } 2493 2494 return result; 2495 } 2496 2497 private long insertSettings(Uri uri, ContentValues values) { 2498 final long id = mDb.insert(Tables.SETTINGS, null, values); 2499 2500 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2501 mVisibleTouched = true; 2502 } 2503 2504 return id; 2505 } 2506 2507 /** 2508 * Inserts a status update. 2509 */ 2510 public long insertStatusUpdate(ContentValues values) { 2511 final String handle = values.getAsString(StatusUpdates.IM_HANDLE); 2512 final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL); 2513 String customProtocol = null; 2514 2515 if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) { 2516 customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL); 2517 if (TextUtils.isEmpty(customProtocol)) { 2518 throw new IllegalArgumentException( 2519 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 2520 } 2521 } 2522 2523 long rawContactId = -1; 2524 long contactId = -1; 2525 Long dataId = values.getAsLong(StatusUpdates.DATA_ID); 2526 mSb.setLength(0); 2527 if (dataId != null) { 2528 // Lookup the contact info for the given data row. 2529 2530 mSb.append(Tables.DATA + "." + Data._ID + "="); 2531 mSb.append(dataId); 2532 } else { 2533 // Lookup the data row to attach this presence update to 2534 2535 if (TextUtils.isEmpty(handle) || protocol == null) { 2536 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 2537 } 2538 2539 // TODO: generalize to allow other providers to match against email 2540 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 2541 2542 if (matchEmail) { 2543 2544 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise 2545 // the "OR" conjunction confuses it and it switches to a full scan of 2546 // the raw_contacts table. 2547 2548 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same 2549 // column - Data.DATA1 2550 mSb.append(DataColumns.MIMETYPE_ID + " IN (") 2551 .append(mMimeTypeIdEmail) 2552 .append(",") 2553 .append(mMimeTypeIdIm) 2554 .append(")" + " AND " + Data.DATA1 + "="); 2555 DatabaseUtils.appendEscapedSQLString(mSb, handle); 2556 mSb.append(" AND ((" + DataColumns.MIMETYPE_ID + "=") 2557 .append(mMimeTypeIdIm) 2558 .append(" AND " + Im.PROTOCOL + "=") 2559 .append(protocol); 2560 if (customProtocol != null) { 2561 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "="); 2562 DatabaseUtils.appendEscapedSQLString(mSb, customProtocol); 2563 } 2564 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=") 2565 .append(mMimeTypeIdEmail) 2566 .append("))"); 2567 } else { 2568 mSb.append(DataColumns.MIMETYPE_ID + "=") 2569 .append(mMimeTypeIdIm) 2570 .append(" AND " + Im.PROTOCOL + "=") 2571 .append(protocol) 2572 .append(" AND " + Im.DATA + "="); 2573 DatabaseUtils.appendEscapedSQLString(mSb, handle); 2574 if (customProtocol != null) { 2575 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "="); 2576 DatabaseUtils.appendEscapedSQLString(mSb, customProtocol); 2577 } 2578 } 2579 2580 if (values.containsKey(StatusUpdates.DATA_ID)) { 2581 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=") 2582 .append(values.getAsLong(StatusUpdates.DATA_ID)); 2583 } 2584 } 2585 mSb.append(" AND ").append(getContactsRestrictions()); 2586 2587 Cursor cursor = null; 2588 try { 2589 cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 2590 mSb.toString(), null, null, null, 2591 Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID); 2592 if (cursor.moveToFirst()) { 2593 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 2594 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 2595 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 2596 } else { 2597 // No contact found, return a null URI 2598 return -1; 2599 } 2600 } finally { 2601 if (cursor != null) { 2602 cursor.close(); 2603 } 2604 } 2605 2606 if (values.containsKey(StatusUpdates.PRESENCE)) { 2607 if (customProtocol == null) { 2608 // We cannot allow a null in the custom protocol field, because SQLite3 does not 2609 // properly enforce uniqueness of null values 2610 customProtocol = ""; 2611 } 2612 2613 mValues.clear(); 2614 mValues.put(StatusUpdates.DATA_ID, dataId); 2615 mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 2616 mValues.put(PresenceColumns.CONTACT_ID, contactId); 2617 mValues.put(StatusUpdates.PROTOCOL, protocol); 2618 mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol); 2619 mValues.put(StatusUpdates.IM_HANDLE, handle); 2620 if (values.containsKey(StatusUpdates.IM_ACCOUNT)) { 2621 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT)); 2622 } 2623 mValues.put(StatusUpdates.PRESENCE, 2624 values.getAsString(StatusUpdates.PRESENCE)); 2625 2626 // Insert the presence update 2627 mDb.replace(Tables.PRESENCE, null, mValues); 2628 } 2629 2630 2631 if (values.containsKey(StatusUpdates.STATUS)) { 2632 String status = values.getAsString(StatusUpdates.STATUS); 2633 String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE); 2634 Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL); 2635 2636 if (TextUtils.isEmpty(resPackage) 2637 && (labelResource == null || labelResource == 0) 2638 && protocol != null) { 2639 labelResource = Im.getProtocolLabelResource(protocol); 2640 } 2641 2642 Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON); 2643 // TODO compute the default icon based on the protocol 2644 2645 if (TextUtils.isEmpty(status)) { 2646 mStatusUpdateDelete.bindLong(1, dataId); 2647 mStatusUpdateDelete.execute(); 2648 } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) { 2649 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP); 2650 mStatusUpdateReplace.bindLong(1, dataId); 2651 mStatusUpdateReplace.bindLong(2, timestamp); 2652 DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 3, status); 2653 DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 4, resPackage); 2654 DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 5, iconResource); 2655 DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 6, labelResource); 2656 mStatusUpdateReplace.execute(); 2657 } else { 2658 2659 try { 2660 mStatusUpdateInsert.bindLong(1, dataId); 2661 DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 2, status); 2662 DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 3, resPackage); 2663 DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 4, iconResource); 2664 DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 5, labelResource); 2665 mStatusUpdateInsert.executeInsert(); 2666 } catch (SQLiteConstraintException e) { 2667 // The row already exists - update it 2668 long timestamp = System.currentTimeMillis(); 2669 mStatusUpdateAutoTimestamp.bindLong(1, timestamp); 2670 DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 2, status); 2671 mStatusUpdateAutoTimestamp.bindLong(3, dataId); 2672 DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 4, status); 2673 mStatusUpdateAutoTimestamp.execute(); 2674 2675 DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 1, resPackage); 2676 DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 2, iconResource); 2677 DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 3, labelResource); 2678 mStatusAttributionUpdate.bindLong(4, dataId); 2679 mStatusAttributionUpdate.execute(); 2680 } 2681 } 2682 } 2683 2684 if (contactId != -1) { 2685 mLastStatusUpdate.bindLong(1, contactId); 2686 mLastStatusUpdate.bindLong(2, contactId); 2687 mLastStatusUpdate.execute(); 2688 } 2689 2690 return dataId; 2691 } 2692 2693 @Override 2694 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 2695 if (VERBOSE_LOGGING) { 2696 Log.v(TAG, "deleteInTransaction: " + uri); 2697 } 2698 flushTransactionalChanges(); 2699 final boolean callerIsSyncAdapter = 2700 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2701 final int match = sUriMatcher.match(uri); 2702 switch (match) { 2703 case SYNCSTATE: 2704 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 2705 2706 case SYNCSTATE_ID: 2707 String selectionWithId = 2708 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 2709 + (selection == null ? "" : " AND (" + selection + ")"); 2710 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); 2711 2712 case CONTACTS: { 2713 // TODO 2714 return 0; 2715 } 2716 2717 case CONTACTS_ID: { 2718 long contactId = ContentUris.parseId(uri); 2719 return deleteContact(contactId); 2720 } 2721 2722 case CONTACTS_LOOKUP: 2723 case CONTACTS_LOOKUP_ID: { 2724 final List<String> pathSegments = uri.getPathSegments(); 2725 final int segmentCount = pathSegments.size(); 2726 if (segmentCount < 3) { 2727 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 2728 } 2729 final String lookupKey = pathSegments.get(2); 2730 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 2731 return deleteContact(contactId); 2732 } 2733 2734 case RAW_CONTACTS: { 2735 int numDeletes = 0; 2736 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2737 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2738 try { 2739 while (c.moveToNext()) { 2740 final long rawContactId = c.getLong(0); 2741 numDeletes += deleteRawContact(rawContactId, callerIsSyncAdapter); 2742 } 2743 } finally { 2744 c.close(); 2745 } 2746 return numDeletes; 2747 } 2748 2749 case RAW_CONTACTS_ID: { 2750 final long rawContactId = ContentUris.parseId(uri); 2751 return deleteRawContact(rawContactId, callerIsSyncAdapter); 2752 } 2753 2754 case DATA: { 2755 mSyncToNetwork |= !callerIsSyncAdapter; 2756 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 2757 callerIsSyncAdapter); 2758 } 2759 2760 case DATA_ID: 2761 case PHONES_ID: 2762 case EMAILS_ID: 2763 case POSTALS_ID: { 2764 long dataId = ContentUris.parseId(uri); 2765 mSyncToNetwork |= !callerIsSyncAdapter; 2766 mSelectionArgs1[0] = String.valueOf(dataId); 2767 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); 2768 } 2769 2770 case GROUPS_ID: { 2771 mSyncToNetwork |= !callerIsSyncAdapter; 2772 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter); 2773 } 2774 2775 case GROUPS: { 2776 int numDeletes = 0; 2777 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 2778 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2779 try { 2780 while (c.moveToNext()) { 2781 numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter); 2782 } 2783 } finally { 2784 c.close(); 2785 } 2786 if (numDeletes > 0) { 2787 mSyncToNetwork |= !callerIsSyncAdapter; 2788 } 2789 return numDeletes; 2790 } 2791 2792 case SETTINGS: { 2793 mSyncToNetwork |= !callerIsSyncAdapter; 2794 return deleteSettings(uri, selection, selectionArgs); 2795 } 2796 2797 case STATUS_UPDATES: { 2798 return deleteStatusUpdates(selection, selectionArgs); 2799 } 2800 2801 default: { 2802 mSyncToNetwork = true; 2803 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 2804 } 2805 } 2806 } 2807 2808 private int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) { 2809 mGroupIdCache.clear(); 2810 final long groupMembershipMimetypeId = mDbHelper 2811 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 2812 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 2813 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 2814 + groupId, null); 2815 2816 try { 2817 if (callerIsSyncAdapter) { 2818 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 2819 } else { 2820 mValues.clear(); 2821 mValues.put(Groups.DELETED, 1); 2822 mValues.put(Groups.DIRTY, 1); 2823 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 2824 } 2825 } finally { 2826 mVisibleTouched = true; 2827 } 2828 } 2829 2830 private int deleteSettings(Uri uri, String selection, String[] selectionArgs) { 2831 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 2832 mVisibleTouched = true; 2833 return count; 2834 } 2835 2836 private int deleteContact(long contactId) { 2837 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2838 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null); 2839 try { 2840 while (c.moveToNext()) { 2841 long rawContactId = c.getLong(0); 2842 markRawContactAsDeleted(rawContactId); 2843 } 2844 } finally { 2845 c.close(); 2846 } 2847 2848 return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); 2849 } 2850 2851 public int deleteRawContact(long rawContactId, boolean callerIsSyncAdapter) { 2852 mContactAggregator.invalidateAggregationExceptionCache(); 2853 if (callerIsSyncAdapter) { 2854 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 2855 return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 2856 } else { 2857 mDbHelper.removeContactIfSingleton(rawContactId); 2858 return markRawContactAsDeleted(rawContactId); 2859 } 2860 } 2861 2862 private int deleteStatusUpdates(String selection, String[] selectionArgs) { 2863 // delete from both tables: presence and status_updates 2864 // TODO should account type/name be appended to the where clause? 2865 if (VERBOSE_LOGGING) { 2866 Log.v(TAG, "deleting data from status_updates for " + selection); 2867 } 2868 mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection), 2869 selectionArgs); 2870 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 2871 } 2872 2873 private int markRawContactAsDeleted(long rawContactId) { 2874 mSyncToNetwork = true; 2875 2876 mValues.clear(); 2877 mValues.put(RawContacts.DELETED, 1); 2878 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2879 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 2880 mValues.putNull(RawContacts.CONTACT_ID); 2881 mValues.put(RawContacts.DIRTY, 1); 2882 return updateRawContact(rawContactId, mValues); 2883 } 2884 2885 @Override 2886 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 2887 String[] selectionArgs) { 2888 if (VERBOSE_LOGGING) { 2889 Log.v(TAG, "updateInTransaction: " + uri); 2890 } 2891 2892 int count = 0; 2893 2894 final int match = sUriMatcher.match(uri); 2895 if (match == SYNCSTATE_ID && selection == null) { 2896 long rowId = ContentUris.parseId(uri); 2897 Object data = values.get(ContactsContract.SyncState.DATA); 2898 mUpdatedSyncStates.put(rowId, data); 2899 return 1; 2900 } 2901 flushTransactionalChanges(); 2902 final boolean callerIsSyncAdapter = 2903 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2904 switch(match) { 2905 case SYNCSTATE: 2906 return mDbHelper.getSyncState().update(mDb, values, 2907 appendAccountToSelection(uri, selection), selectionArgs); 2908 2909 case SYNCSTATE_ID: { 2910 selection = appendAccountToSelection(uri, selection); 2911 String selectionWithId = 2912 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 2913 + (selection == null ? "" : " AND (" + selection + ")"); 2914 return mDbHelper.getSyncState().update(mDb, values, 2915 selectionWithId, selectionArgs); 2916 } 2917 2918 case CONTACTS: { 2919 count = updateContactOptions(values, selection, selectionArgs); 2920 break; 2921 } 2922 2923 case CONTACTS_ID: { 2924 count = updateContactOptions(ContentUris.parseId(uri), values); 2925 break; 2926 } 2927 2928 case CONTACTS_LOOKUP: 2929 case CONTACTS_LOOKUP_ID: { 2930 final List<String> pathSegments = uri.getPathSegments(); 2931 final int segmentCount = pathSegments.size(); 2932 if (segmentCount < 3) { 2933 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 2934 } 2935 final String lookupKey = pathSegments.get(2); 2936 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 2937 count = updateContactOptions(contactId, values); 2938 break; 2939 } 2940 2941 case RAW_CONTACTS_DATA: { 2942 final String rawContactId = uri.getPathSegments().get(1); 2943 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") 2944 + (selection == null ? "" : " AND " + selection); 2945 2946 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); 2947 2948 break; 2949 } 2950 2951 case DATA: { 2952 count = updateData(uri, values, appendAccountToSelection(uri, selection), 2953 selectionArgs, callerIsSyncAdapter); 2954 if (count > 0) { 2955 mSyncToNetwork |= !callerIsSyncAdapter; 2956 } 2957 break; 2958 } 2959 2960 case DATA_ID: 2961 case PHONES_ID: 2962 case EMAILS_ID: 2963 case POSTALS_ID: { 2964 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); 2965 if (count > 0) { 2966 mSyncToNetwork |= !callerIsSyncAdapter; 2967 } 2968 break; 2969 } 2970 2971 case RAW_CONTACTS: { 2972 selection = appendAccountToSelection(uri, selection); 2973 count = updateRawContacts(values, selection, selectionArgs); 2974 break; 2975 } 2976 2977 case RAW_CONTACTS_ID: { 2978 long rawContactId = ContentUris.parseId(uri); 2979 if (selection != null) { 2980 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 2981 count = updateRawContacts(values, RawContacts._ID + "=?" 2982 + " AND(" + selection + ")", selectionArgs); 2983 } else { 2984 mSelectionArgs1[0] = String.valueOf(rawContactId); 2985 count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1); 2986 } 2987 break; 2988 } 2989 2990 case GROUPS: { 2991 count = updateGroups(uri, values, appendAccountToSelection(uri, selection), 2992 selectionArgs, callerIsSyncAdapter); 2993 if (count > 0) { 2994 mSyncToNetwork |= !callerIsSyncAdapter; 2995 } 2996 break; 2997 } 2998 2999 case GROUPS_ID: { 3000 long groupId = ContentUris.parseId(uri); 3001 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); 3002 String selectionWithId = Groups._ID + "=? " 3003 + (selection == null ? "" : " AND " + selection); 3004 count = updateGroups(uri, values, selectionWithId, selectionArgs, 3005 callerIsSyncAdapter); 3006 if (count > 0) { 3007 mSyncToNetwork |= !callerIsSyncAdapter; 3008 } 3009 break; 3010 } 3011 3012 case AGGREGATION_EXCEPTIONS: { 3013 count = updateAggregationException(mDb, values); 3014 break; 3015 } 3016 3017 case SETTINGS: { 3018 count = updateSettings(uri, values, selection, selectionArgs); 3019 mSyncToNetwork |= !callerIsSyncAdapter; 3020 break; 3021 } 3022 3023 case STATUS_UPDATES: { 3024 count = updateStatusUpdate(uri, values, selection, selectionArgs); 3025 break; 3026 } 3027 3028 default: { 3029 mSyncToNetwork = true; 3030 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 3031 } 3032 } 3033 3034 return count; 3035 } 3036 3037 private int updateStatusUpdate(Uri uri, ContentValues values, String selection, 3038 String[] selectionArgs) { 3039 // update status_updates table, if status is provided 3040 // TODO should account type/name be appended to the where clause? 3041 int updateCount = 0; 3042 ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values); 3043 if (settableValues.size() > 0) { 3044 updateCount = mDb.update(Tables.STATUS_UPDATES, 3045 settableValues, 3046 getWhereClauseForStatusUpdatesTable(selection), 3047 selectionArgs); 3048 } 3049 3050 // now update the Presence table 3051 settableValues = getSettableColumnsForPresenceTable(values); 3052 if (settableValues.size() > 0) { 3053 updateCount = mDb.update(Tables.PRESENCE, settableValues, 3054 selection, selectionArgs); 3055 } 3056 // TODO updateCount is not entirely a valid count of updated rows because 2 tables could 3057 // potentially get updated in this method. 3058 return updateCount; 3059 } 3060 3061 /** 3062 * Build a where clause to select the rows to be updated in status_updates table. 3063 */ 3064 private String getWhereClauseForStatusUpdatesTable(String selection) { 3065 mSb.setLength(0); 3066 mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE); 3067 mSb.append(selection); 3068 mSb.append(")"); 3069 return mSb.toString(); 3070 } 3071 3072 private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) { 3073 mValues.clear(); 3074 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values, 3075 StatusUpdates.STATUS); 3076 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values, 3077 StatusUpdates.STATUS_TIMESTAMP); 3078 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values, 3079 StatusUpdates.STATUS_RES_PACKAGE); 3080 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values, 3081 StatusUpdates.STATUS_LABEL); 3082 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values, 3083 StatusUpdates.STATUS_ICON); 3084 return mValues; 3085 } 3086 3087 private ContentValues getSettableColumnsForPresenceTable(ContentValues values) { 3088 mValues.clear(); 3089 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values, 3090 StatusUpdates.PRESENCE); 3091 return mValues; 3092 } 3093 3094 private int updateGroups(Uri uri, ContentValues values, String selectionWithId, 3095 String[] selectionArgs, boolean callerIsSyncAdapter) { 3096 3097 mGroupIdCache.clear(); 3098 3099 ContentValues updatedValues; 3100 if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) { 3101 updatedValues = mValues; 3102 updatedValues.clear(); 3103 updatedValues.putAll(values); 3104 updatedValues.put(Groups.DIRTY, 1); 3105 } else { 3106 updatedValues = values; 3107 } 3108 3109 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 3110 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 3111 mVisibleTouched = true; 3112 } 3113 if (updatedValues.containsKey(Groups.SHOULD_SYNC) 3114 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) { 3115 final long groupId = ContentUris.parseId(uri); 3116 mSelectionArgs1[0] = String.valueOf(groupId); 3117 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME, 3118 Groups.ACCOUNT_TYPE}, Groups._ID + "=?", mSelectionArgs1, null, 3119 null, null); 3120 String accountName; 3121 String accountType; 3122 try { 3123 while (c.moveToNext()) { 3124 accountName = c.getString(0); 3125 accountType = c.getString(1); 3126 if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 3127 Account account = new Account(accountName, accountType); 3128 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, 3129 new Bundle()); 3130 break; 3131 } 3132 } 3133 } finally { 3134 c.close(); 3135 } 3136 } 3137 return count; 3138 } 3139 3140 private int updateSettings(Uri uri, ContentValues values, String selection, 3141 String[] selectionArgs) { 3142 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 3143 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 3144 mVisibleTouched = true; 3145 } 3146 return count; 3147 } 3148 3149 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) { 3150 if (values.containsKey(RawContacts.CONTACT_ID)) { 3151 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 3152 "in content values. Contact IDs are assigned automatically"); 3153 } 3154 3155 int count = 0; 3156 Cursor cursor = mDb.query(mDbHelper.getRawContactView(), 3157 new String[] { RawContacts._ID }, selection, 3158 selectionArgs, null, null, null); 3159 try { 3160 while (cursor.moveToNext()) { 3161 long rawContactId = cursor.getLong(0); 3162 updateRawContact(rawContactId, values); 3163 count++; 3164 } 3165 } finally { 3166 cursor.close(); 3167 } 3168 3169 return count; 3170 } 3171 3172 private int updateRawContact(long rawContactId, ContentValues values) { 3173 final String selection = RawContacts._ID + " = " + rawContactId; 3174 final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED) 3175 && values.getAsInteger(RawContacts.DELETED) == 0); 3176 int previousDeleted = 0; 3177 String accountType = null; 3178 String accountName = null; 3179 if (requestUndoDelete) { 3180 Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection, 3181 null, null, null, null); 3182 try { 3183 if (cursor.moveToFirst()) { 3184 previousDeleted = cursor.getInt(RawContactsQuery.DELETED); 3185 accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE); 3186 accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME); 3187 } 3188 } finally { 3189 cursor.close(); 3190 } 3191 values.put(ContactsContract.RawContacts.AGGREGATION_MODE, 3192 ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT); 3193 } 3194 int count = mDb.update(Tables.RAW_CONTACTS, values, selection, null); 3195 if (count != 0) { 3196 if (values.containsKey(RawContacts.STARRED)) { 3197 mContactAggregator.updateStarred(rawContactId); 3198 } 3199 if (values.containsKey(RawContacts.SOURCE_ID)) { 3200 mContactAggregator.updateLookupKey(mDb, rawContactId); 3201 } 3202 if (requestUndoDelete && previousDeleted == 1) { 3203 // undo delete, needs aggregation again. 3204 mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType)); 3205 } 3206 } 3207 return count; 3208 } 3209 3210 private int updateData(Uri uri, ContentValues values, String selection, 3211 String[] selectionArgs, boolean callerIsSyncAdapter) { 3212 mValues.clear(); 3213 mValues.putAll(values); 3214 mValues.remove(Data._ID); 3215 mValues.remove(Data.RAW_CONTACT_ID); 3216 mValues.remove(Data.MIMETYPE); 3217 3218 String packageName = values.getAsString(Data.RES_PACKAGE); 3219 if (packageName != null) { 3220 mValues.remove(Data.RES_PACKAGE); 3221 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 3222 } 3223 3224 boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY); 3225 boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY); 3226 3227 // Remove primary or super primary values being set to 0. This is disallowed by the 3228 // content provider. 3229 if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { 3230 containsIsSuperPrimary = false; 3231 mValues.remove(Data.IS_SUPER_PRIMARY); 3232 } 3233 if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) { 3234 containsIsPrimary = false; 3235 mValues.remove(Data.IS_PRIMARY); 3236 } 3237 3238 int count = 0; 3239 3240 // Note that the query will return data according to the access restrictions, 3241 // so we don't need to worry about updating data we don't have permission to read. 3242 Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null); 3243 try { 3244 while(c.moveToNext()) { 3245 count += updateData(mValues, c, callerIsSyncAdapter); 3246 } 3247 } finally { 3248 c.close(); 3249 } 3250 3251 return count; 3252 } 3253 3254 private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { 3255 if (values.size() == 0) { 3256 return 0; 3257 } 3258 3259 final String mimeType = c.getString(DataUpdateQuery.MIMETYPE); 3260 DataRowHandler rowHandler = getDataRowHandler(mimeType); 3261 rowHandler.update(mDb, values, c, callerIsSyncAdapter); 3262 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 3263 if (rowHandler.isAggregationRequired()) { 3264 triggerAggregation(rawContactId); 3265 } 3266 3267 return 1; 3268 } 3269 3270 private int updateContactOptions(ContentValues values, String selection, 3271 String[] selectionArgs) { 3272 int count = 0; 3273 Cursor cursor = mDb.query(mDbHelper.getContactView(), 3274 new String[] { Contacts._ID }, selection, 3275 selectionArgs, null, null, null); 3276 try { 3277 while (cursor.moveToNext()) { 3278 long contactId = cursor.getLong(0); 3279 updateContactOptions(contactId, values); 3280 count++; 3281 } 3282 } finally { 3283 cursor.close(); 3284 } 3285 3286 return count; 3287 } 3288 3289 private int updateContactOptions(long contactId, ContentValues values) { 3290 3291 mValues.clear(); 3292 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 3293 values, Contacts.CUSTOM_RINGTONE); 3294 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 3295 values, Contacts.SEND_TO_VOICEMAIL); 3296 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 3297 values, Contacts.LAST_TIME_CONTACTED); 3298 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 3299 values, Contacts.TIMES_CONTACTED); 3300 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 3301 values, Contacts.STARRED); 3302 3303 // Nothing to update - just return 3304 if (mValues.size() == 0) { 3305 return 0; 3306 } 3307 3308 if (mValues.containsKey(RawContacts.STARRED)) { 3309 // Mark dirty when changing starred to trigger sync 3310 mValues.put(RawContacts.DIRTY, 1); 3311 } 3312 3313 mSelectionArgs1[0] = String.valueOf(contactId); 3314 mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1); 3315 3316 // Copy changeable values to prevent automatically managed fields from 3317 // being explicitly updated by clients. 3318 mValues.clear(); 3319 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 3320 values, Contacts.CUSTOM_RINGTONE); 3321 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 3322 values, Contacts.SEND_TO_VOICEMAIL); 3323 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 3324 values, Contacts.LAST_TIME_CONTACTED); 3325 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 3326 values, Contacts.TIMES_CONTACTED); 3327 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 3328 values, Contacts.STARRED); 3329 3330 return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1); 3331 } 3332 3333 public void updateContactLastContactedTime(long contactId, long lastTimeContacted) { 3334 mContactsLastTimeContactedUpdate.bindLong(1, lastTimeContacted); 3335 mContactsLastTimeContactedUpdate.bindLong(2, contactId); 3336 mContactsLastTimeContactedUpdate.execute(); 3337 } 3338 3339 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 3340 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 3341 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 3342 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 3343 3344 long rawContactId1, rawContactId2; 3345 if (rcId1 < rcId2) { 3346 rawContactId1 = rcId1; 3347 rawContactId2 = rcId2; 3348 } else { 3349 rawContactId2 = rcId1; 3350 rawContactId1 = rcId2; 3351 } 3352 3353 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 3354 mSelectionArgs2[0] = String.valueOf(rawContactId1); 3355 mSelectionArgs2[1] = String.valueOf(rawContactId2); 3356 db.delete(Tables.AGGREGATION_EXCEPTIONS, 3357 AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " 3358 + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2); 3359 } else { 3360 ContentValues exceptionValues = new ContentValues(3); 3361 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 3362 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 3363 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 3364 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 3365 exceptionValues); 3366 } 3367 3368 mContactAggregator.invalidateAggregationExceptionCache(); 3369 mContactAggregator.markForAggregation(rawContactId1); 3370 mContactAggregator.markForAggregation(rawContactId2); 3371 3372 long contactId1 = mDbHelper.getContactId(rawContactId1); 3373 mContactAggregator.aggregateContact(db, rawContactId1, contactId1); 3374 3375 long contactId2 = mDbHelper.getContactId(rawContactId2); 3376 mContactAggregator.aggregateContact(db, rawContactId2, contactId2); 3377 3378 // The return value is fake - we just confirm that we made a change, not count actual 3379 // rows changed. 3380 return 1; 3381 } 3382 3383 public void onAccountsUpdated(Account[] accounts) { 3384 mDb = mDbHelper.getWritableDatabase(); 3385 if (mDb == null) return; 3386 3387 HashSet<Account> existingAccounts = new HashSet<Account>(); 3388 boolean hasUnassignedContacts[] = new boolean[]{false}; 3389 mDb.beginTransaction(); 3390 try { 3391 findValidAccounts(existingAccounts, hasUnassignedContacts, 3392 Tables.RAW_CONTACTS, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE); 3393 findValidAccounts(existingAccounts, hasUnassignedContacts, 3394 Tables.GROUPS, Groups.ACCOUNT_NAME, Groups.ACCOUNT_TYPE); 3395 findValidAccounts(existingAccounts, hasUnassignedContacts, 3396 Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE); 3397 3398 // Remove all valid accounts from the existing account set. What is left 3399 // in the existingAccounts set will be extra accounts whose data must be deleted. 3400 HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts); 3401 for (Account account : accounts) { 3402 accountsToDelete.remove(account); 3403 } 3404 3405 for (Account account : accountsToDelete) { 3406 Log.d(TAG, "removing data for removed account " + account); 3407 String[] params = new String[] {account.name, account.type}; 3408 mDb.execSQL( 3409 "DELETE FROM " + Tables.GROUPS + 3410 " WHERE " + Groups.ACCOUNT_NAME + " = ?" + 3411 " AND " + Groups.ACCOUNT_TYPE + " = ?", params); 3412 mDb.execSQL( 3413 "DELETE FROM " + Tables.PRESENCE + 3414 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" + 3415 "SELECT " + RawContacts._ID + 3416 " FROM " + Tables.RAW_CONTACTS + 3417 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 3418 " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params); 3419 mDb.execSQL( 3420 "DELETE FROM " + Tables.RAW_CONTACTS + 3421 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 3422 " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params); 3423 mDb.execSQL( 3424 "DELETE FROM " + Tables.SETTINGS + 3425 " WHERE " + Settings.ACCOUNT_NAME + " = ?" + 3426 " AND " + Settings.ACCOUNT_TYPE + " = ?", params); 3427 } 3428 3429 if (hasUnassignedContacts[0]) { 3430 3431 Account primaryAccount = null; 3432 for (Account account : accounts) { 3433 if (isWritableAccount(account)) { 3434 primaryAccount = account; 3435 break; 3436 } 3437 } 3438 3439 if (primaryAccount != null) { 3440 String[] params = new String[] {primaryAccount.name, primaryAccount.type}; 3441 3442 mDb.execSQL( 3443 "UPDATE " + Tables.RAW_CONTACTS + 3444 " SET " + RawContacts.ACCOUNT_NAME + "=?," 3445 + RawContacts.ACCOUNT_TYPE + "=?" + 3446 " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" + 3447 " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params); 3448 3449 // We don't currently support groups for unsynced accounts, so this is for 3450 // the future 3451 mDb.execSQL( 3452 "UPDATE " + Tables.GROUPS + 3453 " SET " + Groups.ACCOUNT_NAME + "=?," 3454 + Groups.ACCOUNT_TYPE + "=?" + 3455 " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" + 3456 " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params); 3457 } 3458 } 3459 3460 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 3461 mDb.setTransactionSuccessful(); 3462 } finally { 3463 mDb.endTransaction(); 3464 } 3465 } 3466 3467 /** 3468 * Finds all distinct accounts present in the specified table. 3469 */ 3470 private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts, 3471 String table, String accountNameColumn, String accountTypeColumn) { 3472 Cursor c = mDb.rawQuery("SELECT DISTINCT " + accountNameColumn + "," + accountTypeColumn 3473 + " FROM " + table, null); 3474 try { 3475 while (c.moveToNext()) { 3476 if (c.isNull(0) && c.isNull(1)) { 3477 hasUnassignedContacts[0] = true; 3478 } else { 3479 validAccounts.add(new Account(c.getString(0), c.getString(1))); 3480 } 3481 } 3482 } finally { 3483 c.close(); 3484 } 3485 } 3486 3487 /** 3488 * Test all against {@link TextUtils#isEmpty(CharSequence)}. 3489 */ 3490 private static boolean areAllEmpty(ContentValues values, String[] keys) { 3491 for (String key : keys) { 3492 if (!TextUtils.isEmpty(values.getAsString(key))) { 3493 return false; 3494 } 3495 } 3496 return true; 3497 } 3498 3499 /** 3500 * Returns true if a value (possibly null) is specified for at least one of the supplied keys. 3501 */ 3502 private static boolean areAnySpecified(ContentValues values, String[] keys) { 3503 for (String key : keys) { 3504 if (values.containsKey(key)) { 3505 return true; 3506 } 3507 } 3508 return false; 3509 } 3510 3511 @Override 3512 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 3513 String sortOrder) { 3514 if (VERBOSE_LOGGING) { 3515 Log.v(TAG, "query: " + uri); 3516 } 3517 3518 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 3519 3520 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3521 String groupBy = null; 3522 String limit = getLimit(uri); 3523 3524 // TODO: Consider writing a test case for RestrictionExceptions when you 3525 // write a new query() block to make sure it protects restricted data. 3526 final int match = sUriMatcher.match(uri); 3527 switch (match) { 3528 case SYNCSTATE: 3529 return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, 3530 sortOrder); 3531 3532 case CONTACTS: { 3533 setTablesAndProjectionMapForContacts(qb, uri, projection); 3534 break; 3535 } 3536 3537 case CONTACTS_ID: { 3538 long contactId = ContentUris.parseId(uri); 3539 setTablesAndProjectionMapForContacts(qb, uri, projection); 3540 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3541 qb.appendWhere(Contacts._ID + "=?"); 3542 break; 3543 } 3544 3545 case CONTACTS_LOOKUP: 3546 case CONTACTS_LOOKUP_ID: { 3547 List<String> pathSegments = uri.getPathSegments(); 3548 int segmentCount = pathSegments.size(); 3549 if (segmentCount < 3) { 3550 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 3551 } 3552 String lookupKey = pathSegments.get(2); 3553 if (segmentCount == 4) { 3554 long contactId = Long.parseLong(pathSegments.get(3)); 3555 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3556 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 3557 String[] args; 3558 if (selectionArgs == null) { 3559 args = new String[2]; 3560 } else { 3561 args = new String[selectionArgs.length + 2]; 3562 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 3563 } 3564 args[0] = String.valueOf(contactId); 3565 args[1] = lookupKey; 3566 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); 3567 Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, 3568 groupBy, limit); 3569 if (c.getCount() != 0) { 3570 return c; 3571 } 3572 3573 c.close(); 3574 } 3575 3576 setTablesAndProjectionMapForContacts(qb, uri, projection); 3577 selectionArgs = insertSelectionArg(selectionArgs, 3578 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 3579 qb.appendWhere(Contacts._ID + "=?"); 3580 break; 3581 } 3582 3583 case CONTACTS_AS_VCARD: { 3584 // When reading as vCard always use restricted view 3585 final String lookupKey = uri.getPathSegments().get(2); 3586 qb.setTables(mDbHelper.getContactView(true /* require restricted */)); 3587 qb.setProjectionMap(sContactsVCardProjectionMap); 3588 selectionArgs = insertSelectionArg(selectionArgs, 3589 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 3590 qb.appendWhere(Contacts._ID + "=?"); 3591 break; 3592 } 3593 3594 case CONTACTS_FILTER: { 3595 setTablesAndProjectionMapForContacts(qb, uri, projection); 3596 if (uri.getPathSegments().size() > 2) { 3597 String filterParam = uri.getLastPathSegment(); 3598 StringBuilder sb = new StringBuilder(); 3599 sb.append(Contacts._ID + " IN "); 3600 appendContactFilterAsNestedQuery(sb, filterParam); 3601 qb.appendWhere(sb.toString()); 3602 } 3603 break; 3604 } 3605 3606 case CONTACTS_STREQUENT_FILTER: 3607 case CONTACTS_STREQUENT: { 3608 String filterSql = null; 3609 if (match == CONTACTS_STREQUENT_FILTER 3610 && uri.getPathSegments().size() > 3) { 3611 String filterParam = uri.getLastPathSegment(); 3612 StringBuilder sb = new StringBuilder(); 3613 sb.append(Contacts._ID + " IN "); 3614 appendContactFilterAsNestedQuery(sb, filterParam); 3615 filterSql = sb.toString(); 3616 } 3617 3618 setTablesAndProjectionMapForContacts(qb, uri, projection); 3619 3620 String[] starredProjection = null; 3621 String[] frequentProjection = null; 3622 if (projection != null) { 3623 starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN); 3624 frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN); 3625 } 3626 3627 // Build the first query for starred 3628 if (filterSql != null) { 3629 qb.appendWhere(filterSql); 3630 } 3631 qb.setProjectionMap(sStrequentStarredProjectionMap); 3632 final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1", 3633 null, Contacts._ID, null, null, null); 3634 3635 // Build the second query for frequent 3636 qb = new SQLiteQueryBuilder(); 3637 setTablesAndProjectionMapForContacts(qb, uri, projection); 3638 if (filterSql != null) { 3639 qb.appendWhere(filterSql); 3640 } 3641 qb.setProjectionMap(sStrequentFrequentProjectionMap); 3642 final String frequentQuery = qb.buildQuery(frequentProjection, 3643 Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED 3644 + " = 0 OR " + Contacts.STARRED + " IS NULL)", 3645 null, Contacts._ID, null, null, null); 3646 3647 // Put them together 3648 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 3649 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 3650 Cursor c = db.rawQuery(query, null); 3651 if (c != null) { 3652 c.setNotificationUri(getContext().getContentResolver(), 3653 ContactsContract.AUTHORITY_URI); 3654 } 3655 return c; 3656 } 3657 3658 case CONTACTS_GROUP: { 3659 setTablesAndProjectionMapForContacts(qb, uri, projection); 3660 if (uri.getPathSegments().size() > 2) { 3661 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 3662 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3663 } 3664 break; 3665 } 3666 3667 case CONTACTS_DATA: { 3668 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3669 setTablesAndProjectionMapForData(qb, uri, projection, false); 3670 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 3671 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 3672 break; 3673 } 3674 3675 case CONTACTS_PHOTO: { 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 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 3681 break; 3682 } 3683 3684 case PHONES: { 3685 setTablesAndProjectionMapForData(qb, uri, projection, false); 3686 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3687 break; 3688 } 3689 3690 case PHONES_ID: { 3691 setTablesAndProjectionMapForData(qb, uri, projection, false); 3692 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3693 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3694 qb.appendWhere(" AND " + Data._ID + "=?"); 3695 break; 3696 } 3697 3698 case PHONES_FILTER: { 3699 setTablesAndProjectionMapForData(qb, uri, projection, true); 3700 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3701 if (uri.getPathSegments().size() > 2) { 3702 String filterParam = uri.getLastPathSegment(); 3703 StringBuilder sb = new StringBuilder(); 3704 sb.append(" AND ("); 3705 3706 boolean orNeeded = false; 3707 String normalizedName = NameNormalizer.normalize(filterParam); 3708 if (normalizedName.length() > 0) { 3709 sb.append(Data.RAW_CONTACT_ID + " IN "); 3710 appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false); 3711 orNeeded = true; 3712 } 3713 3714 if (isPhoneNumber(filterParam)) { 3715 if (orNeeded) { 3716 sb.append(" OR "); 3717 } 3718 String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam); 3719 String reversed = PhoneNumberUtils.getStrippedReversed(number); 3720 sb.append(Data._ID + 3721 " IN (SELECT " + PhoneLookupColumns.DATA_ID 3722 + " FROM " + Tables.PHONE_LOOKUP 3723 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%"); 3724 sb.append(reversed); 3725 sb.append("')"); 3726 } 3727 sb.append(")"); 3728 qb.appendWhere(sb); 3729 } 3730 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 3731 if (sortOrder == null) { 3732 sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; 3733 } 3734 break; 3735 } 3736 3737 case EMAILS: { 3738 setTablesAndProjectionMapForData(qb, uri, projection, false); 3739 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3740 break; 3741 } 3742 3743 case EMAILS_ID: { 3744 setTablesAndProjectionMapForData(qb, uri, projection, false); 3745 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3746 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'" 3747 + " AND " + Data._ID + "=?"); 3748 break; 3749 } 3750 3751 case EMAILS_LOOKUP: { 3752 setTablesAndProjectionMapForData(qb, uri, projection, false); 3753 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3754 if (uri.getPathSegments().size() > 2) { 3755 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3756 qb.appendWhere(" AND " + Email.DATA + "=?"); 3757 } 3758 break; 3759 } 3760 3761 case EMAILS_FILTER: { 3762 setTablesAndProjectionMapForData(qb, uri, projection, true); 3763 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3764 if (uri.getPathSegments().size() > 2) { 3765 String filterParam = uri.getLastPathSegment(); 3766 StringBuilder sb = new StringBuilder(); 3767 sb.append(" AND ("); 3768 3769 if (!filterParam.contains("@")) { 3770 String normalizedName = NameNormalizer.normalize(filterParam); 3771 if (normalizedName.length() > 0) { 3772 sb.append(Data.RAW_CONTACT_ID + " IN "); 3773 appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false); 3774 sb.append(" OR "); 3775 } 3776 } 3777 3778 sb.append(Email.DATA + " LIKE "); 3779 sb.append(DatabaseUtils.sqlEscapeString(filterParam + '%')); 3780 sb.append(")"); 3781 qb.appendWhere(sb); 3782 } 3783 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 3784 if (sortOrder == null) { 3785 sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; 3786 } 3787 break; 3788 } 3789 3790 case POSTALS: { 3791 setTablesAndProjectionMapForData(qb, uri, projection, false); 3792 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 3793 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 3794 break; 3795 } 3796 3797 case POSTALS_ID: { 3798 setTablesAndProjectionMapForData(qb, uri, projection, false); 3799 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3800 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 3801 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 3802 qb.appendWhere(" AND " + Data._ID + "=?"); 3803 break; 3804 } 3805 3806 case RAW_CONTACTS: { 3807 setTablesAndProjectionMapForRawContacts(qb, uri); 3808 break; 3809 } 3810 3811 case RAW_CONTACTS_ID: { 3812 long rawContactId = ContentUris.parseId(uri); 3813 setTablesAndProjectionMapForRawContacts(qb, uri); 3814 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3815 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 3816 break; 3817 } 3818 3819 case RAW_CONTACTS_DATA: { 3820 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 3821 setTablesAndProjectionMapForData(qb, uri, projection, false); 3822 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3823 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?"); 3824 break; 3825 } 3826 3827 case DATA: { 3828 setTablesAndProjectionMapForData(qb, uri, projection, false); 3829 break; 3830 } 3831 3832 case DATA_ID: { 3833 setTablesAndProjectionMapForData(qb, uri, projection, false); 3834 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3835 qb.appendWhere(" AND " + Data._ID + "=?"); 3836 break; 3837 } 3838 3839 case PHONE_LOOKUP: { 3840 3841 if (TextUtils.isEmpty(sortOrder)) { 3842 // Default the sort order to something reasonable so we get consistent 3843 // results when callers don't request an ordering 3844 sortOrder = RawContactsColumns.CONCRETE_ID; 3845 } 3846 3847 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 3848 mDbHelper.buildPhoneLookupAndContactQuery(qb, number); 3849 qb.setProjectionMap(sPhoneLookupProjectionMap); 3850 3851 // Phone lookup cannot be combined with a selection 3852 selection = null; 3853 selectionArgs = null; 3854 break; 3855 } 3856 3857 case GROUPS: { 3858 qb.setTables(mDbHelper.getGroupView()); 3859 qb.setProjectionMap(sGroupsProjectionMap); 3860 appendAccountFromParameter(qb, uri); 3861 break; 3862 } 3863 3864 case GROUPS_ID: { 3865 qb.setTables(mDbHelper.getGroupView()); 3866 qb.setProjectionMap(sGroupsProjectionMap); 3867 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3868 qb.appendWhere(Groups._ID + "=?"); 3869 break; 3870 } 3871 3872 case GROUPS_SUMMARY: { 3873 qb.setTables(mDbHelper.getGroupView() + " AS groups"); 3874 qb.setProjectionMap(sGroupsSummaryProjectionMap); 3875 appendAccountFromParameter(qb, uri); 3876 groupBy = Groups._ID; 3877 break; 3878 } 3879 3880 case AGGREGATION_EXCEPTIONS: { 3881 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 3882 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 3883 break; 3884 } 3885 3886 case AGGREGATION_SUGGESTIONS: { 3887 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3888 String filter = null; 3889 if (uri.getPathSegments().size() > 3) { 3890 filter = uri.getPathSegments().get(3); 3891 } 3892 final int maxSuggestions; 3893 if (limit != null) { 3894 maxSuggestions = Integer.parseInt(limit); 3895 } else { 3896 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 3897 } 3898 3899 setTablesAndProjectionMapForContacts(qb, uri, projection); 3900 3901 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, 3902 maxSuggestions, filter); 3903 } 3904 3905 case SETTINGS: { 3906 qb.setTables(Tables.SETTINGS); 3907 qb.setProjectionMap(sSettingsProjectionMap); 3908 appendAccountFromParameter(qb, uri); 3909 3910 // When requesting specific columns, this query requires 3911 // late-binding of the GroupMembership MIME-type. 3912 final String groupMembershipMimetypeId = Long.toString(mDbHelper 3913 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 3914 if (projection != null && projection.length != 0 && 3915 mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 3916 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3917 } 3918 if (projection != null && projection.length != 0 && 3919 mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 3920 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3921 } 3922 3923 break; 3924 } 3925 3926 case STATUS_UPDATES: { 3927 setTableAndProjectionMapForStatusUpdates(qb, projection); 3928 break; 3929 } 3930 3931 case STATUS_UPDATES_ID: { 3932 setTableAndProjectionMapForStatusUpdates(qb, projection); 3933 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3934 qb.appendWhere(DataColumns.CONCRETE_ID + "=?"); 3935 break; 3936 } 3937 3938 case SEARCH_SUGGESTIONS: { 3939 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); 3940 } 3941 3942 case SEARCH_SHORTCUT: { 3943 long contactId = ContentUris.parseId(uri); 3944 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection); 3945 } 3946 3947 case LIVE_FOLDERS_CONTACTS: 3948 qb.setTables(mDbHelper.getContactView()); 3949 qb.setProjectionMap(sLiveFoldersProjectionMap); 3950 break; 3951 3952 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 3953 qb.setTables(mDbHelper.getContactView()); 3954 qb.setProjectionMap(sLiveFoldersProjectionMap); 3955 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 3956 break; 3957 3958 case LIVE_FOLDERS_CONTACTS_FAVORITES: 3959 qb.setTables(mDbHelper.getContactView()); 3960 qb.setProjectionMap(sLiveFoldersProjectionMap); 3961 qb.appendWhere(Contacts.STARRED + "=1"); 3962 break; 3963 3964 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 3965 qb.setTables(mDbHelper.getContactView()); 3966 qb.setProjectionMap(sLiveFoldersProjectionMap); 3967 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 3968 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3969 break; 3970 3971 case RAW_CONTACT_ENTITIES: { 3972 setTablesAndProjectionMapForRawContactsEntities(qb, uri); 3973 break; 3974 } 3975 3976 case RAW_CONTACT_ENTITY_ID: { 3977 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 3978 setTablesAndProjectionMapForRawContactsEntities(qb, uri); 3979 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3980 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 3981 break; 3982 } 3983 3984 default: 3985 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 3986 sortOrder, limit); 3987 } 3988 3989 return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 3990 } 3991 3992 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 3993 String selection, String[] selectionArgs, String sortOrder, String groupBy, 3994 String limit) { 3995 if (projection != null && projection.length == 1 3996 && BaseColumns._COUNT.equals(projection[0])) { 3997 qb.setProjectionMap(sCountProjectionMap); 3998 } 3999 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 4000 sortOrder, limit); 4001 if (c != null) { 4002 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 4003 } 4004 return c; 4005 } 4006 4007 private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 4008 ContactLookupKey key = new ContactLookupKey(); 4009 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 4010 4011 long contactId = lookupContactIdBySourceIds(db, segments); 4012 if (contactId == -1) { 4013 contactId = lookupContactIdByDisplayNames(db, segments); 4014 } 4015 4016 return contactId; 4017 } 4018 4019 private interface LookupBySourceIdQuery { 4020 String TABLE = Tables.RAW_CONTACTS; 4021 4022 String COLUMNS[] = { 4023 RawContacts.CONTACT_ID, 4024 RawContacts.ACCOUNT_TYPE, 4025 RawContacts.ACCOUNT_NAME, 4026 RawContacts.SOURCE_ID 4027 }; 4028 4029 int CONTACT_ID = 0; 4030 int ACCOUNT_TYPE = 1; 4031 int ACCOUNT_NAME = 2; 4032 int SOURCE_ID = 3; 4033 } 4034 4035 private long lookupContactIdBySourceIds(SQLiteDatabase db, 4036 ArrayList<LookupKeySegment> segments) { 4037 int sourceIdCount = 0; 4038 for (int i = 0; i < segments.size(); i++) { 4039 LookupKeySegment segment = segments.get(i); 4040 if (segment.sourceIdLookup) { 4041 sourceIdCount++; 4042 } 4043 } 4044 4045 if (sourceIdCount == 0) { 4046 return -1; 4047 } 4048 4049 // First try sync ids 4050 StringBuilder sb = new StringBuilder(); 4051 sb.append(RawContacts.SOURCE_ID + " IN ("); 4052 for (int i = 0; i < segments.size(); i++) { 4053 LookupKeySegment segment = segments.get(i); 4054 if (segment.sourceIdLookup) { 4055 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 4056 sb.append(","); 4057 } 4058 } 4059 sb.setLength(sb.length() - 1); // Last comma 4060 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 4061 4062 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 4063 sb.toString(), null, null, null, null); 4064 try { 4065 while (c.moveToNext()) { 4066 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); 4067 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 4068 int accountHashCode = 4069 ContactLookupKey.getAccountHashCode(accountType, accountName); 4070 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 4071 for (int i = 0; i < segments.size(); i++) { 4072 LookupKeySegment segment = segments.get(i); 4073 if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode 4074 && segment.key.equals(sourceId)) { 4075 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 4076 break; 4077 } 4078 } 4079 } 4080 } finally { 4081 c.close(); 4082 } 4083 4084 return getMostReferencedContactId(segments); 4085 } 4086 4087 private interface LookupByDisplayNameQuery { 4088 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 4089 4090 String COLUMNS[] = { 4091 RawContacts.CONTACT_ID, 4092 RawContacts.ACCOUNT_TYPE, 4093 RawContacts.ACCOUNT_NAME, 4094 NameLookupColumns.NORMALIZED_NAME 4095 }; 4096 4097 int CONTACT_ID = 0; 4098 int ACCOUNT_TYPE = 1; 4099 int ACCOUNT_NAME = 2; 4100 int NORMALIZED_NAME = 3; 4101 } 4102 4103 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 4104 ArrayList<LookupKeySegment> segments) { 4105 int displayNameCount = 0; 4106 for (int i = 0; i < segments.size(); i++) { 4107 LookupKeySegment segment = segments.get(i); 4108 if (!segment.sourceIdLookup) { 4109 displayNameCount++; 4110 } 4111 } 4112 4113 if (displayNameCount == 0) { 4114 return -1; 4115 } 4116 4117 // First try sync ids 4118 StringBuilder sb = new StringBuilder(); 4119 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 4120 for (int i = 0; i < segments.size(); i++) { 4121 LookupKeySegment segment = segments.get(i); 4122 if (!segment.sourceIdLookup) { 4123 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 4124 sb.append(","); 4125 } 4126 } 4127 sb.setLength(sb.length() - 1); // Last comma 4128 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 4129 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 4130 4131 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 4132 sb.toString(), null, null, null, null); 4133 try { 4134 while (c.moveToNext()) { 4135 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); 4136 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 4137 int accountHashCode = 4138 ContactLookupKey.getAccountHashCode(accountType, accountName); 4139 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 4140 for (int i = 0; i < segments.size(); i++) { 4141 LookupKeySegment segment = segments.get(i); 4142 if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode 4143 && segment.key.equals(name)) { 4144 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 4145 break; 4146 } 4147 } 4148 } 4149 } finally { 4150 c.close(); 4151 } 4152 4153 return getMostReferencedContactId(segments); 4154 } 4155 4156 /** 4157 * Returns the contact ID that is mentioned the highest number of times. 4158 */ 4159 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 4160 Collections.sort(segments); 4161 4162 long bestContactId = -1; 4163 int bestRefCount = 0; 4164 4165 long contactId = -1; 4166 int count = 0; 4167 4168 int segmentCount = segments.size(); 4169 for (int i = 0; i < segmentCount; i++) { 4170 LookupKeySegment segment = segments.get(i); 4171 if (segment.contactId != -1) { 4172 if (segment.contactId == contactId) { 4173 count++; 4174 } else { 4175 if (count > bestRefCount) { 4176 bestContactId = contactId; 4177 bestRefCount = count; 4178 } 4179 contactId = segment.contactId; 4180 count = 1; 4181 } 4182 } 4183 } 4184 if (count > bestRefCount) { 4185 return contactId; 4186 } else { 4187 return bestContactId; 4188 } 4189 } 4190 4191 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 4192 String[] projection) { 4193 StringBuilder sb = new StringBuilder(); 4194 boolean excludeRestrictedData = false; 4195 String requestingPackage = getQueryParameter(uri, 4196 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4197 if (requestingPackage != null) { 4198 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4199 } 4200 sb.append(mDbHelper.getContactView(excludeRestrictedData)); 4201 if (mDbHelper.isInProjection(projection, 4202 Contacts.CONTACT_PRESENCE)) { 4203 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 4204 " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")"); 4205 } 4206 if (mDbHelper.isInProjection(projection, 4207 Contacts.CONTACT_STATUS, 4208 Contacts.CONTACT_STATUS_RES_PACKAGE, 4209 Contacts.CONTACT_STATUS_ICON, 4210 Contacts.CONTACT_STATUS_LABEL, 4211 Contacts.CONTACT_STATUS_TIMESTAMP)) { 4212 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 4213 + ContactsStatusUpdatesColumns.ALIAS + 4214 " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" 4215 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 4216 } 4217 qb.setTables(sb.toString()); 4218 qb.setProjectionMap(sContactsProjectionMap); 4219 } 4220 4221 private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) { 4222 StringBuilder sb = new StringBuilder(); 4223 boolean excludeRestrictedData = false; 4224 String requestingPackage = getQueryParameter(uri, 4225 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4226 if (requestingPackage != null) { 4227 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4228 } 4229 sb.append(mDbHelper.getRawContactView(excludeRestrictedData)); 4230 qb.setTables(sb.toString()); 4231 qb.setProjectionMap(sRawContactsProjectionMap); 4232 appendAccountFromParameter(qb, uri); 4233 } 4234 4235 private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) { 4236 // Note: currently, "export only" equals to "restricted", but may not in the future. 4237 boolean excludeRestrictedData = readBooleanQueryParameter(uri, 4238 Data.FOR_EXPORT_ONLY, false); 4239 4240 String requestingPackage = getQueryParameter(uri, 4241 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4242 if (requestingPackage != null) { 4243 excludeRestrictedData = excludeRestrictedData 4244 || !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4245 } 4246 qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData)); 4247 qb.setProjectionMap(sRawContactsEntityProjectionMap); 4248 appendAccountFromParameter(qb, uri); 4249 } 4250 4251 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 4252 String[] projection, boolean distinct) { 4253 StringBuilder sb = new StringBuilder(); 4254 // Note: currently, "export only" equals to "restricted", but may not in the future. 4255 boolean excludeRestrictedData = readBooleanQueryParameter(uri, 4256 Data.FOR_EXPORT_ONLY, false); 4257 4258 String requestingPackage = getQueryParameter(uri, 4259 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 4260 if (requestingPackage != null) { 4261 excludeRestrictedData = excludeRestrictedData 4262 || !mDbHelper.hasAccessToRestrictedData(requestingPackage); 4263 } 4264 4265 sb.append(mDbHelper.getDataView(excludeRestrictedData)); 4266 sb.append(" data"); 4267 4268 // Include aggregated presence when requested 4269 if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) { 4270 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 4271 " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "=" 4272 + RawContacts.CONTACT_ID + ")"); 4273 } 4274 4275 // Include aggregated status updates when requested 4276 if (mDbHelper.isInProjection(projection, 4277 Data.CONTACT_STATUS, 4278 Data.CONTACT_STATUS_RES_PACKAGE, 4279 Data.CONTACT_STATUS_ICON, 4280 Data.CONTACT_STATUS_LABEL, 4281 Data.CONTACT_STATUS_TIMESTAMP)) { 4282 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 4283 + ContactsStatusUpdatesColumns.ALIAS + 4284 " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" 4285 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 4286 } 4287 4288 // Include individual presence when requested 4289 if (mDbHelper.isInProjection(projection, Data.PRESENCE)) { 4290 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 4291 " ON (" + StatusUpdates.DATA_ID + "=" 4292 + DataColumns.CONCRETE_ID + ")"); 4293 } 4294 4295 // Include individual status updates when requested 4296 if (mDbHelper.isInProjection(projection, 4297 Data.STATUS, 4298 Data.STATUS_RES_PACKAGE, 4299 Data.STATUS_ICON, 4300 Data.STATUS_LABEL, 4301 Data.STATUS_TIMESTAMP)) { 4302 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 4303 " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" 4304 + DataColumns.CONCRETE_ID + ")"); 4305 } 4306 4307 qb.setTables(sb.toString()); 4308 qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap); 4309 appendAccountFromParameter(qb, uri); 4310 } 4311 4312 private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, 4313 String[] projection) { 4314 StringBuilder sb = new StringBuilder(); 4315 sb.append(mDbHelper.getDataView()); 4316 sb.append(" data"); 4317 4318 if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) { 4319 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 4320 " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID 4321 + "=" + DataColumns.CONCRETE_ID + ")"); 4322 } 4323 4324 if (mDbHelper.isInProjection(projection, 4325 StatusUpdates.STATUS, 4326 StatusUpdates.STATUS_RES_PACKAGE, 4327 StatusUpdates.STATUS_ICON, 4328 StatusUpdates.STATUS_LABEL, 4329 StatusUpdates.STATUS_TIMESTAMP)) { 4330 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 4331 " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID 4332 + "=" + DataColumns.CONCRETE_ID + ")"); 4333 } 4334 qb.setTables(sb.toString()); 4335 qb.setProjectionMap(sStatusUpdatesProjectionMap); 4336 } 4337 4338 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 4339 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 4340 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 4341 if (!TextUtils.isEmpty(accountName)) { 4342 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 4343 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 4344 + RawContacts.ACCOUNT_TYPE + "=" 4345 + DatabaseUtils.sqlEscapeString(accountType)); 4346 } else { 4347 qb.appendWhere("1"); 4348 } 4349 } 4350 4351 private String appendAccountToSelection(Uri uri, String selection) { 4352 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 4353 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 4354 if (!TextUtils.isEmpty(accountName)) { 4355 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 4356 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 4357 + RawContacts.ACCOUNT_TYPE + "=" 4358 + DatabaseUtils.sqlEscapeString(accountType)); 4359 if (!TextUtils.isEmpty(selection)) { 4360 selectionSb.append(" AND ("); 4361 selectionSb.append(selection); 4362 selectionSb.append(')'); 4363 } 4364 return selectionSb.toString(); 4365 } else { 4366 return selection; 4367 } 4368 } 4369 4370 /** 4371 * Gets the value of the "limit" URI query parameter. 4372 * 4373 * @return A string containing a non-negative integer, or <code>null</code> if 4374 * the parameter is not set, or is set to an invalid value. 4375 */ 4376 private String getLimit(Uri uri) { 4377 String limitParam = getQueryParameter(uri, "limit"); 4378 if (limitParam == null) { 4379 return null; 4380 } 4381 // make sure that the limit is a non-negative integer 4382 try { 4383 int l = Integer.parseInt(limitParam); 4384 if (l < 0) { 4385 Log.w(TAG, "Invalid limit parameter: " + limitParam); 4386 return null; 4387 } 4388 return String.valueOf(l); 4389 } catch (NumberFormatException ex) { 4390 Log.w(TAG, "Invalid limit parameter: " + limitParam); 4391 return null; 4392 } 4393 } 4394 4395 /** 4396 * Returns true if all the characters are meaningful as digits 4397 * in a phone number -- letters, digits, and a few punctuation marks. 4398 */ 4399 private boolean isPhoneNumber(CharSequence cons) { 4400 int len = cons.length(); 4401 4402 for (int i = 0; i < len; i++) { 4403 char c = cons.charAt(i); 4404 4405 if ((c >= '0') && (c <= '9')) { 4406 continue; 4407 } 4408 if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+') 4409 || (c == '#') || (c == '*')) { 4410 continue; 4411 } 4412 if ((c >= 'A') && (c <= 'Z')) { 4413 continue; 4414 } 4415 if ((c >= 'a') && (c <= 'z')) { 4416 continue; 4417 } 4418 4419 return false; 4420 } 4421 4422 return true; 4423 } 4424 4425 String getContactsRestrictions() { 4426 if (mDbHelper.hasAccessToRestrictedData()) { 4427 return "1"; 4428 } else { 4429 return RawContacts.IS_RESTRICTED + "=0"; 4430 } 4431 } 4432 4433 public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { 4434 if (mDbHelper.hasAccessToRestrictedData()) { 4435 return "1"; 4436 } else { 4437 return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS 4438 + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; 4439 } 4440 } 4441 4442 @Override 4443 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 4444 int match = sUriMatcher.match(uri); 4445 switch (match) { 4446 case CONTACTS_PHOTO: { 4447 if (!"r".equals(mode)) { 4448 throw new FileNotFoundException("Mode " + mode + " not supported."); 4449 } 4450 4451 String sql = 4452 "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() + 4453 " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID 4454 + " AND " + RawContacts.CONTACT_ID + "=?"; 4455 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4456 return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, 4457 new String[]{uri.getPathSegments().get(1)}); 4458 } 4459 4460 case CONTACTS_AS_VCARD: { 4461 final String lookupKey = uri.getPathSegments().get(2); 4462 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 4463 final String selection = Contacts._ID + "=" + contactId; 4464 4465 // When opening a contact as file, we pass back contents as a 4466 // vCard-encoded stream. We build into a local buffer first, 4467 // then pipe into MemoryFile once the exact size is known. 4468 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 4469 outputRawContactsAsVCard(localStream, selection, null); 4470 return buildAssetFileDescriptor(localStream); 4471 } 4472 4473 default: 4474 throw new FileNotFoundException("No file at: " + uri); 4475 } 4476 } 4477 4478 private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; 4479 private static final String VCARD_TYPE_DEFAULT = "default"; 4480 4481 /** 4482 * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the 4483 * contents of the given {@link ByteArrayOutputStream}. 4484 */ 4485 private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { 4486 AssetFileDescriptor fd = null; 4487 try { 4488 stream.flush(); 4489 4490 final byte[] byteData = stream.toByteArray(); 4491 final int size = byteData.length; 4492 4493 final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size); 4494 memoryFile.writeBytes(byteData, 0, 0, size); 4495 memoryFile.deactivate(); 4496 4497 fd = AssetFileDescriptor.fromMemoryFile(memoryFile); 4498 } catch (IOException e) { 4499 Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString()); 4500 } 4501 return fd; 4502 } 4503 4504 /** 4505 * Output {@link RawContacts} matching the requested selection in the vCard 4506 * format to the given {@link OutputStream}. This method returns silently if 4507 * any errors encountered. 4508 */ 4509 private void outputRawContactsAsVCard(OutputStream stream, String selection, 4510 String[] selectionArgs) { 4511 final Context context = this.getContext(); 4512 final VCardComposer composer = new VCardComposer(context, VCARD_TYPE_DEFAULT, false); 4513 composer.addHandler(composer.new HandlerForOutputStream(stream)); 4514 4515 // No extra checks since composer always uses restricted views 4516 if (!composer.init(selection, selectionArgs)) 4517 return; 4518 4519 while (!composer.isAfterLast()) { 4520 if (!composer.createOneEntry()) { 4521 Log.w(TAG, "Failed to output a contact."); 4522 } 4523 } 4524 composer.terminate(); 4525 } 4526 4527 /** 4528 * An implementation of EntityIterator that joins the contacts and data tables 4529 * and consumes all the data rows for a contact in order to build the Entity for a contact. 4530 */ 4531 private static class RawContactsEntityIterator implements EntityIterator { 4532 private final Cursor mEntityCursor; 4533 private volatile boolean mIsClosed; 4534 4535 private static final String[] DATA_KEYS = new String[]{ 4536 Data.DATA1, 4537 Data.DATA2, 4538 Data.DATA3, 4539 Data.DATA4, 4540 Data.DATA5, 4541 Data.DATA6, 4542 Data.DATA7, 4543 Data.DATA8, 4544 Data.DATA9, 4545 Data.DATA10, 4546 Data.DATA11, 4547 Data.DATA12, 4548 Data.DATA13, 4549 Data.DATA14, 4550 Data.DATA15, 4551 Data.SYNC1, 4552 Data.SYNC2, 4553 Data.SYNC3, 4554 Data.SYNC4}; 4555 4556 public static final String[] PROJECTION = new String[]{ 4557 RawContacts.ACCOUNT_NAME, 4558 RawContacts.ACCOUNT_TYPE, 4559 RawContacts.SOURCE_ID, 4560 RawContacts.VERSION, 4561 RawContacts.DIRTY, 4562 RawContacts.Entity.DATA_ID, 4563 Data.RES_PACKAGE, 4564 Data.MIMETYPE, 4565 Data.DATA1, 4566 Data.DATA2, 4567 Data.DATA3, 4568 Data.DATA4, 4569 Data.DATA5, 4570 Data.DATA6, 4571 Data.DATA7, 4572 Data.DATA8, 4573 Data.DATA9, 4574 Data.DATA10, 4575 Data.DATA11, 4576 Data.DATA12, 4577 Data.DATA13, 4578 Data.DATA14, 4579 Data.DATA15, 4580 Data.SYNC1, 4581 Data.SYNC2, 4582 Data.SYNC3, 4583 Data.SYNC4, 4584 RawContacts._ID, 4585 Data.IS_PRIMARY, 4586 Data.IS_SUPER_PRIMARY, 4587 Data.DATA_VERSION, 4588 GroupMembership.GROUP_SOURCE_ID, 4589 RawContacts.SYNC1, 4590 RawContacts.SYNC2, 4591 RawContacts.SYNC3, 4592 RawContacts.SYNC4, 4593 RawContacts.DELETED, 4594 RawContacts.CONTACT_ID, 4595 RawContacts.STARRED, 4596 RawContacts.IS_RESTRICTED}; 4597 4598 private static final int COLUMN_ACCOUNT_NAME = 0; 4599 private static final int COLUMN_ACCOUNT_TYPE = 1; 4600 private static final int COLUMN_SOURCE_ID = 2; 4601 private static final int COLUMN_VERSION = 3; 4602 private static final int COLUMN_DIRTY = 4; 4603 private static final int COLUMN_DATA_ID = 5; 4604 private static final int COLUMN_RES_PACKAGE = 6; 4605 private static final int COLUMN_MIMETYPE = 7; 4606 private static final int COLUMN_DATA1 = 8; 4607 private static final int COLUMN_RAW_CONTACT_ID = 27; 4608 private static final int COLUMN_IS_PRIMARY = 28; 4609 private static final int COLUMN_IS_SUPER_PRIMARY = 29; 4610 private static final int COLUMN_DATA_VERSION = 30; 4611 private static final int COLUMN_GROUP_SOURCE_ID = 31; 4612 private static final int COLUMN_SYNC1 = 32; 4613 private static final int COLUMN_SYNC2 = 33; 4614 private static final int COLUMN_SYNC3 = 34; 4615 private static final int COLUMN_SYNC4 = 35; 4616 private static final int COLUMN_DELETED = 36; 4617 private static final int COLUMN_CONTACT_ID = 37; 4618 private static final int COLUMN_STARRED = 38; 4619 private static final int COLUMN_IS_RESTRICTED = 39; 4620 4621 public RawContactsEntityIterator(ContactsProvider2 provider, Uri entityUri, 4622 String contactsIdString, 4623 String selection, String[] selectionArgs, String sortOrder) { 4624 mIsClosed = false; 4625 Uri uri; 4626 if (contactsIdString != null) { 4627 uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, contactsIdString); 4628 uri = Uri.withAppendedPath(uri, RawContacts.Entity.CONTENT_DIRECTORY); 4629 } else { 4630 uri = ContactsContract.RawContactsEntity.CONTENT_URI; 4631 } 4632 final Uri.Builder builder = uri.buildUpon(); 4633 String query = entityUri.getQuery(); 4634 builder.encodedQuery(query); 4635 mEntityCursor = provider.query(builder.build(), 4636 PROJECTION, selection, selectionArgs, sortOrder); 4637 mEntityCursor.moveToFirst(); 4638 } 4639 4640 public void reset() throws RemoteException { 4641 if (mIsClosed) { 4642 throw new IllegalStateException("calling reset() when the iterator is closed"); 4643 } 4644 mEntityCursor.moveToFirst(); 4645 } 4646 4647 public void close() { 4648 if (mIsClosed) { 4649 throw new IllegalStateException("closing when already closed"); 4650 } 4651 mIsClosed = true; 4652 mEntityCursor.close(); 4653 } 4654 4655 public boolean hasNext() throws RemoteException { 4656 if (mIsClosed) { 4657 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 4658 } 4659 4660 return !mEntityCursor.isAfterLast(); 4661 } 4662 4663 public Entity next() throws RemoteException { 4664 if (mIsClosed) { 4665 throw new IllegalStateException("calling next() when the iterator is closed"); 4666 } 4667 if (!hasNext()) { 4668 throw new IllegalStateException("you may only call next() if hasNext() is true"); 4669 } 4670 4671 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 4672 4673 final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID); 4674 4675 // we expect the cursor is already at the row we need to read from 4676 ContentValues contactValues = new ContentValues(); 4677 contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 4678 contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 4679 contactValues.put(RawContacts._ID, rawContactId); 4680 contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY)); 4681 contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION)); 4682 contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 4683 contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1)); 4684 contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2)); 4685 contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3)); 4686 contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4)); 4687 contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED)); 4688 contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID)); 4689 contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED)); 4690 contactValues.put(RawContacts.IS_RESTRICTED, c.getInt(COLUMN_IS_RESTRICTED)); 4691 Entity contact = new Entity(contactValues); 4692 4693 // read data rows until the contact id changes 4694 do { 4695 if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) { 4696 break; 4697 } 4698// if (c.isNull(COLUMN_CONTACT_ID)) { 4699// continue; 4700// } 4701 // add the data to to the contact 4702 ContentValues dataValues = new ContentValues(); 4703 dataValues.put(Data._ID, c.getLong(COLUMN_DATA_ID)); 4704 dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE)); 4705 dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE)); 4706 dataValues.put(Data.IS_PRIMARY, c.getLong(COLUMN_IS_PRIMARY)); 4707 dataValues.put(Data.IS_SUPER_PRIMARY, c.getLong(COLUMN_IS_SUPER_PRIMARY)); 4708 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 4709 if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) { 4710 dataValues.put(GroupMembership.GROUP_SOURCE_ID, 4711 c.getString(COLUMN_GROUP_SOURCE_ID)); 4712 } 4713 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 4714 for (int i = 0; i < DATA_KEYS.length; i++) { 4715 final int columnIndex = i + COLUMN_DATA1; 4716 String key = DATA_KEYS[i]; 4717 if (c.isNull(columnIndex)) { 4718 // don't put anything 4719 } else if (c.isLong(columnIndex)) { 4720 dataValues.put(key, c.getLong(columnIndex)); 4721 } else if (c.isFloat(columnIndex)) { 4722 dataValues.put(key, c.getFloat(columnIndex)); 4723 } else if (c.isString(columnIndex)) { 4724 dataValues.put(key, c.getString(columnIndex)); 4725 } else if (c.isBlob(columnIndex)) { 4726 dataValues.put(key, c.getBlob(columnIndex)); 4727 } 4728 } 4729 contact.addSubValue(Data.CONTENT_URI, dataValues); 4730 } while (mEntityCursor.moveToNext()); 4731 4732 return contact; 4733 } 4734 } 4735 4736 /** 4737 * An implementation of EntityIterator that joins the contacts and data tables 4738 * and consumes all the data rows for a contact in order to build the Entity for a contact. 4739 */ 4740 private static class GroupsEntityIterator implements EntityIterator { 4741 private final Cursor mEntityCursor; 4742 private volatile boolean mIsClosed; 4743 4744 private static final String[] PROJECTION = new String[]{ 4745 Groups._ID, 4746 Groups.ACCOUNT_NAME, 4747 Groups.ACCOUNT_TYPE, 4748 Groups.SOURCE_ID, 4749 Groups.DIRTY, 4750 Groups.VERSION, 4751 Groups.RES_PACKAGE, 4752 Groups.TITLE, 4753 Groups.TITLE_RES, 4754 Groups.GROUP_VISIBLE, 4755 Groups.SYNC1, 4756 Groups.SYNC2, 4757 Groups.SYNC3, 4758 Groups.SYNC4, 4759 Groups.SYSTEM_ID, 4760 Groups.NOTES, 4761 Groups.DELETED, 4762 Groups.SHOULD_SYNC}; 4763 4764 private static final int COLUMN_ID = 0; 4765 private static final int COLUMN_ACCOUNT_NAME = 1; 4766 private static final int COLUMN_ACCOUNT_TYPE = 2; 4767 private static final int COLUMN_SOURCE_ID = 3; 4768 private static final int COLUMN_DIRTY = 4; 4769 private static final int COLUMN_VERSION = 5; 4770 private static final int COLUMN_RES_PACKAGE = 6; 4771 private static final int COLUMN_TITLE = 7; 4772 private static final int COLUMN_TITLE_RES = 8; 4773 private static final int COLUMN_GROUP_VISIBLE = 9; 4774 private static final int COLUMN_SYNC1 = 10; 4775 private static final int COLUMN_SYNC2 = 11; 4776 private static final int COLUMN_SYNC3 = 12; 4777 private static final int COLUMN_SYNC4 = 13; 4778 private static final int COLUMN_SYSTEM_ID = 14; 4779 private static final int COLUMN_NOTES = 15; 4780 private static final int COLUMN_DELETED = 16; 4781 private static final int COLUMN_SHOULD_SYNC = 17; 4782 4783 public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri, 4784 String selection, String[] selectionArgs, String sortOrder) { 4785 mIsClosed = false; 4786 4787 final String updatedSortOrder = (sortOrder == null) 4788 ? Groups._ID 4789 : (Groups._ID + "," + sortOrder); 4790 4791 final SQLiteDatabase db = provider.mDbHelper.getReadableDatabase(); 4792 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4793 qb.setTables(provider.mDbHelper.getGroupView()); 4794 qb.setProjectionMap(sGroupsProjectionMap); 4795 if (groupIdString != null) { 4796 qb.appendWhere(Groups._ID + "=" + groupIdString); 4797 } 4798 final String accountName = getQueryParameter(uri, Groups.ACCOUNT_NAME); 4799 final String accountType = getQueryParameter(uri, Groups.ACCOUNT_TYPE); 4800 if (!TextUtils.isEmpty(accountName)) { 4801 qb.appendWhere(Groups.ACCOUNT_NAME + "=" 4802 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 4803 + Groups.ACCOUNT_TYPE + "=" 4804 + DatabaseUtils.sqlEscapeString(accountType)); 4805 } 4806 mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs, 4807 null, null, updatedSortOrder); 4808 mEntityCursor.moveToFirst(); 4809 } 4810 4811 public void close() { 4812 if (mIsClosed) { 4813 throw new IllegalStateException("closing when already closed"); 4814 } 4815 mIsClosed = true; 4816 mEntityCursor.close(); 4817 } 4818 4819 public boolean hasNext() throws RemoteException { 4820 if (mIsClosed) { 4821 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 4822 } 4823 4824 return !mEntityCursor.isAfterLast(); 4825 } 4826 4827 public void reset() throws RemoteException { 4828 if (mIsClosed) { 4829 throw new IllegalStateException("calling reset() when the iterator is closed"); 4830 } 4831 mEntityCursor.moveToFirst(); 4832 } 4833 4834 public Entity next() throws RemoteException { 4835 if (mIsClosed) { 4836 throw new IllegalStateException("calling next() when the iterator is closed"); 4837 } 4838 if (!hasNext()) { 4839 throw new IllegalStateException("you may only call next() if hasNext() is true"); 4840 } 4841 4842 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 4843 4844 final long groupId = c.getLong(COLUMN_ID); 4845 4846 // we expect the cursor is already at the row we need to read from 4847 ContentValues groupValues = new ContentValues(); 4848 groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 4849 groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 4850 groupValues.put(Groups._ID, groupId); 4851 groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY)); 4852 groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION)); 4853 groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 4854 groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE)); 4855 groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE)); 4856 groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES)); 4857 groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE)); 4858 groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1)); 4859 groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2)); 4860 groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3)); 4861 groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4)); 4862 groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID)); 4863 groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED)); 4864 groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES)); 4865 groupValues.put(Groups.SHOULD_SYNC, c.getString(COLUMN_SHOULD_SYNC)); 4866 Entity group = new Entity(groupValues); 4867 4868 mEntityCursor.moveToNext(); 4869 4870 return group; 4871 } 4872 } 4873 4874 @Override 4875 public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, 4876 String sortOrder) { 4877 waitForAccess(); 4878 4879 final int match = sUriMatcher.match(uri); 4880 switch (match) { 4881 case RAW_CONTACTS: 4882 case RAW_CONTACTS_ID: 4883 String contactsIdString = null; 4884 if (match == RAW_CONTACTS_ID) { 4885 contactsIdString = uri.getPathSegments().get(1); 4886 } 4887 4888 return new RawContactsEntityIterator(this, uri, contactsIdString, 4889 selection, selectionArgs, sortOrder); 4890 case GROUPS: 4891 case GROUPS_ID: 4892 String idString = null; 4893 if (match == GROUPS_ID) { 4894 idString = uri.getPathSegments().get(1); 4895 } 4896 4897 return new GroupsEntityIterator(this, idString, 4898 uri, selection, selectionArgs, sortOrder); 4899 default: 4900 throw new UnsupportedOperationException("Unknown uri: " + uri); 4901 } 4902 } 4903 4904 @Override 4905 public String getType(Uri uri) { 4906 final int match = sUriMatcher.match(uri); 4907 switch (match) { 4908 case CONTACTS: 4909 case CONTACTS_LOOKUP: 4910 return Contacts.CONTENT_TYPE; 4911 case CONTACTS_ID: 4912 case CONTACTS_LOOKUP_ID: 4913 return Contacts.CONTENT_ITEM_TYPE; 4914 case CONTACTS_AS_VCARD: 4915 return Contacts.CONTENT_VCARD_TYPE; 4916 case RAW_CONTACTS: 4917 return RawContacts.CONTENT_TYPE; 4918 case RAW_CONTACTS_ID: 4919 return RawContacts.CONTENT_ITEM_TYPE; 4920 case DATA_ID: 4921 return mDbHelper.getDataMimeType(ContentUris.parseId(uri)); 4922 case PHONES: 4923 return Phone.CONTENT_TYPE; 4924 case PHONES_ID: 4925 return Phone.CONTENT_ITEM_TYPE; 4926 case EMAILS: 4927 return Email.CONTENT_TYPE; 4928 case EMAILS_ID: 4929 return Email.CONTENT_ITEM_TYPE; 4930 case POSTALS: 4931 return StructuredPostal.CONTENT_TYPE; 4932 case POSTALS_ID: 4933 return StructuredPostal.CONTENT_ITEM_TYPE; 4934 case AGGREGATION_EXCEPTIONS: 4935 return AggregationExceptions.CONTENT_TYPE; 4936 case AGGREGATION_EXCEPTION_ID: 4937 return AggregationExceptions.CONTENT_ITEM_TYPE; 4938 case SETTINGS: 4939 return Settings.CONTENT_TYPE; 4940 case AGGREGATION_SUGGESTIONS: 4941 return Contacts.CONTENT_TYPE; 4942 case SEARCH_SUGGESTIONS: 4943 return SearchManager.SUGGEST_MIME_TYPE; 4944 case SEARCH_SHORTCUT: 4945 return SearchManager.SHORTCUT_MIME_TYPE; 4946 default: 4947 return mLegacyApiSupport.getType(uri); 4948 } 4949 } 4950 4951 private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) { 4952 if (displayName != null) { 4953 mRawContactDisplayNameUpdate.bindString(1, displayName); 4954 } else { 4955 mRawContactDisplayNameUpdate.bindNull(1); 4956 } 4957 mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource); 4958 mRawContactDisplayNameUpdate.bindLong(3, rawContactId); 4959 mRawContactDisplayNameUpdate.execute(); 4960 } 4961 4962 /** 4963 * Sets the {@link RawContacts#DIRTY} for the specified raw contact. 4964 */ 4965 private void setRawContactDirty(long rawContactId) { 4966 mDirtyRawContacts.add(rawContactId); 4967 } 4968 4969 /* 4970 * Sets the given dataId record in the "data" table to primary, and resets all data records of 4971 * the same mimetype and under the same contact to not be primary. 4972 * 4973 * @param dataId the id of the data record to be set to primary. 4974 */ 4975 private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) { 4976 mSetPrimaryStatement.bindLong(1, dataId); 4977 mSetPrimaryStatement.bindLong(2, mimeTypeId); 4978 mSetPrimaryStatement.bindLong(3, rawContactId); 4979 mSetPrimaryStatement.execute(); 4980 } 4981 4982 /* 4983 * Sets the given dataId record in the "data" table to "super primary", and resets all data 4984 * records of the same mimetype and under the same aggregate to not be "super primary". 4985 * 4986 * @param dataId the id of the data record to be set to primary. 4987 */ 4988 private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) { 4989 mSetSuperPrimaryStatement.bindLong(1, dataId); 4990 mSetSuperPrimaryStatement.bindLong(2, mimeTypeId); 4991 mSetSuperPrimaryStatement.bindLong(3, rawContactId); 4992 mSetSuperPrimaryStatement.execute(); 4993 } 4994 4995 public void insertNameLookupForEmail(long rawContactId, long dataId, String email) { 4996 if (TextUtils.isEmpty(email)) { 4997 return; 4998 } 4999 5000 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email); 5001 if (tokens.length == 0) { 5002 return; 5003 } 5004 5005 String address = tokens[0].getAddress(); 5006 int at = address.indexOf('@'); 5007 if (at != -1) { 5008 address = address.substring(0, at); 5009 } 5010 5011 insertNameLookup(rawContactId, dataId, 5012 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address)); 5013 } 5014 5015 /** 5016 * Normalizes the nickname and inserts it in the name lookup table. 5017 */ 5018 public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) { 5019 if (TextUtils.isEmpty(nickname)) { 5020 return; 5021 } 5022 5023 insertNameLookup(rawContactId, dataId, 5024 NameLookupType.NICKNAME, NameNormalizer.normalize(nickname)); 5025 } 5026 5027 public void insertNameLookupForOrganization(long rawContactId, long dataId, String company, 5028 String title) { 5029 if (!TextUtils.isEmpty(company)) { 5030 insertNameLookup(rawContactId, dataId, 5031 NameLookupType.ORGANIZATION, NameNormalizer.normalize(company)); 5032 } 5033 if (!TextUtils.isEmpty(title)) { 5034 insertNameLookup(rawContactId, dataId, 5035 NameLookupType.ORGANIZATION, NameNormalizer.normalize(title)); 5036 } 5037 } 5038 5039 public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name) { 5040 mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name); 5041 } 5042 5043 private interface NicknameLookupPreloadQuery { 5044 String TABLE = Tables.NICKNAME_LOOKUP; 5045 5046 String[] COLUMNS = new String[] { 5047 NicknameLookupColumns.NAME 5048 }; 5049 5050 int NAME = 0; 5051 } 5052 5053 /** 5054 * Read all known common nicknames from the database and populate a Bloom 5055 * filter using the corresponding hash codes. The idea is to eliminate most 5056 * of unnecessary database lookups for nicknames. Given a name, we will take 5057 * its hash code and see if it is set in the Bloom filter. If not, we will know 5058 * that the name is not in the database. If it is, we still need to run a 5059 * query. 5060 * <p> 5061 * Given the size of the filter and the expected size of the nickname table, 5062 * we should expect the combination of the Bloom filter and cache will 5063 * prevent around 98-99% of unnecessary queries from running. 5064 */ 5065 private void preloadNicknameBloomFilter() { 5066 mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1); 5067 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 5068 Cursor cursor = db.query(NicknameLookupPreloadQuery.TABLE, 5069 NicknameLookupPreloadQuery.COLUMNS, 5070 null, null, null, null, null); 5071 try { 5072 int count = cursor.getCount(); 5073 for (int i = 0; i < count; i++) { 5074 cursor.moveToNext(); 5075 String normalizedName = cursor.getString(NicknameLookupPreloadQuery.NAME); 5076 int hashCode = normalizedName.hashCode(); 5077 mNicknameBloomFilter.set(hashCode & NICKNAME_BLOOM_FILTER_SIZE); 5078 } 5079 } finally { 5080 cursor.close(); 5081 } 5082 } 5083 5084 5085 /** 5086 * Returns nickname cluster IDs or null. Maintains cache. 5087 */ 5088 protected String[] getCommonNicknameClusters(String normalizedName) { 5089 int hashCode = normalizedName.hashCode(); 5090 if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) { 5091 return null; 5092 } 5093 5094 SoftReference<String[]> ref; 5095 String[] clusters = null; 5096 synchronized (mNicknameClusterCache) { 5097 if (mNicknameClusterCache.containsKey(normalizedName)) { 5098 ref = mNicknameClusterCache.get(normalizedName); 5099 if (ref == null) { 5100 return null; 5101 } 5102 clusters = ref.get(); 5103 } 5104 } 5105 5106 if (clusters == null) { 5107 clusters = loadNicknameClusters(normalizedName); 5108 ref = clusters == null ? null : new SoftReference<String[]>(clusters); 5109 synchronized (mNicknameClusterCache) { 5110 mNicknameClusterCache.put(normalizedName, ref); 5111 } 5112 } 5113 return clusters; 5114 } 5115 5116 private interface NicknameLookupQuery { 5117 String TABLE = Tables.NICKNAME_LOOKUP; 5118 5119 String[] COLUMNS = new String[] { 5120 NicknameLookupColumns.CLUSTER 5121 }; 5122 5123 int CLUSTER = 0; 5124 } 5125 5126 protected String[] loadNicknameClusters(String normalizedName) { 5127 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 5128 String[] clusters = null; 5129 Cursor cursor = db.query(NicknameLookupQuery.TABLE, NicknameLookupQuery.COLUMNS, 5130 NicknameLookupColumns.NAME + "=?", new String[] { normalizedName }, 5131 null, null, null); 5132 try { 5133 int count = cursor.getCount(); 5134 if (count > 0) { 5135 clusters = new String[count]; 5136 for (int i = 0; i < count; i++) { 5137 cursor.moveToNext(); 5138 clusters[i] = cursor.getString(NicknameLookupQuery.CLUSTER); 5139 } 5140 } 5141 } finally { 5142 cursor.close(); 5143 } 5144 return clusters; 5145 } 5146 5147 private class StructuredNameLookupBuilder extends NameLookupBuilder { 5148 5149 public StructuredNameLookupBuilder(NameSplitter splitter) { 5150 super(splitter); 5151 } 5152 5153 @Override 5154 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 5155 String name) { 5156 ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name); 5157 } 5158 5159 @Override 5160 protected String[] getCommonNicknameClusters(String normalizedName) { 5161 return ContactsProvider2.this.getCommonNicknameClusters(normalizedName); 5162 } 5163 } 5164 5165 /** 5166 * Inserts a record in the {@link Tables#NAME_LOOKUP} table. 5167 */ 5168 public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) { 5169 DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 1, rawContactId); 5170 DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 2, dataId); 5171 DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 3, lookupType); 5172 DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 4, name); 5173 mNameLookupInsert.executeInsert(); 5174 } 5175 5176 /** 5177 * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element. 5178 */ 5179 public void deleteNameLookup(long dataId) { 5180 DatabaseUtils.bindObjectToProgram(mNameLookupDelete, 1, dataId); 5181 mNameLookupDelete.execute(); 5182 } 5183 5184 public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 5185 sb.append("(" + 5186 "SELECT DISTINCT " + RawContacts.CONTACT_ID + 5187 " FROM " + Tables.RAW_CONTACTS + 5188 " JOIN " + Tables.NAME_LOOKUP + 5189 " ON(" + RawContactsColumns.CONCRETE_ID + "=" 5190 + NameLookupColumns.RAW_CONTACT_ID + ")" + 5191 " WHERE normalized_name GLOB '"); 5192 sb.append(NameNormalizer.normalize(filterParam)); 5193 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN(" 5194 + NameLookupType.NAME_COLLATION_KEY + "," 5195 + NameLookupType.EMAIL_BASED_NICKNAME + "," 5196 + NameLookupType.NICKNAME + "," 5197 + NameLookupType.ORGANIZATION + "))"); 5198 } 5199 5200 public String getRawContactsByFilterAsNestedQuery(String filterParam) { 5201 StringBuilder sb = new StringBuilder(); 5202 appendRawContactsByFilterAsNestedQuery(sb, filterParam, null); 5203 return sb.toString(); 5204 } 5205 5206 public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam, 5207 String limit) { 5208 appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit, 5209 true); 5210 } 5211 5212 private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, 5213 String limit, boolean allowEmailMatch) { 5214 sb.append("(" + 5215 "SELECT DISTINCT " + NameLookupColumns.RAW_CONTACT_ID + 5216 " FROM " + Tables.NAME_LOOKUP + 5217 " WHERE " + NameLookupColumns.NORMALIZED_NAME + 5218 " GLOB '"); 5219 sb.append(normalizedName); 5220 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN (" 5221 + NameLookupType.NAME_COLLATION_KEY + "," 5222 + NameLookupType.NICKNAME + "," 5223 + NameLookupType.ORGANIZATION); 5224 if (allowEmailMatch) { 5225 sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME); 5226 } 5227 sb.append(")"); 5228 5229 if (limit != null) { 5230 sb.append(" LIMIT ").append(limit); 5231 } 5232 sb.append(")"); 5233 } 5234 5235 /** 5236 * Inserts an argument at the beginning of the selection arg list. 5237 */ 5238 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 5239 if (selectionArgs == null) { 5240 return new String[] {arg}; 5241 } else { 5242 int newLength = selectionArgs.length + 1; 5243 String[] newSelectionArgs = new String[newLength]; 5244 newSelectionArgs[0] = arg; 5245 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 5246 return newSelectionArgs; 5247 } 5248 } 5249 5250 private String[] appendProjectionArg(String[] projection, String arg) { 5251 if (projection == null) { 5252 return null; 5253 } 5254 final int length = projection.length; 5255 String[] newProjection = new String[length + 1]; 5256 System.arraycopy(projection, 0, newProjection, 0, length); 5257 newProjection[length] = arg; 5258 return newProjection; 5259 } 5260 5261 protected Account getDefaultAccount() { 5262 AccountManager accountManager = AccountManager.get(getContext()); 5263 try { 5264 Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE, 5265 new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult(); 5266 if (accounts != null && accounts.length > 0) { 5267 return accounts[0]; 5268 } 5269 } catch (Throwable e) { 5270 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 5271 } 5272 return null; 5273 } 5274 5275 protected boolean isWritableAccount(Account account) { 5276 IContentService contentService = ContentResolver.getContentService(); 5277 try { 5278 for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) { 5279 if (ContactsContract.AUTHORITY.equals(sync.authority) && 5280 account.type.equals(sync.accountType)) { 5281 return sync.supportsUploading(); 5282 } 5283 } 5284 } catch (RemoteException e) { 5285 Log.e(TAG, "Could not acquire sync adapter types"); 5286 } 5287 return false; 5288 } 5289 5290 /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter, 5291 boolean defaultValue) { 5292 5293 // Manually parse the query, which is much faster than calling uri.getQueryParameter 5294 String query = uri.getEncodedQuery(); 5295 if (query == null) { 5296 return defaultValue; 5297 } 5298 5299 int index = query.indexOf(parameter); 5300 if (index == -1) { 5301 return defaultValue; 5302 } 5303 5304 index += parameter.length(); 5305 5306 return !matchQueryParameter(query, index, "=0", false) 5307 && !matchQueryParameter(query, index, "=false", true); 5308 } 5309 5310 private static boolean matchQueryParameter(String query, int index, String value, 5311 boolean ignoreCase) { 5312 int length = value.length(); 5313 return query.regionMatches(ignoreCase, index, value, 0, length) 5314 && (query.length() == index + length || query.charAt(index + length) == '&'); 5315 } 5316 5317 /** 5318 * A fast re-implementation of {@link Uri#getQueryParameter} 5319 */ 5320 /* package */ static String getQueryParameter(Uri uri, String parameter) { 5321 String query = uri.getEncodedQuery(); 5322 if (query == null) { 5323 return null; 5324 } 5325 5326 int queryLength = query.length(); 5327 int parameterLength = parameter.length(); 5328 5329 String value; 5330 int index = 0; 5331 while (true) { 5332 index = query.indexOf(parameter, index); 5333 if (index == -1) { 5334 return null; 5335 } 5336 5337 index += parameterLength; 5338 5339 if (queryLength == index) { 5340 return null; 5341 } 5342 5343 if (query.charAt(index) == '=') { 5344 index++; 5345 break; 5346 } 5347 } 5348 5349 int ampIndex = query.indexOf('&', index); 5350 if (ampIndex == -1) { 5351 value = query.substring(index); 5352 } else { 5353 value = query.substring(index, ampIndex); 5354 } 5355 5356 return Uri.decode(value); 5357 } 5358} 5359