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