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