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