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