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