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