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