ContactsProvider2.java revision c9e6d75562621a3dee26a99c2b082e2fd9b0c8b3
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.GroupsColumns; 28import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; 29import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; 30import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; 31import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns; 32import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; 33import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; 34import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; 35import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns; 36import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns; 37import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 38import com.google.android.collect.Lists; 39import com.google.android.collect.Maps; 40import com.google.android.collect.Sets; 41 42import android.accounts.Account; 43import android.accounts.AccountManager; 44import android.accounts.OnAccountsUpdateListener; 45import android.app.Notification; 46import android.app.NotificationManager; 47import android.app.PendingIntent; 48import android.app.SearchManager; 49import android.content.ContentProviderOperation; 50import android.content.ContentProviderResult; 51import android.content.ContentResolver; 52import android.content.ContentUris; 53import android.content.ContentValues; 54import android.content.Context; 55import android.content.IContentService; 56import android.content.Intent; 57import android.content.OperationApplicationException; 58import android.content.SharedPreferences; 59import android.content.SyncAdapterType; 60import android.content.UriMatcher; 61import android.content.res.AssetFileDescriptor; 62import android.content.res.Configuration; 63import android.database.CharArrayBuffer; 64import android.database.Cursor; 65import android.database.CursorWrapper; 66import android.database.DatabaseUtils; 67import android.database.MatrixCursor; 68import android.database.MatrixCursor.RowBuilder; 69import android.database.sqlite.SQLiteConstraintException; 70import android.database.sqlite.SQLiteContentHelper; 71import android.database.sqlite.SQLiteDatabase; 72import android.database.sqlite.SQLiteQueryBuilder; 73import android.database.sqlite.SQLiteStatement; 74import android.net.Uri; 75import android.os.AsyncTask; 76import android.os.Bundle; 77import android.os.MemoryFile; 78import android.os.RemoteException; 79import android.os.SystemProperties; 80import android.pim.vcard.VCardComposer; 81import android.pim.vcard.VCardConfig; 82import android.preference.PreferenceManager; 83import android.provider.BaseColumns; 84import android.provider.ContactsContract; 85import android.provider.LiveFolders; 86import android.provider.OpenableColumns; 87import android.provider.SyncStateContract; 88import android.provider.ContactsContract.AggregationExceptions; 89import android.provider.ContactsContract.ContactCounts; 90import android.provider.ContactsContract.Contacts; 91import android.provider.ContactsContract.Data; 92import android.provider.ContactsContract.DisplayNameSources; 93import android.provider.ContactsContract.FullNameStyle; 94import android.provider.ContactsContract.Groups; 95import android.provider.ContactsContract.Intents; 96import android.provider.ContactsContract.PhoneLookup; 97import android.provider.ContactsContract.PhoneticNameStyle; 98import android.provider.ContactsContract.ProviderStatus; 99import android.provider.ContactsContract.RawContacts; 100import android.provider.ContactsContract.SearchSnippetColumns; 101import android.provider.ContactsContract.Settings; 102import android.provider.ContactsContract.StatusUpdates; 103import android.provider.ContactsContract.CommonDataKinds.BaseTypes; 104import android.provider.ContactsContract.CommonDataKinds.Email; 105import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 106import android.provider.ContactsContract.CommonDataKinds.Im; 107import android.provider.ContactsContract.CommonDataKinds.Nickname; 108import android.provider.ContactsContract.CommonDataKinds.Organization; 109import android.provider.ContactsContract.CommonDataKinds.Phone; 110import android.provider.ContactsContract.CommonDataKinds.Photo; 111import android.provider.ContactsContract.CommonDataKinds.StructuredName; 112import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 113import android.telephony.PhoneNumberUtils; 114import android.text.TextUtils; 115import android.util.Log; 116 117import java.io.ByteArrayOutputStream; 118import java.io.FileNotFoundException; 119import java.io.IOException; 120import java.io.OutputStream; 121import java.text.SimpleDateFormat; 122import java.util.ArrayList; 123import java.util.Collections; 124import java.util.Date; 125import java.util.HashMap; 126import java.util.HashSet; 127import java.util.List; 128import java.util.Locale; 129import java.util.Map; 130import java.util.Set; 131import java.util.concurrent.CountDownLatch; 132 133/** 134 * Contacts content provider. The contract between this provider and applications 135 * is defined in {@link ContactsContract}. 136 */ 137public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener { 138 139 private static final String TAG = "ContactsProvider"; 140 141 private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 142 143 // TODO: carefully prevent all incoming nested queries; they can be gaping security holes 144 // TODO: check for restricted flag during insert(), update(), and delete() calls 145 146 /** Default for the maximum number of returned aggregation suggestions. */ 147 private static final int DEFAULT_MAX_SUGGESTIONS = 5; 148 149 /** 150 * Property key for the legacy contact import version. The need for a version 151 * as opposed to a boolean flag is that if we discover bugs in the contact import process, 152 * we can trigger re-import by incrementing the import version. 153 */ 154 private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1"; 155 private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1; 156 private static final String PREF_LOCALE = "locale"; 157 158 private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate"; 159 160 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 161 162 private static final String TIMES_CONTACED_SORT_COLUMN = "times_contacted_sort"; 163 164 private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, " 165 + TIMES_CONTACED_SORT_COLUMN + " DESC, " 166 + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 167 private static final String STREQUENT_LIMIT = 168 "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE " 169 + Contacts.STARRED + "=1) + 25"; 170 171 /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE = 172 "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" + 173 " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " + 174 " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?"; 175 176 /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE = 177 "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" + 178 " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " + 179 " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?"; 180 181 /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK"; 182 183 private static final int CONTACTS = 1000; 184 private static final int CONTACTS_ID = 1001; 185 private static final int CONTACTS_LOOKUP = 1002; 186 private static final int CONTACTS_LOOKUP_ID = 1003; 187 private static final int CONTACTS_DATA = 1004; 188 private static final int CONTACTS_FILTER = 1005; 189 private static final int CONTACTS_STREQUENT = 1006; 190 private static final int CONTACTS_STREQUENT_FILTER = 1007; 191 private static final int CONTACTS_GROUP = 1008; 192 private static final int CONTACTS_PHOTO = 1009; 193 private static final int CONTACTS_AS_VCARD = 1010; 194 private static final int CONTACTS_AS_MULTI_VCARD = 1011; 195 196 private static final int RAW_CONTACTS = 2002; 197 private static final int RAW_CONTACTS_ID = 2003; 198 private static final int RAW_CONTACTS_DATA = 2004; 199 private static final int RAW_CONTACT_ENTITY_ID = 2005; 200 201 private static final int DATA = 3000; 202 private static final int DATA_ID = 3001; 203 private static final int PHONES = 3002; 204 private static final int PHONES_ID = 3003; 205 private static final int PHONES_FILTER = 3004; 206 private static final int EMAILS = 3005; 207 private static final int EMAILS_ID = 3006; 208 private static final int EMAILS_LOOKUP = 3007; 209 private static final int EMAILS_FILTER = 3008; 210 private static final int POSTALS = 3009; 211 private static final int POSTALS_ID = 3010; 212 213 private static final int PHONE_LOOKUP = 4000; 214 215 private static final int AGGREGATION_EXCEPTIONS = 6000; 216 private static final int AGGREGATION_EXCEPTION_ID = 6001; 217 218 private static final int STATUS_UPDATES = 7000; 219 private static final int STATUS_UPDATES_ID = 7001; 220 221 private static final int AGGREGATION_SUGGESTIONS = 8000; 222 223 private static final int SETTINGS = 9000; 224 225 private static final int GROUPS = 10000; 226 private static final int GROUPS_ID = 10001; 227 private static final int GROUPS_SUMMARY = 10003; 228 229 private static final int SYNCSTATE = 11000; 230 private static final int SYNCSTATE_ID = 11001; 231 232 private static final int SEARCH_SUGGESTIONS = 12001; 233 private static final int SEARCH_SHORTCUT = 12002; 234 235 private static final int LIVE_FOLDERS_CONTACTS = 14000; 236 private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001; 237 private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002; 238 private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003; 239 240 private static final int RAW_CONTACT_ENTITIES = 15001; 241 242 private static final int PROVIDER_STATUS = 16001; 243 244 private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID = 245 RawContactsColumns.CONCRETE_ID + "=? AND " 246 + GroupsColumns.CONCRETE_ACCOUNT_NAME 247 + "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND " 248 + GroupsColumns.CONCRETE_ACCOUNT_TYPE 249 + "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE 250 + " AND " + Groups.FAVORITES + " != 0"; 251 252 private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID = 253 RawContactsColumns.CONCRETE_ID + "=? AND " 254 + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" 255 + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND " 256 + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" 257 + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND " 258 + Groups.AUTO_ADD + " != 0"; 259 260 private static final String[] PROJECTION_GROUP_ID 261 = new String[]{Tables.GROUPS + "." + Groups._ID}; 262 263 private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? " 264 + "AND " + GroupMembership.GROUP_ROW_ID + "=? " 265 + "AND " + GroupMembership.RAW_CONTACT_ID + "=?"; 266 267 private static final String SELECTION_STARRED_FROM_RAW_CONTACTS = 268 "SELECT " + RawContacts.STARRED 269 + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?"; 270 271 private interface DataContactsQuery { 272 public static final String TABLE = "data " 273 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) " 274 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; 275 276 public static final String[] PROJECTION = new String[] { 277 RawContactsColumns.CONCRETE_ID, 278 DataColumns.CONCRETE_ID, 279 ContactsColumns.CONCRETE_ID 280 }; 281 282 public static final int RAW_CONTACT_ID = 0; 283 public static final int DATA_ID = 1; 284 public static final int CONTACT_ID = 2; 285 } 286 287 private interface DataDeleteQuery { 288 public static final String TABLE = Tables.DATA_JOIN_MIMETYPES; 289 290 public static final String[] CONCRETE_COLUMNS = new String[] { 291 DataColumns.CONCRETE_ID, 292 MimetypesColumns.MIMETYPE, 293 Data.RAW_CONTACT_ID, 294 Data.IS_PRIMARY, 295 Data.DATA1, 296 }; 297 298 public static final String[] COLUMNS = new String[] { 299 Data._ID, 300 MimetypesColumns.MIMETYPE, 301 Data.RAW_CONTACT_ID, 302 Data.IS_PRIMARY, 303 Data.DATA1, 304 }; 305 306 public static final int _ID = 0; 307 public static final int MIMETYPE = 1; 308 public static final int RAW_CONTACT_ID = 2; 309 public static final int IS_PRIMARY = 3; 310 public static final int DATA1 = 4; 311 } 312 313 private interface DataUpdateQuery { 314 String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE }; 315 316 int _ID = 0; 317 int RAW_CONTACT_ID = 1; 318 int MIMETYPE = 2; 319 } 320 321 322 private interface RawContactsQuery { 323 String TABLE = Tables.RAW_CONTACTS; 324 325 String[] COLUMNS = new String[] { 326 RawContacts.DELETED, 327 RawContacts.ACCOUNT_TYPE, 328 RawContacts.ACCOUNT_NAME, 329 }; 330 331 int DELETED = 0; 332 int ACCOUNT_TYPE = 1; 333 int ACCOUNT_NAME = 2; 334 } 335 336 public static final String DEFAULT_ACCOUNT_TYPE = "com.google"; 337 public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google"; 338 339 /** Sql where statement for filtering on groups. */ 340 private static final String CONTACTS_IN_GROUP_SELECT = 341 Contacts._ID + " IN " 342 + "(SELECT " + RawContacts.CONTACT_ID 343 + " FROM " + Tables.RAW_CONTACTS 344 + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN " 345 + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID 346 + " FROM " + Tables.DATA_JOIN_MIMETYPES 347 + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 348 + "' AND " + GroupMembership.GROUP_ROW_ID + "=" 349 + "(SELECT " + Tables.GROUPS + "." + Groups._ID 350 + " FROM " + Tables.GROUPS 351 + " WHERE " + Groups.TITLE + "=?)))"; 352 353 /** Sql for updating DIRTY flag on multiple raw contacts */ 354 private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL = 355 "UPDATE " + Tables.RAW_CONTACTS + 356 " SET " + RawContacts.DIRTY + "=1" + 357 " WHERE " + RawContacts._ID + " IN ("; 358 359 /** Sql for updating VERSION on multiple raw contacts */ 360 private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL = 361 "UPDATE " + Tables.RAW_CONTACTS + 362 " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" + 363 " WHERE " + RawContacts._ID + " IN ("; 364 365 /** Name lookup types used for contact filtering */ 366 private static final String CONTACT_LOOKUP_NAME_TYPES = 367 NameLookupType.NAME_COLLATION_KEY + "," + 368 NameLookupType.EMAIL_BASED_NICKNAME + "," + 369 NameLookupType.NICKNAME + "," + 370 NameLookupType.NAME_SHORTHAND + "," + 371 NameLookupType.ORGANIZATION + "," + 372 NameLookupType.NAME_CONSONANTS; 373 374 375 /** Contains just BaseColumns._COUNT */ 376 private static final HashMap<String, String> sCountProjectionMap; 377 /** Contains just the contacts columns */ 378 private static final HashMap<String, String> sContactsProjectionMap; 379 /** Contains just the contacts columns */ 380 private static final HashMap<String, String> sContactsProjectionWithSnippetMap; 381 382 /** Used for pushing starred contacts to the top of a times contacted list **/ 383 private static final HashMap<String, String> sStrequentStarredProjectionMap; 384 private static final HashMap<String, String> sStrequentFrequentProjectionMap; 385 /** Contains just the contacts vCard columns */ 386 private static final HashMap<String, String> sContactsVCardProjectionMap; 387 /** Contains just the raw contacts columns */ 388 private static final HashMap<String, String> sRawContactsProjectionMap; 389 /** Contains the columns from the raw contacts entity view*/ 390 private static final HashMap<String, String> sRawContactsEntityProjectionMap; 391 /** Contains columns from the data view */ 392 private static final HashMap<String, String> sDataProjectionMap; 393 /** Contains columns from the data view */ 394 private static final HashMap<String, String> sDistinctDataProjectionMap; 395 /** Contains the data and contacts columns, for joined tables */ 396 private static final HashMap<String, String> sPhoneLookupProjectionMap; 397 /** Contains the just the {@link Groups} columns */ 398 private static final HashMap<String, String> sGroupsProjectionMap; 399 /** Contains {@link Groups} columns along with summary details */ 400 private static final HashMap<String, String> sGroupsSummaryProjectionMap; 401 /** Contains the agg_exceptions columns */ 402 private static final HashMap<String, String> sAggregationExceptionsProjectionMap; 403 /** Contains the agg_exceptions columns */ 404 private static final HashMap<String, String> sSettingsProjectionMap; 405 /** Contains StatusUpdates columns */ 406 private static final HashMap<String, String> sStatusUpdatesProjectionMap; 407 /** Contains Live Folders columns */ 408 private static final HashMap<String, String> sLiveFoldersProjectionMap; 409 410 // where clause to update the status_updates table 411 private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE = 412 StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID + 413 " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE + 414 " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE "; 415 416 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 417 418 /** 419 * Notification ID for failure to import contacts. 420 */ 421 private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1; 422 423 /** Precompiled sql statement for setting a data record to the primary. */ 424 private SQLiteStatement mSetPrimaryStatement; 425 /** Precompiled sql statement for setting a data record to the super primary. */ 426 private SQLiteStatement mSetSuperPrimaryStatement; 427 /** Precompiled sql statement for updating a contact display name */ 428 private SQLiteStatement mRawContactDisplayNameUpdate; 429 /** Precompiled sql statement for updating an aggregated status update */ 430 private SQLiteStatement mLastStatusUpdate; 431 private SQLiteStatement mNameLookupInsert; 432 private SQLiteStatement mNameLookupDelete; 433 private SQLiteStatement mStatusUpdateAutoTimestamp; 434 private SQLiteStatement mStatusUpdateInsert; 435 private SQLiteStatement mStatusUpdateReplace; 436 private SQLiteStatement mStatusAttributionUpdate; 437 private SQLiteStatement mStatusUpdateDelete; 438 private SQLiteStatement mResetNameVerifiedForOtherRawContacts; 439 440 private long mMimeTypeIdEmail; 441 private long mMimeTypeIdIm; 442 private long mMimeTypeIdStructuredName; 443 private long mMimeTypeIdOrganization; 444 private long mMimeTypeIdNickname; 445 private long mMimeTypeIdPhone; 446 private StringBuilder mSb = new StringBuilder(); 447 private String[] mSelectionArgs1 = new String[1]; 448 private String[] mSelectionArgs2 = new String[2]; 449 private ArrayList<String> mSelectionArgs = Lists.newArrayList(); 450 451 private Account mAccount; 452 453 static { 454 // Contacts URI matching table 455 final UriMatcher matcher = sUriMatcher; 456 matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS); 457 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 458 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA); 459 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", 460 AGGREGATION_SUGGESTIONS); 461 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*", 462 AGGREGATION_SUGGESTIONS); 463 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO); 464 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER); 465 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER); 466 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP); 467 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID); 468 matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD); 469 matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*", 470 CONTACTS_AS_MULTI_VCARD); 471 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT); 472 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", 473 CONTACTS_STREQUENT_FILTER); 474 matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP); 475 476 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS); 477 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID); 478 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA); 479 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID); 480 481 matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES); 482 483 matcher.addURI(ContactsContract.AUTHORITY, "data", DATA); 484 matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID); 485 matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES); 486 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID); 487 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER); 488 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER); 489 matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS); 490 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID); 491 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP); 492 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER); 493 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER); 494 matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS); 495 matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID); 496 497 matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS); 498 matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID); 499 matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY); 500 501 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE); 502 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#", 503 SYNCSTATE_ID); 504 505 matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP); 506 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", 507 AGGREGATION_EXCEPTIONS); 508 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", 509 AGGREGATION_EXCEPTION_ID); 510 511 matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS); 512 513 matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES); 514 matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID); 515 516 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, 517 SEARCH_SUGGESTIONS); 518 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 519 SEARCH_SUGGESTIONS); 520 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", 521 SEARCH_SHORTCUT); 522 523 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts", 524 LIVE_FOLDERS_CONTACTS); 525 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*", 526 LIVE_FOLDERS_CONTACTS_GROUP_NAME); 527 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones", 528 LIVE_FOLDERS_CONTACTS_WITH_PHONES); 529 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites", 530 LIVE_FOLDERS_CONTACTS_FAVORITES); 531 532 matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS); 533 } 534 535 static { 536 sCountProjectionMap = new HashMap<String, String>(); 537 sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)"); 538 539 sContactsProjectionMap = new HashMap<String, String>(); 540 sContactsProjectionMap.put(Contacts._ID, Contacts._ID); 541 sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY); 542 sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, 543 Contacts.DISPLAY_NAME_ALTERNATIVE); 544 sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); 545 sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); 546 sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); 547 sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); 548 sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE); 549 sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 550 sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 551 sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 552 sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); 553 sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 554 sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 555 sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER); 556 sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 557 sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 558 sContactsProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID); 559 560 // Handle projections for Contacts-level statuses 561 addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE, 562 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); 563 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS, 564 ContactsStatusUpdatesColumns.CONCRETE_STATUS); 565 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, 566 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 567 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, 568 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 569 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL, 570 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); 571 addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON, 572 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); 573 574 sContactsProjectionWithSnippetMap = new HashMap<String, String>(); 575 sContactsProjectionWithSnippetMap.putAll(sContactsProjectionMap); 576 sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_MIMETYPE, 577 SearchSnippetColumns.SNIPPET_MIMETYPE); 578 sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA_ID, 579 SearchSnippetColumns.SNIPPET_DATA_ID); 580 sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA1, 581 SearchSnippetColumns.SNIPPET_DATA1); 582 sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA2, 583 SearchSnippetColumns.SNIPPET_DATA2); 584 sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA3, 585 SearchSnippetColumns.SNIPPET_DATA3); 586 sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA4, 587 SearchSnippetColumns.SNIPPET_DATA4); 588 589 sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap); 590 sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, 591 Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN); 592 593 sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap); 594 sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN, 595 Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN); 596 597 sContactsVCardProjectionMap = Maps.newHashMap(); 598 sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME 599 + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME); 600 sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE); 601 602 sRawContactsProjectionMap = new HashMap<String, String>(); 603 sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID); 604 sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 605 sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 606 sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 607 sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 608 sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); 609 sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); 610 sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED); 611 sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY, 612 RawContacts.DISPLAY_NAME_PRIMARY); 613 sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, 614 RawContacts.DISPLAY_NAME_ALTERNATIVE); 615 sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE, 616 RawContacts.DISPLAY_NAME_SOURCE); 617 sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME, 618 RawContacts.PHONETIC_NAME); 619 sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE, 620 RawContacts.PHONETIC_NAME_STYLE); 621 sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED, 622 RawContacts.NAME_VERIFIED); 623 sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY, 624 RawContacts.SORT_KEY_PRIMARY); 625 sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE, 626 RawContacts.SORT_KEY_ALTERNATIVE); 627 sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED); 628 sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED, 629 RawContacts.LAST_TIME_CONTACTED); 630 sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE); 631 sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL); 632 sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED); 633 sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE); 634 sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1); 635 sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2); 636 sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3); 637 sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4); 638 639 sDataProjectionMap = new HashMap<String, String>(); 640 sDataProjectionMap.put(Data._ID, Data._ID); 641 sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID); 642 sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); 643 sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 644 sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 645 sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 646 sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); 647 sDataProjectionMap.put(Data.DATA1, Data.DATA1); 648 sDataProjectionMap.put(Data.DATA2, Data.DATA2); 649 sDataProjectionMap.put(Data.DATA3, Data.DATA3); 650 sDataProjectionMap.put(Data.DATA4, Data.DATA4); 651 sDataProjectionMap.put(Data.DATA5, Data.DATA5); 652 sDataProjectionMap.put(Data.DATA6, Data.DATA6); 653 sDataProjectionMap.put(Data.DATA7, Data.DATA7); 654 sDataProjectionMap.put(Data.DATA8, Data.DATA8); 655 sDataProjectionMap.put(Data.DATA9, Data.DATA9); 656 sDataProjectionMap.put(Data.DATA10, Data.DATA10); 657 sDataProjectionMap.put(Data.DATA11, Data.DATA11); 658 sDataProjectionMap.put(Data.DATA12, Data.DATA12); 659 sDataProjectionMap.put(Data.DATA13, Data.DATA13); 660 sDataProjectionMap.put(Data.DATA14, Data.DATA14); 661 sDataProjectionMap.put(Data.DATA15, Data.DATA15); 662 sDataProjectionMap.put(Data.SYNC1, Data.SYNC1); 663 sDataProjectionMap.put(Data.SYNC2, Data.SYNC2); 664 sDataProjectionMap.put(Data.SYNC3, Data.SYNC3); 665 sDataProjectionMap.put(Data.SYNC4, Data.SYNC4); 666 sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID); 667 sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 668 sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 669 sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 670 sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); 671 sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); 672 sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED); 673 sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 674 sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 675 sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, 676 Contacts.DISPLAY_NAME_ALTERNATIVE); 677 sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); 678 sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); 679 sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); 680 sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); 681 sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE); 682 sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 683 sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 684 sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 685 sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 686 sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 687 sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 688 sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); 689 sDataProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID); 690 sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID); 691 692 HashMap<String, String> columns; 693 columns = new HashMap<String, String>(); 694 columns.put(RawContacts._ID, RawContacts._ID); 695 columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 696 columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 697 columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 698 columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 699 columns.put(RawContacts.VERSION, RawContacts.VERSION); 700 columns.put(RawContacts.DIRTY, RawContacts.DIRTY); 701 columns.put(RawContacts.DELETED, RawContacts.DELETED); 702 columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED); 703 columns.put(RawContacts.SYNC1, RawContacts.SYNC1); 704 columns.put(RawContacts.SYNC2, RawContacts.SYNC2); 705 columns.put(RawContacts.SYNC3, RawContacts.SYNC3); 706 columns.put(RawContacts.SYNC4, RawContacts.SYNC4); 707 columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED); 708 columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 709 columns.put(Data.MIMETYPE, Data.MIMETYPE); 710 columns.put(Data.DATA1, Data.DATA1); 711 columns.put(Data.DATA2, Data.DATA2); 712 columns.put(Data.DATA3, Data.DATA3); 713 columns.put(Data.DATA4, Data.DATA4); 714 columns.put(Data.DATA5, Data.DATA5); 715 columns.put(Data.DATA6, Data.DATA6); 716 columns.put(Data.DATA7, Data.DATA7); 717 columns.put(Data.DATA8, Data.DATA8); 718 columns.put(Data.DATA9, Data.DATA9); 719 columns.put(Data.DATA10, Data.DATA10); 720 columns.put(Data.DATA11, Data.DATA11); 721 columns.put(Data.DATA12, Data.DATA12); 722 columns.put(Data.DATA13, Data.DATA13); 723 columns.put(Data.DATA14, Data.DATA14); 724 columns.put(Data.DATA15, Data.DATA15); 725 columns.put(Data.SYNC1, Data.SYNC1); 726 columns.put(Data.SYNC2, Data.SYNC2); 727 columns.put(Data.SYNC3, Data.SYNC3); 728 columns.put(Data.SYNC4, Data.SYNC4); 729 columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID); 730 columns.put(Data.STARRED, Data.STARRED); 731 columns.put(Data.DATA_VERSION, Data.DATA_VERSION); 732 columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 733 columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 734 columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID); 735 sRawContactsEntityProjectionMap = columns; 736 737 // Handle projections for Contacts-level statuses 738 addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE, 739 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); 740 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS, 741 ContactsStatusUpdatesColumns.CONCRETE_STATUS); 742 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, 743 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 744 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, 745 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 746 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, 747 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); 748 addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON, 749 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); 750 751 // Handle projections for Data-level statuses 752 addProjection(sDataProjectionMap, Data.PRESENCE, 753 Tables.PRESENCE + "." + StatusUpdates.PRESENCE); 754 addProjection(sDataProjectionMap, Data.STATUS, 755 StatusUpdatesColumns.CONCRETE_STATUS); 756 addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP, 757 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 758 addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE, 759 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 760 addProjection(sDataProjectionMap, Data.STATUS_LABEL, 761 StatusUpdatesColumns.CONCRETE_STATUS_LABEL); 762 addProjection(sDataProjectionMap, Data.STATUS_ICON, 763 StatusUpdatesColumns.CONCRETE_STATUS_ICON); 764 765 // Projection map for data grouped by contact (not raw contact) and some data field(s) 766 sDistinctDataProjectionMap = new HashMap<String, String>(); 767 sDistinctDataProjectionMap.put(Data._ID, 768 "MIN(" + Data._ID + ") AS " + Data._ID); 769 sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); 770 sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 771 sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 772 sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 773 sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); 774 sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1); 775 sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2); 776 sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3); 777 sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4); 778 sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5); 779 sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6); 780 sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7); 781 sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8); 782 sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9); 783 sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10); 784 sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11); 785 sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12); 786 sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13); 787 sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14); 788 sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15); 789 sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1); 790 sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2); 791 sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3); 792 sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4); 793 sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 794 sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 795 sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 796 sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE, 797 Contacts.DISPLAY_NAME_ALTERNATIVE); 798 sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE); 799 sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME); 800 sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE); 801 sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY); 802 sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, 803 Contacts.SORT_KEY_ALTERNATIVE); 804 sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 805 sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 806 sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 807 sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 808 sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 809 sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 810 sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); 811 sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, 812 GroupMembership.GROUP_SOURCE_ID); 813 814 // Handle projections for Contacts-level statuses 815 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE, 816 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE); 817 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS, 818 ContactsStatusUpdatesColumns.CONCRETE_STATUS); 819 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP, 820 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 821 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE, 822 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 823 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL, 824 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL); 825 addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON, 826 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON); 827 828 // Handle projections for Data-level statuses 829 addProjection(sDistinctDataProjectionMap, Data.PRESENCE, 830 Tables.PRESENCE + "." + StatusUpdates.PRESENCE); 831 addProjection(sDistinctDataProjectionMap, Data.STATUS, 832 StatusUpdatesColumns.CONCRETE_STATUS); 833 addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP, 834 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP); 835 addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE, 836 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE); 837 addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL, 838 StatusUpdatesColumns.CONCRETE_STATUS_LABEL); 839 addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON, 840 StatusUpdatesColumns.CONCRETE_STATUS_ICON); 841 842 sPhoneLookupProjectionMap = new HashMap<String, String>(); 843 sPhoneLookupProjectionMap.put(PhoneLookup._ID, 844 "contacts_view." + Contacts._ID 845 + " AS " + PhoneLookup._ID); 846 sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY, 847 "contacts_view." + Contacts.LOOKUP_KEY 848 + " AS " + PhoneLookup.LOOKUP_KEY); 849 sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME, 850 "contacts_view." + Contacts.DISPLAY_NAME 851 + " AS " + PhoneLookup.DISPLAY_NAME); 852 sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED, 853 "contacts_view." + Contacts.LAST_TIME_CONTACTED 854 + " AS " + PhoneLookup.LAST_TIME_CONTACTED); 855 sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED, 856 "contacts_view." + Contacts.TIMES_CONTACTED 857 + " AS " + PhoneLookup.TIMES_CONTACTED); 858 sPhoneLookupProjectionMap.put(PhoneLookup.STARRED, 859 "contacts_view." + Contacts.STARRED 860 + " AS " + PhoneLookup.STARRED); 861 sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP, 862 "contacts_view." + Contacts.IN_VISIBLE_GROUP 863 + " AS " + PhoneLookup.IN_VISIBLE_GROUP); 864 sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID, 865 "contacts_view." + Contacts.PHOTO_ID 866 + " AS " + PhoneLookup.PHOTO_ID); 867 sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE, 868 "contacts_view." + Contacts.CUSTOM_RINGTONE 869 + " AS " + PhoneLookup.CUSTOM_RINGTONE); 870 sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER, 871 "contacts_view." + Contacts.HAS_PHONE_NUMBER 872 + " AS " + PhoneLookup.HAS_PHONE_NUMBER); 873 sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL, 874 "contacts_view." + Contacts.SEND_TO_VOICEMAIL 875 + " AS " + PhoneLookup.SEND_TO_VOICEMAIL); 876 sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER, 877 Phone.NUMBER + " AS " + PhoneLookup.NUMBER); 878 sPhoneLookupProjectionMap.put(PhoneLookup.TYPE, 879 Phone.TYPE + " AS " + PhoneLookup.TYPE); 880 sPhoneLookupProjectionMap.put(PhoneLookup.LABEL, 881 Phone.LABEL + " AS " + PhoneLookup.LABEL); 882 883 // Groups projection map 884 columns = new HashMap<String, String>(); 885 columns.put(Groups._ID, Groups._ID); 886 columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME); 887 columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE); 888 columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID); 889 columns.put(Groups.DIRTY, Groups.DIRTY); 890 columns.put(Groups.VERSION, Groups.VERSION); 891 columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE); 892 columns.put(Groups.TITLE, Groups.TITLE); 893 columns.put(Groups.TITLE_RES, Groups.TITLE_RES); 894 columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE); 895 columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID); 896 columns.put(Groups.DELETED, Groups.DELETED); 897 columns.put(Groups.NOTES, Groups.NOTES); 898 columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC); 899 columns.put(Groups.FAVORITES, Groups.FAVORITES); 900 columns.put(Groups.AUTO_ADD, Groups.AUTO_ADD); 901 columns.put(Groups.SYNC1, Groups.SYNC1); 902 columns.put(Groups.SYNC2, Groups.SYNC2); 903 columns.put(Groups.SYNC3, Groups.SYNC3); 904 columns.put(Groups.SYNC4, Groups.SYNC4); 905 sGroupsProjectionMap = columns; 906 907 // RawContacts and groups projection map 908 columns = new HashMap<String, String>(); 909 columns.putAll(sGroupsProjectionMap); 910 columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID 911 + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " 912 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 913 + ") AS " + Groups.SUMMARY_COUNT); 914 columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT " 915 + ContactsColumns.CONCRETE_ID + ") FROM " 916 + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " 917 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 918 + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES); 919 sGroupsSummaryProjectionMap = columns; 920 921 // Aggregate exception projection map 922 columns = new HashMap<String, String>(); 923 columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id"); 924 columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE); 925 columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1); 926 columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2); 927 sAggregationExceptionsProjectionMap = columns; 928 929 // Settings projection map 930 columns = new HashMap<String, String>(); 931 columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME); 932 columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE); 933 columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE); 934 columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC); 935 columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC 936 + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN(" 937 + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE " 938 + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME 939 + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" 940 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS " 941 + Settings.ANY_UNSYNCED); 942 columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM " 943 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY " 944 + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS 945 + ")) AS " + Settings.UNGROUPED_COUNT); 946 columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM " 947 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE " 948 + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID 949 + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS " 950 + Settings.UNGROUPED_WITH_PHONES); 951 sSettingsProjectionMap = columns; 952 953 columns = new HashMap<String, String>(); 954 columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID); 955 columns.put(StatusUpdates.DATA_ID, 956 DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID); 957 columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT); 958 columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE); 959 columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL); 960 // We cannot allow a null in the custom protocol field, because SQLite3 does not 961 // properly enforce uniqueness of null values 962 columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL 963 + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS " 964 + StatusUpdates.CUSTOM_PROTOCOL); 965 columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE); 966 columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS); 967 columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP); 968 columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE); 969 columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON); 970 columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL); 971 sStatusUpdatesProjectionMap = columns; 972 973 // Live folder projection 974 sLiveFoldersProjectionMap = new HashMap<String, String>(); 975 sLiveFoldersProjectionMap.put(LiveFolders._ID, 976 Contacts._ID + " AS " + LiveFolders._ID); 977 sLiveFoldersProjectionMap.put(LiveFolders.NAME, 978 Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME); 979 // TODO: Put contact photo back when we have a way to display a default icon 980 // for contacts without a photo 981 // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP, 982 // Photos.DATA + " AS " + LiveFolders.ICON_BITMAP); 983 } 984 985 private static void addProjection(HashMap<String, String> map, String toField, String fromField) { 986 map.put(toField, fromField + " AS " + toField); 987 } 988 989 /** 990 * Handles inserts and update for a specific Data type. 991 */ 992 private abstract class DataRowHandler { 993 994 protected final String mMimetype; 995 protected long mMimetypeId; 996 997 @SuppressWarnings("all") 998 public DataRowHandler(String mimetype) { 999 mMimetype = mimetype; 1000 1001 // To ensure the data column position. This is dead code if properly configured. 1002 if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1 1003 || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1 1004 || Email.DATA != Data.DATA1) { 1005 throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary" 1006 + " data is not in DATA1 column"); 1007 } 1008 } 1009 1010 protected long getMimeTypeId() { 1011 if (mMimetypeId == 0) { 1012 mMimetypeId = mDbHelper.getMimeTypeId(mMimetype); 1013 } 1014 return mMimetypeId; 1015 } 1016 1017 /** 1018 * Inserts a row into the {@link Data} table. 1019 */ 1020 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1021 final long dataId = db.insert(Tables.DATA, null, values); 1022 1023 Integer primary = values.getAsInteger(Data.IS_PRIMARY); 1024 if (primary != null && primary != 0) { 1025 setIsPrimary(rawContactId, dataId, getMimeTypeId()); 1026 } 1027 1028 return dataId; 1029 } 1030 1031 /** 1032 * Validates data and updates a {@link Data} row using the cursor, which contains 1033 * the current data. 1034 * 1035 * @return true if update changed something 1036 */ 1037 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1038 boolean callerIsSyncAdapter) { 1039 long dataId = c.getLong(DataUpdateQuery._ID); 1040 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1041 1042 if (values.containsKey(Data.IS_SUPER_PRIMARY)) { 1043 long mimeTypeId = getMimeTypeId(); 1044 setIsSuperPrimary(rawContactId, dataId, mimeTypeId); 1045 setIsPrimary(rawContactId, dataId, mimeTypeId); 1046 1047 // Now that we've taken care of setting these, remove them from "values". 1048 values.remove(Data.IS_SUPER_PRIMARY); 1049 values.remove(Data.IS_PRIMARY); 1050 } else if (values.containsKey(Data.IS_PRIMARY)) { 1051 setIsPrimary(rawContactId, dataId, getMimeTypeId()); 1052 1053 // Now that we've taken care of setting this, remove it from "values". 1054 values.remove(Data.IS_PRIMARY); 1055 } 1056 1057 if (values.size() > 0) { 1058 mSelectionArgs1[0] = String.valueOf(dataId); 1059 mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1); 1060 } 1061 1062 if (!callerIsSyncAdapter) { 1063 setRawContactDirty(rawContactId); 1064 } 1065 1066 return true; 1067 } 1068 1069 public int delete(SQLiteDatabase db, Cursor c) { 1070 long dataId = c.getLong(DataDeleteQuery._ID); 1071 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1072 boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0; 1073 mSelectionArgs1[0] = String.valueOf(dataId); 1074 int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1); 1075 mSelectionArgs1[0] = String.valueOf(rawContactId); 1076 db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1); 1077 if (count != 0 && primary) { 1078 fixPrimary(db, rawContactId); 1079 } 1080 return count; 1081 } 1082 1083 private void fixPrimary(SQLiteDatabase db, long rawContactId) { 1084 long mimeTypeId = getMimeTypeId(); 1085 long primaryId = -1; 1086 int primaryType = -1; 1087 mSelectionArgs1[0] = String.valueOf(rawContactId); 1088 Cursor c = db.query(DataDeleteQuery.TABLE, 1089 DataDeleteQuery.CONCRETE_COLUMNS, 1090 Data.RAW_CONTACT_ID + "=?" + 1091 " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId, 1092 mSelectionArgs1, null, null, null); 1093 try { 1094 while (c.moveToNext()) { 1095 long dataId = c.getLong(DataDeleteQuery._ID); 1096 int type = c.getInt(DataDeleteQuery.DATA1); 1097 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) { 1098 primaryId = dataId; 1099 primaryType = type; 1100 } 1101 } 1102 } finally { 1103 c.close(); 1104 } 1105 if (primaryId != -1) { 1106 setIsPrimary(rawContactId, primaryId, mimeTypeId); 1107 } 1108 } 1109 1110 /** 1111 * Returns the rank of a specific record type to be used in determining the primary 1112 * row. Lower number represents higher priority. 1113 */ 1114 protected int getTypeRank(int type) { 1115 return 0; 1116 } 1117 1118 protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 1119 if (!isNewRawContact(rawContactId)) { 1120 updateRawContactDisplayName(db, rawContactId); 1121 mContactAggregator.updateDisplayNameForRawContact(db, rawContactId); 1122 } 1123 } 1124 1125 /** 1126 * Return set of values, using current values at given {@link Data#_ID} 1127 * as baseline, but augmented with any updates. Returns null if there is 1128 * no change. 1129 */ 1130 public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId, 1131 ContentValues update) { 1132 boolean changing = false; 1133 final ContentValues values = new ContentValues(); 1134 mSelectionArgs1[0] = String.valueOf(dataId); 1135 final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?", 1136 mSelectionArgs1, null, null, null); 1137 try { 1138 if (cursor.moveToFirst()) { 1139 for (int i = 0; i < cursor.getColumnCount(); i++) { 1140 final String key = cursor.getColumnName(i); 1141 final String value = cursor.getString(i); 1142 if (!changing && update.containsKey(key)) { 1143 Object newValue = update.get(key); 1144 String newString = newValue == null ? null : newValue.toString(); 1145 changing |= !TextUtils.equals(newString, value); 1146 } 1147 values.put(key, value); 1148 } 1149 } 1150 } finally { 1151 cursor.close(); 1152 } 1153 if (!changing) { 1154 return null; 1155 } 1156 1157 values.putAll(update); 1158 return values; 1159 } 1160 } 1161 1162 public class CustomDataRowHandler extends DataRowHandler { 1163 1164 public CustomDataRowHandler(String mimetype) { 1165 super(mimetype); 1166 } 1167 } 1168 1169 public class StructuredNameRowHandler extends DataRowHandler { 1170 private final NameSplitter mSplitter; 1171 1172 public StructuredNameRowHandler(NameSplitter splitter) { 1173 super(StructuredName.CONTENT_ITEM_TYPE); 1174 mSplitter = splitter; 1175 } 1176 1177 @Override 1178 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1179 fixStructuredNameComponents(values, values); 1180 1181 long dataId = super.insert(db, rawContactId, values); 1182 1183 String name = values.getAsString(StructuredName.DISPLAY_NAME); 1184 Integer fullNameStyle = values.getAsInteger(StructuredName.FULL_NAME_STYLE); 1185 insertNameLookupForStructuredName(rawContactId, dataId, name, 1186 fullNameStyle != null 1187 ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle) 1188 : FullNameStyle.UNDEFINED); 1189 insertNameLookupForPhoneticName(rawContactId, dataId, values); 1190 fixRawContactDisplayName(db, rawContactId); 1191 triggerAggregation(rawContactId); 1192 return dataId; 1193 } 1194 1195 @Override 1196 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1197 boolean callerIsSyncAdapter) { 1198 final long dataId = c.getLong(DataUpdateQuery._ID); 1199 final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1200 1201 final ContentValues augmented = getAugmentedValues(db, dataId, values); 1202 if (augmented == null) { // No change 1203 return false; 1204 } 1205 1206 fixStructuredNameComponents(augmented, values); 1207 1208 super.update(db, values, c, callerIsSyncAdapter); 1209 if (values.containsKey(StructuredName.DISPLAY_NAME) || 1210 values.containsKey(StructuredName.PHONETIC_FAMILY_NAME) || 1211 values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME) || 1212 values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) { 1213 augmented.putAll(values); 1214 String name = augmented.getAsString(StructuredName.DISPLAY_NAME); 1215 deleteNameLookup(dataId); 1216 Integer fullNameStyle = augmented.getAsInteger(StructuredName.FULL_NAME_STYLE); 1217 insertNameLookupForStructuredName(rawContactId, dataId, name, 1218 fullNameStyle != null 1219 ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle) 1220 : FullNameStyle.UNDEFINED); 1221 insertNameLookupForPhoneticName(rawContactId, dataId, augmented); 1222 } 1223 fixRawContactDisplayName(db, rawContactId); 1224 triggerAggregation(rawContactId); 1225 return true; 1226 } 1227 1228 @Override 1229 public int delete(SQLiteDatabase db, Cursor c) { 1230 long dataId = c.getLong(DataDeleteQuery._ID); 1231 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1232 1233 int count = super.delete(db, c); 1234 1235 deleteNameLookup(dataId); 1236 fixRawContactDisplayName(db, rawContactId); 1237 triggerAggregation(rawContactId); 1238 return count; 1239 } 1240 1241 /** 1242 * Specific list of structured fields. 1243 */ 1244 private final String[] STRUCTURED_FIELDS = new String[] { 1245 StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME, 1246 StructuredName.FAMILY_NAME, StructuredName.SUFFIX 1247 }; 1248 1249 /** 1250 * Parses the supplied display name, but only if the incoming values do 1251 * not already contain structured name parts. Also, if the display name 1252 * is not provided, generate one by concatenating first name and last 1253 * name. 1254 */ 1255 private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) { 1256 final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME); 1257 1258 final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); 1259 final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); 1260 1261 if (touchedUnstruct && !touchedStruct) { 1262 NameSplitter.Name name = new NameSplitter.Name(); 1263 mSplitter.split(name, unstruct); 1264 name.toValues(update); 1265 } else if (!touchedUnstruct 1266 && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) { 1267 // We need to update the display name when any structured components 1268 // are specified, even when they are null, which is why we are checking 1269 // areAnySpecified. The touchedStruct in the condition is an optimization: 1270 // if there are non-null values, we know for a fact that some values are present. 1271 NameSplitter.Name name = new NameSplitter.Name(); 1272 name.fromValues(augmented); 1273 // As the name could be changed, let's guess the name style again. 1274 name.fullNameStyle = FullNameStyle.UNDEFINED; 1275 mSplitter.guessNameStyle(name); 1276 int unadjustedFullNameStyle = name.fullNameStyle; 1277 name.fullNameStyle = mSplitter.getAdjustedFullNameStyle(name.fullNameStyle); 1278 final String joined = mSplitter.join(name, true); 1279 update.put(StructuredName.DISPLAY_NAME, joined); 1280 1281 update.put(StructuredName.FULL_NAME_STYLE, unadjustedFullNameStyle); 1282 update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle); 1283 } else if (touchedUnstruct && touchedStruct){ 1284 if (!update.containsKey(StructuredName.FULL_NAME_STYLE)) { 1285 update.put(StructuredName.FULL_NAME_STYLE, 1286 mSplitter.guessFullNameStyle(unstruct)); 1287 } 1288 if (!update.containsKey(StructuredName.PHONETIC_NAME_STYLE)) { 1289 update.put(StructuredName.PHONETIC_NAME_STYLE, 1290 mSplitter.guessPhoneticNameStyle(unstruct)); 1291 } 1292 } 1293 } 1294 } 1295 1296 public class StructuredPostalRowHandler extends DataRowHandler { 1297 private PostalSplitter mSplitter; 1298 1299 public StructuredPostalRowHandler(PostalSplitter splitter) { 1300 super(StructuredPostal.CONTENT_ITEM_TYPE); 1301 mSplitter = splitter; 1302 } 1303 1304 @Override 1305 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1306 fixStructuredPostalComponents(values, values); 1307 return super.insert(db, rawContactId, values); 1308 } 1309 1310 @Override 1311 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1312 boolean callerIsSyncAdapter) { 1313 final long dataId = c.getLong(DataUpdateQuery._ID); 1314 final ContentValues augmented = getAugmentedValues(db, dataId, values); 1315 if (augmented == null) { // No change 1316 return false; 1317 } 1318 1319 fixStructuredPostalComponents(augmented, values); 1320 super.update(db, values, c, callerIsSyncAdapter); 1321 return true; 1322 } 1323 1324 /** 1325 * Specific list of structured fields. 1326 */ 1327 private final String[] STRUCTURED_FIELDS = new String[] { 1328 StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD, 1329 StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE, 1330 StructuredPostal.COUNTRY, 1331 }; 1332 1333 /** 1334 * Prepares the given {@link StructuredPostal} row, building 1335 * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured 1336 * values when missing. When structured components are missing, the 1337 * unstructured value is assigned to {@link StructuredPostal#STREET}. 1338 */ 1339 private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) { 1340 final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS); 1341 1342 final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); 1343 final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); 1344 1345 final PostalSplitter.Postal postal = new PostalSplitter.Postal(); 1346 1347 if (touchedUnstruct && !touchedStruct) { 1348 mSplitter.split(postal, unstruct); 1349 postal.toValues(update); 1350 } else if (!touchedUnstruct 1351 && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) { 1352 // See comment in 1353 postal.fromValues(augmented); 1354 final String joined = mSplitter.join(postal); 1355 update.put(StructuredPostal.FORMATTED_ADDRESS, joined); 1356 } 1357 } 1358 } 1359 1360 public class CommonDataRowHandler extends DataRowHandler { 1361 1362 private final String mTypeColumn; 1363 private final String mLabelColumn; 1364 1365 public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) { 1366 super(mimetype); 1367 mTypeColumn = typeColumn; 1368 mLabelColumn = labelColumn; 1369 } 1370 1371 @Override 1372 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1373 enforceTypeAndLabel(values, values); 1374 return super.insert(db, rawContactId, values); 1375 } 1376 1377 @Override 1378 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1379 boolean callerIsSyncAdapter) { 1380 final long dataId = c.getLong(DataUpdateQuery._ID); 1381 final ContentValues augmented = getAugmentedValues(db, dataId, values); 1382 if (augmented == null) { // No change 1383 return false; 1384 } 1385 enforceTypeAndLabel(augmented, values); 1386 return super.update(db, values, c, callerIsSyncAdapter); 1387 } 1388 1389 /** 1390 * If the given {@link ContentValues} defines {@link #mTypeColumn}, 1391 * enforce that {@link #mLabelColumn} only appears when type is 1392 * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise. 1393 */ 1394 private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) { 1395 final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn)); 1396 final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn)); 1397 1398 if (hasLabel && !hasType) { 1399 // When label exists, assert that some type is defined 1400 throw new IllegalArgumentException(mTypeColumn + " must be specified when " 1401 + mLabelColumn + " is defined."); 1402 } 1403 } 1404 } 1405 1406 public class OrganizationDataRowHandler extends CommonDataRowHandler { 1407 1408 public OrganizationDataRowHandler() { 1409 super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL); 1410 } 1411 1412 @Override 1413 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1414 String company = values.getAsString(Organization.COMPANY); 1415 String title = values.getAsString(Organization.TITLE); 1416 1417 long dataId = super.insert(db, rawContactId, values); 1418 1419 fixRawContactDisplayName(db, rawContactId); 1420 insertNameLookupForOrganization(rawContactId, dataId, company, title); 1421 return dataId; 1422 } 1423 1424 @Override 1425 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1426 boolean callerIsSyncAdapter) { 1427 if (!super.update(db, values, c, callerIsSyncAdapter)) { 1428 return false; 1429 } 1430 1431 boolean containsCompany = values.containsKey(Organization.COMPANY); 1432 boolean containsTitle = values.containsKey(Organization.TITLE); 1433 if (containsCompany || containsTitle) { 1434 long dataId = c.getLong(DataUpdateQuery._ID); 1435 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1436 1437 String company; 1438 1439 if (containsCompany) { 1440 company = values.getAsString(Organization.COMPANY); 1441 } else { 1442 mSelectionArgs1[0] = String.valueOf(dataId); 1443 company = DatabaseUtils.stringForQuery(db, 1444 "SELECT " + Organization.COMPANY + 1445 " FROM " + Tables.DATA + 1446 " WHERE " + Data._ID + "=?", mSelectionArgs1); 1447 } 1448 1449 String title; 1450 if (containsTitle) { 1451 title = values.getAsString(Organization.TITLE); 1452 } else { 1453 mSelectionArgs1[0] = String.valueOf(dataId); 1454 title = DatabaseUtils.stringForQuery(db, 1455 "SELECT " + Organization.TITLE + 1456 " FROM " + Tables.DATA + 1457 " WHERE " + Data._ID + "=?", mSelectionArgs1); 1458 } 1459 1460 deleteNameLookup(dataId); 1461 insertNameLookupForOrganization(rawContactId, dataId, company, title); 1462 1463 fixRawContactDisplayName(db, rawContactId); 1464 } 1465 return true; 1466 } 1467 1468 @Override 1469 public int delete(SQLiteDatabase db, Cursor c) { 1470 long dataId = c.getLong(DataUpdateQuery._ID); 1471 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1472 1473 int count = super.delete(db, c); 1474 fixRawContactDisplayName(db, rawContactId); 1475 deleteNameLookup(dataId); 1476 return count; 1477 } 1478 1479 @Override 1480 protected int getTypeRank(int type) { 1481 switch (type) { 1482 case Organization.TYPE_WORK: return 0; 1483 case Organization.TYPE_CUSTOM: return 1; 1484 case Organization.TYPE_OTHER: return 2; 1485 default: return 1000; 1486 } 1487 } 1488 } 1489 1490 public class EmailDataRowHandler extends CommonDataRowHandler { 1491 1492 public EmailDataRowHandler() { 1493 super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL); 1494 } 1495 1496 @Override 1497 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1498 String email = values.getAsString(Email.DATA); 1499 1500 long dataId = super.insert(db, rawContactId, values); 1501 1502 fixRawContactDisplayName(db, rawContactId); 1503 String address = insertNameLookupForEmail(rawContactId, dataId, email); 1504 if (address != null) { 1505 triggerAggregation(rawContactId); 1506 } 1507 return dataId; 1508 } 1509 1510 @Override 1511 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1512 boolean callerIsSyncAdapter) { 1513 if (!super.update(db, values, c, callerIsSyncAdapter)) { 1514 return false; 1515 } 1516 1517 if (values.containsKey(Email.DATA)) { 1518 long dataId = c.getLong(DataUpdateQuery._ID); 1519 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1520 1521 String address = values.getAsString(Email.DATA); 1522 deleteNameLookup(dataId); 1523 insertNameLookupForEmail(rawContactId, dataId, address); 1524 fixRawContactDisplayName(db, rawContactId); 1525 triggerAggregation(rawContactId); 1526 } 1527 1528 return true; 1529 } 1530 1531 @Override 1532 public int delete(SQLiteDatabase db, Cursor c) { 1533 long dataId = c.getLong(DataDeleteQuery._ID); 1534 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1535 1536 int count = super.delete(db, c); 1537 1538 deleteNameLookup(dataId); 1539 fixRawContactDisplayName(db, rawContactId); 1540 triggerAggregation(rawContactId); 1541 return count; 1542 } 1543 1544 @Override 1545 protected int getTypeRank(int type) { 1546 switch (type) { 1547 case Email.TYPE_HOME: return 0; 1548 case Email.TYPE_WORK: return 1; 1549 case Email.TYPE_CUSTOM: return 2; 1550 case Email.TYPE_OTHER: return 3; 1551 default: return 1000; 1552 } 1553 } 1554 } 1555 1556 public class NicknameDataRowHandler extends CommonDataRowHandler { 1557 1558 public NicknameDataRowHandler() { 1559 super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL); 1560 } 1561 1562 @Override 1563 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1564 String nickname = values.getAsString(Nickname.NAME); 1565 1566 long dataId = super.insert(db, rawContactId, values); 1567 1568 if (!TextUtils.isEmpty(nickname)) { 1569 fixRawContactDisplayName(db, rawContactId); 1570 insertNameLookupForNickname(rawContactId, dataId, nickname); 1571 triggerAggregation(rawContactId); 1572 } 1573 return dataId; 1574 } 1575 1576 @Override 1577 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1578 boolean callerIsSyncAdapter) { 1579 long dataId = c.getLong(DataUpdateQuery._ID); 1580 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1581 1582 if (!super.update(db, values, c, callerIsSyncAdapter)) { 1583 return false; 1584 } 1585 1586 if (values.containsKey(Nickname.NAME)) { 1587 String nickname = values.getAsString(Nickname.NAME); 1588 deleteNameLookup(dataId); 1589 insertNameLookupForNickname(rawContactId, dataId, nickname); 1590 fixRawContactDisplayName(db, rawContactId); 1591 triggerAggregation(rawContactId); 1592 } 1593 1594 return true; 1595 } 1596 1597 @Override 1598 public int delete(SQLiteDatabase db, Cursor c) { 1599 long dataId = c.getLong(DataDeleteQuery._ID); 1600 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1601 1602 int count = super.delete(db, c); 1603 1604 deleteNameLookup(dataId); 1605 fixRawContactDisplayName(db, rawContactId); 1606 triggerAggregation(rawContactId); 1607 return count; 1608 } 1609 } 1610 1611 public class PhoneDataRowHandler extends CommonDataRowHandler { 1612 1613 public PhoneDataRowHandler() { 1614 super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL); 1615 } 1616 1617 @Override 1618 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1619 long dataId; 1620 if (values.containsKey(Phone.NUMBER)) { 1621 String number = values.getAsString(Phone.NUMBER); 1622 String normalizedNumber = computeNormalizedNumber(number); 1623 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber); 1624 dataId = super.insert(db, rawContactId, values); 1625 1626 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); 1627 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1628 fixRawContactDisplayName(db, rawContactId); 1629 if (normalizedNumber != null) { 1630 triggerAggregation(rawContactId); 1631 } 1632 } else { 1633 dataId = super.insert(db, rawContactId, values); 1634 } 1635 return dataId; 1636 } 1637 1638 @Override 1639 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1640 boolean callerIsSyncAdapter) { 1641 String number = null; 1642 String normalizedNumber = null; 1643 if (values.containsKey(Phone.NUMBER)) { 1644 number = values.getAsString(Phone.NUMBER); 1645 normalizedNumber = computeNormalizedNumber(number); 1646 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber); 1647 } 1648 1649 if (!super.update(db, values, c, callerIsSyncAdapter)) { 1650 return false; 1651 } 1652 1653 if (values.containsKey(Phone.NUMBER)) { 1654 long dataId = c.getLong(DataUpdateQuery._ID); 1655 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1656 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); 1657 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1658 fixRawContactDisplayName(db, rawContactId); 1659 triggerAggregation(rawContactId); 1660 } 1661 return true; 1662 } 1663 1664 @Override 1665 public int delete(SQLiteDatabase db, Cursor c) { 1666 long dataId = c.getLong(DataDeleteQuery._ID); 1667 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1668 1669 int count = super.delete(db, c); 1670 1671 updatePhoneLookup(db, rawContactId, dataId, null, null); 1672 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1673 fixRawContactDisplayName(db, rawContactId); 1674 triggerAggregation(rawContactId); 1675 return count; 1676 } 1677 1678 private String computeNormalizedNumber(String number) { 1679 String normalizedNumber = null; 1680 if (number != null) { 1681 normalizedNumber = PhoneNumberUtils.getStrippedReversed(number); 1682 } 1683 return normalizedNumber; 1684 } 1685 1686 private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId, 1687 String number, String normalizedNumber) { 1688 if (number != null) { 1689 ContentValues phoneValues = new ContentValues(); 1690 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId); 1691 phoneValues.put(PhoneLookupColumns.DATA_ID, dataId); 1692 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber); 1693 phoneValues.put(PhoneLookupColumns.MIN_MATCH, 1694 PhoneNumberUtils.toCallerIDMinMatch(number)); 1695 1696 db.replace(Tables.PHONE_LOOKUP, null, phoneValues); 1697 } else { 1698 mSelectionArgs1[0] = String.valueOf(dataId); 1699 db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1); 1700 } 1701 } 1702 1703 @Override 1704 protected int getTypeRank(int type) { 1705 switch (type) { 1706 case Phone.TYPE_MOBILE: return 0; 1707 case Phone.TYPE_WORK: return 1; 1708 case Phone.TYPE_HOME: return 2; 1709 case Phone.TYPE_PAGER: return 3; 1710 case Phone.TYPE_CUSTOM: return 4; 1711 case Phone.TYPE_OTHER: return 5; 1712 case Phone.TYPE_FAX_WORK: return 6; 1713 case Phone.TYPE_FAX_HOME: return 7; 1714 default: return 1000; 1715 } 1716 } 1717 } 1718 1719 public class GroupMembershipRowHandler extends DataRowHandler { 1720 1721 private static final String SELECTION_RAW_CONTACT_ID = RawContacts._ID + "=?"; 1722 1723 private static final String QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID = 1724 "SELECT COUNT(*) FROM " + Tables.DATA + " LEFT OUTER JOIN " + Tables .GROUPS 1725 + " ON " + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID 1726 + "=" + GroupsColumns.CONCRETE_ID 1727 + " WHERE " + DataColumns.MIMETYPE_ID + "=?" 1728 + " AND " + Tables.DATA + "." + GroupMembership.RAW_CONTACT_ID + "=?" 1729 + " AND " + Groups.FAVORITES + "!=0"; 1730 1731 public GroupMembershipRowHandler() { 1732 super(GroupMembership.CONTENT_ITEM_TYPE); 1733 } 1734 1735 @Override 1736 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1737 resolveGroupSourceIdInValues(rawContactId, db, values, true); 1738 long dataId = super.insert(db, rawContactId, values); 1739 if (hasFavoritesGroupMembership(db, rawContactId)) { 1740 updateRawContactsStar(db, rawContactId, true /* starred */); 1741 } 1742 updateVisibility(rawContactId); 1743 return dataId; 1744 } 1745 1746 @Override 1747 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1748 boolean callerIsSyncAdapter) { 1749 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1750 boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId); 1751 resolveGroupSourceIdInValues(rawContactId, db, values, false); 1752 if (!super.update(db, values, c, callerIsSyncAdapter)) { 1753 return false; 1754 } 1755 boolean isStarred = hasFavoritesGroupMembership(db, rawContactId); 1756 if (wasStarred != isStarred) { 1757 updateRawContactsStar(db, rawContactId, isStarred); 1758 } 1759 updateVisibility(rawContactId); 1760 return true; 1761 } 1762 1763 private void updateRawContactsStar(SQLiteDatabase db, long rawContactId, boolean starred) { 1764 ContentValues rawContactValues = new ContentValues(); 1765 rawContactValues.put(RawContacts.STARRED, starred ? 1 : 0); 1766 if (db.update(Tables.RAW_CONTACTS, rawContactValues, SELECTION_RAW_CONTACT_ID, 1767 new String[]{Long.toString(rawContactId)}) > 0) { 1768 mContactAggregator.updateStarred(rawContactId); 1769 } 1770 } 1771 1772 private boolean hasFavoritesGroupMembership(SQLiteDatabase db, long rawContactId) { 1773 final long groupMembershipMimetypeId = mDbHelper 1774 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 1775 boolean isStarred = 0 < DatabaseUtils 1776 .longForQuery(db, QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID, 1777 new String[]{Long.toString(groupMembershipMimetypeId), Long.toString(rawContactId)}); 1778 return isStarred; 1779 } 1780 1781 @Override 1782 public int delete(SQLiteDatabase db, Cursor c) { 1783 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1784 boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId); 1785 int count = super.delete(db, c); 1786 boolean isStarred = hasFavoritesGroupMembership(db, rawContactId); 1787 if (wasStarred && !isStarred) { 1788 updateRawContactsStar(db, rawContactId, false /* starred */); 1789 } 1790 updateVisibility(rawContactId); 1791 return count; 1792 } 1793 1794 private void updateVisibility(long rawContactId) { 1795 long contactId = mDbHelper.getContactId(rawContactId); 1796 if (contactId != 0) { 1797 mDbHelper.updateContactVisible(contactId); 1798 } 1799 } 1800 1801 private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db, 1802 ContentValues values, boolean isInsert) { 1803 boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID); 1804 boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID); 1805 if (containsGroupSourceId && containsGroupId) { 1806 throw new IllegalArgumentException( 1807 "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID " 1808 + "and GroupMembership.GROUP_ROW_ID"); 1809 } 1810 1811 if (!containsGroupSourceId && !containsGroupId) { 1812 if (isInsert) { 1813 throw new IllegalArgumentException( 1814 "you must set exactly one of GroupMembership.GROUP_SOURCE_ID " 1815 + "and GroupMembership.GROUP_ROW_ID"); 1816 } else { 1817 return; 1818 } 1819 } 1820 1821 if (containsGroupSourceId) { 1822 final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID); 1823 final long groupId = getOrMakeGroup(db, rawContactId, sourceId, 1824 mInsertedRawContacts.get(rawContactId)); 1825 values.remove(GroupMembership.GROUP_SOURCE_ID); 1826 values.put(GroupMembership.GROUP_ROW_ID, groupId); 1827 } 1828 } 1829 } 1830 1831 public class PhotoDataRowHandler extends DataRowHandler { 1832 1833 public PhotoDataRowHandler() { 1834 super(Photo.CONTENT_ITEM_TYPE); 1835 } 1836 1837 @Override 1838 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1839 long dataId = super.insert(db, rawContactId, values); 1840 if (!isNewRawContact(rawContactId)) { 1841 mContactAggregator.updatePhotoId(db, rawContactId); 1842 } 1843 return dataId; 1844 } 1845 1846 @Override 1847 public boolean update(SQLiteDatabase db, ContentValues values, Cursor c, 1848 boolean callerIsSyncAdapter) { 1849 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1850 if (!super.update(db, values, c, callerIsSyncAdapter)) { 1851 return false; 1852 } 1853 1854 mContactAggregator.updatePhotoId(db, rawContactId); 1855 return true; 1856 } 1857 1858 @Override 1859 public int delete(SQLiteDatabase db, Cursor c) { 1860 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1861 int count = super.delete(db, c); 1862 mContactAggregator.updatePhotoId(db, rawContactId); 1863 return count; 1864 } 1865 } 1866 1867 /** 1868 * An entry in group id cache. It maps the combination of (account type, account name 1869 * and source id) to group row id. 1870 */ 1871 public class GroupIdCacheEntry { 1872 String accountType; 1873 String accountName; 1874 String sourceId; 1875 long groupId; 1876 } 1877 1878 private HashMap<String, DataRowHandler> mDataRowHandlers; 1879 private ContactsDatabaseHelper mDbHelper; 1880 1881 private NameSplitter mNameSplitter; 1882 private NameLookupBuilder mNameLookupBuilder; 1883 1884 private PostalSplitter mPostalSplitter; 1885 1886 // We don't need a soft cache for groups - the assumption is that there will only 1887 // be a small number of contact groups. The cache is keyed off source id. The value 1888 // is a list of groups with this group id. 1889 private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap(); 1890 1891 private ContactAggregator mContactAggregator; 1892 private LegacyApiSupport mLegacyApiSupport; 1893 private GlobalSearchSupport mGlobalSearchSupport; 1894 private CommonNicknameCache mCommonNicknameCache; 1895 1896 private ContentValues mValues = new ContentValues(); 1897 private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128); 1898 private NameSplitter.Name mName = new NameSplitter.Name(); 1899 private HashMap<String, Boolean> mAccountWritability = Maps.newHashMap(); 1900 1901 private int mProviderStatus = ProviderStatus.STATUS_NORMAL; 1902 private long mEstimatedStorageRequirement = 0; 1903 private volatile CountDownLatch mAccessLatch; 1904 1905 private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap(); 1906 private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet(); 1907 private HashSet<Long> mDirtyRawContacts = Sets.newHashSet(); 1908 private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap(); 1909 1910 private boolean mVisibleTouched = false; 1911 1912 private boolean mSyncToNetwork; 1913 1914 private Locale mCurrentLocale; 1915 1916 1917 @Override 1918 public boolean onCreate() { 1919 super.onCreate(); 1920 try { 1921 return initialize(); 1922 } catch (RuntimeException e) { 1923 Log.e(TAG, "Cannot start provider", e); 1924 return false; 1925 } 1926 } 1927 1928 private boolean initialize() { 1929 final Context context = getContext(); 1930 mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper(); 1931 mGlobalSearchSupport = new GlobalSearchSupport(this); 1932 mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport); 1933 mContactAggregator = new ContactAggregator(this, mDbHelper, 1934 createPhotoPriorityResolver(context)); 1935 mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); 1936 1937 mDb = mDbHelper.getWritableDatabase(); 1938 1939 initForDefaultLocale(); 1940 1941 mSetPrimaryStatement = mDb.compileStatement( 1942 "UPDATE " + Tables.DATA + 1943 " SET " + Data.IS_PRIMARY + "=(_id=?)" + 1944 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1945 " AND " + Data.RAW_CONTACT_ID + "=?"); 1946 1947 mSetSuperPrimaryStatement = mDb.compileStatement( 1948 "UPDATE " + Tables.DATA + 1949 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" + 1950 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1951 " AND " + Data.RAW_CONTACT_ID + " IN (" + 1952 "SELECT " + RawContacts._ID + 1953 " FROM " + Tables.RAW_CONTACTS + 1954 " WHERE " + RawContacts.CONTACT_ID + " =(" + 1955 "SELECT " + RawContacts.CONTACT_ID + 1956 " FROM " + Tables.RAW_CONTACTS + 1957 " WHERE " + RawContacts._ID + "=?))"); 1958 1959 mRawContactDisplayNameUpdate = mDb.compileStatement( 1960 "UPDATE " + Tables.RAW_CONTACTS + 1961 " SET " + 1962 RawContacts.DISPLAY_NAME_SOURCE + "=?," + 1963 RawContacts.DISPLAY_NAME_PRIMARY + "=?," + 1964 RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," + 1965 RawContacts.PHONETIC_NAME + "=?," + 1966 RawContacts.PHONETIC_NAME_STYLE + "=?," + 1967 RawContacts.SORT_KEY_PRIMARY + "=?," + 1968 RawContacts.SORT_KEY_ALTERNATIVE + "=?" + 1969 " WHERE " + RawContacts._ID + "=?"); 1970 1971 mLastStatusUpdate = mDb.compileStatement( 1972 "UPDATE " + Tables.CONTACTS + 1973 " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" + 1974 "(SELECT " + DataColumns.CONCRETE_ID + 1975 " FROM " + Tables.STATUS_UPDATES + 1976 " JOIN " + Tables.DATA + 1977 " ON (" + StatusUpdatesColumns.DATA_ID + "=" 1978 + DataColumns.CONCRETE_ID + ")" + 1979 " JOIN " + Tables.RAW_CONTACTS + 1980 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" 1981 + RawContactsColumns.CONCRETE_ID + ")" + 1982 " WHERE " + RawContacts.CONTACT_ID + "=?" + 1983 " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC," 1984 + StatusUpdates.STATUS + 1985 " LIMIT 1)" + 1986 " WHERE " + ContactsColumns.CONCRETE_ID + "=?"); 1987 1988 mNameLookupInsert = mDb.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "(" 1989 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + "," 1990 + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME 1991 + ") VALUES (?,?,?,?)"); 1992 mNameLookupDelete = mDb.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE " 1993 + NameLookupColumns.DATA_ID + "=?"); 1994 1995 mStatusUpdateInsert = mDb.compileStatement( 1996 "INSERT INTO " + Tables.STATUS_UPDATES + "(" 1997 + StatusUpdatesColumns.DATA_ID + ", " 1998 + StatusUpdates.STATUS + "," 1999 + StatusUpdates.STATUS_RES_PACKAGE + "," 2000 + StatusUpdates.STATUS_ICON + "," 2001 + StatusUpdates.STATUS_LABEL + ")" + 2002 " VALUES (?,?,?,?,?)"); 2003 2004 mStatusUpdateReplace = mDb.compileStatement( 2005 "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "(" 2006 + StatusUpdatesColumns.DATA_ID + ", " 2007 + StatusUpdates.STATUS_TIMESTAMP + "," 2008 + StatusUpdates.STATUS + "," 2009 + StatusUpdates.STATUS_RES_PACKAGE + "," 2010 + StatusUpdates.STATUS_ICON + "," 2011 + StatusUpdates.STATUS_LABEL + ")" + 2012 " VALUES (?,?,?,?,?,?)"); 2013 2014 mStatusUpdateAutoTimestamp = mDb.compileStatement( 2015 "UPDATE " + Tables.STATUS_UPDATES + 2016 " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?," 2017 + StatusUpdates.STATUS + "=?" + 2018 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?" 2019 + " AND " + StatusUpdates.STATUS + "!=?"); 2020 2021 mStatusAttributionUpdate = mDb.compileStatement( 2022 "UPDATE " + Tables.STATUS_UPDATES + 2023 " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?," 2024 + StatusUpdates.STATUS_ICON + "=?," 2025 + StatusUpdates.STATUS_LABEL + "=?" + 2026 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); 2027 2028 mStatusUpdateDelete = mDb.compileStatement( 2029 "DELETE FROM " + Tables.STATUS_UPDATES + 2030 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"); 2031 2032 // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0 2033 // on all other raw contacts in the same aggregate 2034 mResetNameVerifiedForOtherRawContacts = mDb.compileStatement( 2035 "UPDATE " + Tables.RAW_CONTACTS + 2036 " SET " + RawContacts.NAME_VERIFIED + "=0" + 2037 " WHERE " + RawContacts.CONTACT_ID + "=(" + 2038 "SELECT " + RawContacts.CONTACT_ID + 2039 " FROM " + Tables.RAW_CONTACTS + 2040 " WHERE " + RawContacts._ID + "=?)" + 2041 " AND " + RawContacts._ID + "!=?"); 2042 2043 mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); 2044 mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE); 2045 mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE); 2046 mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE); 2047 mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE); 2048 mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); 2049 2050 verifyAccounts(); 2051 verifyLocale(); 2052 2053 if (isLegacyContactImportNeeded()) { 2054 importLegacyContactsAsync(); 2055 } 2056 2057 return (mDb != null); 2058 } 2059 2060 private void initDataRowHandlers() { 2061 mDataRowHandlers = new HashMap<String, DataRowHandler>(); 2062 2063 mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler()); 2064 mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE, 2065 new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL)); 2066 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler( 2067 StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL)); 2068 mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler()); 2069 mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler()); 2070 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler()); 2071 mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE, 2072 new StructuredNameRowHandler(mNameSplitter)); 2073 mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, 2074 new StructuredPostalRowHandler(mPostalSplitter)); 2075 mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler()); 2076 mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler()); 2077 } 2078 /** 2079 * Visible for testing. 2080 */ 2081 /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) { 2082 return new PhotoPriorityResolver(context); 2083 } 2084 2085 /** 2086 * (Re)allocates all locale-sensitive structures. 2087 */ 2088 private void initForDefaultLocale() { 2089 mCurrentLocale = getLocale(); 2090 mNameSplitter = mDbHelper.createNameSplitter(); 2091 mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter); 2092 mPostalSplitter = new PostalSplitter(mCurrentLocale); 2093 mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase()); 2094 ContactLocaleUtils.getIntance().setLocale(mCurrentLocale); 2095 initDataRowHandlers(); 2096 } 2097 2098 @Override 2099 public void onConfigurationChanged(Configuration newConfig) { 2100 initForDefaultLocale(); 2101 verifyLocale(); 2102 } 2103 2104 protected void verifyAccounts() { 2105 AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false); 2106 onAccountsUpdated(AccountManager.get(getContext()).getAccounts()); 2107 } 2108 2109 /** 2110 * Verifies that the contacts database is properly configured for the current locale. 2111 * If not, changes the database locale to the current locale using an asynchronous task. 2112 * This needs to be done asynchronously because the process involves rebuilding 2113 * large data structures (name lookup, sort keys), which can take minutes on 2114 * a large set of contacts. 2115 */ 2116 protected void verifyLocale() { 2117 2118 // The process is already running - postpone the change 2119 if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) { 2120 return; 2121 } 2122 2123 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 2124 final String providerLocale = prefs.getString(PREF_LOCALE, null); 2125 if (providerLocale == null) { 2126 // The provider has just been created for the first time. There are no 2127 // contacts in the database, so we can safely set locale on the UI thread. 2128 mDbHelper.setLocale(ContactsProvider2.this, mCurrentLocale); 2129 prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit(); 2130 return; 2131 } 2132 2133 final Locale currentLocale = mCurrentLocale; 2134 if (currentLocale.toString().equals(providerLocale)) { 2135 return; 2136 } 2137 2138 int providerStatus = mProviderStatus; 2139 setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE); 2140 2141 AsyncTask<Integer, Void, Void> task = new AsyncTask<Integer, Void, Void>() { 2142 2143 int savedProviderStatus; 2144 2145 @Override 2146 protected Void doInBackground(Integer... params) { 2147 savedProviderStatus = params[0]; 2148 mDbHelper.setLocale(ContactsProvider2.this, currentLocale); 2149 return null; 2150 } 2151 2152 @Override 2153 protected void onPostExecute(Void result) { 2154 prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit(); 2155 setProviderStatus(savedProviderStatus); 2156 2157 // Recursive invocation, needed to cover the case where locale 2158 // changes once and then changes again before the db upgrade is completed. 2159 verifyLocale(); 2160 } 2161 }; 2162 2163 task.execute(providerStatus); 2164 } 2165 2166 /* Visible for testing */ 2167 @Override 2168 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 2169 return ContactsDatabaseHelper.getInstance(context); 2170 } 2171 2172 /* package */ NameSplitter getNameSplitter() { 2173 return mNameSplitter; 2174 } 2175 2176 /* Visible for testing */ 2177 protected Locale getLocale() { 2178 return Locale.getDefault(); 2179 } 2180 2181 protected boolean isLegacyContactImportNeeded() { 2182 int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0")); 2183 return version < PROPERTY_CONTACTS_IMPORT_VERSION; 2184 } 2185 2186 protected LegacyContactImporter getLegacyContactImporter() { 2187 return new LegacyContactImporter(getContext(), this); 2188 } 2189 2190 /** 2191 * Imports legacy contacts in a separate thread. As long as the import process is running 2192 * all other access to the contacts is blocked. 2193 */ 2194 private void importLegacyContactsAsync() { 2195 Log.v(TAG, "Importing legacy contacts"); 2196 setProviderStatus(ProviderStatus.STATUS_UPGRADING); 2197 if (mAccessLatch == null) { 2198 mAccessLatch = new CountDownLatch(1); 2199 } 2200 2201 Thread importThread = new Thread("LegacyContactImport") { 2202 @Override 2203 public void run() { 2204 LegacyContactImporter importer = getLegacyContactImporter(); 2205 if (importLegacyContacts(importer)) { 2206 onLegacyContactImportSuccess(); 2207 } else { 2208 onLegacyContactImportFailure(); 2209 } 2210 } 2211 }; 2212 2213 importThread.start(); 2214 } 2215 2216 /** 2217 * Unlocks the provider and declares that the import process is complete. 2218 */ 2219 private void onLegacyContactImportSuccess() { 2220 NotificationManager nm = 2221 (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE); 2222 nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION); 2223 2224 // Store a property in the database indicating that the conversion process succeeded 2225 mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED, 2226 String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION)); 2227 setProviderStatus(ProviderStatus.STATUS_NORMAL); 2228 mAccessLatch.countDown(); 2229 mAccessLatch = null; 2230 Log.v(TAG, "Completed import of legacy contacts"); 2231 } 2232 2233 /** 2234 * Announces the provider status and keeps the provider locked. 2235 */ 2236 private void onLegacyContactImportFailure() { 2237 Context context = getContext(); 2238 NotificationManager nm = 2239 (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); 2240 2241 // Show a notification 2242 Notification n = new Notification(android.R.drawable.stat_notify_error, 2243 context.getString(R.string.upgrade_out_of_memory_notification_ticker), 2244 System.currentTimeMillis()); 2245 n.setLatestEventInfo(context, 2246 context.getString(R.string.upgrade_out_of_memory_notification_title), 2247 context.getString(R.string.upgrade_out_of_memory_notification_text), 2248 PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0)); 2249 n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT; 2250 2251 nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n); 2252 2253 setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY); 2254 Log.v(TAG, "Failed to import legacy contacts"); 2255 } 2256 2257 /* Visible for testing */ 2258 /* package */ boolean importLegacyContacts(LegacyContactImporter importer) { 2259 boolean aggregatorEnabled = mContactAggregator.isEnabled(); 2260 mContactAggregator.setEnabled(false); 2261 try { 2262 if (importer.importContacts()) { 2263 2264 // TODO aggregate all newly added raw contacts 2265 mContactAggregator.setEnabled(aggregatorEnabled); 2266 return true; 2267 } 2268 } catch (Throwable e) { 2269 Log.e(TAG, "Legacy contact import failed", e); 2270 } 2271 mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement(); 2272 return false; 2273 } 2274 2275 /** 2276 * Wipes all data from the contacts database. 2277 */ 2278 /* package */ void wipeData() { 2279 mDbHelper.wipeData(); 2280 } 2281 2282 /** 2283 * While importing and aggregating contacts, this content provider will 2284 * block all attempts to change contacts data. In particular, it will hold 2285 * up all contact syncs. As soon as the import process is complete, all 2286 * processes waiting to write to the provider are unblocked and can proceed 2287 * to compete for the database transaction monitor. 2288 */ 2289 private void waitForAccess() { 2290 CountDownLatch latch = mAccessLatch; 2291 if (latch != null) { 2292 while (true) { 2293 try { 2294 latch.await(); 2295 mAccessLatch = null; 2296 return; 2297 } catch (InterruptedException e) { 2298 Thread.currentThread().interrupt(); 2299 } 2300 } 2301 } 2302 } 2303 2304 @Override 2305 public Uri insert(Uri uri, ContentValues values) { 2306 waitForAccess(); 2307 return super.insert(uri, values); 2308 } 2309 2310 @Override 2311 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 2312 if (mAccessLatch != null) { 2313 // We are stuck trying to upgrade contacts db. The only update request 2314 // allowed in this case is an update of provider status, which will trigger 2315 // an attempt to upgrade contacts again. 2316 int match = sUriMatcher.match(uri); 2317 if (match == PROVIDER_STATUS && isLegacyContactImportNeeded()) { 2318 Integer newStatus = values.getAsInteger(ProviderStatus.STATUS); 2319 if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) { 2320 importLegacyContactsAsync(); 2321 return 1; 2322 } else { 2323 return 0; 2324 } 2325 } 2326 } 2327 waitForAccess(); 2328 return super.update(uri, values, selection, selectionArgs); 2329 } 2330 2331 @Override 2332 public int delete(Uri uri, String selection, String[] selectionArgs) { 2333 waitForAccess(); 2334 return super.delete(uri, selection, selectionArgs); 2335 } 2336 2337 @Override 2338 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 2339 throws OperationApplicationException { 2340 waitForAccess(); 2341 return super.applyBatch(operations); 2342 } 2343 2344 @Override 2345 protected void onBeginTransaction() { 2346 if (VERBOSE_LOGGING) { 2347 Log.v(TAG, "onBeginTransaction"); 2348 } 2349 super.onBeginTransaction(); 2350 mContactAggregator.clearPendingAggregations(); 2351 clearTransactionalChanges(); 2352 } 2353 2354 private void clearTransactionalChanges() { 2355 mInsertedRawContacts.clear(); 2356 mUpdatedRawContacts.clear(); 2357 mUpdatedSyncStates.clear(); 2358 mDirtyRawContacts.clear(); 2359 } 2360 2361 @Override 2362 protected void beforeTransactionCommit() { 2363 2364 if (VERBOSE_LOGGING) { 2365 Log.v(TAG, "beforeTransactionCommit"); 2366 } 2367 super.beforeTransactionCommit(); 2368 flushTransactionalChanges(); 2369 mContactAggregator.aggregateInTransaction(mDb); 2370 if (mVisibleTouched) { 2371 mVisibleTouched = false; 2372 mDbHelper.updateAllVisible(); 2373 } 2374 } 2375 2376 private void flushTransactionalChanges() { 2377 if (VERBOSE_LOGGING) { 2378 Log.v(TAG, "flushTransactionChanges"); 2379 } 2380 2381 for (long rawContactId : mInsertedRawContacts.keySet()) { 2382 updateRawContactDisplayName(mDb, rawContactId); 2383 mContactAggregator.onRawContactInsert(mDb, rawContactId); 2384 } 2385 2386 if (!mDirtyRawContacts.isEmpty()) { 2387 mSb.setLength(0); 2388 mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL); 2389 appendIds(mSb, mDirtyRawContacts); 2390 mSb.append(")"); 2391 mDb.execSQL(mSb.toString()); 2392 } 2393 2394 if (!mUpdatedRawContacts.isEmpty()) { 2395 mSb.setLength(0); 2396 mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL); 2397 appendIds(mSb, mUpdatedRawContacts); 2398 mSb.append(")"); 2399 mDb.execSQL(mSb.toString()); 2400 } 2401 2402 for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) { 2403 long id = entry.getKey(); 2404 if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) { 2405 throw new IllegalStateException( 2406 "unable to update sync state, does it still exist?"); 2407 } 2408 } 2409 2410 clearTransactionalChanges(); 2411 } 2412 2413 /** 2414 * Appends comma separated ids. 2415 * @param ids Should not be empty 2416 */ 2417 private void appendIds(StringBuilder sb, HashSet<Long> ids) { 2418 for (long id : ids) { 2419 sb.append(id).append(','); 2420 } 2421 2422 sb.setLength(sb.length() - 1); // Yank the last comma 2423 } 2424 2425 @Override 2426 protected void notifyChange() { 2427 notifyChange(mSyncToNetwork); 2428 mSyncToNetwork = false; 2429 } 2430 2431 protected void notifyChange(boolean syncToNetwork) { 2432 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, 2433 syncToNetwork); 2434 } 2435 2436 protected void setProviderStatus(int status) { 2437 mProviderStatus = status; 2438 getContext().getContentResolver().notifyChange(ContactsContract.ProviderStatus.CONTENT_URI, 2439 null, false); 2440 } 2441 2442 private boolean isNewRawContact(long rawContactId) { 2443 return mInsertedRawContacts.containsKey(rawContactId); 2444 } 2445 2446 private DataRowHandler getDataRowHandler(final String mimeType) { 2447 DataRowHandler handler = mDataRowHandlers.get(mimeType); 2448 if (handler == null) { 2449 handler = new CustomDataRowHandler(mimeType); 2450 mDataRowHandlers.put(mimeType, handler); 2451 } 2452 return handler; 2453 } 2454 2455 @Override 2456 protected Uri insertInTransaction(Uri uri, ContentValues values) { 2457 if (VERBOSE_LOGGING) { 2458 Log.v(TAG, "insertInTransaction: " + uri + " " + values); 2459 } 2460 2461 final boolean callerIsSyncAdapter = 2462 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2463 2464 final int match = sUriMatcher.match(uri); 2465 long id = 0; 2466 2467 switch (match) { 2468 case SYNCSTATE: 2469 id = mDbHelper.getSyncState().insert(mDb, values); 2470 break; 2471 2472 case CONTACTS: { 2473 insertContact(values); 2474 break; 2475 } 2476 2477 case RAW_CONTACTS: { 2478 id = insertRawContact(uri, values, callerIsSyncAdapter); 2479 mSyncToNetwork |= !callerIsSyncAdapter; 2480 break; 2481 } 2482 2483 case RAW_CONTACTS_DATA: { 2484 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 2485 id = insertData(values, callerIsSyncAdapter); 2486 mSyncToNetwork |= !callerIsSyncAdapter; 2487 break; 2488 } 2489 2490 case DATA: { 2491 id = insertData(values, callerIsSyncAdapter); 2492 mSyncToNetwork |= !callerIsSyncAdapter; 2493 break; 2494 } 2495 2496 case GROUPS: { 2497 id = insertGroup(uri, values, callerIsSyncAdapter); 2498 mSyncToNetwork |= !callerIsSyncAdapter; 2499 break; 2500 } 2501 2502 case SETTINGS: { 2503 id = insertSettings(uri, values); 2504 mSyncToNetwork |= !callerIsSyncAdapter; 2505 break; 2506 } 2507 2508 case STATUS_UPDATES: { 2509 id = insertStatusUpdate(values); 2510 break; 2511 } 2512 2513 default: 2514 mSyncToNetwork = true; 2515 return mLegacyApiSupport.insert(uri, values); 2516 } 2517 2518 if (id < 0) { 2519 return null; 2520 } 2521 2522 return ContentUris.withAppendedId(uri, id); 2523 } 2524 2525 /** 2526 * If account is non-null then store it in the values. If the account is 2527 * already specified in the values then it must be consistent with the 2528 * account, if it is non-null. 2529 * 2530 * @param uri Current {@link Uri} being operated on. 2531 * @param values {@link ContentValues} to read and possibly update. 2532 * @throws IllegalArgumentException when only one of 2533 * {@link RawContacts#ACCOUNT_NAME} or 2534 * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the 2535 * other undefined. 2536 * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME} 2537 * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between 2538 * the given {@link Uri} and {@link ContentValues}. 2539 */ 2540 private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException { 2541 String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 2542 String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 2543 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 2544 2545 String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME); 2546 String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 2547 final boolean partialValues = TextUtils.isEmpty(valueAccountName) 2548 ^ TextUtils.isEmpty(valueAccountType); 2549 2550 if (partialUri || partialValues) { 2551 // Throw when either account is incomplete 2552 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 2553 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 2554 } 2555 2556 // Accounts are valid by only checking one parameter, since we've 2557 // already ruled out partial accounts. 2558 final boolean validUri = !TextUtils.isEmpty(accountName); 2559 final boolean validValues = !TextUtils.isEmpty(valueAccountName); 2560 2561 if (validValues && validUri) { 2562 // Check that accounts match when both present 2563 final boolean accountMatch = TextUtils.equals(accountName, valueAccountName) 2564 && TextUtils.equals(accountType, valueAccountType); 2565 if (!accountMatch) { 2566 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 2567 "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri)); 2568 } 2569 } else if (validUri) { 2570 // Fill values from Uri when not present 2571 values.put(RawContacts.ACCOUNT_NAME, accountName); 2572 values.put(RawContacts.ACCOUNT_TYPE, accountType); 2573 } else if (validValues) { 2574 accountName = valueAccountName; 2575 accountType = valueAccountType; 2576 } else { 2577 return null; 2578 } 2579 2580 // Use cached Account object when matches, otherwise create 2581 if (mAccount == null 2582 || !mAccount.name.equals(accountName) 2583 || !mAccount.type.equals(accountType)) { 2584 mAccount = new Account(accountName, accountType); 2585 } 2586 2587 return mAccount; 2588 } 2589 2590 /** 2591 * Inserts an item in the contacts table 2592 * 2593 * @param values the values for the new row 2594 * @return the row ID of the newly created row 2595 */ 2596 private long insertContact(ContentValues values) { 2597 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 2598 } 2599 2600 /** 2601 * Inserts an item in the contacts table 2602 * 2603 * @param uri the values for the new row 2604 * @param values the account this contact should be associated with. may be null. 2605 * @param callerIsSyncAdapter 2606 * @return the row ID of the newly created row 2607 */ 2608 private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 2609 mValues.clear(); 2610 mValues.putAll(values); 2611 mValues.putNull(RawContacts.CONTACT_ID); 2612 2613 final Account account = resolveAccount(uri, mValues); 2614 2615 if (values.containsKey(RawContacts.DELETED) 2616 && values.getAsInteger(RawContacts.DELETED) != 0) { 2617 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2618 } 2619 2620 long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues); 2621 int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT; 2622 if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) { 2623 aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE); 2624 } 2625 mContactAggregator.markNewForAggregation(rawContactId, aggregationMode); 2626 2627 // Trigger creation of a Contact based on this RawContact at the end of transaction 2628 mInsertedRawContacts.put(rawContactId, account); 2629 2630 if (!callerIsSyncAdapter) { 2631 addAutoAddMembership(rawContactId); 2632 final Long starred = values.getAsLong(RawContacts.STARRED); 2633 if (starred != null && starred != 0) { 2634 updateFavoritesMembership(rawContactId, starred != 0); 2635 } 2636 } 2637 2638 return rawContactId; 2639 } 2640 2641 private void addAutoAddMembership(long rawContactId) { 2642 final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID, 2643 rawContactId); 2644 if (groupId != null) { 2645 insertDataGroupMembership(rawContactId, groupId); 2646 } 2647 } 2648 2649 private Long findGroupByRawContactId(String selection, long rawContactId) { 2650 Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID, 2651 selection, 2652 new String[]{Long.toString(rawContactId)}, 2653 null /* groupBy */, null /* having */, null /* orderBy */); 2654 try { 2655 while (c.moveToNext()) { 2656 return c.getLong(0); 2657 } 2658 return null; 2659 } finally { 2660 c.close(); 2661 } 2662 } 2663 2664 private void updateFavoritesMembership(long rawContactId, boolean isStarred) { 2665 final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID, 2666 rawContactId); 2667 if (groupId != null) { 2668 if (isStarred) { 2669 insertDataGroupMembership(rawContactId, groupId); 2670 } else { 2671 deleteDataGroupMembership(rawContactId, groupId); 2672 } 2673 } 2674 } 2675 2676 private void insertDataGroupMembership(long rawContactId, long groupId) { 2677 ContentValues groupMembershipValues = new ContentValues(); 2678 groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId); 2679 groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId); 2680 groupMembershipValues.put(DataColumns.MIMETYPE_ID, 2681 mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 2682 mDb.insert(Tables.DATA, null, groupMembershipValues); 2683 } 2684 2685 private void deleteDataGroupMembership(long rawContactId, long groupId) { 2686 final String[] selectionArgs = { 2687 Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)), 2688 Long.toString(groupId), 2689 Long.toString(rawContactId)}; 2690 mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs); 2691 } 2692 2693 /** 2694 * Inserts an item in the data table 2695 * 2696 * @param values the values for the new row 2697 * @return the row ID of the newly created row 2698 */ 2699 private long insertData(ContentValues values, boolean callerIsSyncAdapter) { 2700 long id = 0; 2701 mValues.clear(); 2702 mValues.putAll(values); 2703 2704 long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 2705 2706 // Replace package with internal mapping 2707 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 2708 if (packageName != null) { 2709 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 2710 } 2711 mValues.remove(Data.RES_PACKAGE); 2712 2713 // Replace mimetype with internal mapping 2714 final String mimeType = mValues.getAsString(Data.MIMETYPE); 2715 if (TextUtils.isEmpty(mimeType)) { 2716 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 2717 } 2718 2719 mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType)); 2720 mValues.remove(Data.MIMETYPE); 2721 2722 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2723 id = rowHandler.insert(mDb, rawContactId, mValues); 2724 if (!callerIsSyncAdapter) { 2725 setRawContactDirty(rawContactId); 2726 } 2727 mUpdatedRawContacts.add(rawContactId); 2728 return id; 2729 } 2730 2731 private void triggerAggregation(long rawContactId) { 2732 if (!mContactAggregator.isEnabled()) { 2733 return; 2734 } 2735 2736 int aggregationMode = mDbHelper.getAggregationMode(rawContactId); 2737 switch (aggregationMode) { 2738 case RawContacts.AGGREGATION_MODE_DISABLED: 2739 break; 2740 2741 case RawContacts.AGGREGATION_MODE_DEFAULT: { 2742 mContactAggregator.markForAggregation(rawContactId, aggregationMode, false); 2743 break; 2744 } 2745 2746 case RawContacts.AGGREGATION_MODE_SUSPENDED: { 2747 long contactId = mDbHelper.getContactId(rawContactId); 2748 2749 if (contactId != 0) { 2750 mContactAggregator.updateAggregateData(contactId); 2751 } 2752 break; 2753 } 2754 2755 case RawContacts.AGGREGATION_MODE_IMMEDIATE: { 2756 long contactId = mDbHelper.getContactId(rawContactId); 2757 mContactAggregator.aggregateContact(mDb, rawContactId, contactId); 2758 break; 2759 } 2760 } 2761 } 2762 2763 /** 2764 * Returns the group id of the group with sourceId and the same account as rawContactId. 2765 * If the group doesn't already exist then it is first created, 2766 * @param db SQLiteDatabase to use for this operation 2767 * @param rawContactId the contact this group is associated with 2768 * @param sourceId the sourceIf of the group to query or create 2769 * @return the group id of the existing or created group 2770 * @throws IllegalArgumentException if the contact is not associated with an account 2771 * @throws IllegalStateException if a group needs to be created but the creation failed 2772 */ 2773 private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId, 2774 Account account) { 2775 2776 if (account == null) { 2777 mSelectionArgs1[0] = String.valueOf(rawContactId); 2778 Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, 2779 RawContacts._ID + "=?", mSelectionArgs1, null, null, null); 2780 try { 2781 if (c.moveToFirst()) { 2782 String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME); 2783 String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE); 2784 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 2785 account = new Account(accountName, accountType); 2786 } 2787 } 2788 } finally { 2789 c.close(); 2790 } 2791 } 2792 2793 if (account == null) { 2794 throw new IllegalArgumentException("if the groupmembership only " 2795 + "has a sourceid the the contact must be associated with " 2796 + "an account"); 2797 } 2798 2799 ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId); 2800 if (entries == null) { 2801 entries = new ArrayList<GroupIdCacheEntry>(1); 2802 mGroupIdCache.put(sourceId, entries); 2803 } 2804 2805 int count = entries.size(); 2806 for (int i = 0; i < count; i++) { 2807 GroupIdCacheEntry entry = entries.get(i); 2808 if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) { 2809 return entry.groupId; 2810 } 2811 } 2812 2813 GroupIdCacheEntry entry = new GroupIdCacheEntry(); 2814 entry.accountName = account.name; 2815 entry.accountType = account.type; 2816 entry.sourceId = sourceId; 2817 entries.add(0, entry); 2818 2819 // look up the group that contains this sourceId and has the same account name and type 2820 // as the contact refered to by rawContactId 2821 Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID}, 2822 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID, 2823 new String[]{sourceId, account.name, account.type}, null, null, null); 2824 try { 2825 if (c.moveToFirst()) { 2826 entry.groupId = c.getLong(0); 2827 } else { 2828 ContentValues groupValues = new ContentValues(); 2829 groupValues.put(Groups.ACCOUNT_NAME, account.name); 2830 groupValues.put(Groups.ACCOUNT_TYPE, account.type); 2831 groupValues.put(Groups.SOURCE_ID, sourceId); 2832 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues); 2833 if (groupId < 0) { 2834 throw new IllegalStateException("unable to create a new group with " 2835 + "this sourceid: " + groupValues); 2836 } 2837 entry.groupId = groupId; 2838 } 2839 } finally { 2840 c.close(); 2841 } 2842 2843 return entry.groupId; 2844 } 2845 2846 private interface DisplayNameQuery { 2847 public static final String RAW_SQL = 2848 "SELECT " 2849 + DataColumns.MIMETYPE_ID + "," 2850 + Data.IS_PRIMARY + "," 2851 + Data.DATA1 + "," 2852 + Data.DATA2 + "," 2853 + Data.DATA3 + "," 2854 + Data.DATA4 + "," 2855 + Data.DATA5 + "," 2856 + Data.DATA6 + "," 2857 + Data.DATA7 + "," 2858 + Data.DATA8 + "," 2859 + Data.DATA9 + "," 2860 + Data.DATA10 + "," 2861 + Data.DATA11 + 2862 " FROM " + Tables.DATA + 2863 " WHERE " + Data.RAW_CONTACT_ID + "=?" + 2864 " AND (" + Data.DATA1 + " NOT NULL OR " + 2865 Organization.TITLE + " NOT NULL)"; 2866 2867 public static final int MIMETYPE = 0; 2868 public static final int IS_PRIMARY = 1; 2869 public static final int DATA1 = 2; 2870 public static final int GIVEN_NAME = 3; // data2 2871 public static final int FAMILY_NAME = 4; // data3 2872 public static final int PREFIX = 5; // data4 2873 public static final int TITLE = 5; // data4 2874 public static final int MIDDLE_NAME = 6; // data5 2875 public static final int SUFFIX = 7; // data6 2876 public static final int PHONETIC_GIVEN_NAME = 8; // data7 2877 public static final int PHONETIC_MIDDLE_NAME = 9; // data8 2878 public static final int ORGANIZATION_PHONETIC_NAME = 9; // data8 2879 public static final int PHONETIC_FAMILY_NAME = 10; // data9 2880 public static final int FULL_NAME_STYLE = 11; // data10 2881 public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11; // data10 2882 public static final int PHONETIC_NAME_STYLE = 12; // data11 2883 } 2884 2885 /** 2886 * Updates a raw contact display name based on data rows, e.g. structured name, 2887 * organization, email etc. 2888 */ 2889 public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 2890 int bestDisplayNameSource = DisplayNameSources.UNDEFINED; 2891 NameSplitter.Name bestName = null; 2892 String bestDisplayName = null; 2893 String bestPhoneticName = null; 2894 int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2895 2896 mSelectionArgs1[0] = String.valueOf(rawContactId); 2897 Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1); 2898 try { 2899 while (c.moveToNext()) { 2900 int mimeType = c.getInt(DisplayNameQuery.MIMETYPE); 2901 int source = getDisplayNameSource(mimeType); 2902 if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) { 2903 continue; 2904 } 2905 2906 if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) { 2907 continue; 2908 } 2909 2910 if (mimeType == mMimeTypeIdStructuredName) { 2911 NameSplitter.Name name; 2912 if (bestName != null) { 2913 name = new NameSplitter.Name(); 2914 } else { 2915 name = mName; 2916 name.clear(); 2917 } 2918 name.prefix = c.getString(DisplayNameQuery.PREFIX); 2919 name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME); 2920 name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME); 2921 name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME); 2922 name.suffix = c.getString(DisplayNameQuery.SUFFIX); 2923 name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE) 2924 ? FullNameStyle.UNDEFINED 2925 : c.getInt(DisplayNameQuery.FULL_NAME_STYLE); 2926 name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME); 2927 name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME); 2928 name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME); 2929 name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE) 2930 ? PhoneticNameStyle.UNDEFINED 2931 : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE); 2932 if (!name.isEmpty()) { 2933 bestDisplayNameSource = source; 2934 bestName = name; 2935 } 2936 } else if (mimeType == mMimeTypeIdOrganization) { 2937 mCharArrayBuffer.sizeCopied = 0; 2938 c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer); 2939 if (mCharArrayBuffer.sizeCopied != 0) { 2940 bestDisplayNameSource = source; 2941 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2942 mCharArrayBuffer.sizeCopied); 2943 bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME); 2944 bestPhoneticNameStyle = 2945 c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE) 2946 ? PhoneticNameStyle.UNDEFINED 2947 : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE); 2948 } else { 2949 c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer); 2950 if (mCharArrayBuffer.sizeCopied != 0) { 2951 bestDisplayNameSource = source; 2952 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2953 mCharArrayBuffer.sizeCopied); 2954 bestPhoneticName = null; 2955 bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2956 } 2957 } 2958 } else { 2959 // Display name is at DATA1 in all other types. 2960 // This is ensured in the constructor. 2961 2962 mCharArrayBuffer.sizeCopied = 0; 2963 c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer); 2964 if (mCharArrayBuffer.sizeCopied != 0) { 2965 bestDisplayNameSource = source; 2966 bestDisplayName = new String(mCharArrayBuffer.data, 0, 2967 mCharArrayBuffer.sizeCopied); 2968 bestPhoneticName = null; 2969 bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED; 2970 } 2971 } 2972 } 2973 2974 } finally { 2975 c.close(); 2976 } 2977 2978 String displayNamePrimary; 2979 String displayNameAlternative; 2980 String sortKeyPrimary = null; 2981 String sortKeyAlternative = null; 2982 int displayNameStyle = FullNameStyle.UNDEFINED; 2983 2984 if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) { 2985 displayNameStyle = bestName.fullNameStyle; 2986 if (displayNameStyle == FullNameStyle.CJK 2987 || displayNameStyle == FullNameStyle.UNDEFINED) { 2988 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle); 2989 bestName.fullNameStyle = displayNameStyle; 2990 } 2991 2992 displayNamePrimary = mNameSplitter.join(bestName, true); 2993 displayNameAlternative = mNameSplitter.join(bestName, false); 2994 2995 bestPhoneticName = mNameSplitter.joinPhoneticName(bestName); 2996 bestPhoneticNameStyle = bestName.phoneticNameStyle; 2997 } else { 2998 displayNamePrimary = displayNameAlternative = bestDisplayName; 2999 } 3000 3001 if (bestPhoneticName != null) { 3002 sortKeyPrimary = sortKeyAlternative = bestPhoneticName; 3003 if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) { 3004 bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName); 3005 } 3006 } else { 3007 if (displayNameStyle == FullNameStyle.UNDEFINED) { 3008 displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName); 3009 if (displayNameStyle == FullNameStyle.UNDEFINED 3010 || displayNameStyle == FullNameStyle.CJK) { 3011 displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle( 3012 displayNameStyle, bestPhoneticNameStyle); 3013 } 3014 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle); 3015 } 3016 if (displayNameStyle == FullNameStyle.CHINESE || 3017 displayNameStyle == FullNameStyle.CJK) { 3018 sortKeyPrimary = sortKeyAlternative = 3019 ContactLocaleUtils.getIntance().getSortKey( 3020 displayNamePrimary, displayNameStyle); 3021 } 3022 } 3023 3024 if (sortKeyPrimary == null) { 3025 sortKeyPrimary = displayNamePrimary; 3026 sortKeyAlternative = displayNameAlternative; 3027 } 3028 3029 setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary, 3030 displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle, 3031 sortKeyPrimary, sortKeyAlternative); 3032 } 3033 3034 private int getDisplayNameSource(int mimeTypeId) { 3035 if (mimeTypeId == mMimeTypeIdStructuredName) { 3036 return DisplayNameSources.STRUCTURED_NAME; 3037 } else if (mimeTypeId == mMimeTypeIdEmail) { 3038 return DisplayNameSources.EMAIL; 3039 } else if (mimeTypeId == mMimeTypeIdPhone) { 3040 return DisplayNameSources.PHONE; 3041 } else if (mimeTypeId == mMimeTypeIdOrganization) { 3042 return DisplayNameSources.ORGANIZATION; 3043 } else if (mimeTypeId == mMimeTypeIdNickname) { 3044 return DisplayNameSources.NICKNAME; 3045 } else { 3046 return DisplayNameSources.UNDEFINED; 3047 } 3048 } 3049 3050 /** 3051 * Delete data row by row so that fixing of primaries etc work correctly. 3052 */ 3053 private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { 3054 int count = 0; 3055 3056 // Note that the query will return data according to the access restrictions, 3057 // so we don't need to worry about deleting data we don't have permission to read. 3058 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null); 3059 try { 3060 while(c.moveToNext()) { 3061 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 3062 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 3063 DataRowHandler rowHandler = getDataRowHandler(mimeType); 3064 count += rowHandler.delete(mDb, c); 3065 if (!callerIsSyncAdapter) { 3066 setRawContactDirty(rawContactId); 3067 } 3068 } 3069 } finally { 3070 c.close(); 3071 } 3072 3073 return count; 3074 } 3075 3076 /** 3077 * Delete a data row provided that it is one of the allowed mime types. 3078 */ 3079 public int deleteData(long dataId, String[] allowedMimeTypes) { 3080 3081 // Note that the query will return data according to the access restrictions, 3082 // so we don't need to worry about deleting data we don't have permission to read. 3083 mSelectionArgs1[0] = String.valueOf(dataId); 3084 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?", 3085 mSelectionArgs1, null); 3086 3087 try { 3088 if (!c.moveToFirst()) { 3089 return 0; 3090 } 3091 3092 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 3093 boolean valid = false; 3094 for (int i = 0; i < allowedMimeTypes.length; i++) { 3095 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 3096 valid = true; 3097 break; 3098 } 3099 } 3100 3101 if (!valid) { 3102 throw new IllegalArgumentException("Data type mismatch: expected " 3103 + Lists.newArrayList(allowedMimeTypes)); 3104 } 3105 3106 DataRowHandler rowHandler = getDataRowHandler(mimeType); 3107 return rowHandler.delete(mDb, c); 3108 } finally { 3109 c.close(); 3110 } 3111 } 3112 3113 /** 3114 * Inserts an item in the groups table 3115 */ 3116 private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 3117 mValues.clear(); 3118 mValues.putAll(values); 3119 3120 final Account account = resolveAccount(uri, mValues); 3121 3122 // Replace package with internal mapping 3123 final String packageName = mValues.getAsString(Groups.RES_PACKAGE); 3124 if (packageName != null) { 3125 mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 3126 } 3127 mValues.remove(Groups.RES_PACKAGE); 3128 3129 final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null 3130 ? mValues.getAsLong(Groups.FAVORITES) != 0 3131 : false; 3132 3133 if (!callerIsSyncAdapter) { 3134 mValues.put(Groups.DIRTY, 1); 3135 } 3136 3137 long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues); 3138 3139 if (!callerIsSyncAdapter && isFavoritesGroup) { 3140 // add all starred raw contacts to this group 3141 String selection; 3142 String[] selectionArgs; 3143 if (account == null) { 3144 selection = RawContacts.ACCOUNT_NAME + " IS NULL AND " 3145 + RawContacts.ACCOUNT_TYPE + " IS NULL"; 3146 selectionArgs = null; 3147 } else { 3148 selection = RawContacts.ACCOUNT_NAME + "=? AND " 3149 + RawContacts.ACCOUNT_TYPE + "=?"; 3150 selectionArgs = new String[]{account.name, account.type}; 3151 } 3152 Cursor c = mDb.query(Tables.RAW_CONTACTS, 3153 new String[]{RawContacts._ID, RawContacts.STARRED}, 3154 selection, selectionArgs, null, null, null); 3155 while (c.moveToNext()) { 3156 if (c.getLong(1) != 0) { 3157 final long rawContactId = c.getLong(0); 3158 insertDataGroupMembership(rawContactId, result); 3159 setRawContactDirty(rawContactId); 3160 } 3161 } 3162 } 3163 3164 if (mValues.containsKey(Groups.GROUP_VISIBLE)) { 3165 mVisibleTouched = true; 3166 } 3167 3168 return result; 3169 } 3170 3171 private long insertSettings(Uri uri, ContentValues values) { 3172 final long id = mDb.insert(Tables.SETTINGS, null, values); 3173 3174 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 3175 mVisibleTouched = true; 3176 } 3177 3178 return id; 3179 } 3180 3181 /** 3182 * Inserts a status update. 3183 */ 3184 public long insertStatusUpdate(ContentValues values) { 3185 final String handle = values.getAsString(StatusUpdates.IM_HANDLE); 3186 final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL); 3187 String customProtocol = null; 3188 3189 if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) { 3190 customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL); 3191 if (TextUtils.isEmpty(customProtocol)) { 3192 throw new IllegalArgumentException( 3193 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 3194 } 3195 } 3196 3197 long rawContactId = -1; 3198 long contactId = -1; 3199 Long dataId = values.getAsLong(StatusUpdates.DATA_ID); 3200 mSb.setLength(0); 3201 mSelectionArgs.clear(); 3202 if (dataId != null) { 3203 // Lookup the contact info for the given data row. 3204 3205 mSb.append(Tables.DATA + "." + Data._ID + "=?"); 3206 mSelectionArgs.add(String.valueOf(dataId)); 3207 } else { 3208 // Lookup the data row to attach this presence update to 3209 3210 if (TextUtils.isEmpty(handle) || protocol == null) { 3211 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 3212 } 3213 3214 // TODO: generalize to allow other providers to match against email 3215 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 3216 3217 String mimeTypeIdIm = String.valueOf(mMimeTypeIdIm); 3218 if (matchEmail) { 3219 String mimeTypeIdEmail = String.valueOf(mMimeTypeIdEmail); 3220 3221 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise 3222 // the "OR" conjunction confuses it and it switches to a full scan of 3223 // the raw_contacts table. 3224 3225 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same 3226 // column - Data.DATA1 3227 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" + 3228 " AND " + Data.DATA1 + "=?" + 3229 " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?"); 3230 mSelectionArgs.add(mimeTypeIdEmail); 3231 mSelectionArgs.add(mimeTypeIdIm); 3232 mSelectionArgs.add(handle); 3233 mSelectionArgs.add(mimeTypeIdIm); 3234 mSelectionArgs.add(String.valueOf(protocol)); 3235 if (customProtocol != null) { 3236 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 3237 mSelectionArgs.add(customProtocol); 3238 } 3239 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))"); 3240 mSelectionArgs.add(mimeTypeIdEmail); 3241 } else { 3242 mSb.append(DataColumns.MIMETYPE_ID + "=?" + 3243 " AND " + Im.PROTOCOL + "=?" + 3244 " AND " + Im.DATA + "=?"); 3245 mSelectionArgs.add(mimeTypeIdIm); 3246 mSelectionArgs.add(String.valueOf(protocol)); 3247 mSelectionArgs.add(handle); 3248 if (customProtocol != null) { 3249 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 3250 mSelectionArgs.add(customProtocol); 3251 } 3252 } 3253 3254 if (values.containsKey(StatusUpdates.DATA_ID)) { 3255 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?"); 3256 mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID)); 3257 } 3258 } 3259 mSb.append(" AND ").append(getContactsRestrictions()); 3260 3261 Cursor cursor = null; 3262 try { 3263 cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 3264 mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null, 3265 Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID); 3266 if (cursor.moveToFirst()) { 3267 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 3268 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 3269 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 3270 } else { 3271 // No contact found, return a null URI 3272 return -1; 3273 } 3274 } finally { 3275 if (cursor != null) { 3276 cursor.close(); 3277 } 3278 } 3279 3280 if (values.containsKey(StatusUpdates.PRESENCE)) { 3281 if (customProtocol == null) { 3282 // We cannot allow a null in the custom protocol field, because SQLite3 does not 3283 // properly enforce uniqueness of null values 3284 customProtocol = ""; 3285 } 3286 3287 mValues.clear(); 3288 mValues.put(StatusUpdates.DATA_ID, dataId); 3289 mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 3290 mValues.put(PresenceColumns.CONTACT_ID, contactId); 3291 mValues.put(StatusUpdates.PROTOCOL, protocol); 3292 mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol); 3293 mValues.put(StatusUpdates.IM_HANDLE, handle); 3294 if (values.containsKey(StatusUpdates.IM_ACCOUNT)) { 3295 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT)); 3296 } 3297 mValues.put(StatusUpdates.PRESENCE, 3298 values.getAsString(StatusUpdates.PRESENCE)); 3299 3300 // Insert the presence update 3301 mDb.replace(Tables.PRESENCE, null, mValues); 3302 } 3303 3304 3305 if (values.containsKey(StatusUpdates.STATUS)) { 3306 String status = values.getAsString(StatusUpdates.STATUS); 3307 String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE); 3308 Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL); 3309 3310 if (TextUtils.isEmpty(resPackage) 3311 && (labelResource == null || labelResource == 0) 3312 && protocol != null) { 3313 labelResource = Im.getProtocolLabelResource(protocol); 3314 } 3315 3316 Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON); 3317 // TODO compute the default icon based on the protocol 3318 3319 if (TextUtils.isEmpty(status)) { 3320 mStatusUpdateDelete.bindLong(1, dataId); 3321 mStatusUpdateDelete.execute(); 3322 } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) { 3323 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP); 3324 mStatusUpdateReplace.bindLong(1, dataId); 3325 mStatusUpdateReplace.bindLong(2, timestamp); 3326 bindString(mStatusUpdateReplace, 3, status); 3327 bindString(mStatusUpdateReplace, 4, resPackage); 3328 bindLong(mStatusUpdateReplace, 5, iconResource); 3329 bindLong(mStatusUpdateReplace, 6, labelResource); 3330 mStatusUpdateReplace.execute(); 3331 } else { 3332 3333 try { 3334 mStatusUpdateInsert.bindLong(1, dataId); 3335 bindString(mStatusUpdateInsert, 2, status); 3336 bindString(mStatusUpdateInsert, 3, resPackage); 3337 bindLong(mStatusUpdateInsert, 4, iconResource); 3338 bindLong(mStatusUpdateInsert, 5, labelResource); 3339 mStatusUpdateInsert.executeInsert(); 3340 } catch (SQLiteConstraintException e) { 3341 // The row already exists - update it 3342 long timestamp = System.currentTimeMillis(); 3343 mStatusUpdateAutoTimestamp.bindLong(1, timestamp); 3344 bindString(mStatusUpdateAutoTimestamp, 2, status); 3345 mStatusUpdateAutoTimestamp.bindLong(3, dataId); 3346 bindString(mStatusUpdateAutoTimestamp, 4, status); 3347 mStatusUpdateAutoTimestamp.execute(); 3348 3349 bindString(mStatusAttributionUpdate, 1, resPackage); 3350 bindLong(mStatusAttributionUpdate, 2, iconResource); 3351 bindLong(mStatusAttributionUpdate, 3, labelResource); 3352 mStatusAttributionUpdate.bindLong(4, dataId); 3353 mStatusAttributionUpdate.execute(); 3354 } 3355 } 3356 } 3357 3358 if (contactId != -1) { 3359 mLastStatusUpdate.bindLong(1, contactId); 3360 mLastStatusUpdate.bindLong(2, contactId); 3361 mLastStatusUpdate.execute(); 3362 } 3363 3364 return dataId; 3365 } 3366 3367 @Override 3368 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 3369 if (VERBOSE_LOGGING) { 3370 Log.v(TAG, "deleteInTransaction: " + uri); 3371 } 3372 flushTransactionalChanges(); 3373 final boolean callerIsSyncAdapter = 3374 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3375 final int match = sUriMatcher.match(uri); 3376 switch (match) { 3377 case SYNCSTATE: 3378 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs); 3379 3380 case SYNCSTATE_ID: 3381 String selectionWithId = 3382 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3383 + (selection == null ? "" : " AND (" + selection + ")"); 3384 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); 3385 3386 case CONTACTS: { 3387 // TODO 3388 return 0; 3389 } 3390 3391 case CONTACTS_ID: { 3392 long contactId = ContentUris.parseId(uri); 3393 return deleteContact(contactId, callerIsSyncAdapter); 3394 } 3395 3396 case CONTACTS_LOOKUP: { 3397 final List<String> pathSegments = uri.getPathSegments(); 3398 final int segmentCount = pathSegments.size(); 3399 if (segmentCount < 3) { 3400 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3401 "Missing a lookup key", uri)); 3402 } 3403 final String lookupKey = pathSegments.get(2); 3404 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 3405 return deleteContact(contactId, callerIsSyncAdapter); 3406 } 3407 3408 case CONTACTS_LOOKUP_ID: { 3409 // lookup contact by id and lookup key to see if they still match the actual record 3410 long contactId = ContentUris.parseId(uri); 3411 final List<String> pathSegments = uri.getPathSegments(); 3412 final String lookupKey = pathSegments.get(2); 3413 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3414 setTablesAndProjectionMapForContacts(lookupQb, uri, null); 3415 String[] args; 3416 if (selectionArgs == null) { 3417 args = new String[2]; 3418 } else { 3419 args = new String[selectionArgs.length + 2]; 3420 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 3421 } 3422 args[0] = String.valueOf(contactId); 3423 args[1] = Uri.encode(lookupKey); 3424 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); 3425 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 3426 Cursor c = query(db, lookupQb, null, selection, args, null, null, null); 3427 try { 3428 if (c.getCount() == 1) { 3429 // contact was unmodified so go ahead and delete it 3430 return deleteContact(contactId, callerIsSyncAdapter); 3431 } else { 3432 // row was changed (e.g. the merging might have changed), we got multiple 3433 // rows or the supplied selection filtered the record out 3434 return 0; 3435 } 3436 } finally { 3437 c.close(); 3438 } 3439 } 3440 3441 case RAW_CONTACTS: { 3442 int numDeletes = 0; 3443 Cursor c = mDb.query(Tables.RAW_CONTACTS, 3444 new String[]{RawContacts._ID, RawContacts.CONTACT_ID}, 3445 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 3446 try { 3447 while (c.moveToNext()) { 3448 final long rawContactId = c.getLong(0); 3449 long contactId = c.getLong(1); 3450 numDeletes += deleteRawContact(rawContactId, contactId, 3451 callerIsSyncAdapter); 3452 } 3453 } finally { 3454 c.close(); 3455 } 3456 return numDeletes; 3457 } 3458 3459 case RAW_CONTACTS_ID: { 3460 final long rawContactId = ContentUris.parseId(uri); 3461 return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId), 3462 callerIsSyncAdapter); 3463 } 3464 3465 case DATA: { 3466 mSyncToNetwork |= !callerIsSyncAdapter; 3467 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 3468 callerIsSyncAdapter); 3469 } 3470 3471 case DATA_ID: 3472 case PHONES_ID: 3473 case EMAILS_ID: 3474 case POSTALS_ID: { 3475 long dataId = ContentUris.parseId(uri); 3476 mSyncToNetwork |= !callerIsSyncAdapter; 3477 mSelectionArgs1[0] = String.valueOf(dataId); 3478 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); 3479 } 3480 3481 case GROUPS_ID: { 3482 mSyncToNetwork |= !callerIsSyncAdapter; 3483 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter); 3484 } 3485 3486 case GROUPS: { 3487 int numDeletes = 0; 3488 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 3489 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 3490 try { 3491 while (c.moveToNext()) { 3492 numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter); 3493 } 3494 } finally { 3495 c.close(); 3496 } 3497 if (numDeletes > 0) { 3498 mSyncToNetwork |= !callerIsSyncAdapter; 3499 } 3500 return numDeletes; 3501 } 3502 3503 case SETTINGS: { 3504 mSyncToNetwork |= !callerIsSyncAdapter; 3505 return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs); 3506 } 3507 3508 case STATUS_UPDATES: { 3509 return deleteStatusUpdates(selection, selectionArgs); 3510 } 3511 3512 default: { 3513 mSyncToNetwork = true; 3514 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 3515 } 3516 } 3517 } 3518 3519 public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) { 3520 mGroupIdCache.clear(); 3521 final long groupMembershipMimetypeId = mDbHelper 3522 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 3523 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 3524 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 3525 + groupId, null); 3526 3527 try { 3528 if (callerIsSyncAdapter) { 3529 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 3530 } else { 3531 mValues.clear(); 3532 mValues.put(Groups.DELETED, 1); 3533 mValues.put(Groups.DIRTY, 1); 3534 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 3535 } 3536 } finally { 3537 mVisibleTouched = true; 3538 } 3539 } 3540 3541 private int deleteSettings(Uri uri, String selection, String[] selectionArgs) { 3542 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 3543 mVisibleTouched = true; 3544 return count; 3545 } 3546 3547 private int deleteContact(long contactId, boolean callerIsSyncAdapter) { 3548 mSelectionArgs1[0] = Long.toString(contactId); 3549 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 3550 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, 3551 null, null, null); 3552 try { 3553 while (c.moveToNext()) { 3554 long rawContactId = c.getLong(0); 3555 markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); 3556 } 3557 } finally { 3558 c.close(); 3559 } 3560 3561 return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); 3562 } 3563 3564 public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) { 3565 mContactAggregator.invalidateAggregationExceptionCache(); 3566 if (callerIsSyncAdapter) { 3567 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 3568 int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 3569 mContactAggregator.updateDisplayNameForContact(mDb, contactId); 3570 return count; 3571 } else { 3572 mDbHelper.removeContactIfSingleton(rawContactId); 3573 return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter); 3574 } 3575 } 3576 3577 private int deleteStatusUpdates(String selection, String[] selectionArgs) { 3578 // delete from both tables: presence and status_updates 3579 // TODO should account type/name be appended to the where clause? 3580 if (VERBOSE_LOGGING) { 3581 Log.v(TAG, "deleting data from status_updates for " + selection); 3582 } 3583 mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection), 3584 selectionArgs); 3585 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 3586 } 3587 3588 private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) { 3589 mSyncToNetwork = true; 3590 3591 mValues.clear(); 3592 mValues.put(RawContacts.DELETED, 1); 3593 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 3594 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 3595 mValues.putNull(RawContacts.CONTACT_ID); 3596 mValues.put(RawContacts.DIRTY, 1); 3597 return updateRawContact(rawContactId, mValues, callerIsSyncAdapter); 3598 } 3599 3600 @Override 3601 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 3602 String[] selectionArgs) { 3603 if (VERBOSE_LOGGING) { 3604 Log.v(TAG, "updateInTransaction: " + uri); 3605 } 3606 3607 int count = 0; 3608 3609 final int match = sUriMatcher.match(uri); 3610 if (match == SYNCSTATE_ID && selection == null) { 3611 long rowId = ContentUris.parseId(uri); 3612 Object data = values.get(ContactsContract.SyncState.DATA); 3613 mUpdatedSyncStates.put(rowId, data); 3614 return 1; 3615 } 3616 flushTransactionalChanges(); 3617 final boolean callerIsSyncAdapter = 3618 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3619 switch(match) { 3620 case SYNCSTATE: 3621 return mDbHelper.getSyncState().update(mDb, values, 3622 appendAccountToSelection(uri, selection), selectionArgs); 3623 3624 case SYNCSTATE_ID: { 3625 selection = appendAccountToSelection(uri, selection); 3626 String selectionWithId = 3627 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3628 + (selection == null ? "" : " AND (" + selection + ")"); 3629 return mDbHelper.getSyncState().update(mDb, values, 3630 selectionWithId, selectionArgs); 3631 } 3632 3633 case CONTACTS: { 3634 count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter); 3635 break; 3636 } 3637 3638 case CONTACTS_ID: { 3639 count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter); 3640 break; 3641 } 3642 3643 case CONTACTS_LOOKUP: 3644 case CONTACTS_LOOKUP_ID: { 3645 final List<String> pathSegments = uri.getPathSegments(); 3646 final int segmentCount = pathSegments.size(); 3647 if (segmentCount < 3) { 3648 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 3649 "Missing a lookup key", uri)); 3650 } 3651 final String lookupKey = pathSegments.get(2); 3652 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 3653 count = updateContactOptions(contactId, values, callerIsSyncAdapter); 3654 break; 3655 } 3656 3657 case RAW_CONTACTS_DATA: { 3658 final String rawContactId = uri.getPathSegments().get(1); 3659 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") 3660 + (selection == null ? "" : " AND " + selection); 3661 3662 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); 3663 3664 break; 3665 } 3666 3667 case DATA: { 3668 count = updateData(uri, values, appendAccountToSelection(uri, selection), 3669 selectionArgs, callerIsSyncAdapter); 3670 if (count > 0) { 3671 mSyncToNetwork |= !callerIsSyncAdapter; 3672 } 3673 break; 3674 } 3675 3676 case DATA_ID: 3677 case PHONES_ID: 3678 case EMAILS_ID: 3679 case POSTALS_ID: { 3680 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); 3681 if (count > 0) { 3682 mSyncToNetwork |= !callerIsSyncAdapter; 3683 } 3684 break; 3685 } 3686 3687 case RAW_CONTACTS: { 3688 selection = appendAccountToSelection(uri, selection); 3689 count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter); 3690 break; 3691 } 3692 3693 case RAW_CONTACTS_ID: { 3694 long rawContactId = ContentUris.parseId(uri); 3695 if (selection != null) { 3696 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3697 count = updateRawContacts(values, RawContacts._ID + "=?" 3698 + " AND(" + selection + ")", selectionArgs, 3699 callerIsSyncAdapter); 3700 } else { 3701 mSelectionArgs1[0] = String.valueOf(rawContactId); 3702 count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1, 3703 callerIsSyncAdapter); 3704 } 3705 break; 3706 } 3707 3708 case GROUPS: { 3709 count = updateGroups(uri, values, appendAccountToSelection(uri, selection), 3710 selectionArgs, callerIsSyncAdapter); 3711 if (count > 0) { 3712 mSyncToNetwork |= !callerIsSyncAdapter; 3713 } 3714 break; 3715 } 3716 3717 case GROUPS_ID: { 3718 long groupId = ContentUris.parseId(uri); 3719 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); 3720 String selectionWithId = Groups._ID + "=? " 3721 + (selection == null ? "" : " AND " + selection); 3722 count = updateGroups(uri, values, selectionWithId, selectionArgs, 3723 callerIsSyncAdapter); 3724 if (count > 0) { 3725 mSyncToNetwork |= !callerIsSyncAdapter; 3726 } 3727 break; 3728 } 3729 3730 case AGGREGATION_EXCEPTIONS: { 3731 count = updateAggregationException(mDb, values); 3732 break; 3733 } 3734 3735 case SETTINGS: { 3736 count = updateSettings(uri, values, appendAccountToSelection(uri, selection), 3737 selectionArgs); 3738 mSyncToNetwork |= !callerIsSyncAdapter; 3739 break; 3740 } 3741 3742 case STATUS_UPDATES: { 3743 count = updateStatusUpdate(uri, values, selection, selectionArgs); 3744 break; 3745 } 3746 3747 default: { 3748 mSyncToNetwork = true; 3749 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 3750 } 3751 } 3752 3753 return count; 3754 } 3755 3756 private int updateStatusUpdate(Uri uri, ContentValues values, String selection, 3757 String[] selectionArgs) { 3758 // update status_updates table, if status is provided 3759 // TODO should account type/name be appended to the where clause? 3760 int updateCount = 0; 3761 ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values); 3762 if (settableValues.size() > 0) { 3763 updateCount = mDb.update(Tables.STATUS_UPDATES, 3764 settableValues, 3765 getWhereClauseForStatusUpdatesTable(selection), 3766 selectionArgs); 3767 } 3768 3769 // now update the Presence table 3770 settableValues = getSettableColumnsForPresenceTable(values); 3771 if (settableValues.size() > 0) { 3772 updateCount = mDb.update(Tables.PRESENCE, settableValues, 3773 selection, selectionArgs); 3774 } 3775 // TODO updateCount is not entirely a valid count of updated rows because 2 tables could 3776 // potentially get updated in this method. 3777 return updateCount; 3778 } 3779 3780 /** 3781 * Build a where clause to select the rows to be updated in status_updates table. 3782 */ 3783 private String getWhereClauseForStatusUpdatesTable(String selection) { 3784 mSb.setLength(0); 3785 mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE); 3786 mSb.append(selection); 3787 mSb.append(")"); 3788 return mSb.toString(); 3789 } 3790 3791 private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) { 3792 mValues.clear(); 3793 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values, 3794 StatusUpdates.STATUS); 3795 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values, 3796 StatusUpdates.STATUS_TIMESTAMP); 3797 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values, 3798 StatusUpdates.STATUS_RES_PACKAGE); 3799 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values, 3800 StatusUpdates.STATUS_LABEL); 3801 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values, 3802 StatusUpdates.STATUS_ICON); 3803 return mValues; 3804 } 3805 3806 private ContentValues getSettableColumnsForPresenceTable(ContentValues values) { 3807 mValues.clear(); 3808 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values, 3809 StatusUpdates.PRESENCE); 3810 return mValues; 3811 } 3812 3813 private int updateGroups(Uri uri, ContentValues values, String selectionWithId, 3814 String[] selectionArgs, boolean callerIsSyncAdapter) { 3815 3816 mGroupIdCache.clear(); 3817 3818 ContentValues updatedValues; 3819 if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) { 3820 updatedValues = mValues; 3821 updatedValues.clear(); 3822 updatedValues.putAll(values); 3823 updatedValues.put(Groups.DIRTY, 1); 3824 } else { 3825 updatedValues = values; 3826 } 3827 3828 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 3829 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 3830 mVisibleTouched = true; 3831 } 3832 if (updatedValues.containsKey(Groups.SHOULD_SYNC) 3833 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) { 3834 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME, 3835 Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null, 3836 null, null); 3837 String accountName; 3838 String accountType; 3839 try { 3840 while (c.moveToNext()) { 3841 accountName = c.getString(0); 3842 accountType = c.getString(1); 3843 if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 3844 Account account = new Account(accountName, accountType); 3845 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, 3846 new Bundle()); 3847 break; 3848 } 3849 } 3850 } finally { 3851 c.close(); 3852 } 3853 } 3854 return count; 3855 } 3856 3857 private int updateSettings(Uri uri, ContentValues values, String selection, 3858 String[] selectionArgs) { 3859 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 3860 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 3861 mVisibleTouched = true; 3862 } 3863 return count; 3864 } 3865 3866 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs, 3867 boolean callerIsSyncAdapter) { 3868 if (values.containsKey(RawContacts.CONTACT_ID)) { 3869 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 3870 "in content values. Contact IDs are assigned automatically"); 3871 } 3872 3873 int count = 0; 3874 Cursor cursor = mDb.query(mDbHelper.getRawContactView(), 3875 new String[] { RawContacts._ID }, selection, 3876 selectionArgs, null, null, null); 3877 try { 3878 while (cursor.moveToNext()) { 3879 long rawContactId = cursor.getLong(0); 3880 updateRawContact(rawContactId, values, callerIsSyncAdapter); 3881 count++; 3882 } 3883 } finally { 3884 cursor.close(); 3885 } 3886 3887 return count; 3888 } 3889 3890 private int updateRawContact(long rawContactId, ContentValues values, 3891 boolean callerIsSyncAdapter) { 3892 final String selection = RawContacts._ID + " = ?"; 3893 mSelectionArgs1[0] = Long.toString(rawContactId); 3894 final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED) 3895 && values.getAsInteger(RawContacts.DELETED) == 0); 3896 int previousDeleted = 0; 3897 String accountType = null; 3898 String accountName = null; 3899 if (requestUndoDelete) { 3900 Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection, 3901 mSelectionArgs1, null, null, null); 3902 try { 3903 if (cursor.moveToFirst()) { 3904 previousDeleted = cursor.getInt(RawContactsQuery.DELETED); 3905 accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE); 3906 accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME); 3907 } 3908 } finally { 3909 cursor.close(); 3910 } 3911 values.put(ContactsContract.RawContacts.AGGREGATION_MODE, 3912 ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT); 3913 } 3914 3915 int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1); 3916 if (count != 0) { 3917 if (values.containsKey(RawContacts.AGGREGATION_MODE)) { 3918 int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE); 3919 3920 // As per ContactsContract documentation, changing aggregation mode 3921 // to DEFAULT should not trigger aggregation 3922 if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) { 3923 mContactAggregator.markForAggregation(rawContactId, aggregationMode, false); 3924 } 3925 } 3926 if (values.containsKey(RawContacts.STARRED)) { 3927 if (!callerIsSyncAdapter) { 3928 updateFavoritesMembership(rawContactId, 3929 values.getAsLong(RawContacts.STARRED) != 0); 3930 } 3931 mContactAggregator.updateStarred(rawContactId); 3932 } else { 3933 // if this raw contact is being associated with an account, then update the 3934 // favorites group membership based on whether or not this contact is starred. 3935 // If it is starred, add a group membership, if one doesn't already exist 3936 // otherwise delete any matching group memberships. 3937 if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { 3938 boolean starred = 0 != DatabaseUtils.longForQuery(mDb, 3939 SELECTION_STARRED_FROM_RAW_CONTACTS, 3940 new String[]{Long.toString(rawContactId)}); 3941 updateFavoritesMembership(rawContactId, starred); 3942 } 3943 } 3944 3945 // if this raw contact is being associated with an account, then add a 3946 // group membership to the group marked as AutoAdd, if any. 3947 if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) { 3948 addAutoAddMembership(rawContactId); 3949 } 3950 3951 if (values.containsKey(RawContacts.SOURCE_ID)) { 3952 mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId); 3953 } 3954 if (values.containsKey(RawContacts.NAME_VERIFIED)) { 3955 3956 // If setting NAME_VERIFIED for this raw contact, reset it for all 3957 // other raw contacts in the same aggregate 3958 if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) { 3959 mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId); 3960 mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId); 3961 mResetNameVerifiedForOtherRawContacts.execute(); 3962 } 3963 mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId); 3964 } 3965 if (requestUndoDelete && previousDeleted == 1) { 3966 // undo delete, needs aggregation again. 3967 mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType)); 3968 } 3969 } 3970 return count; 3971 } 3972 3973 private int updateData(Uri uri, ContentValues values, String selection, 3974 String[] selectionArgs, boolean callerIsSyncAdapter) { 3975 mValues.clear(); 3976 mValues.putAll(values); 3977 mValues.remove(Data._ID); 3978 mValues.remove(Data.RAW_CONTACT_ID); 3979 mValues.remove(Data.MIMETYPE); 3980 3981 String packageName = values.getAsString(Data.RES_PACKAGE); 3982 if (packageName != null) { 3983 mValues.remove(Data.RES_PACKAGE); 3984 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName)); 3985 } 3986 3987 boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY); 3988 boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY); 3989 3990 // Remove primary or super primary values being set to 0. This is disallowed by the 3991 // content provider. 3992 if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { 3993 containsIsSuperPrimary = false; 3994 mValues.remove(Data.IS_SUPER_PRIMARY); 3995 } 3996 if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) { 3997 containsIsPrimary = false; 3998 mValues.remove(Data.IS_PRIMARY); 3999 } 4000 4001 int count = 0; 4002 4003 // Note that the query will return data according to the access restrictions, 4004 // so we don't need to worry about updating data we don't have permission to read. 4005 Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null); 4006 try { 4007 while(c.moveToNext()) { 4008 count += updateData(mValues, c, callerIsSyncAdapter); 4009 } 4010 } finally { 4011 c.close(); 4012 } 4013 4014 return count; 4015 } 4016 4017 private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { 4018 if (values.size() == 0) { 4019 return 0; 4020 } 4021 4022 final String mimeType = c.getString(DataUpdateQuery.MIMETYPE); 4023 DataRowHandler rowHandler = getDataRowHandler(mimeType); 4024 if (rowHandler.update(mDb, values, c, callerIsSyncAdapter)) { 4025 return 1; 4026 } else { 4027 return 0; 4028 } 4029 } 4030 4031 private int updateContactOptions(ContentValues values, String selection, 4032 String[] selectionArgs, boolean callerIsSyncAdapter) { 4033 int count = 0; 4034 Cursor cursor = mDb.query(mDbHelper.getContactView(), 4035 new String[] { Contacts._ID }, selection, 4036 selectionArgs, null, null, null); 4037 try { 4038 while (cursor.moveToNext()) { 4039 long contactId = cursor.getLong(0); 4040 updateContactOptions(contactId, values, callerIsSyncAdapter); 4041 count++; 4042 } 4043 } finally { 4044 cursor.close(); 4045 } 4046 4047 return count; 4048 } 4049 4050 private int updateContactOptions(long contactId, ContentValues values, 4051 boolean callerIsSyncAdapter) { 4052 4053 mValues.clear(); 4054 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 4055 values, Contacts.CUSTOM_RINGTONE); 4056 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 4057 values, Contacts.SEND_TO_VOICEMAIL); 4058 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 4059 values, Contacts.LAST_TIME_CONTACTED); 4060 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 4061 values, Contacts.TIMES_CONTACTED); 4062 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 4063 values, Contacts.STARRED); 4064 4065 // Nothing to update - just return 4066 if (mValues.size() == 0) { 4067 return 0; 4068 } 4069 4070 if (mValues.containsKey(RawContacts.STARRED)) { 4071 // Mark dirty when changing starred to trigger sync 4072 mValues.put(RawContacts.DIRTY, 1); 4073 } 4074 4075 mSelectionArgs1[0] = String.valueOf(contactId); 4076 mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1); 4077 4078 if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) { 4079 Cursor cursor = mDb.query(mDbHelper.getRawContactView(), 4080 new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?", 4081 mSelectionArgs1, null, null, null); 4082 try { 4083 while (cursor.moveToNext()) { 4084 long rawContactId = cursor.getLong(0); 4085 updateFavoritesMembership(rawContactId, 4086 mValues.getAsLong(RawContacts.STARRED) != 0); 4087 } 4088 } finally { 4089 cursor.close(); 4090 } 4091 } 4092 4093 // Copy changeable values to prevent automatically managed fields from 4094 // being explicitly updated by clients. 4095 mValues.clear(); 4096 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 4097 values, Contacts.CUSTOM_RINGTONE); 4098 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 4099 values, Contacts.SEND_TO_VOICEMAIL); 4100 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 4101 values, Contacts.LAST_TIME_CONTACTED); 4102 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 4103 values, Contacts.TIMES_CONTACTED); 4104 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 4105 values, Contacts.STARRED); 4106 4107 int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1); 4108 4109 if (values.containsKey(Contacts.LAST_TIME_CONTACTED) && 4110 !values.containsKey(Contacts.TIMES_CONTACTED)) { 4111 mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1); 4112 mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1); 4113 } 4114 return rslt; 4115 } 4116 4117 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 4118 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 4119 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 4120 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 4121 4122 long rawContactId1, rawContactId2; 4123 if (rcId1 < rcId2) { 4124 rawContactId1 = rcId1; 4125 rawContactId2 = rcId2; 4126 } else { 4127 rawContactId2 = rcId1; 4128 rawContactId1 = rcId2; 4129 } 4130 4131 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 4132 mSelectionArgs2[0] = String.valueOf(rawContactId1); 4133 mSelectionArgs2[1] = String.valueOf(rawContactId2); 4134 db.delete(Tables.AGGREGATION_EXCEPTIONS, 4135 AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " 4136 + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2); 4137 } else { 4138 ContentValues exceptionValues = new ContentValues(3); 4139 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 4140 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 4141 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 4142 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 4143 exceptionValues); 4144 } 4145 4146 mContactAggregator.invalidateAggregationExceptionCache(); 4147 mContactAggregator.markForAggregation(rawContactId1, 4148 RawContacts.AGGREGATION_MODE_DEFAULT, true); 4149 mContactAggregator.markForAggregation(rawContactId2, 4150 RawContacts.AGGREGATION_MODE_DEFAULT, true); 4151 4152 long contactId1 = mDbHelper.getContactId(rawContactId1); 4153 mContactAggregator.aggregateContact(db, rawContactId1, contactId1); 4154 4155 long contactId2 = mDbHelper.getContactId(rawContactId2); 4156 mContactAggregator.aggregateContact(db, rawContactId2, contactId2); 4157 4158 // The return value is fake - we just confirm that we made a change, not count actual 4159 // rows changed. 4160 return 1; 4161 } 4162 4163 public void onAccountsUpdated(Account[] accounts) { 4164 // TODO : Check the unit test. 4165 HashSet<Account> existingAccounts = new HashSet<Account>(); 4166 mDb.beginTransaction(); 4167 try { 4168 findValidAccounts(existingAccounts); 4169 4170 // Add a row to the ACCOUNTS table for each new account 4171 for (Account account : accounts) { 4172 if (!existingAccounts.contains(account)) { 4173 mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME 4174 + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)", 4175 new String[] {account.name, account.type}); 4176 } 4177 } 4178 4179 // Remove all valid accounts from the existing account set. What is left 4180 // in the accountsToDelete set will be extra accounts whose data must be deleted. 4181 HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts); 4182 for (Account account : accounts) { 4183 accountsToDelete.remove(account); 4184 } 4185 4186 for (Account account : accountsToDelete) { 4187 Log.d(TAG, "removing data for removed account " + account); 4188 String[] params = new String[] {account.name, account.type}; 4189 mDb.execSQL( 4190 "DELETE FROM " + Tables.GROUPS + 4191 " WHERE " + Groups.ACCOUNT_NAME + " = ?" + 4192 " AND " + Groups.ACCOUNT_TYPE + " = ?", params); 4193 mDb.execSQL( 4194 "DELETE FROM " + Tables.PRESENCE + 4195 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" + 4196 "SELECT " + RawContacts._ID + 4197 " FROM " + Tables.RAW_CONTACTS + 4198 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 4199 " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params); 4200 mDb.execSQL( 4201 "DELETE FROM " + Tables.RAW_CONTACTS + 4202 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" + 4203 " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params); 4204 mDb.execSQL( 4205 "DELETE FROM " + Tables.SETTINGS + 4206 " WHERE " + Settings.ACCOUNT_NAME + " = ?" + 4207 " AND " + Settings.ACCOUNT_TYPE + " = ?", params); 4208 mDb.execSQL( 4209 "DELETE FROM " + Tables.ACCOUNTS + 4210 " WHERE " + RawContacts.ACCOUNT_NAME + "=?" + 4211 " AND " + RawContacts.ACCOUNT_TYPE + "=?", params); 4212 } 4213 4214 if (!accountsToDelete.isEmpty()) { 4215 // Find all aggregated contacts that used to contain the raw contacts 4216 // we have just deleted and see if they are still referencing the deleted 4217 // names of photos. If so, fix up those contacts. 4218 HashSet<Long> orphanContactIds = Sets.newHashSet(); 4219 Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID + 4220 " FROM " + Tables.CONTACTS + 4221 " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " + 4222 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " + 4223 "(SELECT " + RawContacts._ID + 4224 " FROM " + Tables.RAW_CONTACTS + "))" + 4225 " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " + 4226 Contacts.PHOTO_ID + " NOT IN " + 4227 "(SELECT " + Data._ID + 4228 " FROM " + Tables.DATA + "))", null); 4229 try { 4230 while (cursor.moveToNext()) { 4231 orphanContactIds.add(cursor.getLong(0)); 4232 } 4233 } finally { 4234 cursor.close(); 4235 } 4236 4237 for (Long contactId : orphanContactIds) { 4238 mContactAggregator.updateAggregateData(contactId); 4239 } 4240 } 4241 4242 mDbHelper.updateAllVisible(); 4243 4244 mDbHelper.getSyncState().onAccountsChanged(mDb, accounts); 4245 mDb.setTransactionSuccessful(); 4246 } finally { 4247 mDb.endTransaction(); 4248 } 4249 mAccountWritability.clear(); 4250 } 4251 4252 /** 4253 * Finds all distinct accounts present in the specified table. 4254 */ 4255 private void findValidAccounts(Set<Account> validAccounts) { 4256 Cursor c = mDb.rawQuery( 4257 "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE + 4258 " FROM " + Tables.ACCOUNTS, null); 4259 try { 4260 while (c.moveToNext()) { 4261 if (!c.isNull(0) || !c.isNull(1)) { 4262 validAccounts.add(new Account(c.getString(0), c.getString(1))); 4263 } 4264 } 4265 } finally { 4266 c.close(); 4267 } 4268 } 4269 4270 /** 4271 * Test all against {@link TextUtils#isEmpty(CharSequence)}. 4272 */ 4273 private static boolean areAllEmpty(ContentValues values, String[] keys) { 4274 for (String key : keys) { 4275 if (!TextUtils.isEmpty(values.getAsString(key))) { 4276 return false; 4277 } 4278 } 4279 return true; 4280 } 4281 4282 /** 4283 * Returns true if a value (possibly null) is specified for at least one of the supplied keys. 4284 */ 4285 private static boolean areAnySpecified(ContentValues values, String[] keys) { 4286 for (String key : keys) { 4287 if (values.containsKey(key)) { 4288 return true; 4289 } 4290 } 4291 return false; 4292 } 4293 4294 @Override 4295 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 4296 String sortOrder) { 4297 if (VERBOSE_LOGGING) { 4298 Log.v(TAG, "query: " + uri); 4299 } 4300 4301 final SQLiteDatabase db = mDbHelper.getReadableDatabase(); 4302 4303 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4304 String groupBy = null; 4305 String limit = getLimit(uri); 4306 4307 // TODO: Consider writing a test case for RestrictionExceptions when you 4308 // write a new query() block to make sure it protects restricted data. 4309 final int match = sUriMatcher.match(uri); 4310 switch (match) { 4311 case SYNCSTATE: 4312 return mDbHelper.getSyncState().query(db, projection, selection, selectionArgs, 4313 sortOrder); 4314 4315 case CONTACTS: { 4316 setTablesAndProjectionMapForContacts(qb, uri, projection); 4317 break; 4318 } 4319 4320 case CONTACTS_ID: { 4321 long contactId = ContentUris.parseId(uri); 4322 setTablesAndProjectionMapForContacts(qb, uri, projection); 4323 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4324 qb.appendWhere(Contacts._ID + "=?"); 4325 break; 4326 } 4327 4328 case CONTACTS_LOOKUP: 4329 case CONTACTS_LOOKUP_ID: { 4330 List<String> pathSegments = uri.getPathSegments(); 4331 int segmentCount = pathSegments.size(); 4332 if (segmentCount < 3) { 4333 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 4334 "Missing a lookup key", uri)); 4335 } 4336 String lookupKey = pathSegments.get(2); 4337 if (segmentCount == 4) { 4338 // TODO: pull this out into a method and generalize to not require contactId 4339 long contactId = Long.parseLong(pathSegments.get(3)); 4340 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 4341 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 4342 String[] args; 4343 if (selectionArgs == null) { 4344 args = new String[2]; 4345 } else { 4346 args = new String[selectionArgs.length + 2]; 4347 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 4348 } 4349 args[0] = String.valueOf(contactId); 4350 args[1] = Uri.encode(lookupKey); 4351 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); 4352 Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, 4353 groupBy, limit); 4354 if (c.getCount() != 0) { 4355 return c; 4356 } 4357 4358 c.close(); 4359 } 4360 4361 setTablesAndProjectionMapForContacts(qb, uri, projection); 4362 selectionArgs = insertSelectionArg(selectionArgs, 4363 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 4364 qb.appendWhere(Contacts._ID + "=?"); 4365 break; 4366 } 4367 4368 case CONTACTS_AS_VCARD: { 4369 // When reading as vCard always use restricted view 4370 final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); 4371 qb.setTables(mDbHelper.getContactView(true /* require restricted */)); 4372 qb.setProjectionMap(sContactsVCardProjectionMap); 4373 selectionArgs = insertSelectionArg(selectionArgs, 4374 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 4375 qb.appendWhere(Contacts._ID + "=?"); 4376 break; 4377 } 4378 4379 case CONTACTS_AS_MULTI_VCARD: { 4380 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); 4381 String currentDateString = dateFormat.format(new Date()).toString(); 4382 return db.rawQuery( 4383 "SELECT" + 4384 " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," + 4385 " NULL AS " + OpenableColumns.SIZE, 4386 new String[] { currentDateString }); 4387 } 4388 4389 case CONTACTS_FILTER: { 4390 String filterParam = ""; 4391 if (uri.getPathSegments().size() > 2) { 4392 filterParam = uri.getLastPathSegment(); 4393 } 4394 setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam); 4395 break; 4396 } 4397 4398 case CONTACTS_STREQUENT_FILTER: 4399 case CONTACTS_STREQUENT: { 4400 String filterSql = null; 4401 if (match == CONTACTS_STREQUENT_FILTER 4402 && uri.getPathSegments().size() > 3) { 4403 String filterParam = uri.getLastPathSegment(); 4404 StringBuilder sb = new StringBuilder(); 4405 sb.append(Contacts._ID + " IN "); 4406 appendContactFilterAsNestedQuery(sb, filterParam); 4407 filterSql = sb.toString(); 4408 } 4409 4410 setTablesAndProjectionMapForContacts(qb, uri, projection); 4411 4412 String[] starredProjection = null; 4413 String[] frequentProjection = null; 4414 if (projection != null) { 4415 starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN); 4416 frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN); 4417 } 4418 4419 // Build the first query for starred 4420 if (filterSql != null) { 4421 qb.appendWhere(filterSql); 4422 } 4423 qb.setProjectionMap(sStrequentStarredProjectionMap); 4424 final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1", 4425 null, Contacts._ID, null, null, null); 4426 4427 // Build the second query for frequent 4428 qb = new SQLiteQueryBuilder(); 4429 setTablesAndProjectionMapForContacts(qb, uri, projection); 4430 if (filterSql != null) { 4431 qb.appendWhere(filterSql); 4432 } 4433 qb.setProjectionMap(sStrequentFrequentProjectionMap); 4434 final String frequentQuery = qb.buildQuery(frequentProjection, 4435 Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED 4436 + " = 0 OR " + Contacts.STARRED + " IS NULL)", 4437 null, Contacts._ID, null, null, null); 4438 4439 // Put them together 4440 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 4441 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 4442 Cursor c = db.rawQuery(query, null); 4443 if (c != null) { 4444 c.setNotificationUri(getContext().getContentResolver(), 4445 ContactsContract.AUTHORITY_URI); 4446 } 4447 return c; 4448 } 4449 4450 case CONTACTS_GROUP: { 4451 setTablesAndProjectionMapForContacts(qb, uri, projection); 4452 if (uri.getPathSegments().size() > 2) { 4453 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 4454 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4455 } 4456 break; 4457 } 4458 4459 case CONTACTS_DATA: { 4460 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4461 setTablesAndProjectionMapForData(qb, uri, projection, false); 4462 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4463 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 4464 break; 4465 } 4466 4467 case CONTACTS_PHOTO: { 4468 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4469 setTablesAndProjectionMapForData(qb, uri, projection, false); 4470 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 4471 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 4472 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 4473 break; 4474 } 4475 4476 case PHONES: { 4477 setTablesAndProjectionMapForData(qb, uri, projection, false); 4478 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4479 break; 4480 } 4481 4482 case PHONES_ID: { 4483 setTablesAndProjectionMapForData(qb, uri, projection, false); 4484 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4485 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4486 qb.appendWhere(" AND " + Data._ID + "=?"); 4487 break; 4488 } 4489 4490 case PHONES_FILTER: { 4491 setTablesAndProjectionMapForData(qb, uri, projection, true); 4492 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 4493 if (uri.getPathSegments().size() > 2) { 4494 String filterParam = uri.getLastPathSegment(); 4495 StringBuilder sb = new StringBuilder(); 4496 sb.append(" AND ("); 4497 4498 boolean hasCondition = false; 4499 boolean orNeeded = false; 4500 String normalizedName = NameNormalizer.normalize(filterParam); 4501 if (normalizedName.length() > 0) { 4502 sb.append(Data.RAW_CONTACT_ID + " IN "); 4503 appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); 4504 orNeeded = true; 4505 hasCondition = true; 4506 } 4507 4508 if (isPhoneNumber(filterParam)) { 4509 if (orNeeded) { 4510 sb.append(" OR "); 4511 } 4512 String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam); 4513 String reversed = PhoneNumberUtils.getStrippedReversed(number); 4514 sb.append(Data._ID + 4515 " IN (SELECT " + PhoneLookupColumns.DATA_ID 4516 + " FROM " + Tables.PHONE_LOOKUP 4517 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%"); 4518 sb.append(reversed); 4519 sb.append("')"); 4520 hasCondition = true; 4521 } 4522 4523 if (!hasCondition) { 4524 // If it is neither a phone number nor a name, the query should return 4525 // an empty cursor. Let's ensure that. 4526 sb.append("0"); 4527 } 4528 sb.append(")"); 4529 qb.appendWhere(sb); 4530 } 4531 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 4532 if (sortOrder == null) { 4533 sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; 4534 } 4535 break; 4536 } 4537 4538 case EMAILS: { 4539 setTablesAndProjectionMapForData(qb, uri, projection, false); 4540 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 4541 break; 4542 } 4543 4544 case EMAILS_ID: { 4545 setTablesAndProjectionMapForData(qb, uri, projection, false); 4546 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4547 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'" 4548 + " AND " + Data._ID + "=?"); 4549 break; 4550 } 4551 4552 case EMAILS_LOOKUP: { 4553 setTablesAndProjectionMapForData(qb, uri, projection, false); 4554 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 4555 if (uri.getPathSegments().size() > 2) { 4556 String email = uri.getLastPathSegment(); 4557 String address = mDbHelper.extractAddressFromEmailAddress(email); 4558 selectionArgs = insertSelectionArg(selectionArgs, address); 4559 qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)"); 4560 } 4561 break; 4562 } 4563 4564 case EMAILS_FILTER: { 4565 setTablesAndProjectionMapForData(qb, uri, projection, true); 4566 String filterParam = null; 4567 if (uri.getPathSegments().size() > 3) { 4568 filterParam = uri.getLastPathSegment(); 4569 if (TextUtils.isEmpty(filterParam)) { 4570 filterParam = null; 4571 } 4572 } 4573 4574 if (filterParam == null) { 4575 // If the filter is unspecified, return nothing 4576 qb.appendWhere(" AND 0"); 4577 } else { 4578 StringBuilder sb = new StringBuilder(); 4579 sb.append(" AND " + Data._ID + " IN ("); 4580 sb.append( 4581 "SELECT " + Data._ID + 4582 " FROM " + Tables.DATA + 4583 " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail + 4584 " AND " + Data.DATA1 + " LIKE "); 4585 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); 4586 if (!filterParam.contains("@")) { 4587 String normalizedName = NameNormalizer.normalize(filterParam); 4588 if (normalizedName.length() > 0) { 4589 4590 /* 4591 * Using a UNION instead of an "OR" to make SQLite use the right 4592 * indexes. We need it to use the (mimetype,data1) index for the 4593 * email lookup (see above), but not for the name lookup. 4594 * SQLite is not smart enough to use the index on one side of an OR 4595 * but not on the other. Using two separate nested queries 4596 * and a UNION between them does the job. 4597 */ 4598 sb.append( 4599 " UNION SELECT " + Data._ID + 4600 " FROM " + Tables.DATA + 4601 " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail + 4602 " AND " + Data.RAW_CONTACT_ID + " IN "); 4603 appendRawContactsByNormalizedNameFilter(sb, normalizedName, false); 4604 } 4605 } 4606 sb.append(")"); 4607 qb.appendWhere(sb); 4608 } 4609 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 4610 if (sortOrder == null) { 4611 sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID; 4612 } 4613 break; 4614 } 4615 4616 case POSTALS: { 4617 setTablesAndProjectionMapForData(qb, uri, projection, false); 4618 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 4619 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 4620 break; 4621 } 4622 4623 case POSTALS_ID: { 4624 setTablesAndProjectionMapForData(qb, uri, projection, false); 4625 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4626 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" 4627 + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 4628 qb.appendWhere(" AND " + Data._ID + "=?"); 4629 break; 4630 } 4631 4632 case RAW_CONTACTS: { 4633 setTablesAndProjectionMapForRawContacts(qb, uri); 4634 break; 4635 } 4636 4637 case RAW_CONTACTS_ID: { 4638 long rawContactId = ContentUris.parseId(uri); 4639 setTablesAndProjectionMapForRawContacts(qb, uri); 4640 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 4641 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 4642 break; 4643 } 4644 4645 case RAW_CONTACTS_DATA: { 4646 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 4647 setTablesAndProjectionMapForData(qb, uri, projection, false); 4648 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 4649 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?"); 4650 break; 4651 } 4652 4653 case DATA: { 4654 setTablesAndProjectionMapForData(qb, uri, projection, false); 4655 break; 4656 } 4657 4658 case DATA_ID: { 4659 setTablesAndProjectionMapForData(qb, uri, projection, false); 4660 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4661 qb.appendWhere(" AND " + Data._ID + "=?"); 4662 break; 4663 } 4664 4665 case PHONE_LOOKUP: { 4666 4667 if (TextUtils.isEmpty(sortOrder)) { 4668 // Default the sort order to something reasonable so we get consistent 4669 // results when callers don't request an ordering 4670 sortOrder = RawContactsColumns.CONCRETE_ID; 4671 } 4672 4673 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 4674 mDbHelper.buildPhoneLookupAndContactQuery(qb, number); 4675 qb.setProjectionMap(sPhoneLookupProjectionMap); 4676 4677 // Phone lookup cannot be combined with a selection 4678 selection = null; 4679 selectionArgs = null; 4680 break; 4681 } 4682 4683 case GROUPS: { 4684 qb.setTables(mDbHelper.getGroupView()); 4685 qb.setProjectionMap(sGroupsProjectionMap); 4686 appendAccountFromParameter(qb, uri); 4687 break; 4688 } 4689 4690 case GROUPS_ID: { 4691 qb.setTables(mDbHelper.getGroupView()); 4692 qb.setProjectionMap(sGroupsProjectionMap); 4693 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4694 qb.appendWhere(Groups._ID + "=?"); 4695 break; 4696 } 4697 4698 case GROUPS_SUMMARY: { 4699 qb.setTables(mDbHelper.getGroupView() + " AS groups"); 4700 qb.setProjectionMap(sGroupsSummaryProjectionMap); 4701 appendAccountFromParameter(qb, uri); 4702 groupBy = Groups._ID; 4703 break; 4704 } 4705 4706 case AGGREGATION_EXCEPTIONS: { 4707 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 4708 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 4709 break; 4710 } 4711 4712 case AGGREGATION_SUGGESTIONS: { 4713 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 4714 String filter = null; 4715 if (uri.getPathSegments().size() > 3) { 4716 filter = uri.getPathSegments().get(3); 4717 } 4718 final int maxSuggestions; 4719 if (limit != null) { 4720 maxSuggestions = Integer.parseInt(limit); 4721 } else { 4722 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 4723 } 4724 4725 setTablesAndProjectionMapForContacts(qb, uri, projection); 4726 4727 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, 4728 maxSuggestions, filter); 4729 } 4730 4731 case SETTINGS: { 4732 qb.setTables(Tables.SETTINGS); 4733 qb.setProjectionMap(sSettingsProjectionMap); 4734 appendAccountFromParameter(qb, uri); 4735 4736 // When requesting specific columns, this query requires 4737 // late-binding of the GroupMembership MIME-type. 4738 final String groupMembershipMimetypeId = Long.toString(mDbHelper 4739 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 4740 if (projection != null && projection.length != 0 && 4741 mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 4742 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 4743 } 4744 if (projection != null && projection.length != 0 && 4745 mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 4746 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 4747 } 4748 4749 break; 4750 } 4751 4752 case STATUS_UPDATES: { 4753 setTableAndProjectionMapForStatusUpdates(qb, projection); 4754 break; 4755 } 4756 4757 case STATUS_UPDATES_ID: { 4758 setTableAndProjectionMapForStatusUpdates(qb, projection); 4759 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4760 qb.appendWhere(DataColumns.CONCRETE_ID + "=?"); 4761 break; 4762 } 4763 4764 case SEARCH_SUGGESTIONS: { 4765 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); 4766 } 4767 4768 case SEARCH_SHORTCUT: { 4769 String lookupKey = uri.getLastPathSegment(); 4770 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection); 4771 } 4772 4773 case LIVE_FOLDERS_CONTACTS: 4774 qb.setTables(mDbHelper.getContactView()); 4775 qb.setProjectionMap(sLiveFoldersProjectionMap); 4776 break; 4777 4778 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 4779 qb.setTables(mDbHelper.getContactView()); 4780 qb.setProjectionMap(sLiveFoldersProjectionMap); 4781 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 4782 break; 4783 4784 case LIVE_FOLDERS_CONTACTS_FAVORITES: 4785 qb.setTables(mDbHelper.getContactView()); 4786 qb.setProjectionMap(sLiveFoldersProjectionMap); 4787 qb.appendWhere(Contacts.STARRED + "=1"); 4788 break; 4789 4790 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 4791 qb.setTables(mDbHelper.getContactView()); 4792 qb.setProjectionMap(sLiveFoldersProjectionMap); 4793 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 4794 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 4795 break; 4796 4797 case RAW_CONTACT_ENTITIES: { 4798 setTablesAndProjectionMapForRawContactsEntities(qb, uri); 4799 break; 4800 } 4801 4802 case RAW_CONTACT_ENTITY_ID: { 4803 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 4804 setTablesAndProjectionMapForRawContactsEntities(qb, uri); 4805 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 4806 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 4807 break; 4808 } 4809 4810 case PROVIDER_STATUS: { 4811 return queryProviderStatus(uri, projection); 4812 } 4813 4814 default: 4815 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 4816 sortOrder, limit); 4817 } 4818 4819 qb.setStrictProjectionMap(true); 4820 4821 Cursor cursor = 4822 query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 4823 if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) { 4824 cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder); 4825 } 4826 return cursor; 4827 } 4828 4829 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 4830 String selection, String[] selectionArgs, String sortOrder, String groupBy, 4831 String limit) { 4832 if (projection != null && projection.length == 1 4833 && BaseColumns._COUNT.equals(projection[0])) { 4834 qb.setProjectionMap(sCountProjectionMap); 4835 } 4836 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 4837 sortOrder, limit); 4838 if (c != null) { 4839 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 4840 } 4841 return c; 4842 } 4843 4844 /** 4845 * Creates a single-row cursor containing the current status of the provider. 4846 */ 4847 private Cursor queryProviderStatus(Uri uri, String[] projection) { 4848 MatrixCursor cursor = new MatrixCursor(projection); 4849 RowBuilder row = cursor.newRow(); 4850 for (int i = 0; i < projection.length; i++) { 4851 if (ProviderStatus.STATUS.equals(projection[i])) { 4852 row.add(mProviderStatus); 4853 } else if (ProviderStatus.DATA1.equals(projection[i])) { 4854 row.add(mEstimatedStorageRequirement); 4855 } 4856 } 4857 return cursor; 4858 } 4859 4860 4861 private static final class AddressBookIndexQuery { 4862 public static final String LETTER = "letter"; 4863 public static final String TITLE = "title"; 4864 public static final String COUNT = "count"; 4865 4866 public static final String[] COLUMNS = new String[] { 4867 LETTER, TITLE, COUNT 4868 }; 4869 4870 public static final int COLUMN_LETTER = 0; 4871 public static final int COLUMN_TITLE = 1; 4872 public static final int COLUMN_COUNT = 2; 4873 4874 public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME; 4875 } 4876 4877 /** 4878 * Computes counts by the address book index titles and adds the resulting tally 4879 * to the returned cursor as a bundle of extras. 4880 */ 4881 private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db, 4882 SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) { 4883 String sortKey; 4884 4885 // The sort order suffix could be something like "DESC". 4886 // We want to preserve it in the query even though we will change 4887 // the sort column itself. 4888 String sortOrderSuffix = ""; 4889 if (sortOrder != null) { 4890 int spaceIndex = sortOrder.indexOf(' '); 4891 if (spaceIndex != -1) { 4892 sortKey = sortOrder.substring(0, spaceIndex); 4893 sortOrderSuffix = sortOrder.substring(spaceIndex); 4894 } else { 4895 sortKey = sortOrder; 4896 } 4897 } else { 4898 sortKey = Contacts.SORT_KEY_PRIMARY; 4899 } 4900 4901 String locale = getLocale().toString(); 4902 HashMap<String, String> projectionMap = Maps.newHashMap(); 4903 projectionMap.put(AddressBookIndexQuery.LETTER, 4904 "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER); 4905 4906 /** 4907 * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3, 4908 * to map the first letter of the sort key to a character that is traditionally 4909 * used in phonebooks to represent that letter. For example, in Korean it will 4910 * be the first consonant in the letter; for Japanese it will be Hiragana rather 4911 * than Katakana. 4912 */ 4913 projectionMap.put(AddressBookIndexQuery.TITLE, 4914 "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')" 4915 + " AS " + AddressBookIndexQuery.TITLE); 4916 projectionMap.put(AddressBookIndexQuery.COUNT, 4917 "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT); 4918 qb.setProjectionMap(projectionMap); 4919 4920 Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs, 4921 AddressBookIndexQuery.ORDER_BY, null /* having */, 4922 AddressBookIndexQuery.ORDER_BY + sortOrderSuffix); 4923 4924 try { 4925 int groupCount = indexCursor.getCount(); 4926 String titles[] = new String[groupCount]; 4927 int counts[] = new int[groupCount]; 4928 int indexCount = 0; 4929 String currentTitle = null; 4930 4931 // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up 4932 // with multiple entries for the same title. The following code 4933 // collapses those duplicates. 4934 for (int i = 0; i < groupCount; i++) { 4935 indexCursor.moveToNext(); 4936 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE); 4937 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT); 4938 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) { 4939 titles[indexCount] = currentTitle = title; 4940 counts[indexCount] = count; 4941 indexCount++; 4942 } else { 4943 counts[indexCount - 1] += count; 4944 } 4945 } 4946 4947 if (indexCount < groupCount) { 4948 String[] newTitles = new String[indexCount]; 4949 System.arraycopy(titles, 0, newTitles, 0, indexCount); 4950 titles = newTitles; 4951 4952 int[] newCounts = new int[indexCount]; 4953 System.arraycopy(counts, 0, newCounts, 0, indexCount); 4954 counts = newCounts; 4955 } 4956 4957 final Bundle bundle = new Bundle(); 4958 bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles); 4959 bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts); 4960 return new CursorWrapper(cursor) { 4961 4962 @Override 4963 public Bundle getExtras() { 4964 return bundle; 4965 } 4966 }; 4967 } finally { 4968 indexCursor.close(); 4969 } 4970 } 4971 4972 /** 4973 * Returns the contact Id for the contact identified by the lookupKey. 4974 * Robust against changes in the lookup key: if the key has changed, will 4975 * look up the contact by the raw contact IDs or name encoded in the lookup 4976 * key. 4977 */ 4978 public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 4979 ContactLookupKey key = new ContactLookupKey(); 4980 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 4981 4982 long contactId = -1; 4983 if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) { 4984 contactId = lookupContactIdBySourceIds(db, segments); 4985 if (contactId != -1) { 4986 return contactId; 4987 } 4988 } 4989 4990 boolean hasRawContactIds = 4991 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID); 4992 if (hasRawContactIds) { 4993 contactId = lookupContactIdByRawContactIds(db, segments); 4994 if (contactId != -1) { 4995 return contactId; 4996 } 4997 } 4998 4999 if (hasRawContactIds 5000 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) { 5001 contactId = lookupContactIdByDisplayNames(db, segments); 5002 } 5003 5004 return contactId; 5005 } 5006 5007 private interface LookupBySourceIdQuery { 5008 String TABLE = Tables.RAW_CONTACTS; 5009 5010 String COLUMNS[] = { 5011 RawContacts.CONTACT_ID, 5012 RawContacts.ACCOUNT_TYPE, 5013 RawContacts.ACCOUNT_NAME, 5014 RawContacts.SOURCE_ID 5015 }; 5016 5017 int CONTACT_ID = 0; 5018 int ACCOUNT_TYPE = 1; 5019 int ACCOUNT_NAME = 2; 5020 int SOURCE_ID = 3; 5021 } 5022 5023 private long lookupContactIdBySourceIds(SQLiteDatabase db, 5024 ArrayList<LookupKeySegment> segments) { 5025 StringBuilder sb = new StringBuilder(); 5026 sb.append(RawContacts.SOURCE_ID + " IN ("); 5027 for (int i = 0; i < segments.size(); i++) { 5028 LookupKeySegment segment = segments.get(i); 5029 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) { 5030 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 5031 sb.append(","); 5032 } 5033 } 5034 sb.setLength(sb.length() - 1); // Last comma 5035 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5036 5037 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 5038 sb.toString(), null, null, null, null); 5039 try { 5040 while (c.moveToNext()) { 5041 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); 5042 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 5043 int accountHashCode = 5044 ContactLookupKey.getAccountHashCode(accountType, accountName); 5045 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 5046 for (int i = 0; i < segments.size(); i++) { 5047 LookupKeySegment segment = segments.get(i); 5048 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID 5049 && accountHashCode == segment.accountHashCode 5050 && segment.key.equals(sourceId)) { 5051 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 5052 break; 5053 } 5054 } 5055 } 5056 } finally { 5057 c.close(); 5058 } 5059 5060 return getMostReferencedContactId(segments); 5061 } 5062 5063 private interface LookupByRawContactIdQuery { 5064 String TABLE = Tables.RAW_CONTACTS; 5065 5066 String COLUMNS[] = { 5067 RawContacts.CONTACT_ID, 5068 RawContacts.ACCOUNT_TYPE, 5069 RawContacts.ACCOUNT_NAME, 5070 RawContacts._ID, 5071 }; 5072 5073 int CONTACT_ID = 0; 5074 int ACCOUNT_TYPE = 1; 5075 int ACCOUNT_NAME = 2; 5076 int ID = 3; 5077 } 5078 5079 private long lookupContactIdByRawContactIds(SQLiteDatabase db, 5080 ArrayList<LookupKeySegment> segments) { 5081 StringBuilder sb = new StringBuilder(); 5082 sb.append(RawContacts._ID + " IN ("); 5083 for (int i = 0; i < segments.size(); i++) { 5084 LookupKeySegment segment = segments.get(i); 5085 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 5086 sb.append(segment.rawContactId); 5087 sb.append(","); 5088 } 5089 } 5090 sb.setLength(sb.length() - 1); // Last comma 5091 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5092 5093 Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS, 5094 sb.toString(), null, null, null, null); 5095 try { 5096 while (c.moveToNext()) { 5097 String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE); 5098 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME); 5099 int accountHashCode = 5100 ContactLookupKey.getAccountHashCode(accountType, accountName); 5101 String rawContactId = c.getString(LookupByRawContactIdQuery.ID); 5102 for (int i = 0; i < segments.size(); i++) { 5103 LookupKeySegment segment = segments.get(i); 5104 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID 5105 && accountHashCode == segment.accountHashCode 5106 && segment.rawContactId.equals(rawContactId)) { 5107 segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID); 5108 break; 5109 } 5110 } 5111 } 5112 } finally { 5113 c.close(); 5114 } 5115 5116 return getMostReferencedContactId(segments); 5117 } 5118 5119 private interface LookupByDisplayNameQuery { 5120 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 5121 5122 String COLUMNS[] = { 5123 RawContacts.CONTACT_ID, 5124 RawContacts.ACCOUNT_TYPE, 5125 RawContacts.ACCOUNT_NAME, 5126 NameLookupColumns.NORMALIZED_NAME 5127 }; 5128 5129 int CONTACT_ID = 0; 5130 int ACCOUNT_TYPE = 1; 5131 int ACCOUNT_NAME = 2; 5132 int NORMALIZED_NAME = 3; 5133 } 5134 5135 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 5136 ArrayList<LookupKeySegment> segments) { 5137 StringBuilder sb = new StringBuilder(); 5138 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 5139 for (int i = 0; i < segments.size(); i++) { 5140 LookupKeySegment segment = segments.get(i); 5141 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 5142 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 5143 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 5144 sb.append(","); 5145 } 5146 } 5147 sb.setLength(sb.length() - 1); // Last comma 5148 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 5149 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 5150 5151 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 5152 sb.toString(), null, null, null, null); 5153 try { 5154 while (c.moveToNext()) { 5155 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); 5156 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 5157 int accountHashCode = 5158 ContactLookupKey.getAccountHashCode(accountType, accountName); 5159 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 5160 for (int i = 0; i < segments.size(); i++) { 5161 LookupKeySegment segment = segments.get(i); 5162 if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 5163 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) 5164 && accountHashCode == segment.accountHashCode 5165 && segment.key.equals(name)) { 5166 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 5167 break; 5168 } 5169 } 5170 } 5171 } finally { 5172 c.close(); 5173 } 5174 5175 return getMostReferencedContactId(segments); 5176 } 5177 5178 private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) { 5179 for (int i = 0; i < segments.size(); i++) { 5180 LookupKeySegment segment = segments.get(i); 5181 if (segment.lookupType == lookupType) { 5182 return true; 5183 } 5184 } 5185 5186 return false; 5187 } 5188 5189 public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) { 5190 mContactAggregator.updateLookupKeyForRawContact(db, rawContactId); 5191 } 5192 5193 /** 5194 * Returns the contact ID that is mentioned the highest number of times. 5195 */ 5196 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 5197 Collections.sort(segments); 5198 5199 long bestContactId = -1; 5200 int bestRefCount = 0; 5201 5202 long contactId = -1; 5203 int count = 0; 5204 5205 int segmentCount = segments.size(); 5206 for (int i = 0; i < segmentCount; i++) { 5207 LookupKeySegment segment = segments.get(i); 5208 if (segment.contactId != -1) { 5209 if (segment.contactId == contactId) { 5210 count++; 5211 } else { 5212 if (count > bestRefCount) { 5213 bestContactId = contactId; 5214 bestRefCount = count; 5215 } 5216 contactId = segment.contactId; 5217 count = 1; 5218 } 5219 } 5220 } 5221 if (count > bestRefCount) { 5222 return contactId; 5223 } else { 5224 return bestContactId; 5225 } 5226 } 5227 5228 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 5229 String[] projection) { 5230 StringBuilder sb = new StringBuilder(); 5231 appendContactsTables(sb, uri, projection); 5232 qb.setTables(sb.toString()); 5233 qb.setProjectionMap(sContactsProjectionMap); 5234 } 5235 5236 /** 5237 * Finds name lookup records matching the supplied filter, picks one arbitrary match per 5238 * contact and joins that with other contacts tables. 5239 */ 5240 private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri, 5241 String[] projection, String filter) { 5242 5243 StringBuilder sb = new StringBuilder(); 5244 appendContactsTables(sb, uri, projection); 5245 5246 sb.append(" JOIN (SELECT " + 5247 RawContacts.CONTACT_ID + " AS snippet_contact_id"); 5248 5249 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA_ID)) { 5250 sb.append(", " + DataColumns.CONCRETE_ID + " AS " 5251 + SearchSnippetColumns.SNIPPET_DATA_ID); 5252 } 5253 5254 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA1)) { 5255 sb.append(", " + Data.DATA1 + " AS " + SearchSnippetColumns.SNIPPET_DATA1); 5256 } 5257 5258 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA2)) { 5259 sb.append(", " + Data.DATA2 + " AS " + SearchSnippetColumns.SNIPPET_DATA2); 5260 } 5261 5262 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA3)) { 5263 sb.append(", " + Data.DATA3 + " AS " + SearchSnippetColumns.SNIPPET_DATA3); 5264 } 5265 5266 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA4)) { 5267 sb.append(", " + Data.DATA4 + " AS " + SearchSnippetColumns.SNIPPET_DATA4); 5268 } 5269 5270 if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_MIMETYPE)) { 5271 sb.append(", (" + 5272 "SELECT " + MimetypesColumns.MIMETYPE + 5273 " FROM " + Tables.MIMETYPES + 5274 " WHERE " + MimetypesColumns._ID + "=" + DataColumns.MIMETYPE_ID + 5275 ") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE); 5276 } 5277 5278 sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS + " WHERE "); 5279 5280 if (!TextUtils.isEmpty(filter)) { 5281 sb.append(DataColumns.CONCRETE_ID + " IN ("); 5282 5283 // Construct a query that gives us exactly one data _id per matching contact. 5284 // MIN stands in for ANY in this context. 5285 sb.append( 5286 "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" + 5287 " FROM " + Tables.NAME_LOOKUP + 5288 " JOIN " + Tables.RAW_CONTACTS + 5289 " ON (" + RawContactsColumns.CONCRETE_ID 5290 + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" + 5291 " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '"); 5292 sb.append(NameNormalizer.normalize(filter)); 5293 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + 5294 " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" + 5295 " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID + 5296 ")"); 5297 } else { 5298 sb.append("0"); // Empty filter - return an empty set 5299 } 5300 5301 sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)"); 5302 5303 qb.setTables(sb.toString()); 5304 qb.setProjectionMap(sContactsProjectionWithSnippetMap); 5305 } 5306 5307 private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) { 5308 boolean excludeRestrictedData = false; 5309 String requestingPackage = getQueryParameter(uri, 5310 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 5311 if (requestingPackage != null) { 5312 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 5313 } 5314 sb.append(mDbHelper.getContactView(excludeRestrictedData)); 5315 if (mDbHelper.isInProjection(projection, 5316 Contacts.CONTACT_PRESENCE)) { 5317 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 5318 " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")"); 5319 } 5320 if (mDbHelper.isInProjection(projection, 5321 Contacts.CONTACT_STATUS, 5322 Contacts.CONTACT_STATUS_RES_PACKAGE, 5323 Contacts.CONTACT_STATUS_ICON, 5324 Contacts.CONTACT_STATUS_LABEL, 5325 Contacts.CONTACT_STATUS_TIMESTAMP)) { 5326 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 5327 + ContactsStatusUpdatesColumns.ALIAS + 5328 " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" 5329 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 5330 } 5331 } 5332 5333 private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) { 5334 StringBuilder sb = new StringBuilder(); 5335 boolean excludeRestrictedData = false; 5336 String requestingPackage = getQueryParameter(uri, 5337 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 5338 if (requestingPackage != null) { 5339 excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage); 5340 } 5341 sb.append(mDbHelper.getRawContactView(excludeRestrictedData)); 5342 qb.setTables(sb.toString()); 5343 qb.setProjectionMap(sRawContactsProjectionMap); 5344 appendAccountFromParameter(qb, uri); 5345 } 5346 5347 private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) { 5348 // Note: currently, "export only" equals to "restricted", but may not in the future. 5349 boolean excludeRestrictedData = readBooleanQueryParameter(uri, 5350 Data.FOR_EXPORT_ONLY, false); 5351 5352 String requestingPackage = getQueryParameter(uri, 5353 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 5354 if (requestingPackage != null) { 5355 excludeRestrictedData = excludeRestrictedData 5356 || !mDbHelper.hasAccessToRestrictedData(requestingPackage); 5357 } 5358 qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData)); 5359 qb.setProjectionMap(sRawContactsEntityProjectionMap); 5360 appendAccountFromParameter(qb, uri); 5361 } 5362 5363 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 5364 String[] projection, boolean distinct) { 5365 StringBuilder sb = new StringBuilder(); 5366 // Note: currently, "export only" equals to "restricted", but may not in the future. 5367 boolean excludeRestrictedData = readBooleanQueryParameter(uri, 5368 Data.FOR_EXPORT_ONLY, false); 5369 5370 String requestingPackage = getQueryParameter(uri, 5371 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY); 5372 if (requestingPackage != null) { 5373 excludeRestrictedData = excludeRestrictedData 5374 || !mDbHelper.hasAccessToRestrictedData(requestingPackage); 5375 } 5376 5377 sb.append(mDbHelper.getDataView(excludeRestrictedData)); 5378 sb.append(" data"); 5379 5380 // Include aggregated presence when requested 5381 if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) { 5382 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 5383 " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "=" 5384 + RawContacts.CONTACT_ID + ")"); 5385 } 5386 5387 // Include aggregated status updates when requested 5388 if (mDbHelper.isInProjection(projection, 5389 Data.CONTACT_STATUS, 5390 Data.CONTACT_STATUS_RES_PACKAGE, 5391 Data.CONTACT_STATUS_ICON, 5392 Data.CONTACT_STATUS_LABEL, 5393 Data.CONTACT_STATUS_TIMESTAMP)) { 5394 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 5395 + ContactsStatusUpdatesColumns.ALIAS + 5396 " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" 5397 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 5398 } 5399 5400 // Include individual presence when requested 5401 if (mDbHelper.isInProjection(projection, Data.PRESENCE)) { 5402 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 5403 " ON (" + StatusUpdates.DATA_ID + "=" 5404 + DataColumns.CONCRETE_ID + ")"); 5405 } 5406 5407 // Include individual status updates when requested 5408 if (mDbHelper.isInProjection(projection, 5409 Data.STATUS, 5410 Data.STATUS_RES_PACKAGE, 5411 Data.STATUS_ICON, 5412 Data.STATUS_LABEL, 5413 Data.STATUS_TIMESTAMP)) { 5414 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 5415 " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" 5416 + DataColumns.CONCRETE_ID + ")"); 5417 } 5418 5419 qb.setTables(sb.toString()); 5420 qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap); 5421 appendAccountFromParameter(qb, uri); 5422 } 5423 5424 private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, 5425 String[] projection) { 5426 StringBuilder sb = new StringBuilder(); 5427 sb.append(mDbHelper.getDataView()); 5428 sb.append(" data"); 5429 5430 if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) { 5431 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 5432 " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID 5433 + "=" + DataColumns.CONCRETE_ID + ")"); 5434 } 5435 5436 if (mDbHelper.isInProjection(projection, 5437 StatusUpdates.STATUS, 5438 StatusUpdates.STATUS_RES_PACKAGE, 5439 StatusUpdates.STATUS_ICON, 5440 StatusUpdates.STATUS_LABEL, 5441 StatusUpdates.STATUS_TIMESTAMP)) { 5442 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 5443 " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID 5444 + "=" + DataColumns.CONCRETE_ID + ")"); 5445 } 5446 qb.setTables(sb.toString()); 5447 qb.setProjectionMap(sStatusUpdatesProjectionMap); 5448 } 5449 5450 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 5451 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 5452 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 5453 5454 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 5455 if (partialUri) { 5456 // Throw when either account is incomplete 5457 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 5458 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 5459 } 5460 5461 // Accounts are valid by only checking one parameter, since we've 5462 // already ruled out partial accounts. 5463 final boolean validAccount = !TextUtils.isEmpty(accountName); 5464 if (validAccount) { 5465 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 5466 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 5467 + RawContacts.ACCOUNT_TYPE + "=" 5468 + DatabaseUtils.sqlEscapeString(accountType)); 5469 } else { 5470 qb.appendWhere("1"); 5471 } 5472 } 5473 5474 private String appendAccountToSelection(Uri uri, String selection) { 5475 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 5476 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 5477 5478 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 5479 if (partialUri) { 5480 // Throw when either account is incomplete 5481 throw new IllegalArgumentException(mDbHelper.exceptionMessage( 5482 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 5483 } 5484 5485 // Accounts are valid by only checking one parameter, since we've 5486 // already ruled out partial accounts. 5487 final boolean validAccount = !TextUtils.isEmpty(accountName); 5488 if (validAccount) { 5489 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 5490 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 5491 + RawContacts.ACCOUNT_TYPE + "=" 5492 + DatabaseUtils.sqlEscapeString(accountType)); 5493 if (!TextUtils.isEmpty(selection)) { 5494 selectionSb.append(" AND ("); 5495 selectionSb.append(selection); 5496 selectionSb.append(')'); 5497 } 5498 return selectionSb.toString(); 5499 } else { 5500 return selection; 5501 } 5502 } 5503 5504 /** 5505 * Gets the value of the "limit" URI query parameter. 5506 * 5507 * @return A string containing a non-negative integer, or <code>null</code> if 5508 * the parameter is not set, or is set to an invalid value. 5509 */ 5510 private String getLimit(Uri uri) { 5511 String limitParam = getQueryParameter(uri, "limit"); 5512 if (limitParam == null) { 5513 return null; 5514 } 5515 // make sure that the limit is a non-negative integer 5516 try { 5517 int l = Integer.parseInt(limitParam); 5518 if (l < 0) { 5519 Log.w(TAG, "Invalid limit parameter: " + limitParam); 5520 return null; 5521 } 5522 return String.valueOf(l); 5523 } catch (NumberFormatException ex) { 5524 Log.w(TAG, "Invalid limit parameter: " + limitParam); 5525 return null; 5526 } 5527 } 5528 5529 /** 5530 * Returns true if all the characters are meaningful as digits 5531 * in a phone number -- letters, digits, and a few punctuation marks. 5532 */ 5533 private boolean isPhoneNumber(CharSequence cons) { 5534 int len = cons.length(); 5535 5536 for (int i = 0; i < len; i++) { 5537 char c = cons.charAt(i); 5538 5539 if ((c >= '0') && (c <= '9')) { 5540 continue; 5541 } 5542 if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+') 5543 || (c == '#') || (c == '*')) { 5544 continue; 5545 } 5546 if ((c >= 'A') && (c <= 'Z')) { 5547 continue; 5548 } 5549 if ((c >= 'a') && (c <= 'z')) { 5550 continue; 5551 } 5552 5553 return false; 5554 } 5555 5556 return true; 5557 } 5558 5559 String getContactsRestrictions() { 5560 if (mDbHelper.hasAccessToRestrictedData()) { 5561 return "1"; 5562 } else { 5563 return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0"; 5564 } 5565 } 5566 5567 public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { 5568 if (mDbHelper.hasAccessToRestrictedData()) { 5569 return "1"; 5570 } else { 5571 return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS 5572 + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; 5573 } 5574 } 5575 5576 @Override 5577 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 5578 int match = sUriMatcher.match(uri); 5579 switch (match) { 5580 case CONTACTS_PHOTO: { 5581 return openPhotoAssetFile(uri, mode, 5582 Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?", 5583 new String[]{uri.getPathSegments().get(1)}); 5584 } 5585 5586 case DATA_ID: { 5587 return openPhotoAssetFile(uri, mode, 5588 Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'", 5589 new String[]{uri.getPathSegments().get(1)}); 5590 } 5591 5592 case CONTACTS_AS_VCARD: { 5593 final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); 5594 mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(mDb, lookupKey)); 5595 final String selection = Contacts._ID + "=?"; 5596 5597 // When opening a contact as file, we pass back contents as a 5598 // vCard-encoded stream. We build into a local buffer first, 5599 // then pipe into MemoryFile once the exact size is known. 5600 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 5601 outputRawContactsAsVCard(localStream, selection, mSelectionArgs1); 5602 return buildAssetFileDescriptor(localStream); 5603 } 5604 5605 case CONTACTS_AS_MULTI_VCARD: { 5606 final String lookupKeys = uri.getPathSegments().get(2); 5607 final String[] loopupKeyList = lookupKeys.split(":"); 5608 final StringBuilder inBuilder = new StringBuilder(); 5609 int index = 0; 5610 // SQLite has limits on how many parameters can be used 5611 // so the IDs are concatenated to a query string here instead 5612 for (String lookupKey : loopupKeyList) { 5613 if (index == 0) { 5614 inBuilder.append("("); 5615 } else { 5616 inBuilder.append(","); 5617 } 5618 inBuilder.append(lookupContactIdByLookupKey(mDb, lookupKey)); 5619 index++; 5620 } 5621 inBuilder.append(')'); 5622 final String selection = Contacts._ID + " IN " + inBuilder.toString(); 5623 5624 // When opening a contact as file, we pass back contents as a 5625 // vCard-encoded stream. We build into a local buffer first, 5626 // then pipe into MemoryFile once the exact size is known. 5627 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 5628 outputRawContactsAsVCard(localStream, selection, null); 5629 return buildAssetFileDescriptor(localStream); 5630 } 5631 5632 default: 5633 throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist", 5634 uri)); 5635 } 5636 } 5637 5638 private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection, 5639 String[] selectionArgs) 5640 throws FileNotFoundException { 5641 if (!"r".equals(mode)) { 5642 throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode 5643 + " not supported.", uri)); 5644 } 5645 5646 String sql = 5647 "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() + 5648 " WHERE " + selection; 5649 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 5650 return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, 5651 selectionArgs); 5652 } 5653 5654 private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; 5655 5656 /** 5657 * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the 5658 * contents of the given {@link ByteArrayOutputStream}. 5659 */ 5660 private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { 5661 AssetFileDescriptor fd = null; 5662 try { 5663 stream.flush(); 5664 5665 final byte[] byteData = stream.toByteArray(); 5666 final int size = byteData.length; 5667 5668 final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size); 5669 memoryFile.writeBytes(byteData, 0, 0, size); 5670 memoryFile.deactivate(); 5671 5672 fd = AssetFileDescriptor.fromMemoryFile(memoryFile); 5673 } catch (IOException e) { 5674 Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString()); 5675 } 5676 return fd; 5677 } 5678 5679 /** 5680 * Output {@link RawContacts} matching the requested selection in the vCard 5681 * format to the given {@link OutputStream}. This method returns silently if 5682 * any errors encountered. 5683 */ 5684 private void outputRawContactsAsVCard(OutputStream stream, String selection, 5685 String[] selectionArgs) { 5686 final Context context = this.getContext(); 5687 final VCardComposer composer = 5688 new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false); 5689 composer.addHandler(composer.new HandlerForOutputStream(stream)); 5690 5691 // No extra checks since composer always uses restricted views 5692 if (!composer.init(selection, selectionArgs)) { 5693 Log.w(TAG, "Failed to init VCardComposer"); 5694 return; 5695 } 5696 5697 while (!composer.isAfterLast()) { 5698 if (!composer.createOneEntry()) { 5699 Log.w(TAG, "Failed to output a contact."); 5700 } 5701 } 5702 composer.terminate(); 5703 } 5704 5705 @Override 5706 public String getType(Uri uri) { 5707 final int match = sUriMatcher.match(uri); 5708 switch (match) { 5709 case CONTACTS: 5710 return Contacts.CONTENT_TYPE; 5711 case CONTACTS_LOOKUP: 5712 case CONTACTS_ID: 5713 case CONTACTS_LOOKUP_ID: 5714 return Contacts.CONTENT_ITEM_TYPE; 5715 case CONTACTS_AS_VCARD: 5716 case CONTACTS_AS_MULTI_VCARD: 5717 return Contacts.CONTENT_VCARD_TYPE; 5718 case RAW_CONTACTS: 5719 return RawContacts.CONTENT_TYPE; 5720 case RAW_CONTACTS_ID: 5721 return RawContacts.CONTENT_ITEM_TYPE; 5722 case DATA_ID: 5723 return mDbHelper.getDataMimeType(ContentUris.parseId(uri)); 5724 case PHONES: 5725 return Phone.CONTENT_TYPE; 5726 case PHONES_ID: 5727 return Phone.CONTENT_ITEM_TYPE; 5728 case PHONE_LOOKUP: 5729 return PhoneLookup.CONTENT_TYPE; 5730 case EMAILS: 5731 return Email.CONTENT_TYPE; 5732 case EMAILS_ID: 5733 return Email.CONTENT_ITEM_TYPE; 5734 case POSTALS: 5735 return StructuredPostal.CONTENT_TYPE; 5736 case POSTALS_ID: 5737 return StructuredPostal.CONTENT_ITEM_TYPE; 5738 case AGGREGATION_EXCEPTIONS: 5739 return AggregationExceptions.CONTENT_TYPE; 5740 case AGGREGATION_EXCEPTION_ID: 5741 return AggregationExceptions.CONTENT_ITEM_TYPE; 5742 case SETTINGS: 5743 return Settings.CONTENT_TYPE; 5744 case AGGREGATION_SUGGESTIONS: 5745 return Contacts.CONTENT_TYPE; 5746 case SEARCH_SUGGESTIONS: 5747 return SearchManager.SUGGEST_MIME_TYPE; 5748 case SEARCH_SHORTCUT: 5749 return SearchManager.SHORTCUT_MIME_TYPE; 5750 5751 default: 5752 return mLegacyApiSupport.getType(uri); 5753 } 5754 } 5755 5756 private void setDisplayName(long rawContactId, int displayNameSource, 5757 String displayNamePrimary, String displayNameAlternative, String phoneticName, 5758 int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) { 5759 mRawContactDisplayNameUpdate.bindLong(1, displayNameSource); 5760 bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary); 5761 bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative); 5762 bindString(mRawContactDisplayNameUpdate, 4, phoneticName); 5763 mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle); 5764 bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary); 5765 bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative); 5766 mRawContactDisplayNameUpdate.bindLong(8, rawContactId); 5767 mRawContactDisplayNameUpdate.execute(); 5768 } 5769 5770 /** 5771 * Sets the {@link RawContacts#DIRTY} for the specified raw contact. 5772 */ 5773 private void setRawContactDirty(long rawContactId) { 5774 mDirtyRawContacts.add(rawContactId); 5775 } 5776 5777 /* 5778 * Sets the given dataId record in the "data" table to primary, and resets all data records of 5779 * the same mimetype and under the same contact to not be primary. 5780 * 5781 * @param dataId the id of the data record to be set to primary. 5782 */ 5783 private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) { 5784 mSetPrimaryStatement.bindLong(1, dataId); 5785 mSetPrimaryStatement.bindLong(2, mimeTypeId); 5786 mSetPrimaryStatement.bindLong(3, rawContactId); 5787 mSetPrimaryStatement.execute(); 5788 } 5789 5790 /* 5791 * Sets the given dataId record in the "data" table to "super primary", and resets all data 5792 * records of the same mimetype and under the same aggregate to not be "super primary". 5793 * 5794 * @param dataId the id of the data record to be set to primary. 5795 */ 5796 private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) { 5797 mSetSuperPrimaryStatement.bindLong(1, dataId); 5798 mSetSuperPrimaryStatement.bindLong(2, mimeTypeId); 5799 mSetSuperPrimaryStatement.bindLong(3, rawContactId); 5800 mSetSuperPrimaryStatement.execute(); 5801 } 5802 5803 public String insertNameLookupForEmail(long rawContactId, long dataId, String email) { 5804 if (TextUtils.isEmpty(email)) { 5805 return null; 5806 } 5807 5808 String address = mDbHelper.extractHandleFromEmailAddress(email); 5809 if (address == null) { 5810 return null; 5811 } 5812 5813 insertNameLookup(rawContactId, dataId, 5814 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address)); 5815 return address; 5816 } 5817 5818 /** 5819 * Normalizes the nickname and inserts it in the name lookup table. 5820 */ 5821 public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) { 5822 if (TextUtils.isEmpty(nickname)) { 5823 return; 5824 } 5825 5826 insertNameLookup(rawContactId, dataId, 5827 NameLookupType.NICKNAME, NameNormalizer.normalize(nickname)); 5828 } 5829 5830 public void insertNameLookupForOrganization(long rawContactId, long dataId, String company, 5831 String title) { 5832 if (!TextUtils.isEmpty(company)) { 5833 insertNameLookup(rawContactId, dataId, 5834 NameLookupType.ORGANIZATION, NameNormalizer.normalize(company)); 5835 } 5836 if (!TextUtils.isEmpty(title)) { 5837 insertNameLookup(rawContactId, dataId, 5838 NameLookupType.ORGANIZATION, NameNormalizer.normalize(title)); 5839 } 5840 } 5841 5842 public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name, 5843 int fullNameStyle) { 5844 mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle); 5845 } 5846 5847 private class StructuredNameLookupBuilder extends NameLookupBuilder { 5848 5849 public StructuredNameLookupBuilder(NameSplitter splitter) { 5850 super(splitter); 5851 } 5852 5853 @Override 5854 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 5855 String name) { 5856 ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name); 5857 } 5858 5859 @Override 5860 protected String[] getCommonNicknameClusters(String normalizedName) { 5861 return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); 5862 } 5863 } 5864 5865 public void insertNameLookupForPhoneticName(long rawContactId, long dataId, 5866 ContentValues values) { 5867 if (values.containsKey(StructuredName.PHONETIC_FAMILY_NAME) 5868 || values.containsKey(StructuredName.PHONETIC_GIVEN_NAME) 5869 || values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME)) { 5870 insertNameLookupForPhoneticName(rawContactId, dataId, 5871 values.getAsString(StructuredName.PHONETIC_FAMILY_NAME), 5872 values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME), 5873 values.getAsString(StructuredName.PHONETIC_GIVEN_NAME)); 5874 } 5875 } 5876 5877 public void insertNameLookupForPhoneticName(long rawContactId, long dataId, String familyName, 5878 String middleName, String givenName) { 5879 mSb.setLength(0); 5880 if (familyName != null) { 5881 mSb.append(familyName.trim()); 5882 } 5883 if (middleName != null) { 5884 mSb.append(middleName.trim()); 5885 } 5886 if (givenName != null) { 5887 mSb.append(givenName.trim()); 5888 } 5889 5890 if (mSb.length() > 0) { 5891 insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY, 5892 NameNormalizer.normalize(mSb.toString())); 5893 } 5894 5895 if (givenName != null) { 5896 // We want the phonetic given name to be used for search, but not for aggregation, 5897 // which is why we are using NAME_SHORTHAND rather than NAME_COLLATION_KEY 5898 insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND, 5899 NameNormalizer.normalize(givenName.trim())); 5900 } 5901 } 5902 5903 /** 5904 * Inserts a record in the {@link Tables#NAME_LOOKUP} table. 5905 */ 5906 public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) { 5907 mNameLookupInsert.bindLong(1, rawContactId); 5908 mNameLookupInsert.bindLong(2, dataId); 5909 mNameLookupInsert.bindLong(3, lookupType); 5910 bindString(mNameLookupInsert, 4, name); 5911 mNameLookupInsert.executeInsert(); 5912 } 5913 5914 /** 5915 * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element. 5916 */ 5917 public void deleteNameLookup(long dataId) { 5918 mNameLookupDelete.bindLong(1, dataId); 5919 mNameLookupDelete.execute(); 5920 } 5921 5922 public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 5923 sb.append("(" + 5924 "SELECT DISTINCT " + RawContacts.CONTACT_ID + 5925 " FROM " + Tables.RAW_CONTACTS + 5926 " JOIN " + Tables.NAME_LOOKUP + 5927 " ON(" + RawContactsColumns.CONCRETE_ID + "=" 5928 + NameLookupColumns.RAW_CONTACT_ID + ")" + 5929 " WHERE normalized_name GLOB '"); 5930 sb.append(NameNormalizer.normalize(filterParam)); 5931 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + 5932 " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))"); 5933 } 5934 5935 public String getRawContactsByFilterAsNestedQuery(String filterParam) { 5936 StringBuilder sb = new StringBuilder(); 5937 appendRawContactsByFilterAsNestedQuery(sb, filterParam); 5938 return sb.toString(); 5939 } 5940 5941 public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) { 5942 appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true); 5943 } 5944 5945 private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, 5946 boolean allowEmailMatch) { 5947 sb.append("(" + 5948 "SELECT " + NameLookupColumns.RAW_CONTACT_ID + 5949 " FROM " + Tables.NAME_LOOKUP + 5950 " WHERE " + NameLookupColumns.NORMALIZED_NAME + 5951 " GLOB '"); 5952 sb.append(normalizedName); 5953 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN (" 5954 + NameLookupType.NAME_COLLATION_KEY + "," 5955 + NameLookupType.NICKNAME + "," 5956 + NameLookupType.NAME_SHORTHAND + "," 5957 + NameLookupType.ORGANIZATION + "," 5958 + NameLookupType.NAME_CONSONANTS); 5959 if (allowEmailMatch) { 5960 sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME); 5961 } 5962 sb.append("))"); 5963 } 5964 5965 /** 5966 * Inserts an argument at the beginning of the selection arg list. 5967 */ 5968 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 5969 if (selectionArgs == null) { 5970 return new String[] {arg}; 5971 } else { 5972 int newLength = selectionArgs.length + 1; 5973 String[] newSelectionArgs = new String[newLength]; 5974 newSelectionArgs[0] = arg; 5975 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 5976 return newSelectionArgs; 5977 } 5978 } 5979 5980 private String[] appendProjectionArg(String[] projection, String arg) { 5981 if (projection == null) { 5982 return null; 5983 } 5984 final int length = projection.length; 5985 String[] newProjection = new String[length + 1]; 5986 System.arraycopy(projection, 0, newProjection, 0, length); 5987 newProjection[length] = arg; 5988 return newProjection; 5989 } 5990 5991 protected Account getDefaultAccount() { 5992 AccountManager accountManager = AccountManager.get(getContext()); 5993 try { 5994 Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE, 5995 new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult(); 5996 if (accounts != null && accounts.length > 0) { 5997 return accounts[0]; 5998 } 5999 } catch (Throwable e) { 6000 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 6001 } 6002 return null; 6003 } 6004 6005 /** 6006 * Returns true if the specified account type is writable. 6007 */ 6008 protected boolean isWritableAccount(String accountType) { 6009 if (accountType == null) { 6010 return true; 6011 } 6012 6013 Boolean writable = mAccountWritability.get(accountType); 6014 if (writable != null) { 6015 return writable; 6016 } 6017 6018 IContentService contentService = ContentResolver.getContentService(); 6019 try { 6020 for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) { 6021 if (ContactsContract.AUTHORITY.equals(sync.authority) && 6022 accountType.equals(sync.accountType)) { 6023 writable = sync.supportsUploading(); 6024 break; 6025 } 6026 } 6027 } catch (RemoteException e) { 6028 Log.e(TAG, "Could not acquire sync adapter types"); 6029 } 6030 6031 if (writable == null) { 6032 writable = false; 6033 } 6034 6035 mAccountWritability.put(accountType, writable); 6036 return writable; 6037 } 6038 6039 /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter, 6040 boolean defaultValue) { 6041 6042 // Manually parse the query, which is much faster than calling uri.getQueryParameter 6043 String query = uri.getEncodedQuery(); 6044 if (query == null) { 6045 return defaultValue; 6046 } 6047 6048 int index = query.indexOf(parameter); 6049 if (index == -1) { 6050 return defaultValue; 6051 } 6052 6053 index += parameter.length(); 6054 6055 return !matchQueryParameter(query, index, "=0", false) 6056 && !matchQueryParameter(query, index, "=false", true); 6057 } 6058 6059 private static boolean matchQueryParameter(String query, int index, String value, 6060 boolean ignoreCase) { 6061 int length = value.length(); 6062 return query.regionMatches(ignoreCase, index, value, 0, length) 6063 && (query.length() == index + length || query.charAt(index + length) == '&'); 6064 } 6065 6066 /** 6067 * A fast re-implementation of {@link Uri#getQueryParameter} 6068 */ 6069 /* package */ static String getQueryParameter(Uri uri, String parameter) { 6070 String query = uri.getEncodedQuery(); 6071 if (query == null) { 6072 return null; 6073 } 6074 6075 int queryLength = query.length(); 6076 int parameterLength = parameter.length(); 6077 6078 String value; 6079 int index = 0; 6080 while (true) { 6081 index = query.indexOf(parameter, index); 6082 if (index == -1) { 6083 return null; 6084 } 6085 6086 index += parameterLength; 6087 6088 if (queryLength == index) { 6089 return null; 6090 } 6091 6092 if (query.charAt(index) == '=') { 6093 index++; 6094 break; 6095 } 6096 } 6097 6098 int ampIndex = query.indexOf('&', index); 6099 if (ampIndex == -1) { 6100 value = query.substring(index); 6101 } else { 6102 value = query.substring(index, ampIndex); 6103 } 6104 6105 return Uri.decode(value); 6106 } 6107 6108 private void bindString(SQLiteStatement stmt, int index, String value) { 6109 if (value == null) { 6110 stmt.bindNull(index); 6111 } else { 6112 stmt.bindString(index, value); 6113 } 6114 } 6115 6116 private void bindLong(SQLiteStatement stmt, int index, Number value) { 6117 if (value == null) { 6118 stmt.bindNull(index); 6119 } else { 6120 stmt.bindLong(index, value.longValue()); 6121 } 6122 } 6123} 6124