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