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