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