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