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 android.accounts.Account; 20import android.accounts.AccountManager; 21import android.accounts.OnAccountsUpdateListener; 22import android.app.AppOpsManager; 23import android.app.SearchManager; 24import android.content.ContentProviderOperation; 25import android.content.ContentProviderResult; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.IContentService; 31import android.content.OperationApplicationException; 32import android.content.SharedPreferences; 33import android.content.SyncAdapterType; 34import android.content.UriMatcher; 35import android.content.pm.PackageManager; 36import android.content.pm.PackageManager.NameNotFoundException; 37import android.content.pm.ProviderInfo; 38import android.content.res.AssetFileDescriptor; 39import android.content.res.Resources; 40import android.content.res.Resources.NotFoundException; 41import android.database.AbstractCursor; 42import android.database.Cursor; 43import android.database.DatabaseUtils; 44import android.database.MatrixCursor; 45import android.database.MatrixCursor.RowBuilder; 46import android.database.sqlite.SQLiteDatabase; 47import android.database.sqlite.SQLiteDoneException; 48import android.database.sqlite.SQLiteQueryBuilder; 49import android.graphics.Bitmap; 50import android.graphics.BitmapFactory; 51import android.net.Uri; 52import android.net.Uri.Builder; 53import android.os.AsyncTask; 54import android.os.Binder; 55import android.os.Bundle; 56import android.os.CancellationSignal; 57import android.os.Handler; 58import android.os.HandlerThread; 59import android.os.Message; 60import android.os.ParcelFileDescriptor; 61import android.os.ParcelFileDescriptor.AutoCloseInputStream; 62import android.os.Process; 63import android.os.RemoteException; 64import android.os.StrictMode; 65import android.os.SystemClock; 66import android.os.SystemProperties; 67import android.preference.PreferenceManager; 68import android.provider.BaseColumns; 69import android.provider.ContactsContract; 70import android.provider.ContactsContract.AggregationExceptions; 71import android.provider.ContactsContract.Authorization; 72import android.provider.ContactsContract.CommonDataKinds.Contactables; 73import android.provider.ContactsContract.CommonDataKinds.Email; 74import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 75import android.provider.ContactsContract.CommonDataKinds.Identity; 76import android.provider.ContactsContract.CommonDataKinds.Im; 77import android.provider.ContactsContract.CommonDataKinds.Nickname; 78import android.provider.ContactsContract.CommonDataKinds.Note; 79import android.provider.ContactsContract.CommonDataKinds.Organization; 80import android.provider.ContactsContract.CommonDataKinds.Phone; 81import android.provider.ContactsContract.CommonDataKinds.Photo; 82import android.provider.ContactsContract.CommonDataKinds.SipAddress; 83import android.provider.ContactsContract.CommonDataKinds.StructuredName; 84import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 85import android.provider.ContactsContract.ContactCounts; 86import android.provider.ContactsContract.Contacts; 87import android.provider.ContactsContract.Contacts.AggregationSuggestions; 88import android.provider.ContactsContract.Data; 89import android.provider.ContactsContract.DataUsageFeedback; 90import android.provider.ContactsContract.Directory; 91import android.provider.ContactsContract.DisplayPhoto; 92import android.provider.ContactsContract.Groups; 93import android.provider.ContactsContract.PhoneLookup; 94import android.provider.ContactsContract.PhotoFiles; 95import android.provider.ContactsContract.PinnedPositions; 96import android.provider.ContactsContract.Profile; 97import android.provider.ContactsContract.ProviderStatus; 98import android.provider.ContactsContract.RawContacts; 99import android.provider.ContactsContract.RawContactsEntity; 100import android.provider.ContactsContract.SearchSnippetColumns; 101import android.provider.ContactsContract.Settings; 102import android.provider.ContactsContract.StatusUpdates; 103import android.provider.ContactsContract.StreamItemPhotos; 104import android.provider.ContactsContract.StreamItems; 105import android.provider.OpenableColumns; 106import android.provider.SyncStateContract; 107import android.telephony.PhoneNumberUtils; 108import android.telephony.TelephonyManager; 109import android.text.TextUtils; 110import android.util.Log; 111 112import com.android.common.content.ProjectionMap; 113import com.android.common.content.SyncStateContentProviderHelper; 114import com.android.common.io.MoreCloseables; 115import com.android.providers.contacts.ContactLookupKey.LookupKeySegment; 116import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns; 117import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns; 118import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns; 119import com.android.providers.contacts.ContactsDatabaseHelper.Clauses; 120import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns; 121import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns; 122import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; 123import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns; 124import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties; 125import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns; 126import com.android.providers.contacts.ContactsDatabaseHelper.Joins; 127import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns; 128import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; 129import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns; 130import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns; 131import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; 132import com.android.providers.contacts.ContactsDatabaseHelper.Projections; 133import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns; 134import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns; 135import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns; 136import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns; 137import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemPhotosColumns; 138import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns; 139import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 140import com.android.providers.contacts.ContactsDatabaseHelper.ViewGroupsColumns; 141import com.android.providers.contacts.ContactsDatabaseHelper.Views; 142import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder; 143import com.android.providers.contacts.aggregation.ContactAggregator; 144import com.android.providers.contacts.aggregation.ContactAggregator.AggregationSuggestionParameter; 145import com.android.providers.contacts.aggregation.ProfileAggregator; 146import com.android.providers.contacts.aggregation.util.CommonNicknameCache; 147import com.android.providers.contacts.database.ContactsTableUtil; 148import com.android.providers.contacts.database.DeletedContactsTableUtil; 149import com.android.providers.contacts.util.Clock; 150import com.android.providers.contacts.util.DbQueryUtils; 151import com.android.providers.contacts.util.NeededForTesting; 152import com.android.vcard.VCardComposer; 153import com.android.vcard.VCardConfig; 154import com.google.android.collect.Lists; 155import com.google.android.collect.Maps; 156import com.google.android.collect.Sets; 157import com.google.common.annotations.VisibleForTesting; 158import com.google.common.base.Preconditions; 159 160import libcore.io.IoUtils; 161 162import java.io.BufferedWriter; 163import java.io.ByteArrayOutputStream; 164import java.io.File; 165import java.io.FileDescriptor; 166import java.io.FileNotFoundException; 167import java.io.IOException; 168import java.io.OutputStream; 169import java.io.OutputStreamWriter; 170import java.io.PrintWriter; 171import java.io.Writer; 172import java.security.SecureRandom; 173import java.text.SimpleDateFormat; 174import java.util.ArrayList; 175import java.util.Arrays; 176import java.util.Collections; 177import java.util.Date; 178import java.util.HashMap; 179import java.util.HashSet; 180import java.util.List; 181import java.util.Locale; 182import java.util.Map; 183import java.util.Map.Entry; 184import java.util.Set; 185import java.util.concurrent.CountDownLatch; 186 187/** 188 * Contacts content provider. The contract between this provider and applications 189 * is defined in {@link ContactsContract}. 190 */ 191public class ContactsProvider2 extends AbstractContactsProvider 192 implements OnAccountsUpdateListener { 193 194 private static final int BACKGROUND_TASK_INITIALIZE = 0; 195 private static final int BACKGROUND_TASK_OPEN_WRITE_ACCESS = 1; 196 private static final int BACKGROUND_TASK_UPDATE_ACCOUNTS = 3; 197 private static final int BACKGROUND_TASK_UPDATE_LOCALE = 4; 198 private static final int BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM = 5; 199 private static final int BACKGROUND_TASK_UPDATE_SEARCH_INDEX = 6; 200 private static final int BACKGROUND_TASK_UPDATE_PROVIDER_STATUS = 7; 201 private static final int BACKGROUND_TASK_UPDATE_DIRECTORIES = 8; 202 private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9; 203 private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10; 204 private static final int BACKGROUND_TASK_CLEAN_DELETE_LOG = 11; 205 206 /** Default for the maximum number of returned aggregation suggestions. */ 207 private static final int DEFAULT_MAX_SUGGESTIONS = 5; 208 209 /** Limit for the maximum number of social stream items to store under a raw contact. */ 210 private static final int MAX_STREAM_ITEMS_PER_RAW_CONTACT = 5; 211 212 /** Rate limit (in ms) for photo cleanup. Do it at most once per day. */ 213 private static final int PHOTO_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000; 214 215 /** 216 * Default expiration duration for pre-authorized URIs. May be overridden from a secure 217 * setting. 218 */ 219 private static final int DEFAULT_PREAUTHORIZED_URI_EXPIRATION = 5 * 60 * 1000; 220 221 private static final int USAGE_TYPE_ALL = -1; 222 223 /** 224 * Random URI parameter that will be appended to preauthorized URIs for uniqueness. 225 */ 226 private static final String PREAUTHORIZED_URI_TOKEN = "perm_token"; 227 228 private static final String PREF_LOCALE = "locale"; 229 230 private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 3; 231 232 private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate"; 233 234 /** 235 * If set to "1", we don't remove account data when accounts have been removed. 236 * 237 * This should be used sparingly; even though there are data still available, the UI 238 * don't know anything about them, so they won't show up in the contact filter screen, and 239 * the contact card/editor may get confused to see unknown custom mimetypes. 240 * 241 * We can't spell it out because a property name must be less than 32 chars. 242 */ 243 private static final String DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA = 244 "debug.contacts.ksad"; 245 246 private static final ProfileAwareUriMatcher sUriMatcher = 247 new ProfileAwareUriMatcher(UriMatcher.NO_MATCH); 248 249 private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.TIMES_USED + " DESC," 250 + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 251 252 /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE = 253 "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" + 254 " ifnull(" + Contacts.TIMES_CONTACTED + ",0)+1" + 255 " WHERE " + Contacts._ID + "=?"; 256 257 /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE = 258 "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" + 259 " ifnull(" + RawContacts.TIMES_CONTACTED + ",0)+1 " + 260 " WHERE " + RawContacts.CONTACT_ID + "=?"; 261 262 /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK"; 263 264 // Regex for splitting query strings - we split on any group of non-alphanumeric characters, 265 // excluding the @ symbol. 266 /* package */ static final String QUERY_TOKENIZER_REGEX = "[^\\w@]+"; 267 268 private static final int CONTACTS = 1000; 269 private static final int CONTACTS_ID = 1001; 270 private static final int CONTACTS_LOOKUP = 1002; 271 private static final int CONTACTS_LOOKUP_ID = 1003; 272 private static final int CONTACTS_ID_DATA = 1004; 273 private static final int CONTACTS_FILTER = 1005; 274 private static final int CONTACTS_STREQUENT = 1006; 275 private static final int CONTACTS_STREQUENT_FILTER = 1007; 276 private static final int CONTACTS_GROUP = 1008; 277 private static final int CONTACTS_ID_PHOTO = 1009; 278 private static final int CONTACTS_LOOKUP_PHOTO = 1010; 279 private static final int CONTACTS_LOOKUP_ID_PHOTO = 1011; 280 private static final int CONTACTS_ID_DISPLAY_PHOTO = 1012; 281 private static final int CONTACTS_LOOKUP_DISPLAY_PHOTO = 1013; 282 private static final int CONTACTS_LOOKUP_ID_DISPLAY_PHOTO = 1014; 283 private static final int CONTACTS_AS_VCARD = 1015; 284 private static final int CONTACTS_AS_MULTI_VCARD = 1016; 285 private static final int CONTACTS_LOOKUP_DATA = 1017; 286 private static final int CONTACTS_LOOKUP_ID_DATA = 1018; 287 private static final int CONTACTS_ID_ENTITIES = 1019; 288 private static final int CONTACTS_LOOKUP_ENTITIES = 1020; 289 private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1021; 290 private static final int CONTACTS_ID_STREAM_ITEMS = 1022; 291 private static final int CONTACTS_LOOKUP_STREAM_ITEMS = 1023; 292 private static final int CONTACTS_LOOKUP_ID_STREAM_ITEMS = 1024; 293 private static final int CONTACTS_FREQUENT = 1025; 294 private static final int CONTACTS_DELETE_USAGE = 1026; 295 296 private static final int RAW_CONTACTS = 2002; 297 private static final int RAW_CONTACTS_ID = 2003; 298 private static final int RAW_CONTACTS_ID_DATA = 2004; 299 private static final int RAW_CONTACT_ID_ENTITY = 2005; 300 private static final int RAW_CONTACTS_ID_DISPLAY_PHOTO = 2006; 301 private static final int RAW_CONTACTS_ID_STREAM_ITEMS = 2007; 302 private static final int RAW_CONTACTS_ID_STREAM_ITEMS_ID = 2008; 303 304 private static final int DATA = 3000; 305 private static final int DATA_ID = 3001; 306 private static final int PHONES = 3002; 307 private static final int PHONES_ID = 3003; 308 private static final int PHONES_FILTER = 3004; 309 private static final int EMAILS = 3005; 310 private static final int EMAILS_ID = 3006; 311 private static final int EMAILS_LOOKUP = 3007; 312 private static final int EMAILS_FILTER = 3008; 313 private static final int POSTALS = 3009; 314 private static final int POSTALS_ID = 3010; 315 private static final int CALLABLES = 3011; 316 private static final int CALLABLES_ID = 3012; 317 private static final int CALLABLES_FILTER = 3013; 318 private static final int CONTACTABLES = 3014; 319 private static final int CONTACTABLES_FILTER = 3015; 320 321 private static final int PHONE_LOOKUP = 4000; 322 323 private static final int AGGREGATION_EXCEPTIONS = 6000; 324 private static final int AGGREGATION_EXCEPTION_ID = 6001; 325 326 private static final int STATUS_UPDATES = 7000; 327 private static final int STATUS_UPDATES_ID = 7001; 328 329 private static final int AGGREGATION_SUGGESTIONS = 8000; 330 331 private static final int SETTINGS = 9000; 332 333 private static final int GROUPS = 10000; 334 private static final int GROUPS_ID = 10001; 335 private static final int GROUPS_SUMMARY = 10003; 336 337 private static final int SYNCSTATE = 11000; 338 private static final int SYNCSTATE_ID = 11001; 339 private static final int PROFILE_SYNCSTATE = 11002; 340 private static final int PROFILE_SYNCSTATE_ID = 11003; 341 342 private static final int SEARCH_SUGGESTIONS = 12001; 343 private static final int SEARCH_SHORTCUT = 12002; 344 345 private static final int RAW_CONTACT_ENTITIES = 15001; 346 347 private static final int PROVIDER_STATUS = 16001; 348 349 private static final int DIRECTORIES = 17001; 350 private static final int DIRECTORIES_ID = 17002; 351 352 private static final int COMPLETE_NAME = 18000; 353 354 private static final int PROFILE = 19000; 355 private static final int PROFILE_ENTITIES = 19001; 356 private static final int PROFILE_DATA = 19002; 357 private static final int PROFILE_DATA_ID = 19003; 358 private static final int PROFILE_AS_VCARD = 19004; 359 private static final int PROFILE_RAW_CONTACTS = 19005; 360 private static final int PROFILE_RAW_CONTACTS_ID = 19006; 361 private static final int PROFILE_RAW_CONTACTS_ID_DATA = 19007; 362 private static final int PROFILE_RAW_CONTACTS_ID_ENTITIES = 19008; 363 private static final int PROFILE_STATUS_UPDATES = 19009; 364 private static final int PROFILE_RAW_CONTACT_ENTITIES = 19010; 365 private static final int PROFILE_PHOTO = 19011; 366 private static final int PROFILE_DISPLAY_PHOTO = 19012; 367 368 private static final int DATA_USAGE_FEEDBACK_ID = 20001; 369 370 private static final int STREAM_ITEMS = 21000; 371 private static final int STREAM_ITEMS_PHOTOS = 21001; 372 private static final int STREAM_ITEMS_ID = 21002; 373 private static final int STREAM_ITEMS_ID_PHOTOS = 21003; 374 private static final int STREAM_ITEMS_ID_PHOTOS_ID = 21004; 375 private static final int STREAM_ITEMS_LIMIT = 21005; 376 377 private static final int DISPLAY_PHOTO_ID = 22000; 378 private static final int PHOTO_DIMENSIONS = 22001; 379 380 private static final int DELETED_CONTACTS = 23000; 381 private static final int DELETED_CONTACTS_ID = 23001; 382 383 private static final int PINNED_POSITION_UPDATE = 24001; 384 385 // Inserts into URIs in this map will direct to the profile database if the parent record's 386 // value (looked up from the ContentValues object with the key specified by the value in this 387 // map) is in the profile ID-space (see {@link ProfileDatabaseHelper#PROFILE_ID_SPACE}). 388 private static final Map<Integer, String> INSERT_URI_ID_VALUE_MAP = Maps.newHashMap(); 389 static { 390 INSERT_URI_ID_VALUE_MAP.put(DATA, Data.RAW_CONTACT_ID); 391 INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_DATA, Data.RAW_CONTACT_ID); 392 INSERT_URI_ID_VALUE_MAP.put(STATUS_UPDATES, StatusUpdates.DATA_ID); 393 INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS, StreamItems.RAW_CONTACT_ID); 394 INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_STREAM_ITEMS, StreamItems.RAW_CONTACT_ID); 395 INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID); 396 INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS_ID_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID); 397 } 398 399 // Any interactions that involve these URIs will also require the calling package to have either 400 // android.permission.READ_SOCIAL_STREAM permission or android.permission.WRITE_SOCIAL_STREAM 401 // permission, depending on the type of operation being performed. 402 private static final List<Integer> SOCIAL_STREAM_URIS = Lists.newArrayList( 403 CONTACTS_ID_STREAM_ITEMS, 404 CONTACTS_LOOKUP_STREAM_ITEMS, 405 CONTACTS_LOOKUP_ID_STREAM_ITEMS, 406 RAW_CONTACTS_ID_STREAM_ITEMS, 407 RAW_CONTACTS_ID_STREAM_ITEMS_ID, 408 STREAM_ITEMS, 409 STREAM_ITEMS_PHOTOS, 410 STREAM_ITEMS_ID, 411 STREAM_ITEMS_ID_PHOTOS, 412 STREAM_ITEMS_ID_PHOTOS_ID 413 ); 414 415 private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID = 416 RawContactsColumns.CONCRETE_ID + "=? AND " 417 + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID 418 + " AND " + Groups.FAVORITES + " != 0"; 419 420 private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID = 421 RawContactsColumns.CONCRETE_ID + "=? AND " 422 + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID 423 + " AND " + Groups.AUTO_ADD + " != 0"; 424 425 private static final String[] PROJECTION_GROUP_ID 426 = new String[]{Tables.GROUPS + "." + Groups._ID}; 427 428 private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? " 429 + "AND " + GroupMembership.GROUP_ROW_ID + "=? " 430 + "AND " + GroupMembership.RAW_CONTACT_ID + "=?"; 431 432 private static final String SELECTION_STARRED_FROM_RAW_CONTACTS = 433 "SELECT " + RawContacts.STARRED 434 + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?"; 435 436 private interface DataContactsQuery { 437 public static final String TABLE = "data " 438 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) " 439 + "JOIN " + Tables.ACCOUNTS + " ON (" 440 + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID 441 + ")" 442 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; 443 444 public static final String[] PROJECTION = new String[] { 445 RawContactsColumns.CONCRETE_ID, 446 AccountsColumns.CONCRETE_ACCOUNT_TYPE, 447 AccountsColumns.CONCRETE_ACCOUNT_NAME, 448 AccountsColumns.CONCRETE_DATA_SET, 449 DataColumns.CONCRETE_ID, 450 ContactsColumns.CONCRETE_ID 451 }; 452 453 public static final int RAW_CONTACT_ID = 0; 454 public static final int ACCOUNT_TYPE = 1; 455 public static final int ACCOUNT_NAME = 2; 456 public static final int DATA_SET = 3; 457 public static final int DATA_ID = 4; 458 public static final int CONTACT_ID = 5; 459 } 460 461 interface RawContactsQuery { 462 String TABLE = Tables.RAW_CONTACTS_JOIN_ACCOUNTS; 463 464 String[] COLUMNS = new String[] { 465 RawContacts.DELETED, 466 RawContactsColumns.ACCOUNT_ID, 467 AccountsColumns.CONCRETE_ACCOUNT_TYPE, 468 AccountsColumns.CONCRETE_ACCOUNT_NAME, 469 AccountsColumns.CONCRETE_DATA_SET, 470 }; 471 472 int DELETED = 0; 473 int ACCOUNT_ID = 1; 474 int ACCOUNT_TYPE = 2; 475 int ACCOUNT_NAME = 3; 476 int DATA_SET = 4; 477 } 478 479 public static final String DEFAULT_ACCOUNT_TYPE = "com.google"; 480 481 /** Sql where statement for filtering on groups. */ 482 private static final String CONTACTS_IN_GROUP_SELECT = 483 Contacts._ID + " IN " 484 + "(SELECT " + RawContacts.CONTACT_ID 485 + " FROM " + Tables.RAW_CONTACTS 486 + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN " 487 + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID 488 + " FROM " + Tables.DATA_JOIN_MIMETYPES 489 + " WHERE " + DataColumns.MIMETYPE_ID + "=?" 490 + " AND " + GroupMembership.GROUP_ROW_ID + "=" 491 + "(SELECT " + Tables.GROUPS + "." + Groups._ID 492 + " FROM " + Tables.GROUPS 493 + " WHERE " + Groups.TITLE + "=?)))"; 494 495 /** Sql for updating DIRTY flag on multiple raw contacts */ 496 private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL = 497 "UPDATE " + Tables.RAW_CONTACTS + 498 " SET " + RawContacts.DIRTY + "=1" + 499 " WHERE " + RawContacts._ID + " IN ("; 500 501 /** Sql for updating VERSION on multiple raw contacts */ 502 private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL = 503 "UPDATE " + Tables.RAW_CONTACTS + 504 " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" + 505 " WHERE " + RawContacts._ID + " IN ("; 506 507 /** Sql for undemoting a demoted contact **/ 508 private static final String UNDEMOTE_CONTACT = 509 "UPDATE " + Tables.CONTACTS + 510 " SET " + Contacts.PINNED + " = " + PinnedPositions.UNPINNED + 511 " WHERE " + Contacts._ID + " = ?1 AND " + Contacts.PINNED + " <= " + 512 PinnedPositions.DEMOTED; 513 514 /** Sql for undemoting a demoted raw contact **/ 515 private static final String UNDEMOTE_RAW_CONTACT = 516 "UPDATE " + Tables.RAW_CONTACTS + 517 " SET " + RawContacts.PINNED + " = " + PinnedPositions.UNPINNED + 518 " WHERE " + RawContacts.CONTACT_ID + " = ?1 AND " + Contacts.PINNED + " <= " + 519 PinnedPositions.DEMOTED; 520 521 // Contacts contacted within the last 3 days (in seconds) 522 private static final long LAST_TIME_USED_3_DAYS_SEC = 3L * 24 * 60 * 60; 523 524 // Contacts contacted within the last 7 days (in seconds) 525 private static final long LAST_TIME_USED_7_DAYS_SEC = 7L * 24 * 60 * 60; 526 527 // Contacts contacted within the last 14 days (in seconds) 528 private static final long LAST_TIME_USED_14_DAYS_SEC = 14L * 24 * 60 * 60; 529 530 // Contacts contacted within the last 30 days (in seconds) 531 private static final long LAST_TIME_USED_30_DAYS_SEC = 30L * 24 * 60 * 60; 532 533 private static final String TIME_SINCE_LAST_USED_SEC = 534 "(strftime('%s', 'now') - " + DataUsageStatColumns.LAST_TIME_USED + "/1000)"; 535 536 private static final String SORT_BY_DATA_USAGE = 537 "(CASE WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_3_DAYS_SEC + 538 " THEN 0 " + 539 " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_7_DAYS_SEC + 540 " THEN 1 " + 541 " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_14_DAYS_SEC + 542 " THEN 2 " + 543 " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_30_DAYS_SEC + 544 " THEN 3 " + 545 " ELSE 4 END), " + 546 DataUsageStatColumns.TIMES_USED + " DESC"; 547 548 /* 549 * Sorting order for email address suggestions: first starred, then the rest. 550 * Within the two groups: 551 * - three buckets: very recently contacted, then fairly recently contacted, then the rest. 552 * Within each of the bucket - descending count of times contacted (both for data row and for 553 * contact row). 554 * If all else fails, in_visible_group, alphabetical. 555 * (Super)primary email address is returned before other addresses for the same contact. 556 */ 557 private static final String EMAIL_FILTER_SORT_ORDER = 558 Contacts.STARRED + " DESC, " 559 + Data.IS_SUPER_PRIMARY + " DESC, " 560 + SORT_BY_DATA_USAGE + ", " 561 + Contacts.IN_VISIBLE_GROUP + " DESC, " 562 + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC, " 563 + Data.CONTACT_ID + ", " 564 + Data.IS_PRIMARY + " DESC"; 565 566 /** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */ 567 private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER; 568 569 /** Name lookup types used for contact filtering */ 570 private static final String CONTACT_LOOKUP_NAME_TYPES = 571 NameLookupType.NAME_COLLATION_KEY + "," + 572 NameLookupType.EMAIL_BASED_NICKNAME + "," + 573 NameLookupType.NICKNAME; 574 575 /** 576 * If any of these columns are used in a Data projection, there is no point in 577 * using the DISTINCT keyword, which can negatively affect performance. 578 */ 579 private static final String[] DISTINCT_DATA_PROHIBITING_COLUMNS = { 580 Data._ID, 581 Data.RAW_CONTACT_ID, 582 Data.NAME_RAW_CONTACT_ID, 583 RawContacts.ACCOUNT_NAME, 584 RawContacts.ACCOUNT_TYPE, 585 RawContacts.DATA_SET, 586 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 587 RawContacts.DIRTY, 588 RawContacts.NAME_VERIFIED, 589 RawContacts.SOURCE_ID, 590 RawContacts.VERSION, 591 }; 592 593 private static final ProjectionMap sContactsColumns = ProjectionMap.builder() 594 .add(Contacts.CUSTOM_RINGTONE) 595 .add(Contacts.DISPLAY_NAME) 596 .add(Contacts.DISPLAY_NAME_ALTERNATIVE) 597 .add(Contacts.DISPLAY_NAME_SOURCE) 598 .add(Contacts.IN_VISIBLE_GROUP) 599 .add(Contacts.LAST_TIME_CONTACTED) 600 .add(Contacts.LOOKUP_KEY) 601 .add(Contacts.PHONETIC_NAME) 602 .add(Contacts.PHONETIC_NAME_STYLE) 603 .add(Contacts.PHOTO_ID) 604 .add(Contacts.PHOTO_FILE_ID) 605 .add(Contacts.PHOTO_URI) 606 .add(Contacts.PHOTO_THUMBNAIL_URI) 607 .add(Contacts.SEND_TO_VOICEMAIL) 608 .add(Contacts.SORT_KEY_ALTERNATIVE) 609 .add(Contacts.SORT_KEY_PRIMARY) 610 .add(ContactsColumns.PHONEBOOK_LABEL_PRIMARY) 611 .add(ContactsColumns.PHONEBOOK_BUCKET_PRIMARY) 612 .add(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE) 613 .add(ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE) 614 .add(Contacts.STARRED) 615 .add(Contacts.PINNED) 616 .add(Contacts.TIMES_CONTACTED) 617 .add(Contacts.HAS_PHONE_NUMBER) 618 .add(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP) 619 .build(); 620 621 private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder() 622 .add(Contacts.CONTACT_PRESENCE, 623 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE) 624 .add(Contacts.CONTACT_CHAT_CAPABILITY, 625 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY) 626 .add(Contacts.CONTACT_STATUS, 627 ContactsStatusUpdatesColumns.CONCRETE_STATUS) 628 .add(Contacts.CONTACT_STATUS_TIMESTAMP, 629 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) 630 .add(Contacts.CONTACT_STATUS_RES_PACKAGE, 631 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) 632 .add(Contacts.CONTACT_STATUS_LABEL, 633 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL) 634 .add(Contacts.CONTACT_STATUS_ICON, 635 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON) 636 .build(); 637 638 private static final ProjectionMap sSnippetColumns = ProjectionMap.builder() 639 .add(SearchSnippetColumns.SNIPPET) 640 .build(); 641 642 private static final ProjectionMap sRawContactColumns = ProjectionMap.builder() 643 .add(RawContacts.ACCOUNT_NAME) 644 .add(RawContacts.ACCOUNT_TYPE) 645 .add(RawContacts.DATA_SET) 646 .add(RawContacts.ACCOUNT_TYPE_AND_DATA_SET) 647 .add(RawContacts.DIRTY) 648 .add(RawContacts.NAME_VERIFIED) 649 .add(RawContacts.SOURCE_ID) 650 .add(RawContacts.VERSION) 651 .build(); 652 653 private static final ProjectionMap sRawContactSyncColumns = ProjectionMap.builder() 654 .add(RawContacts.SYNC1) 655 .add(RawContacts.SYNC2) 656 .add(RawContacts.SYNC3) 657 .add(RawContacts.SYNC4) 658 .build(); 659 660 private static final ProjectionMap sDataColumns = ProjectionMap.builder() 661 .add(Data.DATA1) 662 .add(Data.DATA2) 663 .add(Data.DATA3) 664 .add(Data.DATA4) 665 .add(Data.DATA5) 666 .add(Data.DATA6) 667 .add(Data.DATA7) 668 .add(Data.DATA8) 669 .add(Data.DATA9) 670 .add(Data.DATA10) 671 .add(Data.DATA11) 672 .add(Data.DATA12) 673 .add(Data.DATA13) 674 .add(Data.DATA14) 675 .add(Data.DATA15) 676 .add(Data.DATA_VERSION) 677 .add(Data.IS_PRIMARY) 678 .add(Data.IS_SUPER_PRIMARY) 679 .add(Data.MIMETYPE) 680 .add(Data.RES_PACKAGE) 681 .add(Data.SYNC1) 682 .add(Data.SYNC2) 683 .add(Data.SYNC3) 684 .add(Data.SYNC4) 685 .add(GroupMembership.GROUP_SOURCE_ID) 686 .build(); 687 688 private static final ProjectionMap sContactPresenceColumns = ProjectionMap.builder() 689 .add(Contacts.CONTACT_PRESENCE, 690 Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.PRESENCE) 691 .add(Contacts.CONTACT_CHAT_CAPABILITY, 692 Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.CHAT_CAPABILITY) 693 .add(Contacts.CONTACT_STATUS, 694 ContactsStatusUpdatesColumns.CONCRETE_STATUS) 695 .add(Contacts.CONTACT_STATUS_TIMESTAMP, 696 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) 697 .add(Contacts.CONTACT_STATUS_RES_PACKAGE, 698 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) 699 .add(Contacts.CONTACT_STATUS_LABEL, 700 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL) 701 .add(Contacts.CONTACT_STATUS_ICON, 702 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON) 703 .build(); 704 705 private static final ProjectionMap sDataPresenceColumns = ProjectionMap.builder() 706 .add(Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE) 707 .add(Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY) 708 .add(Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS) 709 .add(Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP) 710 .add(Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE) 711 .add(Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL) 712 .add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON) 713 .build(); 714 715 private static final ProjectionMap sDataUsageColumns = ProjectionMap.builder() 716 .add(Data.TIMES_USED, Tables.DATA_USAGE_STAT + "." + Data.TIMES_USED) 717 .add(Data.LAST_TIME_USED, Tables.DATA_USAGE_STAT + "." + Data.LAST_TIME_USED) 718 .build(); 719 720 /** Contains just BaseColumns._COUNT */ 721 private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder() 722 .add(BaseColumns._COUNT, "COUNT(*)") 723 .build(); 724 725 /** Contains just the contacts columns */ 726 private static final ProjectionMap sContactsProjectionMap = ProjectionMap.builder() 727 .add(Contacts._ID) 728 .add(Contacts.HAS_PHONE_NUMBER) 729 .add(Contacts.NAME_RAW_CONTACT_ID) 730 .add(Contacts.IS_USER_PROFILE) 731 .addAll(sContactsColumns) 732 .addAll(sContactsPresenceColumns) 733 .build(); 734 735 /** Contains just the contacts columns */ 736 private static final ProjectionMap sContactsProjectionWithSnippetMap = ProjectionMap.builder() 737 .addAll(sContactsProjectionMap) 738 .addAll(sSnippetColumns) 739 .build(); 740 741 /** Used for pushing starred contacts to the top of a times contacted list **/ 742 private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder() 743 .addAll(sContactsProjectionMap) 744 .add(DataUsageStatColumns.TIMES_USED, String.valueOf(Long.MAX_VALUE)) 745 .add(DataUsageStatColumns.LAST_TIME_USED, String.valueOf(Long.MAX_VALUE)) 746 .build(); 747 748 private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder() 749 .addAll(sContactsProjectionMap) 750 .add(DataUsageStatColumns.TIMES_USED, 751 "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + ")") 752 .add(DataUsageStatColumns.LAST_TIME_USED, 753 "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED + ")") 754 .build(); 755 756 /** 757 * Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows 758 * users to obtain part of Data columns. We hard-code {@link Contacts#IS_USER_PROFILE} to NULL, 759 * because sContactsProjectionMap specifies a field that doesn't exist in the view behind the 760 * query that uses this projection map. 761 **/ 762 private static final ProjectionMap sStrequentPhoneOnlyProjectionMap 763 = ProjectionMap.builder() 764 .addAll(sContactsProjectionMap) 765 .add(DataUsageStatColumns.TIMES_USED, DataUsageStatColumns.CONCRETE_TIMES_USED) 766 .add(DataUsageStatColumns.LAST_TIME_USED, DataUsageStatColumns.CONCRETE_LAST_TIME_USED) 767 .add(Phone.NUMBER) 768 .add(Phone.TYPE) 769 .add(Phone.LABEL) 770 .add(Phone.IS_SUPER_PRIMARY) 771 .add(Phone.CONTACT_ID) 772 .add(Contacts.IS_USER_PROFILE, "NULL") 773 .build(); 774 775 /** Contains just the contacts vCard columns */ 776 private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder() 777 .add(Contacts._ID) 778 .add(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf'") 779 .add(OpenableColumns.SIZE, "NULL") 780 .build(); 781 782 /** Contains just the raw contacts columns */ 783 private static final ProjectionMap sRawContactsProjectionMap = ProjectionMap.builder() 784 .add(RawContacts._ID) 785 .add(RawContacts.CONTACT_ID) 786 .add(RawContacts.DELETED) 787 .add(RawContacts.DISPLAY_NAME_PRIMARY) 788 .add(RawContacts.DISPLAY_NAME_ALTERNATIVE) 789 .add(RawContacts.DISPLAY_NAME_SOURCE) 790 .add(RawContacts.PHONETIC_NAME) 791 .add(RawContacts.PHONETIC_NAME_STYLE) 792 .add(RawContacts.SORT_KEY_PRIMARY) 793 .add(RawContacts.SORT_KEY_ALTERNATIVE) 794 .add(RawContactsColumns.PHONEBOOK_LABEL_PRIMARY) 795 .add(RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY) 796 .add(RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE) 797 .add(RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE) 798 .add(RawContacts.TIMES_CONTACTED) 799 .add(RawContacts.LAST_TIME_CONTACTED) 800 .add(RawContacts.CUSTOM_RINGTONE) 801 .add(RawContacts.SEND_TO_VOICEMAIL) 802 .add(RawContacts.STARRED) 803 .add(RawContacts.PINNED) 804 .add(RawContacts.AGGREGATION_MODE) 805 .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE) 806 .addAll(sRawContactColumns) 807 .addAll(sRawContactSyncColumns) 808 .build(); 809 810 /** Contains the columns from the raw entity view*/ 811 private static final ProjectionMap sRawEntityProjectionMap = ProjectionMap.builder() 812 .add(RawContacts._ID) 813 .add(RawContacts.CONTACT_ID) 814 .add(RawContacts.Entity.DATA_ID) 815 .add(RawContacts.DELETED) 816 .add(RawContacts.STARRED) 817 .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE) 818 .addAll(sRawContactColumns) 819 .addAll(sRawContactSyncColumns) 820 .addAll(sDataColumns) 821 .build(); 822 823 /** Contains the columns from the contact entity view*/ 824 private static final ProjectionMap sEntityProjectionMap = ProjectionMap.builder() 825 .add(Contacts.Entity._ID) 826 .add(Contacts.Entity.CONTACT_ID) 827 .add(Contacts.Entity.RAW_CONTACT_ID) 828 .add(Contacts.Entity.DATA_ID) 829 .add(Contacts.Entity.NAME_RAW_CONTACT_ID) 830 .add(Contacts.Entity.DELETED) 831 .add(Contacts.IS_USER_PROFILE) 832 .addAll(sContactsColumns) 833 .addAll(sContactPresenceColumns) 834 .addAll(sRawContactColumns) 835 .addAll(sRawContactSyncColumns) 836 .addAll(sDataColumns) 837 .addAll(sDataPresenceColumns) 838 .build(); 839 840 /** Contains columns in PhoneLookup which are not contained in the data view. */ 841 private static final ProjectionMap sSipLookupColumns = ProjectionMap.builder() 842 .add(PhoneLookup.NUMBER, SipAddress.SIP_ADDRESS) 843 .add(PhoneLookup.TYPE, "0") 844 .add(PhoneLookup.LABEL, "NULL") 845 .add(PhoneLookup.NORMALIZED_NUMBER, "NULL") 846 .build(); 847 848 /** Contains columns from the data view */ 849 private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder() 850 .add(Data._ID) 851 .add(Data.RAW_CONTACT_ID) 852 .add(Data.CONTACT_ID) 853 .add(Data.NAME_RAW_CONTACT_ID) 854 .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE) 855 .addAll(sDataColumns) 856 .addAll(sDataPresenceColumns) 857 .addAll(sRawContactColumns) 858 .addAll(sContactsColumns) 859 .addAll(sContactPresenceColumns) 860 .addAll(sDataUsageColumns) 861 .build(); 862 863 /** Contains columns from the data view used for SIP address lookup. */ 864 private static final ProjectionMap sDataSipLookupProjectionMap = ProjectionMap.builder() 865 .addAll(sDataProjectionMap) 866 .addAll(sSipLookupColumns) 867 .build(); 868 869 /** Contains columns from the data view */ 870 private static final ProjectionMap sDistinctDataProjectionMap = ProjectionMap.builder() 871 .add(Data._ID, "MIN(" + Data._ID + ")") 872 .add(RawContacts.CONTACT_ID) 873 .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE) 874 .addAll(sDataColumns) 875 .addAll(sDataPresenceColumns) 876 .addAll(sContactsColumns) 877 .addAll(sContactPresenceColumns) 878 .addAll(sDataUsageColumns) 879 .build(); 880 881 /** Contains columns from the data view used for SIP address lookup. */ 882 private static final ProjectionMap sDistinctDataSipLookupProjectionMap = ProjectionMap.builder() 883 .addAll(sDistinctDataProjectionMap) 884 .addAll(sSipLookupColumns) 885 .build(); 886 887 /** Contains the data and contacts columns, for joined tables */ 888 private static final ProjectionMap sPhoneLookupProjectionMap = ProjectionMap.builder() 889 .add(PhoneLookup._ID, "contacts_view." + Contacts._ID) 890 .add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY) 891 .add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME) 892 .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED) 893 .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED) 894 .add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED) 895 .add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP) 896 .add(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID) 897 .add(PhoneLookup.PHOTO_URI, "contacts_view." + Contacts.PHOTO_URI) 898 .add(PhoneLookup.PHOTO_THUMBNAIL_URI, "contacts_view." + Contacts.PHOTO_THUMBNAIL_URI) 899 .add(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE) 900 .add(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER) 901 .add(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL) 902 .add(PhoneLookup.NUMBER, Phone.NUMBER) 903 .add(PhoneLookup.TYPE, Phone.TYPE) 904 .add(PhoneLookup.LABEL, Phone.LABEL) 905 .add(PhoneLookup.NORMALIZED_NUMBER, Phone.NORMALIZED_NUMBER) 906 .build(); 907 908 /** Contains the just the {@link Groups} columns */ 909 private static final ProjectionMap sGroupsProjectionMap = ProjectionMap.builder() 910 .add(Groups._ID) 911 .add(Groups.ACCOUNT_NAME) 912 .add(Groups.ACCOUNT_TYPE) 913 .add(Groups.DATA_SET) 914 .add(Groups.ACCOUNT_TYPE_AND_DATA_SET) 915 .add(Groups.SOURCE_ID) 916 .add(Groups.DIRTY) 917 .add(Groups.VERSION) 918 .add(Groups.RES_PACKAGE) 919 .add(Groups.TITLE) 920 .add(Groups.TITLE_RES) 921 .add(Groups.GROUP_VISIBLE) 922 .add(Groups.SYSTEM_ID) 923 .add(Groups.DELETED) 924 .add(Groups.NOTES) 925 .add(Groups.SHOULD_SYNC) 926 .add(Groups.FAVORITES) 927 .add(Groups.AUTO_ADD) 928 .add(Groups.GROUP_IS_READ_ONLY) 929 .add(Groups.SYNC1) 930 .add(Groups.SYNC2) 931 .add(Groups.SYNC3) 932 .add(Groups.SYNC4) 933 .build(); 934 935 private static final ProjectionMap sDeletedContactsProjectionMap = ProjectionMap.builder() 936 .add(ContactsContract.DeletedContacts.CONTACT_ID) 937 .add(ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP) 938 .build(); 939 940 /** 941 * Contains {@link Groups} columns along with summary details. 942 * 943 * Note {@link Groups#SUMMARY_COUNT} doesn't exist in groups/view_groups. 944 * When we detect this column being requested, we join {@link Joins#GROUP_MEMBER_COUNT} to 945 * generate it. 946 * 947 * TODO Support SUMMARY_GROUP_COUNT_PER_ACCOUNT too. See also queryLocal(). 948 */ 949 private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder() 950 .addAll(sGroupsProjectionMap) 951 .add(Groups.SUMMARY_COUNT, "ifnull(group_member_count, 0)") 952 .add(Groups.SUMMARY_WITH_PHONES, 953 "(SELECT COUNT(" + ContactsColumns.CONCRETE_ID + ") FROM " 954 + Tables.CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP 955 + " WHERE " + Contacts.HAS_PHONE_NUMBER + ")") 956 .add(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, "0") // Always returns 0 for now. 957 .build(); 958 959 /** Contains the agg_exceptions columns */ 960 private static final ProjectionMap sAggregationExceptionsProjectionMap = ProjectionMap.builder() 961 .add(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id") 962 .add(AggregationExceptions.TYPE) 963 .add(AggregationExceptions.RAW_CONTACT_ID1) 964 .add(AggregationExceptions.RAW_CONTACT_ID2) 965 .build(); 966 967 /** Contains the agg_exceptions columns */ 968 private static final ProjectionMap sSettingsProjectionMap = ProjectionMap.builder() 969 .add(Settings.ACCOUNT_NAME) 970 .add(Settings.ACCOUNT_TYPE) 971 .add(Settings.DATA_SET) 972 .add(Settings.UNGROUPED_VISIBLE) 973 .add(Settings.SHOULD_SYNC) 974 .add(Settings.ANY_UNSYNCED, 975 "(CASE WHEN MIN(" + Settings.SHOULD_SYNC 976 + ",(SELECT " 977 + "(CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL" 978 + " THEN 1" 979 + " ELSE MIN(" + Groups.SHOULD_SYNC + ")" 980 + " END)" 981 + " FROM " + Views.GROUPS 982 + " WHERE " + ViewGroupsColumns.CONCRETE_ACCOUNT_NAME + "=" 983 + SettingsColumns.CONCRETE_ACCOUNT_NAME 984 + " AND " + ViewGroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" 985 + SettingsColumns.CONCRETE_ACCOUNT_TYPE 986 + " AND ((" + ViewGroupsColumns.CONCRETE_DATA_SET + " IS NULL AND " 987 + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR (" 988 + ViewGroupsColumns.CONCRETE_DATA_SET + "=" 989 + SettingsColumns.CONCRETE_DATA_SET + "))))=0" 990 + " THEN 1" 991 + " ELSE 0" 992 + " END)") 993 .add(Settings.UNGROUPED_COUNT, 994 "(SELECT COUNT(*)" 995 + " FROM (SELECT 1" 996 + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS 997 + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID 998 + " HAVING " + Clauses.HAVING_NO_GROUPS 999 + "))") 1000 .add(Settings.UNGROUPED_WITH_PHONES, 1001 "(SELECT COUNT(*)" 1002 + " FROM (SELECT 1" 1003 + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS 1004 + " WHERE " + Contacts.HAS_PHONE_NUMBER 1005 + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID 1006 + " HAVING " + Clauses.HAVING_NO_GROUPS 1007 + "))") 1008 .build(); 1009 1010 /** Contains StatusUpdates columns */ 1011 private static final ProjectionMap sStatusUpdatesProjectionMap = ProjectionMap.builder() 1012 .add(PresenceColumns.RAW_CONTACT_ID) 1013 .add(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID) 1014 .add(StatusUpdates.IM_ACCOUNT) 1015 .add(StatusUpdates.IM_HANDLE) 1016 .add(StatusUpdates.PROTOCOL) 1017 // We cannot allow a null in the custom protocol field, because SQLite3 does not 1018 // properly enforce uniqueness of null values 1019 .add(StatusUpdates.CUSTOM_PROTOCOL, 1020 "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "=''" 1021 + " THEN NULL" 1022 + " ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END)") 1023 .add(StatusUpdates.PRESENCE) 1024 .add(StatusUpdates.CHAT_CAPABILITY) 1025 .add(StatusUpdates.STATUS) 1026 .add(StatusUpdates.STATUS_TIMESTAMP) 1027 .add(StatusUpdates.STATUS_RES_PACKAGE) 1028 .add(StatusUpdates.STATUS_ICON) 1029 .add(StatusUpdates.STATUS_LABEL) 1030 .build(); 1031 1032 /** Contains StreamItems columns */ 1033 private static final ProjectionMap sStreamItemsProjectionMap = ProjectionMap.builder() 1034 .add(StreamItems._ID) 1035 .add(StreamItems.CONTACT_ID) 1036 .add(StreamItems.CONTACT_LOOKUP_KEY) 1037 .add(StreamItems.ACCOUNT_NAME) 1038 .add(StreamItems.ACCOUNT_TYPE) 1039 .add(StreamItems.DATA_SET) 1040 .add(StreamItems.RAW_CONTACT_ID) 1041 .add(StreamItems.RAW_CONTACT_SOURCE_ID) 1042 .add(StreamItems.RES_PACKAGE) 1043 .add(StreamItems.RES_ICON) 1044 .add(StreamItems.RES_LABEL) 1045 .add(StreamItems.TEXT) 1046 .add(StreamItems.TIMESTAMP) 1047 .add(StreamItems.COMMENTS) 1048 .add(StreamItems.SYNC1) 1049 .add(StreamItems.SYNC2) 1050 .add(StreamItems.SYNC3) 1051 .add(StreamItems.SYNC4) 1052 .build(); 1053 1054 private static final ProjectionMap sStreamItemPhotosProjectionMap = ProjectionMap.builder() 1055 .add(StreamItemPhotos._ID, StreamItemPhotosColumns.CONCRETE_ID) 1056 .add(StreamItems.RAW_CONTACT_ID) 1057 .add(StreamItems.RAW_CONTACT_SOURCE_ID, RawContactsColumns.CONCRETE_SOURCE_ID) 1058 .add(StreamItemPhotos.STREAM_ITEM_ID) 1059 .add(StreamItemPhotos.SORT_INDEX) 1060 .add(StreamItemPhotos.PHOTO_FILE_ID) 1061 .add(StreamItemPhotos.PHOTO_URI, 1062 "'" + DisplayPhoto.CONTENT_URI + "'||'/'||" + StreamItemPhotos.PHOTO_FILE_ID) 1063 .add(PhotoFiles.HEIGHT) 1064 .add(PhotoFiles.WIDTH) 1065 .add(PhotoFiles.FILESIZE) 1066 .add(StreamItemPhotos.SYNC1) 1067 .add(StreamItemPhotos.SYNC2) 1068 .add(StreamItemPhotos.SYNC3) 1069 .add(StreamItemPhotos.SYNC4) 1070 .build(); 1071 1072 /** Contains {@link Directory} columns */ 1073 private static final ProjectionMap sDirectoryProjectionMap = ProjectionMap.builder() 1074 .add(Directory._ID) 1075 .add(Directory.PACKAGE_NAME) 1076 .add(Directory.TYPE_RESOURCE_ID) 1077 .add(Directory.DISPLAY_NAME) 1078 .add(Directory.DIRECTORY_AUTHORITY) 1079 .add(Directory.ACCOUNT_TYPE) 1080 .add(Directory.ACCOUNT_NAME) 1081 .add(Directory.EXPORT_SUPPORT) 1082 .add(Directory.SHORTCUT_SUPPORT) 1083 .add(Directory.PHOTO_SUPPORT) 1084 .build(); 1085 1086 // where clause to update the status_updates table 1087 private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE = 1088 StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID + 1089 " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE + 1090 " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE "; 1091 1092 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 1093 1094 /** 1095 * Notification ID for failure to import contacts. 1096 */ 1097 private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1; 1098 1099 private static final String DEFAULT_SNIPPET_ARG_START_MATCH = "["; 1100 private static final String DEFAULT_SNIPPET_ARG_END_MATCH = "]"; 1101 private static final String DEFAULT_SNIPPET_ARG_ELLIPSIS = "..."; 1102 private static final int DEFAULT_SNIPPET_ARG_MAX_TOKENS = -10; 1103 1104 private boolean mIsPhoneInitialized; 1105 private boolean mIsPhone; 1106 1107 private final StringBuilder mSb = new StringBuilder(); 1108 private final String[] mSelectionArgs1 = new String[1]; 1109 private final String[] mSelectionArgs2 = new String[2]; 1110 private final String[] mSelectionArgs3 = new String[3]; 1111 private final String[] mSelectionArgs4 = new String[4]; 1112 private final ArrayList<String> mSelectionArgs = Lists.newArrayList(); 1113 1114 private Account mAccount; 1115 1116 static { 1117 // Contacts URI matching table 1118 final UriMatcher matcher = sUriMatcher; 1119 matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS); 1120 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 1121 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_ID_DATA); 1122 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/entities", CONTACTS_ID_ENTITIES); 1123 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", 1124 AGGREGATION_SUGGESTIONS); 1125 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*", 1126 AGGREGATION_SUGGESTIONS); 1127 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO); 1128 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/display_photo", 1129 CONTACTS_ID_DISPLAY_PHOTO); 1130 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/stream_items", 1131 CONTACTS_ID_STREAM_ITEMS); 1132 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER); 1133 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER); 1134 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP); 1135 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA); 1136 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/photo", 1137 CONTACTS_LOOKUP_PHOTO); 1138 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID); 1139 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data", 1140 CONTACTS_LOOKUP_ID_DATA); 1141 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/photo", 1142 CONTACTS_LOOKUP_ID_PHOTO); 1143 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/display_photo", 1144 CONTACTS_LOOKUP_DISPLAY_PHOTO); 1145 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/display_photo", 1146 CONTACTS_LOOKUP_ID_DISPLAY_PHOTO); 1147 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/entities", 1148 CONTACTS_LOOKUP_ENTITIES); 1149 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities", 1150 CONTACTS_LOOKUP_ID_ENTITIES); 1151 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/stream_items", 1152 CONTACTS_LOOKUP_STREAM_ITEMS); 1153 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/stream_items", 1154 CONTACTS_LOOKUP_ID_STREAM_ITEMS); 1155 matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD); 1156 matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*", 1157 CONTACTS_AS_MULTI_VCARD); 1158 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT); 1159 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", 1160 CONTACTS_STREQUENT_FILTER); 1161 matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP); 1162 matcher.addURI(ContactsContract.AUTHORITY, "contacts/frequent", CONTACTS_FREQUENT); 1163 matcher.addURI(ContactsContract.AUTHORITY, "contacts/delete_usage", CONTACTS_DELETE_USAGE); 1164 1165 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS); 1166 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID); 1167 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_ID_DATA); 1168 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/display_photo", 1169 RAW_CONTACTS_ID_DISPLAY_PHOTO); 1170 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ID_ENTITY); 1171 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items", 1172 RAW_CONTACTS_ID_STREAM_ITEMS); 1173 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items/#", 1174 RAW_CONTACTS_ID_STREAM_ITEMS_ID); 1175 1176 matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES); 1177 1178 matcher.addURI(ContactsContract.AUTHORITY, "data", DATA); 1179 matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID); 1180 matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES); 1181 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID); 1182 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER); 1183 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER); 1184 matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS); 1185 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID); 1186 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup", EMAILS_LOOKUP); 1187 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP); 1188 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER); 1189 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER); 1190 matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS); 1191 matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID); 1192 /** "*" is in CSV form with data ids ("123,456,789") */ 1193 matcher.addURI(ContactsContract.AUTHORITY, "data/usagefeedback/*", DATA_USAGE_FEEDBACK_ID); 1194 matcher.addURI(ContactsContract.AUTHORITY, "data/callables/", CALLABLES); 1195 matcher.addURI(ContactsContract.AUTHORITY, "data/callables/#", CALLABLES_ID); 1196 matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter", CALLABLES_FILTER); 1197 matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter/*", CALLABLES_FILTER); 1198 1199 matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/", CONTACTABLES); 1200 matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/filter", CONTACTABLES_FILTER); 1201 matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/filter/*", 1202 CONTACTABLES_FILTER); 1203 1204 matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS); 1205 matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID); 1206 matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY); 1207 1208 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE); 1209 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#", 1210 SYNCSTATE_ID); 1211 matcher.addURI(ContactsContract.AUTHORITY, "profile/" + SyncStateContentProviderHelper.PATH, 1212 PROFILE_SYNCSTATE); 1213 matcher.addURI(ContactsContract.AUTHORITY, 1214 "profile/" + SyncStateContentProviderHelper.PATH + "/#", 1215 PROFILE_SYNCSTATE_ID); 1216 1217 matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP); 1218 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", 1219 AGGREGATION_EXCEPTIONS); 1220 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", 1221 AGGREGATION_EXCEPTION_ID); 1222 1223 matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS); 1224 1225 matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES); 1226 matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID); 1227 1228 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, 1229 SEARCH_SUGGESTIONS); 1230 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 1231 SEARCH_SUGGESTIONS); 1232 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", 1233 SEARCH_SHORTCUT); 1234 1235 matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS); 1236 1237 matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES); 1238 matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID); 1239 1240 matcher.addURI(ContactsContract.AUTHORITY, "complete_name", COMPLETE_NAME); 1241 1242 matcher.addURI(ContactsContract.AUTHORITY, "profile", PROFILE); 1243 matcher.addURI(ContactsContract.AUTHORITY, "profile/entities", PROFILE_ENTITIES); 1244 matcher.addURI(ContactsContract.AUTHORITY, "profile/data", PROFILE_DATA); 1245 matcher.addURI(ContactsContract.AUTHORITY, "profile/data/#", PROFILE_DATA_ID); 1246 matcher.addURI(ContactsContract.AUTHORITY, "profile/photo", PROFILE_PHOTO); 1247 matcher.addURI(ContactsContract.AUTHORITY, "profile/display_photo", PROFILE_DISPLAY_PHOTO); 1248 matcher.addURI(ContactsContract.AUTHORITY, "profile/as_vcard", PROFILE_AS_VCARD); 1249 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts", PROFILE_RAW_CONTACTS); 1250 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#", 1251 PROFILE_RAW_CONTACTS_ID); 1252 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/data", 1253 PROFILE_RAW_CONTACTS_ID_DATA); 1254 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/entity", 1255 PROFILE_RAW_CONTACTS_ID_ENTITIES); 1256 matcher.addURI(ContactsContract.AUTHORITY, "profile/status_updates", 1257 PROFILE_STATUS_UPDATES); 1258 matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contact_entities", 1259 PROFILE_RAW_CONTACT_ENTITIES); 1260 1261 matcher.addURI(ContactsContract.AUTHORITY, "stream_items", STREAM_ITEMS); 1262 matcher.addURI(ContactsContract.AUTHORITY, "stream_items/photo", STREAM_ITEMS_PHOTOS); 1263 matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#", STREAM_ITEMS_ID); 1264 matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo", STREAM_ITEMS_ID_PHOTOS); 1265 matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo/#", 1266 STREAM_ITEMS_ID_PHOTOS_ID); 1267 matcher.addURI(ContactsContract.AUTHORITY, "stream_items_limit", STREAM_ITEMS_LIMIT); 1268 1269 matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", DISPLAY_PHOTO_ID); 1270 matcher.addURI(ContactsContract.AUTHORITY, "photo_dimensions", PHOTO_DIMENSIONS); 1271 1272 matcher.addURI(ContactsContract.AUTHORITY, "deleted_contacts", DELETED_CONTACTS); 1273 matcher.addURI(ContactsContract.AUTHORITY, "deleted_contacts/#", DELETED_CONTACTS_ID); 1274 1275 matcher.addURI(ContactsContract.AUTHORITY, "pinned_position_update", 1276 PINNED_POSITION_UPDATE); 1277 } 1278 1279 private static class DirectoryInfo { 1280 String authority; 1281 String accountName; 1282 String accountType; 1283 } 1284 1285 /** 1286 * Cached information about contact directories. 1287 */ 1288 private HashMap<String, DirectoryInfo> mDirectoryCache = new HashMap<String, DirectoryInfo>(); 1289 private boolean mDirectoryCacheValid = false; 1290 1291 /** 1292 * An entry in group id cache. 1293 * 1294 * TODO: Move this and {@link #mGroupIdCache} to {@link DataRowHandlerForGroupMembership}. 1295 */ 1296 public static class GroupIdCacheEntry { 1297 long accountId; 1298 String sourceId; 1299 long groupId; 1300 } 1301 1302 /** 1303 * Map from group source IDs to lists of {@link GroupIdCacheEntry}s. 1304 * 1305 * We don't need a soft cache for groups - the assumption is that there will only 1306 * be a small number of contact groups. The cache is keyed off source id. The value 1307 * is a list of groups with this group id. 1308 */ 1309 private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap(); 1310 1311 /** 1312 * Sub-provider for handling profile requests against the profile database. 1313 */ 1314 private ProfileProvider mProfileProvider; 1315 1316 private NameSplitter mNameSplitter; 1317 private NameLookupBuilder mNameLookupBuilder; 1318 1319 private PostalSplitter mPostalSplitter; 1320 1321 private ContactDirectoryManager mContactDirectoryManager; 1322 1323 // The database tag to use for representing the contacts DB in contacts transactions. 1324 /* package */ static final String CONTACTS_DB_TAG = "contacts"; 1325 1326 // The database tag to use for representing the profile DB in contacts transactions. 1327 /* package */ static final String PROFILE_DB_TAG = "profile"; 1328 1329 /** 1330 * The thread-local holder of the active transaction. Shared between this and the profile 1331 * provider, to keep transactions on both databases synchronized. 1332 */ 1333 private final ThreadLocal<ContactsTransaction> mTransactionHolder = 1334 new ThreadLocal<ContactsTransaction>(); 1335 1336 // This variable keeps track of whether the current operation is intended for the profile DB. 1337 private final ThreadLocal<Boolean> mInProfileMode = new ThreadLocal<Boolean>(); 1338 1339 // Separate data row handler instances for contact data and profile data. 1340 private HashMap<String, DataRowHandler> mDataRowHandlers; 1341 private HashMap<String, DataRowHandler> mProfileDataRowHandlers; 1342 1343 // Depending on whether the action being performed is for the profile, we will use one of two 1344 // database helper instances. 1345 private final ThreadLocal<ContactsDatabaseHelper> mDbHelper = 1346 new ThreadLocal<ContactsDatabaseHelper>(); 1347 private ContactsDatabaseHelper mContactsHelper; 1348 private ProfileDatabaseHelper mProfileHelper; 1349 1350 // Depending on whether the action being performed is for the profile or not, we will use one of 1351 // two aggregator instances. 1352 private final ThreadLocal<ContactAggregator> mAggregator = new ThreadLocal<ContactAggregator>(); 1353 private ContactAggregator mContactAggregator; 1354 private ContactAggregator mProfileAggregator; 1355 1356 // Depending on whether the action being performed is for the profile or not, we will use one of 1357 // two photo store instances (with their files stored in separate subdirectories). 1358 private final ThreadLocal<PhotoStore> mPhotoStore = new ThreadLocal<PhotoStore>(); 1359 private PhotoStore mContactsPhotoStore; 1360 private PhotoStore mProfilePhotoStore; 1361 1362 // The active transaction context will switch depending on the operation being performed. 1363 // Both transaction contexts will be cleared out when a batch transaction is started, and 1364 // each will be processed separately when a batch transaction completes. 1365 private final TransactionContext mContactTransactionContext = new TransactionContext(false); 1366 private final TransactionContext mProfileTransactionContext = new TransactionContext(true); 1367 private final ThreadLocal<TransactionContext> mTransactionContext = 1368 new ThreadLocal<TransactionContext>(); 1369 1370 // Duration in milliseconds that pre-authorized URIs will remain valid. 1371 private long mPreAuthorizedUriDuration; 1372 1373 // Map of single-use pre-authorized URIs to expiration times. 1374 private final Map<Uri, Long> mPreAuthorizedUris = Maps.newHashMap(); 1375 1376 // Random number generator. 1377 private final SecureRandom mRandom = new SecureRandom(); 1378 1379 private LegacyApiSupport mLegacyApiSupport; 1380 private GlobalSearchSupport mGlobalSearchSupport; 1381 private CommonNicknameCache mCommonNicknameCache; 1382 private SearchIndexManager mSearchIndexManager; 1383 1384 private final ContentValues mValues = new ContentValues(); 1385 private final HashMap<String, Boolean> mAccountWritability = Maps.newHashMap(); 1386 1387 private int mProviderStatus = ProviderStatus.STATUS_NORMAL; 1388 private boolean mProviderStatusUpdateNeeded; 1389 private long mEstimatedStorageRequirement = 0; 1390 private volatile CountDownLatch mReadAccessLatch; 1391 private volatile CountDownLatch mWriteAccessLatch; 1392 private boolean mAccountUpdateListenerRegistered; 1393 private boolean mOkToOpenAccess = true; 1394 1395 private boolean mVisibleTouched = false; 1396 1397 private boolean mSyncToNetwork; 1398 1399 private Locale mCurrentLocale; 1400 private int mContactsAccountCount; 1401 1402 private HandlerThread mBackgroundThread; 1403 private Handler mBackgroundHandler; 1404 1405 private long mLastPhotoCleanup = 0; 1406 1407 private FastScrollingIndexCache mFastScrollingIndexCache; 1408 1409 // Stats about FastScrollingIndex. 1410 private int mFastScrollingIndexCacheRequestCount; 1411 private int mFastScrollingIndexCacheMissCount; 1412 private long mTotalTimeFastScrollingIndexGenerate; 1413 1414 @Override 1415 public boolean onCreate() { 1416 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 1417 Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate start"); 1418 } 1419 super.onCreate(); 1420 setAppOps(AppOpsManager.OP_READ_CONTACTS, AppOpsManager.OP_WRITE_CONTACTS); 1421 try { 1422 return initialize(); 1423 } catch (RuntimeException e) { 1424 Log.e(TAG, "Cannot start provider", e); 1425 // In production code we don't want to throw here, so that phone will still work 1426 // in low storage situations. 1427 // See I5c88a3024ff1c5a06b5756b29a2d903f8f6a2531 1428 if (shouldThrowExceptionForInitializationError()) { 1429 throw e; 1430 } 1431 return false; 1432 } finally { 1433 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { 1434 Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate finish"); 1435 } 1436 } 1437 } 1438 1439 protected boolean shouldThrowExceptionForInitializationError() { 1440 return false; 1441 } 1442 1443 private boolean initialize() { 1444 StrictMode.setThreadPolicy( 1445 new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); 1446 1447 mFastScrollingIndexCache = FastScrollingIndexCache.getInstance(getContext()); 1448 1449 mContactsHelper = getDatabaseHelper(getContext()); 1450 mDbHelper.set(mContactsHelper); 1451 1452 // Set up the DB helper for keeping transactions serialized. 1453 setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG, this); 1454 1455 mContactDirectoryManager = new ContactDirectoryManager(this); 1456 mGlobalSearchSupport = new GlobalSearchSupport(this); 1457 1458 // The provider is closed for business until fully initialized 1459 mReadAccessLatch = new CountDownLatch(1); 1460 mWriteAccessLatch = new CountDownLatch(1); 1461 1462 mBackgroundThread = new HandlerThread("ContactsProviderWorker", 1463 Process.THREAD_PRIORITY_BACKGROUND); 1464 mBackgroundThread.start(); 1465 mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) { 1466 @Override 1467 public void handleMessage(Message msg) { 1468 performBackgroundTask(msg.what, msg.obj); 1469 } 1470 }; 1471 1472 // Set up the sub-provider for handling profiles. 1473 mProfileProvider = newProfileProvider(); 1474 mProfileProvider.setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG, this); 1475 ProviderInfo profileInfo = new ProviderInfo(); 1476 profileInfo.readPermission = "android.permission.READ_PROFILE"; 1477 profileInfo.writePermission = "android.permission.WRITE_PROFILE"; 1478 mProfileProvider.attachInfo(getContext(), profileInfo); 1479 mProfileHelper = mProfileProvider.getDatabaseHelper(getContext()); 1480 1481 // Initialize the pre-authorized URI duration. 1482 mPreAuthorizedUriDuration = DEFAULT_PREAUTHORIZED_URI_EXPIRATION; 1483 1484 scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE); 1485 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS); 1486 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_LOCALE); 1487 scheduleBackgroundTask(BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM); 1488 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_SEARCH_INDEX); 1489 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_PROVIDER_STATUS); 1490 scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS); 1491 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); 1492 1493 return true; 1494 } 1495 1496 /** 1497 * (Re)allocates all locale-sensitive structures. 1498 */ 1499 private void initForDefaultLocale() { 1500 Context context = getContext(); 1501 mLegacyApiSupport = new LegacyApiSupport(context, mContactsHelper, this, 1502 mGlobalSearchSupport); 1503 mCurrentLocale = getLocale(); 1504 mNameSplitter = mContactsHelper.createNameSplitter(mCurrentLocale); 1505 mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter); 1506 mPostalSplitter = new PostalSplitter(mCurrentLocale); 1507 mCommonNicknameCache = new CommonNicknameCache(mContactsHelper.getReadableDatabase()); 1508 ContactLocaleUtils.setLocale(mCurrentLocale); 1509 mContactAggregator = new ContactAggregator(this, mContactsHelper, 1510 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache); 1511 mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); 1512 mProfileAggregator = new ProfileAggregator(this, mProfileHelper, 1513 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache); 1514 mProfileAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true)); 1515 mSearchIndexManager = new SearchIndexManager(this); 1516 1517 mContactsPhotoStore = new PhotoStore(getContext().getFilesDir(), mContactsHelper); 1518 mProfilePhotoStore = new PhotoStore(new File(getContext().getFilesDir(), "profile"), 1519 mProfileHelper); 1520 1521 mDataRowHandlers = new HashMap<String, DataRowHandler>(); 1522 initDataRowHandlers(mDataRowHandlers, mContactsHelper, mContactAggregator, 1523 mContactsPhotoStore); 1524 mProfileDataRowHandlers = new HashMap<String, DataRowHandler>(); 1525 initDataRowHandlers(mProfileDataRowHandlers, mProfileHelper, mProfileAggregator, 1526 mProfilePhotoStore); 1527 1528 // Set initial thread-local state variables for the Contacts DB. 1529 switchToContactMode(); 1530 } 1531 1532 private void initDataRowHandlers(Map<String, DataRowHandler> handlerMap, 1533 ContactsDatabaseHelper dbHelper, ContactAggregator contactAggregator, 1534 PhotoStore photoStore) { 1535 Context context = getContext(); 1536 handlerMap.put(Email.CONTENT_ITEM_TYPE, 1537 new DataRowHandlerForEmail(context, dbHelper, contactAggregator)); 1538 handlerMap.put(Im.CONTENT_ITEM_TYPE, 1539 new DataRowHandlerForIm(context, dbHelper, contactAggregator)); 1540 handlerMap.put(Organization.CONTENT_ITEM_TYPE, 1541 new DataRowHandlerForOrganization(context, dbHelper, contactAggregator)); 1542 handlerMap.put(Phone.CONTENT_ITEM_TYPE, 1543 new DataRowHandlerForPhoneNumber(context, dbHelper, contactAggregator)); 1544 handlerMap.put(Nickname.CONTENT_ITEM_TYPE, 1545 new DataRowHandlerForNickname(context, dbHelper, contactAggregator)); 1546 handlerMap.put(StructuredName.CONTENT_ITEM_TYPE, 1547 new DataRowHandlerForStructuredName(context, dbHelper, contactAggregator, 1548 mNameSplitter, mNameLookupBuilder)); 1549 handlerMap.put(StructuredPostal.CONTENT_ITEM_TYPE, 1550 new DataRowHandlerForStructuredPostal(context, dbHelper, contactAggregator, 1551 mPostalSplitter)); 1552 handlerMap.put(GroupMembership.CONTENT_ITEM_TYPE, 1553 new DataRowHandlerForGroupMembership(context, dbHelper, contactAggregator, 1554 mGroupIdCache)); 1555 handlerMap.put(Photo.CONTENT_ITEM_TYPE, 1556 new DataRowHandlerForPhoto(context, dbHelper, contactAggregator, photoStore, 1557 getMaxDisplayPhotoDim(), getMaxThumbnailDim())); 1558 handlerMap.put(Note.CONTENT_ITEM_TYPE, 1559 new DataRowHandlerForNote(context, dbHelper, contactAggregator)); 1560 handlerMap.put(Identity.CONTENT_ITEM_TYPE, 1561 new DataRowHandlerForIdentity(context, dbHelper, contactAggregator)); 1562 } 1563 1564 @VisibleForTesting 1565 PhotoPriorityResolver createPhotoPriorityResolver(Context context) { 1566 return new PhotoPriorityResolver(context); 1567 } 1568 1569 protected void scheduleBackgroundTask(int task) { 1570 mBackgroundHandler.sendEmptyMessage(task); 1571 } 1572 1573 protected void scheduleBackgroundTask(int task, Object arg) { 1574 mBackgroundHandler.sendMessage(mBackgroundHandler.obtainMessage(task, arg)); 1575 } 1576 1577 protected void performBackgroundTask(int task, Object arg) { 1578 // Make sure we operate on the contacts db by default. 1579 switchToContactMode(); 1580 switch (task) { 1581 case BACKGROUND_TASK_INITIALIZE: { 1582 initForDefaultLocale(); 1583 mReadAccessLatch.countDown(); 1584 mReadAccessLatch = null; 1585 break; 1586 } 1587 1588 case BACKGROUND_TASK_OPEN_WRITE_ACCESS: { 1589 if (mOkToOpenAccess) { 1590 mWriteAccessLatch.countDown(); 1591 mWriteAccessLatch = null; 1592 } 1593 break; 1594 } 1595 1596 case BACKGROUND_TASK_UPDATE_ACCOUNTS: { 1597 Context context = getContext(); 1598 if (!mAccountUpdateListenerRegistered) { 1599 AccountManager.get(context).addOnAccountsUpdatedListener(this, null, false); 1600 mAccountUpdateListenerRegistered = true; 1601 } 1602 1603 // Update the accounts for both the contacts and profile DBs. 1604 Account[] accounts = AccountManager.get(context).getAccounts(); 1605 switchToContactMode(); 1606 boolean accountsChanged = updateAccountsInBackground(accounts); 1607 switchToProfileMode(); 1608 accountsChanged |= updateAccountsInBackground(accounts); 1609 1610 switchToContactMode(); 1611 1612 updateContactsAccountCount(accounts); 1613 updateDirectoriesInBackground(accountsChanged); 1614 break; 1615 } 1616 1617 case BACKGROUND_TASK_UPDATE_LOCALE: { 1618 updateLocaleInBackground(); 1619 break; 1620 } 1621 1622 case BACKGROUND_TASK_CHANGE_LOCALE: { 1623 changeLocaleInBackground(); 1624 break; 1625 } 1626 1627 case BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM: { 1628 if (isAggregationUpgradeNeeded()) { 1629 upgradeAggregationAlgorithmInBackground(); 1630 invalidateFastScrollingIndexCache(); 1631 } 1632 break; 1633 } 1634 1635 case BACKGROUND_TASK_UPDATE_SEARCH_INDEX: { 1636 updateSearchIndexInBackground(); 1637 break; 1638 } 1639 1640 case BACKGROUND_TASK_UPDATE_PROVIDER_STATUS: { 1641 updateProviderStatus(); 1642 break; 1643 } 1644 1645 case BACKGROUND_TASK_UPDATE_DIRECTORIES: { 1646 if (arg != null) { 1647 mContactDirectoryManager.onPackageChanged((String) arg); 1648 } 1649 break; 1650 } 1651 1652 case BACKGROUND_TASK_CLEANUP_PHOTOS: { 1653 // Check rate limit. 1654 long now = System.currentTimeMillis(); 1655 if (now - mLastPhotoCleanup > PHOTO_CLEANUP_RATE_LIMIT) { 1656 mLastPhotoCleanup = now; 1657 1658 // Clean up photo stores for both contacts and profiles. 1659 switchToContactMode(); 1660 cleanupPhotoStore(); 1661 switchToProfileMode(); 1662 cleanupPhotoStore(); 1663 1664 switchToContactMode(); // Switch to the default, just in case. 1665 break; 1666 } 1667 } 1668 1669 case BACKGROUND_TASK_CLEAN_DELETE_LOG: { 1670 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 1671 DeletedContactsTableUtil.deleteOldLogs(db); 1672 } 1673 } 1674 } 1675 1676 public void onLocaleChanged() { 1677 if (mProviderStatus != ProviderStatus.STATUS_NORMAL 1678 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) { 1679 return; 1680 } 1681 1682 scheduleBackgroundTask(BACKGROUND_TASK_CHANGE_LOCALE); 1683 } 1684 1685 private static boolean needsToUpdateLocaleData(SharedPreferences prefs, 1686 Locale locale,ContactsDatabaseHelper contactsHelper, 1687 ProfileDatabaseHelper profileHelper) { 1688 final String providerLocale = prefs.getString(PREF_LOCALE, null); 1689 1690 // If locale matches that of the provider, and neither DB needs 1691 // updating, there's nothing to do. A DB might require updating 1692 // as a result of a system upgrade. 1693 if (!locale.toString().equals(providerLocale)) { 1694 Log.i(TAG, "Locale has changed from " + providerLocale 1695 + " to " + locale.toString()); 1696 return true; 1697 } 1698 if (contactsHelper.needsToUpdateLocaleData(locale) || 1699 profileHelper.needsToUpdateLocaleData(locale)) { 1700 return true; 1701 } 1702 return false; 1703 } 1704 1705 /** 1706 * Verifies that the contacts database is properly configured for the current locale. 1707 * If not, changes the database locale to the current locale using an asynchronous task. 1708 * This needs to be done asynchronously because the process involves rebuilding 1709 * large data structures (name lookup, sort keys), which can take minutes on 1710 * a large set of contacts. 1711 */ 1712 protected void updateLocaleInBackground() { 1713 1714 // The process is already running - postpone the change 1715 if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) { 1716 return; 1717 } 1718 1719 final Locale currentLocale = mCurrentLocale; 1720 final SharedPreferences prefs = 1721 PreferenceManager.getDefaultSharedPreferences(getContext()); 1722 if (!needsToUpdateLocaleData(prefs, currentLocale, 1723 mContactsHelper, mProfileHelper)) { 1724 return; 1725 } 1726 1727 int providerStatus = mProviderStatus; 1728 setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE); 1729 mContactsHelper.setLocale(currentLocale); 1730 mProfileHelper.setLocale(currentLocale); 1731 mSearchIndexManager.updateIndex(true); 1732 prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit(); 1733 setProviderStatus(providerStatus); 1734 } 1735 1736 // Static update routine for use by ContactsUpgradeReceiver during startup. 1737 // This clears the search index and marks it to be rebuilt, but doesn't 1738 // actually rebuild it. That is done later by 1739 // BACKGROUND_TASK_UPDATE_SEARCH_INDEX. 1740 protected static void updateLocaleOffline(Context context, 1741 ContactsDatabaseHelper contactsHelper, 1742 ProfileDatabaseHelper profileHelper) { 1743 final Locale currentLocale = Locale.getDefault(); 1744 final SharedPreferences prefs = 1745 PreferenceManager.getDefaultSharedPreferences(context); 1746 if (!needsToUpdateLocaleData(prefs, currentLocale, 1747 contactsHelper, profileHelper)) { 1748 return; 1749 } 1750 1751 contactsHelper.setLocale(currentLocale); 1752 profileHelper.setLocale(currentLocale); 1753 contactsHelper.rebuildSearchIndex(); 1754 prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit(); 1755 } 1756 1757 /** 1758 * Reinitializes the provider for a new locale. 1759 */ 1760 private void changeLocaleInBackground() { 1761 // Re-initializing the provider without stopping it. 1762 // Locking the database will prevent inserts/updates/deletes from 1763 // running at the same time, but queries may still be running 1764 // on other threads. Those queries may return inconsistent results. 1765 SQLiteDatabase db = mContactsHelper.getWritableDatabase(); 1766 SQLiteDatabase profileDb = mProfileHelper.getWritableDatabase(); 1767 db.beginTransaction(); 1768 profileDb.beginTransaction(); 1769 try { 1770 initForDefaultLocale(); 1771 db.setTransactionSuccessful(); 1772 profileDb.setTransactionSuccessful(); 1773 } finally { 1774 db.endTransaction(); 1775 profileDb.endTransaction(); 1776 } 1777 1778 updateLocaleInBackground(); 1779 } 1780 1781 protected void updateSearchIndexInBackground() { 1782 mSearchIndexManager.updateIndex(false); 1783 } 1784 1785 protected void updateDirectoriesInBackground(boolean rescan) { 1786 mContactDirectoryManager.scanAllPackages(rescan); 1787 } 1788 1789 private void updateProviderStatus() { 1790 if (mProviderStatus != ProviderStatus.STATUS_NORMAL 1791 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) { 1792 return; 1793 } 1794 1795 // No accounts/no contacts status is true if there are no account and 1796 // there are no contacts or one profile contact 1797 if (mContactsAccountCount == 0) { 1798 boolean isContactsEmpty = DatabaseUtils.queryIsEmpty(mContactsHelper.getReadableDatabase(), Tables.CONTACTS); 1799 long profileNum = DatabaseUtils.queryNumEntries(mProfileHelper.getReadableDatabase(), 1800 Tables.CONTACTS, null); 1801 1802 // TODO: Different status if there is a profile but no contacts? 1803 if (isContactsEmpty && profileNum <= 1) { 1804 setProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS); 1805 } else { 1806 setProviderStatus(ProviderStatus.STATUS_NORMAL); 1807 } 1808 } else { 1809 setProviderStatus(ProviderStatus.STATUS_NORMAL); 1810 } 1811 } 1812 1813 @VisibleForTesting 1814 protected void cleanupPhotoStore() { 1815 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 1816 1817 // Assemble the set of photo store file IDs that are in use, and send those to the photo 1818 // store. Any photos that aren't in that set will be deleted, and any photos that no 1819 // longer exist in the photo store will be returned for us to clear out in the DB. 1820 long photoMimeTypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE); 1821 Cursor c = db.query(Views.DATA, new String[]{Data._ID, Photo.PHOTO_FILE_ID}, 1822 DataColumns.MIMETYPE_ID + "=" + photoMimeTypeId + " AND " 1823 + Photo.PHOTO_FILE_ID + " IS NOT NULL", null, null, null, null); 1824 Set<Long> usedPhotoFileIds = Sets.newHashSet(); 1825 Map<Long, Long> photoFileIdToDataId = Maps.newHashMap(); 1826 try { 1827 while (c.moveToNext()) { 1828 long dataId = c.getLong(0); 1829 long photoFileId = c.getLong(1); 1830 usedPhotoFileIds.add(photoFileId); 1831 photoFileIdToDataId.put(photoFileId, dataId); 1832 } 1833 } finally { 1834 c.close(); 1835 } 1836 1837 // Also query for all social stream item photos. 1838 c = db.query(Tables.STREAM_ITEM_PHOTOS + " JOIN " + Tables.STREAM_ITEMS 1839 + " ON " + StreamItemPhotos.STREAM_ITEM_ID + "=" + StreamItemsColumns.CONCRETE_ID, 1840 new String[]{ 1841 StreamItemPhotosColumns.CONCRETE_ID, 1842 StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID, 1843 StreamItemPhotos.PHOTO_FILE_ID 1844 }, 1845 null, null, null, null, null); 1846 Map<Long, Long> photoFileIdToStreamItemPhotoId = Maps.newHashMap(); 1847 Map<Long, Long> streamItemPhotoIdToStreamItemId = Maps.newHashMap(); 1848 try { 1849 while (c.moveToNext()) { 1850 long streamItemPhotoId = c.getLong(0); 1851 long streamItemId = c.getLong(1); 1852 long photoFileId = c.getLong(2); 1853 usedPhotoFileIds.add(photoFileId); 1854 photoFileIdToStreamItemPhotoId.put(photoFileId, streamItemPhotoId); 1855 streamItemPhotoIdToStreamItemId.put(streamItemPhotoId, streamItemId); 1856 } 1857 } finally { 1858 c.close(); 1859 } 1860 1861 // Run the photo store cleanup. 1862 Set<Long> missingPhotoIds = mPhotoStore.get().cleanup(usedPhotoFileIds); 1863 1864 // If any of the keys we're using no longer exist, clean them up. We need to do these 1865 // using internal APIs or direct DB access to avoid permission errors. 1866 if (!missingPhotoIds.isEmpty()) { 1867 try { 1868 // Need to set the db listener because we need to run onCommit afterwards. 1869 // Make sure to use the proper listener depending on the current mode. 1870 db.beginTransactionWithListener(inProfileMode() ? mProfileProvider : this); 1871 for (long missingPhotoId : missingPhotoIds) { 1872 if (photoFileIdToDataId.containsKey(missingPhotoId)) { 1873 long dataId = photoFileIdToDataId.get(missingPhotoId); 1874 ContentValues updateValues = new ContentValues(); 1875 updateValues.putNull(Photo.PHOTO_FILE_ID); 1876 updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId), 1877 updateValues, null, null, false); 1878 } 1879 if (photoFileIdToStreamItemPhotoId.containsKey(missingPhotoId)) { 1880 // For missing photos that were in stream item photos, just delete the 1881 // stream item photo. 1882 long streamItemPhotoId = photoFileIdToStreamItemPhotoId.get(missingPhotoId); 1883 db.delete(Tables.STREAM_ITEM_PHOTOS, StreamItemPhotos._ID + "=?", 1884 new String[]{String.valueOf(streamItemPhotoId)}); 1885 } 1886 } 1887 db.setTransactionSuccessful(); 1888 } catch (Exception e) { 1889 // Cleanup failure is not a fatal problem. We'll try again later. 1890 Log.e(TAG, "Failed to clean up outdated photo references", e); 1891 } finally { 1892 db.endTransaction(); 1893 } 1894 } 1895 } 1896 1897 @Override 1898 protected ContactsDatabaseHelper getDatabaseHelper(final Context context) { 1899 return ContactsDatabaseHelper.getInstance(context); 1900 } 1901 1902 @Override 1903 protected ThreadLocal<ContactsTransaction> getTransactionHolder() { 1904 return mTransactionHolder; 1905 } 1906 1907 public ProfileProvider newProfileProvider() { 1908 return new ProfileProvider(this); 1909 } 1910 1911 @VisibleForTesting 1912 /* package */ PhotoStore getPhotoStore() { 1913 return mContactsPhotoStore; 1914 } 1915 1916 @VisibleForTesting 1917 /* package */ PhotoStore getProfilePhotoStore() { 1918 return mProfilePhotoStore; 1919 } 1920 1921 /** 1922 * Maximum dimension (height or width) of photo thumbnails. 1923 */ 1924 public int getMaxThumbnailDim() { 1925 return PhotoProcessor.getMaxThumbnailSize(); 1926 } 1927 1928 /** 1929 * Maximum dimension (height or width) of display photos. Larger images will be scaled 1930 * to fit. 1931 */ 1932 public int getMaxDisplayPhotoDim() { 1933 return PhotoProcessor.getMaxDisplayPhotoSize(); 1934 } 1935 1936 @VisibleForTesting 1937 public ContactDirectoryManager getContactDirectoryManagerForTest() { 1938 return mContactDirectoryManager; 1939 } 1940 1941 @VisibleForTesting 1942 protected Locale getLocale() { 1943 return Locale.getDefault(); 1944 } 1945 1946 @VisibleForTesting 1947 final boolean inProfileMode() { 1948 Boolean profileMode = mInProfileMode.get(); 1949 return profileMode != null && profileMode; 1950 } 1951 1952 /** 1953 * Wipes all data from the contacts database. 1954 */ 1955 @NeededForTesting 1956 void wipeData() { 1957 invalidateFastScrollingIndexCache(); 1958 mContactsHelper.wipeData(); 1959 mProfileHelper.wipeData(); 1960 mContactsPhotoStore.clear(); 1961 mProfilePhotoStore.clear(); 1962 mProviderStatus = ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS; 1963 } 1964 1965 /** 1966 * During intialization, this content provider will 1967 * block all attempts to change contacts data. In particular, it will hold 1968 * up all contact syncs. As soon as the import process is complete, all 1969 * processes waiting to write to the provider are unblocked and can proceed 1970 * to compete for the database transaction monitor. 1971 */ 1972 private void waitForAccess(CountDownLatch latch) { 1973 if (latch == null) { 1974 return; 1975 } 1976 1977 while (true) { 1978 try { 1979 latch.await(); 1980 return; 1981 } catch (InterruptedException e) { 1982 Thread.currentThread().interrupt(); 1983 } 1984 } 1985 } 1986 1987 private int getIntValue(ContentValues values, String key, int defaultValue) { 1988 final Integer value = values.getAsInteger(key); 1989 return value != null ? value : defaultValue; 1990 } 1991 1992 private boolean flagExists(ContentValues values, String key) { 1993 return values.getAsInteger(key) != null; 1994 } 1995 1996 private boolean flagIsSet(ContentValues values, String key) { 1997 return getIntValue(values, key, 0) != 0; 1998 } 1999 2000 private boolean flagIsClear(ContentValues values, String key) { 2001 return getIntValue(values, key, 1) == 0; 2002 } 2003 2004 /** 2005 * Determines whether the given URI should be directed to the profile 2006 * database rather than the contacts database. This is true under either 2007 * of three conditions: 2008 * 1. The URI itself is specifically for the profile. 2009 * 2. The URI contains ID references that are in the profile ID-space. 2010 * 3. The URI contains lookup key references that match the special profile lookup key. 2011 * @param uri The URI to examine. 2012 * @return Whether to direct the DB operation to the profile database. 2013 */ 2014 private boolean mapsToProfileDb(Uri uri) { 2015 return sUriMatcher.mapsToProfile(uri); 2016 } 2017 2018 /** 2019 * Determines whether the given URI with the given values being inserted 2020 * should be directed to the profile database rather than the contacts 2021 * database. This is true if the URI already maps to the profile DB from 2022 * a call to {@link #mapsToProfileDb} or if the URI matches a URI that 2023 * specifies parent IDs via the ContentValues, and the given ContentValues 2024 * contains an ID in the profile ID-space. 2025 * @param uri The URI to examine. 2026 * @param values The values being inserted. 2027 * @return Whether to direct the DB insert to the profile database. 2028 */ 2029 private boolean mapsToProfileDbWithInsertedValues(Uri uri, ContentValues values) { 2030 if (mapsToProfileDb(uri)) { 2031 return true; 2032 } 2033 int match = sUriMatcher.match(uri); 2034 if (INSERT_URI_ID_VALUE_MAP.containsKey(match)) { 2035 String idField = INSERT_URI_ID_VALUE_MAP.get(match); 2036 Long id = values.getAsLong(idField); 2037 if (id != null && ContactsContract.isProfileId(id)) { 2038 return true; 2039 } 2040 } 2041 return false; 2042 } 2043 2044 /** 2045 * Switches the provider's thread-local context variables to prepare for performing 2046 * a profile operation. 2047 */ 2048 private void switchToProfileMode() { 2049 if (ENABLE_TRANSACTION_LOG) { 2050 Log.i(TAG, "switchToProfileMode", new RuntimeException("switchToProfileMode")); 2051 } 2052 mDbHelper.set(mProfileHelper); 2053 mTransactionContext.set(mProfileTransactionContext); 2054 mAggregator.set(mProfileAggregator); 2055 mPhotoStore.set(mProfilePhotoStore); 2056 mInProfileMode.set(true); 2057 } 2058 2059 /** 2060 * Switches the provider's thread-local context variables to prepare for performing 2061 * a contacts operation. 2062 */ 2063 private void switchToContactMode() { 2064 if (ENABLE_TRANSACTION_LOG) { 2065 Log.i(TAG, "switchToContactMode", new RuntimeException("switchToContactMode")); 2066 } 2067 mDbHelper.set(mContactsHelper); 2068 mTransactionContext.set(mContactTransactionContext); 2069 mAggregator.set(mContactAggregator); 2070 mPhotoStore.set(mContactsPhotoStore); 2071 mInProfileMode.set(false); 2072 } 2073 2074 @Override 2075 public Uri insert(Uri uri, ContentValues values) { 2076 waitForAccess(mWriteAccessLatch); 2077 2078 // Enforce stream items access check if applicable. 2079 enforceSocialStreamWritePermission(uri); 2080 2081 if (mapsToProfileDbWithInsertedValues(uri, values)) { 2082 switchToProfileMode(); 2083 return mProfileProvider.insert(uri, values); 2084 } else { 2085 switchToContactMode(); 2086 return super.insert(uri, values); 2087 } 2088 } 2089 2090 @Override 2091 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 2092 if (mWriteAccessLatch != null) { 2093 // Update on PROVIDER_STATUS used to be used as a trigger to re-start legacy contact 2094 // import. Now that we no longer support it, we just ignore it. 2095 int match = sUriMatcher.match(uri); 2096 if (match == PROVIDER_STATUS) { 2097 return 0; 2098 } 2099 } 2100 waitForAccess(mWriteAccessLatch); 2101 2102 // Enforce stream items access check if applicable. 2103 enforceSocialStreamWritePermission(uri); 2104 2105 if (mapsToProfileDb(uri)) { 2106 switchToProfileMode(); 2107 return mProfileProvider.update(uri, values, selection, selectionArgs); 2108 } else { 2109 switchToContactMode(); 2110 return super.update(uri, values, selection, selectionArgs); 2111 } 2112 } 2113 2114 @Override 2115 public int delete(Uri uri, String selection, String[] selectionArgs) { 2116 waitForAccess(mWriteAccessLatch); 2117 2118 // Enforce stream items access check if applicable. 2119 enforceSocialStreamWritePermission(uri); 2120 2121 if (mapsToProfileDb(uri)) { 2122 switchToProfileMode(); 2123 return mProfileProvider.delete(uri, selection, selectionArgs); 2124 } else { 2125 switchToContactMode(); 2126 return super.delete(uri, selection, selectionArgs); 2127 } 2128 } 2129 2130 @Override 2131 public Bundle call(String method, String arg, Bundle extras) { 2132 waitForAccess(mReadAccessLatch); 2133 switchToContactMode(); 2134 if (method.equals(Authorization.AUTHORIZATION_METHOD)) { 2135 Uri uri = (Uri) extras.getParcelable(Authorization.KEY_URI_TO_AUTHORIZE); 2136 2137 // Check permissions on the caller. The URI can only be pre-authorized if the caller 2138 // already has the necessary permissions. 2139 enforceSocialStreamReadPermission(uri); 2140 if (mapsToProfileDb(uri)) { 2141 mProfileProvider.enforceReadPermission(uri); 2142 } 2143 2144 // If there hasn't been a security violation yet, we're clear to pre-authorize the URI. 2145 Uri authUri = preAuthorizeUri(uri); 2146 Bundle response = new Bundle(); 2147 response.putParcelable(Authorization.KEY_AUTHORIZED_URI, authUri); 2148 return response; 2149 } 2150 return null; 2151 } 2152 2153 /** 2154 * Pre-authorizes the given URI, adding an expiring permission token to it and placing that 2155 * in our map of pre-authorized URIs. 2156 * @param uri The URI to pre-authorize. 2157 * @return A pre-authorized URI that will not require special permissions to use. 2158 */ 2159 private Uri preAuthorizeUri(Uri uri) { 2160 String token = String.valueOf(mRandom.nextLong()); 2161 Uri authUri = uri.buildUpon() 2162 .appendQueryParameter(PREAUTHORIZED_URI_TOKEN, token) 2163 .build(); 2164 long expiration = SystemClock.elapsedRealtime() + mPreAuthorizedUriDuration; 2165 mPreAuthorizedUris.put(authUri, expiration); 2166 2167 return authUri; 2168 } 2169 2170 /** 2171 * Checks whether the given URI has an unexpired permission token that would grant access to 2172 * query the content. If it does, the regular permission check should be skipped. 2173 * @param uri The URI being accessed. 2174 * @return Whether the URI is a pre-authorized URI that is still valid. 2175 */ 2176 public boolean isValidPreAuthorizedUri(Uri uri) { 2177 // Only proceed if the URI has a permission token parameter. 2178 if (uri.getQueryParameter(PREAUTHORIZED_URI_TOKEN) != null) { 2179 // First expire any pre-authorization URIs that are no longer valid. 2180 long now = SystemClock.elapsedRealtime(); 2181 Set<Uri> expiredUris = Sets.newHashSet(); 2182 for (Uri preAuthUri : mPreAuthorizedUris.keySet()) { 2183 if (mPreAuthorizedUris.get(preAuthUri) < now) { 2184 expiredUris.add(preAuthUri); 2185 } 2186 } 2187 for (Uri expiredUri : expiredUris) { 2188 mPreAuthorizedUris.remove(expiredUri); 2189 } 2190 2191 // Now check to see if the pre-authorized URI map contains the URI. 2192 if (mPreAuthorizedUris.containsKey(uri)) { 2193 // Unexpired token - skip the permission check. 2194 return true; 2195 } 2196 } 2197 return false; 2198 } 2199 2200 @Override 2201 protected boolean yield(ContactsTransaction transaction) { 2202 // If there's a profile transaction in progress, and we're yielding, we need to 2203 // end it. Unlike the Contacts DB yield (which re-starts a transaction at its 2204 // conclusion), we can just go back into a state in which we have no active 2205 // profile transaction, and let it be re-created as needed. We can't hold onto 2206 // the transaction without risking a deadlock. 2207 SQLiteDatabase profileDb = transaction.removeDbForTag(PROFILE_DB_TAG); 2208 if (profileDb != null) { 2209 profileDb.setTransactionSuccessful(); 2210 profileDb.endTransaction(); 2211 } 2212 2213 // Now proceed with the Contacts DB yield. 2214 SQLiteDatabase contactsDb = transaction.getDbForTag(CONTACTS_DB_TAG); 2215 return contactsDb != null && contactsDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY); 2216 } 2217 2218 @Override 2219 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 2220 throws OperationApplicationException { 2221 waitForAccess(mWriteAccessLatch); 2222 return super.applyBatch(operations); 2223 } 2224 2225 @Override 2226 public int bulkInsert(Uri uri, ContentValues[] values) { 2227 waitForAccess(mWriteAccessLatch); 2228 return super.bulkInsert(uri, values); 2229 } 2230 2231 @Override 2232 public void onBegin() { 2233 onBeginTransactionInternal(false); 2234 } 2235 2236 protected void onBeginTransactionInternal(boolean forProfile) { 2237 if (ENABLE_TRANSACTION_LOG) { 2238 Log.i(TAG, "onBeginTransaction: " + (forProfile ? "profile" : "contacts"), 2239 new RuntimeException("onBeginTransactionInternal")); 2240 } 2241 if (forProfile) { 2242 switchToProfileMode(); 2243 mProfileAggregator.clearPendingAggregations(); 2244 mProfileTransactionContext.clearExceptSearchIndexUpdates(); 2245 } else { 2246 switchToContactMode(); 2247 mContactAggregator.clearPendingAggregations(); 2248 mContactTransactionContext.clearExceptSearchIndexUpdates(); 2249 } 2250 } 2251 2252 @Override 2253 public void onCommit() { 2254 onCommitTransactionInternal(false); 2255 } 2256 2257 protected void onCommitTransactionInternal(boolean forProfile) { 2258 if (ENABLE_TRANSACTION_LOG) { 2259 Log.i(TAG, "onCommitTransactionInternal: " + (forProfile ? "profile" : "contacts"), 2260 new RuntimeException("onCommitTransactionInternal")); 2261 } 2262 if (forProfile) { 2263 switchToProfileMode(); 2264 } else { 2265 switchToContactMode(); 2266 } 2267 2268 flushTransactionalChanges(); 2269 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2270 mAggregator.get().aggregateInTransaction(mTransactionContext.get(), db); 2271 if (mVisibleTouched) { 2272 mVisibleTouched = false; 2273 mDbHelper.get().updateAllVisible(); 2274 2275 // Need to rebuild the fast-indxer bundle. 2276 invalidateFastScrollingIndexCache(); 2277 } 2278 2279 updateSearchIndexInTransaction(); 2280 2281 if (mProviderStatusUpdateNeeded) { 2282 updateProviderStatus(); 2283 mProviderStatusUpdateNeeded = false; 2284 } 2285 } 2286 2287 @Override 2288 public void onRollback() { 2289 onRollbackTransactionInternal(false); 2290 } 2291 2292 protected void onRollbackTransactionInternal(boolean forProfile) { 2293 if (ENABLE_TRANSACTION_LOG) { 2294 Log.i(TAG, "onRollbackTransactionInternal: " + (forProfile ? "profile" : "contacts"), 2295 new RuntimeException("onRollbackTransactionInternal")); 2296 } 2297 if (forProfile) { 2298 switchToProfileMode(); 2299 } else { 2300 switchToContactMode(); 2301 } 2302 2303 mDbHelper.get().invalidateAllCache(); 2304 } 2305 2306 private void updateSearchIndexInTransaction() { 2307 Set<Long> staleContacts = mTransactionContext.get().getStaleSearchIndexContactIds(); 2308 Set<Long> staleRawContacts = mTransactionContext.get().getStaleSearchIndexRawContactIds(); 2309 if (!staleContacts.isEmpty() || !staleRawContacts.isEmpty()) { 2310 mSearchIndexManager.updateIndexForRawContacts(staleContacts, staleRawContacts); 2311 mTransactionContext.get().clearSearchIndexUpdates(); 2312 } 2313 } 2314 2315 private void flushTransactionalChanges() { 2316 if (VERBOSE_LOGGING) { 2317 Log.v(TAG, "flushTransactionalChanges: " + (inProfileMode() ? "profile" : "contacts")); 2318 } 2319 2320 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2321 for (long rawContactId : mTransactionContext.get().getInsertedRawContactIds()) { 2322 mDbHelper.get().updateRawContactDisplayName(db, rawContactId); 2323 mAggregator.get().onRawContactInsert(mTransactionContext.get(), db, 2324 rawContactId); 2325 } 2326 2327 Set<Long> dirtyRawContacts = mTransactionContext.get().getDirtyRawContactIds(); 2328 if (!dirtyRawContacts.isEmpty()) { 2329 mSb.setLength(0); 2330 mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL); 2331 appendIds(mSb, dirtyRawContacts); 2332 mSb.append(")"); 2333 db.execSQL(mSb.toString()); 2334 } 2335 2336 Set<Long> updatedRawContacts = mTransactionContext.get().getUpdatedRawContactIds(); 2337 if (!updatedRawContacts.isEmpty()) { 2338 mSb.setLength(0); 2339 mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL); 2340 appendIds(mSb, updatedRawContacts); 2341 mSb.append(")"); 2342 db.execSQL(mSb.toString()); 2343 } 2344 2345 final Set<Long> changedRawContacts = mTransactionContext.get().getChangedRawContactIds(); 2346 ContactsTableUtil.updateContactLastUpdateByRawContactId(db, changedRawContacts); 2347 2348 // Update sync states. 2349 for (Map.Entry<Long, Object> entry : mTransactionContext.get().getUpdatedSyncStates()) { 2350 long id = entry.getKey(); 2351 if (mDbHelper.get().getSyncState().update(db, id, entry.getValue()) <= 0) { 2352 throw new IllegalStateException( 2353 "unable to update sync state, does it still exist?"); 2354 } 2355 } 2356 2357 mTransactionContext.get().clearExceptSearchIndexUpdates(); 2358 } 2359 2360 /** 2361 * Appends comma separated ids. 2362 * @param ids Should not be empty 2363 */ 2364 private void appendIds(StringBuilder sb, Set<Long> ids) { 2365 for (long id : ids) { 2366 sb.append(id).append(','); 2367 } 2368 2369 sb.setLength(sb.length() - 1); // Yank the last comma 2370 } 2371 2372 @Override 2373 protected void notifyChange() { 2374 notifyChange(mSyncToNetwork); 2375 mSyncToNetwork = false; 2376 } 2377 2378 protected void notifyChange(boolean syncToNetwork) { 2379 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, 2380 syncToNetwork); 2381 } 2382 2383 protected void setProviderStatus(int status) { 2384 if (mProviderStatus != status) { 2385 mProviderStatus = status; 2386 getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false); 2387 } 2388 } 2389 2390 public DataRowHandler getDataRowHandler(final String mimeType) { 2391 if (inProfileMode()) { 2392 return getDataRowHandlerForProfile(mimeType); 2393 } 2394 DataRowHandler handler = mDataRowHandlers.get(mimeType); 2395 if (handler == null) { 2396 handler = new DataRowHandlerForCustomMimetype( 2397 getContext(), mContactsHelper, mContactAggregator, mimeType); 2398 mDataRowHandlers.put(mimeType, handler); 2399 } 2400 return handler; 2401 } 2402 2403 public DataRowHandler getDataRowHandlerForProfile(final String mimeType) { 2404 DataRowHandler handler = mProfileDataRowHandlers.get(mimeType); 2405 if (handler == null) { 2406 handler = new DataRowHandlerForCustomMimetype( 2407 getContext(), mProfileHelper, mProfileAggregator, mimeType); 2408 mProfileDataRowHandlers.put(mimeType, handler); 2409 } 2410 return handler; 2411 } 2412 2413 @Override 2414 protected Uri insertInTransaction(Uri uri, ContentValues values) { 2415 if (VERBOSE_LOGGING) { 2416 Log.v(TAG, "insertInTransaction: uri=" + uri + " values=[" + values + "]"); 2417 } 2418 2419 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2420 2421 final boolean callerIsSyncAdapter = 2422 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2423 2424 final int match = sUriMatcher.match(uri); 2425 long id = 0; 2426 2427 switch (match) { 2428 case SYNCSTATE: 2429 case PROFILE_SYNCSTATE: 2430 id = mDbHelper.get().getSyncState().insert(db, values); 2431 break; 2432 2433 case CONTACTS: { 2434 invalidateFastScrollingIndexCache(); 2435 insertContact(values); 2436 break; 2437 } 2438 2439 case PROFILE: { 2440 throw new UnsupportedOperationException( 2441 "The profile contact is created automatically"); 2442 } 2443 2444 case RAW_CONTACTS: 2445 case PROFILE_RAW_CONTACTS: { 2446 invalidateFastScrollingIndexCache(); 2447 id = insertRawContact(uri, values, callerIsSyncAdapter); 2448 mSyncToNetwork |= !callerIsSyncAdapter; 2449 break; 2450 } 2451 2452 case RAW_CONTACTS_ID_DATA: 2453 case PROFILE_RAW_CONTACTS_ID_DATA: { 2454 invalidateFastScrollingIndexCache(); 2455 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2; 2456 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(segment)); 2457 id = insertData(values, callerIsSyncAdapter); 2458 mSyncToNetwork |= !callerIsSyncAdapter; 2459 break; 2460 } 2461 2462 case RAW_CONTACTS_ID_STREAM_ITEMS: { 2463 values.put(StreamItems.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 2464 id = insertStreamItem(uri, values); 2465 mSyncToNetwork |= !callerIsSyncAdapter; 2466 break; 2467 } 2468 2469 case DATA: 2470 case PROFILE_DATA: { 2471 invalidateFastScrollingIndexCache(); 2472 id = insertData(values, callerIsSyncAdapter); 2473 mSyncToNetwork |= !callerIsSyncAdapter; 2474 break; 2475 } 2476 2477 case GROUPS: { 2478 id = insertGroup(uri, values, callerIsSyncAdapter); 2479 mSyncToNetwork |= !callerIsSyncAdapter; 2480 break; 2481 } 2482 2483 case SETTINGS: { 2484 id = insertSettings(uri, values); 2485 mSyncToNetwork |= !callerIsSyncAdapter; 2486 break; 2487 } 2488 2489 case STATUS_UPDATES: 2490 case PROFILE_STATUS_UPDATES: { 2491 id = insertStatusUpdate(values); 2492 break; 2493 } 2494 2495 case STREAM_ITEMS: { 2496 id = insertStreamItem(uri, values); 2497 mSyncToNetwork |= !callerIsSyncAdapter; 2498 break; 2499 } 2500 2501 case STREAM_ITEMS_PHOTOS: { 2502 id = insertStreamItemPhoto(uri, values); 2503 mSyncToNetwork |= !callerIsSyncAdapter; 2504 break; 2505 } 2506 2507 case STREAM_ITEMS_ID_PHOTOS: { 2508 values.put(StreamItemPhotos.STREAM_ITEM_ID, uri.getPathSegments().get(1)); 2509 id = insertStreamItemPhoto(uri, values); 2510 mSyncToNetwork |= !callerIsSyncAdapter; 2511 break; 2512 } 2513 2514 default: 2515 mSyncToNetwork = true; 2516 return mLegacyApiSupport.insert(uri, values); 2517 } 2518 2519 if (id < 0) { 2520 return null; 2521 } 2522 2523 return ContentUris.withAppendedId(uri, id); 2524 } 2525 2526 /** 2527 * If account is non-null then store it in the values. If the account is 2528 * already specified in the values then it must be consistent with the 2529 * account, if it is non-null. 2530 * 2531 * @param uri Current {@link Uri} being operated on. 2532 * @param values {@link ContentValues} to read and possibly update. 2533 * @throws IllegalArgumentException when only one of 2534 * {@link RawContacts#ACCOUNT_NAME} or 2535 * {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the 2536 * other undefined. 2537 * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME} 2538 * and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between 2539 * the given {@link Uri} and {@link ContentValues}. 2540 */ 2541 private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException { 2542 String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 2543 String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 2544 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 2545 2546 String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME); 2547 String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 2548 final boolean partialValues = TextUtils.isEmpty(valueAccountName) 2549 ^ TextUtils.isEmpty(valueAccountType); 2550 2551 if (partialUri || partialValues) { 2552 // Throw when either account is incomplete 2553 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 2554 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 2555 } 2556 2557 // Accounts are valid by only checking one parameter, since we've 2558 // already ruled out partial accounts. 2559 final boolean validUri = !TextUtils.isEmpty(accountName); 2560 final boolean validValues = !TextUtils.isEmpty(valueAccountName); 2561 2562 if (validValues && validUri) { 2563 // Check that accounts match when both present 2564 final boolean accountMatch = TextUtils.equals(accountName, valueAccountName) 2565 && TextUtils.equals(accountType, valueAccountType); 2566 if (!accountMatch) { 2567 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 2568 "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri)); 2569 } 2570 } else if (validUri) { 2571 // Fill values from Uri when not present 2572 values.put(RawContacts.ACCOUNT_NAME, accountName); 2573 values.put(RawContacts.ACCOUNT_TYPE, accountType); 2574 } else if (validValues) { 2575 accountName = valueAccountName; 2576 accountType = valueAccountType; 2577 } else { 2578 return null; 2579 } 2580 2581 // Use cached Account object when matches, otherwise create 2582 if (mAccount == null 2583 || !mAccount.name.equals(accountName) 2584 || !mAccount.type.equals(accountType)) { 2585 mAccount = new Account(accountName, accountType); 2586 } 2587 2588 return mAccount; 2589 } 2590 2591 /** 2592 * Resolves the account and builds an {@link AccountWithDataSet} based on the data set specified 2593 * in the URI or values (if any). 2594 * @param uri Current {@link Uri} being operated on. 2595 * @param values {@link ContentValues} to read and possibly update. 2596 */ 2597 private AccountWithDataSet resolveAccountWithDataSet(Uri uri, ContentValues values) { 2598 final Account account = resolveAccount(uri, values); 2599 AccountWithDataSet accountWithDataSet = null; 2600 if (account != null) { 2601 String dataSet = getQueryParameter(uri, RawContacts.DATA_SET); 2602 if (dataSet == null) { 2603 dataSet = values.getAsString(RawContacts.DATA_SET); 2604 } else { 2605 values.put(RawContacts.DATA_SET, dataSet); 2606 } 2607 accountWithDataSet = AccountWithDataSet.get(account.name, account.type, dataSet); 2608 } 2609 return accountWithDataSet; 2610 } 2611 2612 /** 2613 * Same as {@link #resolveAccountWithDataSet}, but returns the account id for the 2614 * {@link AccountWithDataSet}. Used for insert. 2615 * 2616 * May update the account cache; must be used only in a transaction. 2617 */ 2618 private long resolveAccountIdInTransaction(Uri uri, ContentValues values) { 2619 return mDbHelper.get().getOrCreateAccountIdInTransaction( 2620 resolveAccountWithDataSet(uri, mValues)); 2621 } 2622 2623 /** 2624 * Inserts an item in the contacts table 2625 * 2626 * @param values the values for the new row 2627 * @return the row ID of the newly created row 2628 */ 2629 private long insertContact(ContentValues values) { 2630 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 2631 } 2632 2633 /** 2634 * Inserts an item in the raw contacts table 2635 * 2636 * @param uri the values for the new row 2637 * @param values the account this contact should be associated with. may be null. 2638 * @param callerIsSyncAdapter 2639 * @return the row ID of the newly created row 2640 */ 2641 private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 2642 mValues.clear(); 2643 mValues.putAll(values); 2644 mValues.putNull(RawContacts.CONTACT_ID); 2645 2646 final long accountId = resolveAccountIdInTransaction(uri, mValues); 2647 mValues.remove(RawContacts.ACCOUNT_NAME); 2648 mValues.remove(RawContacts.ACCOUNT_TYPE); 2649 mValues.remove(RawContacts.DATA_SET); 2650 mValues.put(RawContactsColumns.ACCOUNT_ID, accountId); 2651 2652 if (flagIsSet(values, RawContacts.DELETED)) { 2653 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2654 } 2655 2656 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2657 2658 long rawContactId = db.insert(Tables.RAW_CONTACTS, 2659 RawContacts.CONTACT_ID, mValues); 2660 int aggregationMode = getIntValue(values, RawContacts.AGGREGATION_MODE, 2661 RawContacts.AGGREGATION_MODE_DEFAULT); 2662 mAggregator.get().markNewForAggregation(rawContactId, aggregationMode); 2663 2664 // Trigger creation of a Contact based on this RawContact at the end of transaction 2665 mTransactionContext.get().rawContactInserted(rawContactId, accountId); 2666 2667 if (!callerIsSyncAdapter) { 2668 addAutoAddMembership(rawContactId); 2669 if (flagIsSet(values, RawContacts.STARRED)) { 2670 updateFavoritesMembership(rawContactId, true); 2671 } 2672 } 2673 2674 mProviderStatusUpdateNeeded = true; 2675 return rawContactId; 2676 } 2677 2678 private void addAutoAddMembership(long rawContactId) { 2679 final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID, 2680 rawContactId); 2681 if (groupId != null) { 2682 insertDataGroupMembership(rawContactId, groupId); 2683 } 2684 } 2685 2686 private Long findGroupByRawContactId(String selection, long rawContactId) { 2687 final SQLiteDatabase db = mDbHelper.get().getReadableDatabase(); 2688 Cursor c = db.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, 2689 PROJECTION_GROUP_ID, selection, 2690 new String[]{Long.toString(rawContactId)}, 2691 null /* groupBy */, null /* having */, null /* orderBy */); 2692 try { 2693 while (c.moveToNext()) { 2694 return c.getLong(0); 2695 } 2696 return null; 2697 } finally { 2698 c.close(); 2699 } 2700 } 2701 2702 private void updateFavoritesMembership(long rawContactId, boolean isStarred) { 2703 final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID, 2704 rawContactId); 2705 if (groupId != null) { 2706 if (isStarred) { 2707 insertDataGroupMembership(rawContactId, groupId); 2708 } else { 2709 deleteDataGroupMembership(rawContactId, groupId); 2710 } 2711 } 2712 } 2713 2714 private void insertDataGroupMembership(long rawContactId, long groupId) { 2715 ContentValues groupMembershipValues = new ContentValues(); 2716 groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId); 2717 groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId); 2718 groupMembershipValues.put(DataColumns.MIMETYPE_ID, 2719 mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 2720 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2721 db.insert(Tables.DATA, null, groupMembershipValues); 2722 } 2723 2724 private void deleteDataGroupMembership(long rawContactId, long groupId) { 2725 final String[] selectionArgs = { 2726 Long.toString(mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)), 2727 Long.toString(groupId), 2728 Long.toString(rawContactId)}; 2729 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2730 db.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs); 2731 } 2732 2733 /** 2734 * Inserts an item in the data table 2735 * 2736 * @param values the values for the new row 2737 * @return the row ID of the newly created row 2738 */ 2739 private long insertData(ContentValues values, boolean callerIsSyncAdapter) { 2740 long id = 0; 2741 mValues.clear(); 2742 mValues.putAll(values); 2743 2744 Long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 2745 if (rawContactId == null) { 2746 throw new IllegalArgumentException(Data.RAW_CONTACT_ID + " is required"); 2747 } 2748 2749 // Replace package with internal mapping 2750 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 2751 if (packageName != null) { 2752 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName)); 2753 } 2754 mValues.remove(Data.RES_PACKAGE); 2755 2756 // Replace mimetype with internal mapping 2757 final String mimeType = mValues.getAsString(Data.MIMETYPE); 2758 if (TextUtils.isEmpty(mimeType)) { 2759 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 2760 } 2761 2762 mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.get().getMimeTypeId(mimeType)); 2763 mValues.remove(Data.MIMETYPE); 2764 2765 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2766 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2767 id = rowHandler.insert(db, mTransactionContext.get(), rawContactId, mValues); 2768 mTransactionContext.get().markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter); 2769 mTransactionContext.get().rawContactUpdated(rawContactId); 2770 return id; 2771 } 2772 2773 /** 2774 * Inserts an item in the stream_items table. The account is checked against the 2775 * account in the raw contact for which the stream item is being inserted. If the 2776 * new stream item results in more stream items under this raw contact than the limit, 2777 * the oldest one will be deleted (note that if the stream item inserted was the 2778 * oldest, it will be immediately deleted, and this will return 0). 2779 * 2780 * @param uri the insertion URI 2781 * @param values the values for the new row 2782 * @return the stream item _ID of the newly created row, or 0 if it was not created 2783 */ 2784 private long insertStreamItem(Uri uri, ContentValues values) { 2785 long id = 0; 2786 mValues.clear(); 2787 mValues.putAll(values); 2788 2789 Long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 2790 if (rawContactId == null) { 2791 throw new IllegalArgumentException(Data.RAW_CONTACT_ID + " is required"); 2792 } 2793 2794 // Don't attempt to insert accounts params - they don't exist in the stream items table. 2795 mValues.remove(RawContacts.ACCOUNT_NAME); 2796 mValues.remove(RawContacts.ACCOUNT_TYPE); 2797 2798 // Insert the new stream item. 2799 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2800 id = db.insert(Tables.STREAM_ITEMS, null, mValues); 2801 if (id == -1) { 2802 // Insertion failed. 2803 return 0; 2804 } 2805 2806 // Check to see if we're over the limit for stream items under this raw contact. 2807 // It's possible that the inserted stream item is older than the the existing 2808 // ones, in which case it may be deleted immediately (resetting the ID to 0). 2809 id = cleanUpOldStreamItems(rawContactId, id); 2810 2811 return id; 2812 } 2813 2814 /** 2815 * Inserts an item in the stream_item_photos table. The account is checked against 2816 * the account in the raw contact that owns the stream item being modified. 2817 * 2818 * @param uri the insertion URI 2819 * @param values the values for the new row 2820 * @return the stream item photo _ID of the newly created row, or 0 if there was an issue 2821 * with processing the photo or creating the row 2822 */ 2823 private long insertStreamItemPhoto(Uri uri, ContentValues values) { 2824 long id = 0; 2825 mValues.clear(); 2826 mValues.putAll(values); 2827 2828 Long streamItemId = mValues.getAsLong(StreamItemPhotos.STREAM_ITEM_ID); 2829 if (streamItemId != null && streamItemId != 0) { 2830 long rawContactId = lookupRawContactIdForStreamId(streamItemId); 2831 2832 // Don't attempt to insert accounts params - they don't exist in the stream item 2833 // photos table. 2834 mValues.remove(RawContacts.ACCOUNT_NAME); 2835 mValues.remove(RawContacts.ACCOUNT_TYPE); 2836 2837 // Process the photo and store it. 2838 if (processStreamItemPhoto(mValues, false)) { 2839 // Insert the stream item photo. 2840 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2841 id = db.insert(Tables.STREAM_ITEM_PHOTOS, null, mValues); 2842 } 2843 } 2844 return id; 2845 } 2846 2847 /** 2848 * Processes the photo contained in the {@link ContactsContract.StreamItemPhotos#PHOTO} 2849 * field of the given values, attempting to store it in the photo store. If successful, 2850 * the resulting photo file ID will be added to the values for insert/update in the table. 2851 * <p> 2852 * If updating, it is valid for the picture to be empty or unspecified (the function will 2853 * still return true). If inserting, a valid picture must be specified. 2854 * @param values The content values provided by the caller. 2855 * @param forUpdate Whether this photo is being processed for update (vs. insert). 2856 * @return Whether the insert or update should proceed. 2857 */ 2858 private boolean processStreamItemPhoto(ContentValues values, boolean forUpdate) { 2859 byte[] photoBytes = values.getAsByteArray(StreamItemPhotos.PHOTO); 2860 if (photoBytes == null) { 2861 return forUpdate; 2862 } 2863 2864 // Process the photo and store it. 2865 try { 2866 long photoFileId = mPhotoStore.get().insert(new PhotoProcessor(photoBytes, 2867 getMaxDisplayPhotoDim(), getMaxThumbnailDim(), true), true); 2868 if (photoFileId != 0) { 2869 values.put(StreamItemPhotos.PHOTO_FILE_ID, photoFileId); 2870 values.remove(StreamItemPhotos.PHOTO); 2871 return true; 2872 } else { 2873 // Couldn't store the photo, return 0. 2874 Log.e(TAG, "Could not process stream item photo for insert"); 2875 return false; 2876 } 2877 } catch (IOException ioe) { 2878 Log.e(TAG, "Could not process stream item photo for insert", ioe); 2879 return false; 2880 } 2881 } 2882 2883 /** 2884 * Looks up the raw contact ID that owns the specified stream item. 2885 * @param streamItemId The ID of the stream item. 2886 * @return The associated raw contact ID, or -1 if no such stream item exists. 2887 */ 2888 private long lookupRawContactIdForStreamId(long streamItemId) { 2889 long rawContactId = -1; 2890 final SQLiteDatabase db = mDbHelper.get().getReadableDatabase(); 2891 Cursor c = db.query(Tables.STREAM_ITEMS, 2892 new String[]{StreamItems.RAW_CONTACT_ID}, 2893 StreamItems._ID + "=?", new String[]{String.valueOf(streamItemId)}, 2894 null, null, null); 2895 try { 2896 if (c.moveToFirst()) { 2897 rawContactId = c.getLong(0); 2898 } 2899 } finally { 2900 c.close(); 2901 } 2902 return rawContactId; 2903 } 2904 2905 /** 2906 * If the given URI is reading stream items or stream photos, this will run a permission check 2907 * for the android.permission.READ_SOCIAL_STREAM permission - otherwise it will do nothing. 2908 * @param uri The URI to check. 2909 */ 2910 private void enforceSocialStreamReadPermission(Uri uri) { 2911 if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri)) 2912 && !isValidPreAuthorizedUri(uri)) { 2913 getContext().enforceCallingOrSelfPermission( 2914 "android.permission.READ_SOCIAL_STREAM", null); 2915 } 2916 } 2917 2918 /** 2919 * If the given URI is modifying stream items or stream photos, this will run a permission check 2920 * for the android.permission.WRITE_SOCIAL_STREAM permission - otherwise it will do nothing. 2921 * @param uri The URI to check. 2922 */ 2923 private void enforceSocialStreamWritePermission(Uri uri) { 2924 if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))) { 2925 getContext().enforceCallingOrSelfPermission( 2926 "android.permission.WRITE_SOCIAL_STREAM", null); 2927 } 2928 } 2929 2930 /** 2931 * Queries the database for stream items under the given raw contact. If there are 2932 * more entries than {@link ContactsProvider2#MAX_STREAM_ITEMS_PER_RAW_CONTACT}, 2933 * the oldest entries (as determined by timestamp) will be deleted. 2934 * @param rawContactId The raw contact ID to examine for stream items. 2935 * @param insertedStreamItemId The ID of the stream item that was just inserted, 2936 * prompting this cleanup. Callers may pass 0 if no insertion prompted the 2937 * cleanup. 2938 * @return The ID of the inserted stream item if it still exists after cleanup; 2939 * 0 otherwise. 2940 */ 2941 private long cleanUpOldStreamItems(long rawContactId, long insertedStreamItemId) { 2942 long postCleanupInsertedStreamId = insertedStreamItemId; 2943 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2944 Cursor c = db.query(Tables.STREAM_ITEMS, new String[]{StreamItems._ID}, 2945 StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)}, 2946 null, null, StreamItems.TIMESTAMP + " DESC, " + StreamItems._ID + " DESC"); 2947 try { 2948 int streamItemCount = c.getCount(); 2949 if (streamItemCount <= MAX_STREAM_ITEMS_PER_RAW_CONTACT) { 2950 // Still under the limit - nothing to clean up! 2951 return insertedStreamItemId; 2952 } else { 2953 c.moveToLast(); 2954 while (c.getPosition() >= MAX_STREAM_ITEMS_PER_RAW_CONTACT) { 2955 long streamItemId = c.getLong(0); 2956 if (insertedStreamItemId == streamItemId) { 2957 // The stream item just inserted is being deleted. 2958 postCleanupInsertedStreamId = 0; 2959 } 2960 deleteStreamItem(db, c.getLong(0)); 2961 c.moveToPrevious(); 2962 } 2963 } 2964 } finally { 2965 c.close(); 2966 } 2967 return postCleanupInsertedStreamId; 2968 } 2969 2970 /** 2971 * Delete data row by row so that fixing of primaries etc work correctly. 2972 */ 2973 private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { 2974 int count = 0; 2975 2976 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 2977 2978 // Note that the query will return data according to the access restrictions, 2979 // so we don't need to worry about deleting data we don't have permission to read. 2980 Uri dataUri = inProfileMode() 2981 ? Uri.withAppendedPath(Profile.CONTENT_URI, RawContacts.Data.CONTENT_DIRECTORY) 2982 : Data.CONTENT_URI; 2983 Cursor c = query(dataUri, DataRowHandler.DataDeleteQuery.COLUMNS, 2984 selection, selectionArgs, null); 2985 try { 2986 while(c.moveToNext()) { 2987 long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID); 2988 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE); 2989 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2990 count += rowHandler.delete(db, mTransactionContext.get(), c); 2991 mTransactionContext.get().markRawContactDirtyAndChanged(rawContactId, 2992 callerIsSyncAdapter); 2993 } 2994 } finally { 2995 c.close(); 2996 } 2997 2998 return count; 2999 } 3000 3001 /** 3002 * Delete a data row provided that it is one of the allowed mime types. 3003 */ 3004 public int deleteData(long dataId, String[] allowedMimeTypes) { 3005 3006 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3007 3008 // Note that the query will return data according to the access restrictions, 3009 // so we don't need to worry about deleting data we don't have permission to read. 3010 mSelectionArgs1[0] = String.valueOf(dataId); 3011 Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, Data._ID + "=?", 3012 mSelectionArgs1, null); 3013 3014 try { 3015 if (!c.moveToFirst()) { 3016 return 0; 3017 } 3018 3019 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE); 3020 boolean valid = false; 3021 for (int i = 0; i < allowedMimeTypes.length; i++) { 3022 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 3023 valid = true; 3024 break; 3025 } 3026 } 3027 3028 if (!valid) { 3029 throw new IllegalArgumentException("Data type mismatch: expected " 3030 + Lists.newArrayList(allowedMimeTypes)); 3031 } 3032 DataRowHandler rowHandler = getDataRowHandler(mimeType); 3033 return rowHandler.delete(db, mTransactionContext.get(), c); 3034 } finally { 3035 c.close(); 3036 } 3037 } 3038 3039 /** 3040 * Inserts an item in the groups table 3041 */ 3042 private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 3043 mValues.clear(); 3044 mValues.putAll(values); 3045 3046 final long accountId = mDbHelper.get().getOrCreateAccountIdInTransaction( 3047 resolveAccountWithDataSet(uri, mValues)); 3048 mValues.remove(Groups.ACCOUNT_NAME); 3049 mValues.remove(Groups.ACCOUNT_TYPE); 3050 mValues.remove(Groups.DATA_SET); 3051 mValues.put(GroupsColumns.ACCOUNT_ID, accountId); 3052 3053 // Replace package with internal mapping 3054 final String packageName = mValues.getAsString(Groups.RES_PACKAGE); 3055 if (packageName != null) { 3056 mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName)); 3057 } 3058 mValues.remove(Groups.RES_PACKAGE); 3059 3060 final boolean isFavoritesGroup = flagIsSet(mValues, Groups.FAVORITES); 3061 3062 if (!callerIsSyncAdapter) { 3063 mValues.put(Groups.DIRTY, 1); 3064 } 3065 3066 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3067 3068 long result = db.insert(Tables.GROUPS, Groups.TITLE, mValues); 3069 3070 if (!callerIsSyncAdapter && isFavoritesGroup) { 3071 // If the inserted group is a favorite group, add all starred raw contacts to it. 3072 mSelectionArgs1[0] = Long.toString(accountId); 3073 Cursor c = db.query(Tables.RAW_CONTACTS, 3074 new String[]{RawContacts._ID, RawContacts.STARRED}, 3075 RawContactsColumns.CONCRETE_ACCOUNT_ID + "=?", mSelectionArgs1, 3076 null, null, null); 3077 try { 3078 while (c.moveToNext()) { 3079 if (c.getLong(1) != 0) { 3080 final long rawContactId = c.getLong(0); 3081 insertDataGroupMembership(rawContactId, result); 3082 mTransactionContext.get().markRawContactDirtyAndChanged(rawContactId, 3083 callerIsSyncAdapter); 3084 } 3085 } 3086 } finally { 3087 c.close(); 3088 } 3089 } 3090 3091 if (mValues.containsKey(Groups.GROUP_VISIBLE)) { 3092 mVisibleTouched = true; 3093 } 3094 3095 return result; 3096 } 3097 3098 private long insertSettings(Uri uri, ContentValues values) { 3099 // Before inserting, ensure that no settings record already exists for the 3100 // values being inserted (this used to be enforced by a primary key, but that no 3101 // longer works with the nullable data_set field added). 3102 String accountName = values.getAsString(Settings.ACCOUNT_NAME); 3103 String accountType = values.getAsString(Settings.ACCOUNT_TYPE); 3104 String dataSet = values.getAsString(Settings.DATA_SET); 3105 Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon(); 3106 if (accountName != null) { 3107 settingsUri.appendQueryParameter(Settings.ACCOUNT_NAME, accountName); 3108 } 3109 if (accountType != null) { 3110 settingsUri.appendQueryParameter(Settings.ACCOUNT_TYPE, accountType); 3111 } 3112 if (dataSet != null) { 3113 settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet); 3114 } 3115 Cursor c = queryLocal(settingsUri.build(), null, null, null, null, 0, null); 3116 try { 3117 if (c.getCount() > 0) { 3118 // If a record was found, replace it with the new values. 3119 String selection = null; 3120 String[] selectionArgs = null; 3121 if (accountName != null && accountType != null) { 3122 selection = Settings.ACCOUNT_NAME + "=? AND " + Settings.ACCOUNT_TYPE + "=?"; 3123 if (dataSet == null) { 3124 selection += " AND " + Settings.DATA_SET + " IS NULL"; 3125 selectionArgs = new String[] {accountName, accountType}; 3126 } else { 3127 selection += " AND " + Settings.DATA_SET + "=?"; 3128 selectionArgs = new String[] {accountName, accountType, dataSet}; 3129 } 3130 } 3131 return updateSettings(uri, values, selection, selectionArgs); 3132 } 3133 } finally { 3134 c.close(); 3135 } 3136 3137 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3138 3139 // If we didn't find a duplicate, we're fine to insert. 3140 final long id = db.insert(Tables.SETTINGS, null, values); 3141 3142 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 3143 mVisibleTouched = true; 3144 } 3145 3146 return id; 3147 } 3148 3149 /** 3150 * Inserts a status update. 3151 */ 3152 public long insertStatusUpdate(ContentValues values) { 3153 final String handle = values.getAsString(StatusUpdates.IM_HANDLE); 3154 final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL); 3155 String customProtocol = null; 3156 3157 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3158 3159 if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) { 3160 customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL); 3161 if (TextUtils.isEmpty(customProtocol)) { 3162 throw new IllegalArgumentException( 3163 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 3164 } 3165 } 3166 3167 long rawContactId = -1; 3168 long contactId = -1; 3169 Long dataId = values.getAsLong(StatusUpdates.DATA_ID); 3170 String accountType = null; 3171 String accountName = null; 3172 mSb.setLength(0); 3173 mSelectionArgs.clear(); 3174 if (dataId != null) { 3175 // Lookup the contact info for the given data row. 3176 3177 mSb.append(Tables.DATA + "." + Data._ID + "=?"); 3178 mSelectionArgs.add(String.valueOf(dataId)); 3179 } else { 3180 // Lookup the data row to attach this presence update to 3181 3182 if (TextUtils.isEmpty(handle) || protocol == null) { 3183 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 3184 } 3185 3186 // TODO: generalize to allow other providers to match against email 3187 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 3188 3189 String mimeTypeIdIm = String.valueOf(mDbHelper.get().getMimeTypeIdForIm()); 3190 if (matchEmail) { 3191 String mimeTypeIdEmail = String.valueOf(mDbHelper.get().getMimeTypeIdForEmail()); 3192 3193 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise 3194 // the "OR" conjunction confuses it and it switches to a full scan of 3195 // the raw_contacts table. 3196 3197 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same 3198 // column - Data.DATA1 3199 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" + 3200 " AND " + Data.DATA1 + "=?" + 3201 " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?"); 3202 mSelectionArgs.add(mimeTypeIdEmail); 3203 mSelectionArgs.add(mimeTypeIdIm); 3204 mSelectionArgs.add(handle); 3205 mSelectionArgs.add(mimeTypeIdIm); 3206 mSelectionArgs.add(String.valueOf(protocol)); 3207 if (customProtocol != null) { 3208 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 3209 mSelectionArgs.add(customProtocol); 3210 } 3211 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))"); 3212 mSelectionArgs.add(mimeTypeIdEmail); 3213 } else { 3214 mSb.append(DataColumns.MIMETYPE_ID + "=?" + 3215 " AND " + Im.PROTOCOL + "=?" + 3216 " AND " + Im.DATA + "=?"); 3217 mSelectionArgs.add(mimeTypeIdIm); 3218 mSelectionArgs.add(String.valueOf(protocol)); 3219 mSelectionArgs.add(handle); 3220 if (customProtocol != null) { 3221 mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?"); 3222 mSelectionArgs.add(customProtocol); 3223 } 3224 } 3225 3226 final String dataID = values.getAsString(StatusUpdates.DATA_ID); 3227 if (dataID != null) { 3228 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?"); 3229 mSelectionArgs.add(dataID); 3230 } 3231 } 3232 3233 Cursor cursor = null; 3234 try { 3235 cursor = db.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 3236 mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null, 3237 Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID); 3238 if (cursor.moveToFirst()) { 3239 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 3240 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 3241 accountType = cursor.getString(DataContactsQuery.ACCOUNT_TYPE); 3242 accountName = cursor.getString(DataContactsQuery.ACCOUNT_NAME); 3243 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 3244 } else { 3245 // No contact found, return a null URI 3246 return -1; 3247 } 3248 } finally { 3249 if (cursor != null) { 3250 cursor.close(); 3251 } 3252 } 3253 3254 final String presence = values.getAsString(StatusUpdates.PRESENCE); 3255 if (presence != null) { 3256 if (customProtocol == null) { 3257 // We cannot allow a null in the custom protocol field, because SQLite3 does not 3258 // properly enforce uniqueness of null values 3259 customProtocol = ""; 3260 } 3261 3262 mValues.clear(); 3263 mValues.put(StatusUpdates.DATA_ID, dataId); 3264 mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 3265 mValues.put(PresenceColumns.CONTACT_ID, contactId); 3266 mValues.put(StatusUpdates.PROTOCOL, protocol); 3267 mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol); 3268 mValues.put(StatusUpdates.IM_HANDLE, handle); 3269 final String imAccount = values.getAsString(StatusUpdates.IM_ACCOUNT); 3270 if (imAccount != null) { 3271 mValues.put(StatusUpdates.IM_ACCOUNT, imAccount); 3272 } 3273 mValues.put(StatusUpdates.PRESENCE, presence); 3274 mValues.put(StatusUpdates.CHAT_CAPABILITY, 3275 values.getAsString(StatusUpdates.CHAT_CAPABILITY)); 3276 3277 // Insert the presence update 3278 db.replace(Tables.PRESENCE, null, mValues); 3279 } 3280 3281 3282 if (values.containsKey(StatusUpdates.STATUS)) { 3283 String status = values.getAsString(StatusUpdates.STATUS); 3284 String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE); 3285 Resources resources = getContext().getResources(); 3286 if (!TextUtils.isEmpty(resPackage)) { 3287 PackageManager pm = getContext().getPackageManager(); 3288 try { 3289 resources = pm.getResourcesForApplication(resPackage); 3290 } catch (NameNotFoundException e) { 3291 Log.w(TAG, "Contact status update resource package not found: " 3292 + resPackage); 3293 } 3294 } 3295 Integer labelResourceId = values.getAsInteger(StatusUpdates.STATUS_LABEL); 3296 3297 if ((labelResourceId == null || labelResourceId == 0) && protocol != null) { 3298 labelResourceId = Im.getProtocolLabelResource(protocol); 3299 } 3300 String labelResource = getResourceName(resources, "string", labelResourceId); 3301 3302 Integer iconResourceId = values.getAsInteger(StatusUpdates.STATUS_ICON); 3303 // TODO compute the default icon based on the protocol 3304 3305 String iconResource = getResourceName(resources, "drawable", iconResourceId); 3306 3307 if (TextUtils.isEmpty(status)) { 3308 mDbHelper.get().deleteStatusUpdate(dataId); 3309 } else { 3310 Long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP); 3311 if (timestamp != null) { 3312 mDbHelper.get().replaceStatusUpdate(dataId, timestamp, status, resPackage, 3313 iconResourceId, labelResourceId); 3314 } else { 3315 mDbHelper.get().insertStatusUpdate(dataId, status, resPackage, iconResourceId, 3316 labelResourceId); 3317 } 3318 3319 // For forward compatibility with the new stream item API, insert this status update 3320 // there as well. If we already have a stream item from this source, update that 3321 // one instead of inserting a new one (since the semantics of the old status update 3322 // API is to only have a single record). 3323 if (rawContactId != -1 && !TextUtils.isEmpty(status)) { 3324 ContentValues streamItemValues = new ContentValues(); 3325 streamItemValues.put(StreamItems.RAW_CONTACT_ID, rawContactId); 3326 // Status updates are text only but stream items are HTML. 3327 streamItemValues.put(StreamItems.TEXT, statusUpdateToHtml(status)); 3328 streamItemValues.put(StreamItems.COMMENTS, ""); 3329 streamItemValues.put(StreamItems.RES_PACKAGE, resPackage); 3330 streamItemValues.put(StreamItems.RES_ICON, iconResource); 3331 streamItemValues.put(StreamItems.RES_LABEL, labelResource); 3332 streamItemValues.put(StreamItems.TIMESTAMP, 3333 timestamp == null ? System.currentTimeMillis() : timestamp); 3334 3335 // Note: The following is basically a workaround for the fact that status 3336 // updates didn't do any sort of account enforcement, while social stream item 3337 // updates do. We can't expect callers of the old API to start passing account 3338 // information along, so we just populate the account params appropriately for 3339 // the raw contact. Data set is not relevant here, as we only check account 3340 // name and type. 3341 if (accountName != null && accountType != null) { 3342 streamItemValues.put(RawContacts.ACCOUNT_NAME, accountName); 3343 streamItemValues.put(RawContacts.ACCOUNT_TYPE, accountType); 3344 } 3345 3346 // Check for an existing stream item from this source, and insert or update. 3347 Uri streamUri = StreamItems.CONTENT_URI; 3348 Cursor c = queryLocal(streamUri, new String[]{StreamItems._ID}, 3349 StreamItems.RAW_CONTACT_ID + "=?", 3350 new String[]{String.valueOf(rawContactId)}, 3351 null, -1 /* directory ID */, null); 3352 try { 3353 if (c.getCount() > 0) { 3354 c.moveToFirst(); 3355 updateInTransaction(ContentUris.withAppendedId(streamUri, c.getLong(0)), 3356 streamItemValues, null, null); 3357 } else { 3358 insertInTransaction(streamUri, streamItemValues); 3359 } 3360 } finally { 3361 c.close(); 3362 } 3363 } 3364 } 3365 } 3366 3367 if (contactId != -1) { 3368 mAggregator.get().updateLastStatusUpdateId(contactId); 3369 } 3370 3371 return dataId; 3372 } 3373 3374 /** Converts a status update to HTML. */ 3375 private String statusUpdateToHtml(String status) { 3376 return TextUtils.htmlEncode(status); 3377 } 3378 3379 private String getResourceName(Resources resources, String expectedType, Integer resourceId) { 3380 try { 3381 if (resourceId == null || resourceId == 0) return null; 3382 3383 // Resource has an invalid type (e.g. a string as icon)? ignore 3384 final String resourceEntryName = resources.getResourceEntryName(resourceId); 3385 final String resourceTypeName = resources.getResourceTypeName(resourceId); 3386 if (!expectedType.equals(resourceTypeName)) { 3387 Log.w(TAG, "Resource " + resourceId + " (" + resourceEntryName + ") is of type " + 3388 resourceTypeName + " but " + expectedType + " is required."); 3389 return null; 3390 } 3391 3392 return resourceEntryName; 3393 } catch (NotFoundException e) { 3394 return null; 3395 } 3396 } 3397 3398 @Override 3399 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 3400 if (VERBOSE_LOGGING) { 3401 Log.v(TAG, "deleteInTransaction: uri=" + uri + 3402 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs)); 3403 } 3404 3405 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3406 3407 flushTransactionalChanges(); 3408 final boolean callerIsSyncAdapter = 3409 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3410 final int match = sUriMatcher.match(uri); 3411 switch (match) { 3412 case SYNCSTATE: 3413 case PROFILE_SYNCSTATE: 3414 return mDbHelper.get().getSyncState().delete(db, selection, 3415 selectionArgs); 3416 3417 case SYNCSTATE_ID: { 3418 String selectionWithId = 3419 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3420 + (selection == null ? "" : " AND (" + selection + ")"); 3421 return mDbHelper.get().getSyncState().delete(db, selectionWithId, 3422 selectionArgs); 3423 } 3424 3425 case PROFILE_SYNCSTATE_ID: { 3426 String selectionWithId = 3427 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3428 + (selection == null ? "" : " AND (" + selection + ")"); 3429 return mProfileHelper.getSyncState().delete(db, selectionWithId, 3430 selectionArgs); 3431 } 3432 3433 case CONTACTS: { 3434 invalidateFastScrollingIndexCache(); 3435 // TODO 3436 return 0; 3437 } 3438 3439 case CONTACTS_ID: { 3440 invalidateFastScrollingIndexCache(); 3441 long contactId = ContentUris.parseId(uri); 3442 return deleteContact(contactId, callerIsSyncAdapter); 3443 } 3444 3445 case CONTACTS_LOOKUP: { 3446 invalidateFastScrollingIndexCache(); 3447 final List<String> pathSegments = uri.getPathSegments(); 3448 final int segmentCount = pathSegments.size(); 3449 if (segmentCount < 3) { 3450 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 3451 "Missing a lookup key", uri)); 3452 } 3453 final String lookupKey = pathSegments.get(2); 3454 final long contactId = lookupContactIdByLookupKey(db, lookupKey); 3455 return deleteContact(contactId, callerIsSyncAdapter); 3456 } 3457 3458 case CONTACTS_LOOKUP_ID: { 3459 invalidateFastScrollingIndexCache(); 3460 // lookup contact by id and lookup key to see if they still match the actual record 3461 final List<String> pathSegments = uri.getPathSegments(); 3462 final String lookupKey = pathSegments.get(2); 3463 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 3464 setTablesAndProjectionMapForContacts(lookupQb, uri, null); 3465 long contactId = ContentUris.parseId(uri); 3466 String[] args; 3467 if (selectionArgs == null) { 3468 args = new String[2]; 3469 } else { 3470 args = new String[selectionArgs.length + 2]; 3471 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 3472 } 3473 args[0] = String.valueOf(contactId); 3474 args[1] = Uri.encode(lookupKey); 3475 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?"); 3476 Cursor c = query(db, lookupQb, null, selection, args, null, null, 3477 null, null, null); 3478 try { 3479 if (c.getCount() == 1) { 3480 // contact was unmodified so go ahead and delete it 3481 return deleteContact(contactId, callerIsSyncAdapter); 3482 } else { 3483 // row was changed (e.g. the merging might have changed), we got multiple 3484 // rows or the supplied selection filtered the record out 3485 return 0; 3486 } 3487 } finally { 3488 c.close(); 3489 } 3490 } 3491 3492 case CONTACTS_DELETE_USAGE: { 3493 return deleteDataUsage(); 3494 } 3495 3496 case RAW_CONTACTS: 3497 case PROFILE_RAW_CONTACTS: { 3498 invalidateFastScrollingIndexCache(); 3499 int numDeletes = 0; 3500 Cursor c = db.query(Views.RAW_CONTACTS, 3501 new String[]{RawContacts._ID, RawContacts.CONTACT_ID}, 3502 appendAccountIdToSelection(uri, selection), selectionArgs, 3503 null, null, null); 3504 try { 3505 while (c.moveToNext()) { 3506 final long rawContactId = c.getLong(0); 3507 long contactId = c.getLong(1); 3508 numDeletes += deleteRawContact(rawContactId, contactId, 3509 callerIsSyncAdapter); 3510 } 3511 } finally { 3512 c.close(); 3513 } 3514 return numDeletes; 3515 } 3516 3517 case RAW_CONTACTS_ID: 3518 case PROFILE_RAW_CONTACTS_ID: { 3519 invalidateFastScrollingIndexCache(); 3520 final long rawContactId = ContentUris.parseId(uri); 3521 return deleteRawContact(rawContactId, mDbHelper.get().getContactId(rawContactId), 3522 callerIsSyncAdapter); 3523 } 3524 3525 case DATA: 3526 case PROFILE_DATA: { 3527 invalidateFastScrollingIndexCache(); 3528 mSyncToNetwork |= !callerIsSyncAdapter; 3529 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 3530 callerIsSyncAdapter); 3531 } 3532 3533 case DATA_ID: 3534 case PHONES_ID: 3535 case EMAILS_ID: 3536 case CALLABLES_ID: 3537 case POSTALS_ID: 3538 case PROFILE_DATA_ID: { 3539 invalidateFastScrollingIndexCache(); 3540 long dataId = ContentUris.parseId(uri); 3541 mSyncToNetwork |= !callerIsSyncAdapter; 3542 mSelectionArgs1[0] = String.valueOf(dataId); 3543 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter); 3544 } 3545 3546 case GROUPS_ID: { 3547 mSyncToNetwork |= !callerIsSyncAdapter; 3548 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter); 3549 } 3550 3551 case GROUPS: { 3552 int numDeletes = 0; 3553 Cursor c = db.query(Views.GROUPS, Projections.ID, 3554 appendAccountIdToSelection(uri, selection), selectionArgs, 3555 null, null, null); 3556 try { 3557 while (c.moveToNext()) { 3558 numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter); 3559 } 3560 } finally { 3561 c.close(); 3562 } 3563 if (numDeletes > 0) { 3564 mSyncToNetwork |= !callerIsSyncAdapter; 3565 } 3566 return numDeletes; 3567 } 3568 3569 case SETTINGS: { 3570 mSyncToNetwork |= !callerIsSyncAdapter; 3571 return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs); 3572 } 3573 3574 case STATUS_UPDATES: 3575 case PROFILE_STATUS_UPDATES: { 3576 return deleteStatusUpdates(selection, selectionArgs); 3577 } 3578 3579 case STREAM_ITEMS: { 3580 mSyncToNetwork |= !callerIsSyncAdapter; 3581 return deleteStreamItems(uri, new ContentValues(), selection, selectionArgs); 3582 } 3583 3584 case STREAM_ITEMS_ID: { 3585 mSyncToNetwork |= !callerIsSyncAdapter; 3586 return deleteStreamItems(uri, new ContentValues(), 3587 StreamItems._ID + "=?", 3588 new String[]{uri.getLastPathSegment()}); 3589 } 3590 3591 case RAW_CONTACTS_ID_STREAM_ITEMS_ID: { 3592 mSyncToNetwork |= !callerIsSyncAdapter; 3593 String rawContactId = uri.getPathSegments().get(1); 3594 String streamItemId = uri.getLastPathSegment(); 3595 return deleteStreamItems(uri, new ContentValues(), 3596 StreamItems.RAW_CONTACT_ID + "=? AND " + StreamItems._ID + "=?", 3597 new String[]{rawContactId, streamItemId}); 3598 3599 } 3600 3601 case STREAM_ITEMS_ID_PHOTOS: { 3602 mSyncToNetwork |= !callerIsSyncAdapter; 3603 String streamItemId = uri.getPathSegments().get(1); 3604 String selectionWithId = 3605 (StreamItemPhotos.STREAM_ITEM_ID + "=" + streamItemId + " ") 3606 + (selection == null ? "" : " AND (" + selection + ")"); 3607 return deleteStreamItemPhotos(uri, new ContentValues(), 3608 selectionWithId, selectionArgs); 3609 } 3610 3611 case STREAM_ITEMS_ID_PHOTOS_ID: { 3612 mSyncToNetwork |= !callerIsSyncAdapter; 3613 String streamItemId = uri.getPathSegments().get(1); 3614 String streamItemPhotoId = uri.getPathSegments().get(3); 3615 return deleteStreamItemPhotos(uri, new ContentValues(), 3616 StreamItemPhotosColumns.CONCRETE_ID + "=? AND " 3617 + StreamItemPhotos.STREAM_ITEM_ID + "=?", 3618 new String[]{streamItemPhotoId, streamItemId}); 3619 } 3620 3621 default: { 3622 mSyncToNetwork = true; 3623 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 3624 } 3625 } 3626 } 3627 3628 public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) { 3629 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3630 mGroupIdCache.clear(); 3631 final long groupMembershipMimetypeId = mDbHelper.get() 3632 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 3633 db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 3634 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 3635 + groupId, null); 3636 3637 try { 3638 if (callerIsSyncAdapter) { 3639 return db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 3640 } else { 3641 mValues.clear(); 3642 mValues.put(Groups.DELETED, 1); 3643 mValues.put(Groups.DIRTY, 1); 3644 return db.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, 3645 null); 3646 } 3647 } finally { 3648 mVisibleTouched = true; 3649 } 3650 } 3651 3652 private int deleteSettings(Uri uri, String selection, String[] selectionArgs) { 3653 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3654 final int count = db.delete(Tables.SETTINGS, selection, selectionArgs); 3655 mVisibleTouched = true; 3656 return count; 3657 } 3658 3659 private int deleteContact(long contactId, boolean callerIsSyncAdapter) { 3660 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3661 mSelectionArgs1[0] = Long.toString(contactId); 3662 Cursor c = db.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 3663 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, 3664 null, null, null); 3665 try { 3666 while (c.moveToNext()) { 3667 long rawContactId = c.getLong(0); 3668 markRawContactAsDeleted(db, rawContactId, callerIsSyncAdapter); 3669 } 3670 } finally { 3671 c.close(); 3672 } 3673 3674 mProviderStatusUpdateNeeded = true; 3675 3676 int result = ContactsTableUtil.deleteContact(db, contactId); 3677 scheduleBackgroundTask(BACKGROUND_TASK_CLEAN_DELETE_LOG); 3678 return result; 3679 } 3680 3681 public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) { 3682 mAggregator.get().invalidateAggregationExceptionCache(); 3683 mProviderStatusUpdateNeeded = true; 3684 3685 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3686 3687 // Find and delete stream items associated with the raw contact. 3688 Cursor c = db.query(Tables.STREAM_ITEMS, 3689 new String[]{StreamItems._ID}, 3690 StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)}, 3691 null, null, null); 3692 try { 3693 while (c.moveToNext()) { 3694 deleteStreamItem(db, c.getLong(0)); 3695 } 3696 } finally { 3697 c.close(); 3698 } 3699 3700 if (callerIsSyncAdapter || rawContactIsLocal(rawContactId)) { 3701 3702 // When a raw contact is deleted, a sqlite trigger deletes the parent contact. 3703 // TODO: all contact deletes was consolidated into ContactTableUtil but this one can't 3704 // because it's in a trigger. Consider removing trigger and replacing with java code. 3705 // This has to happen before the raw contact is deleted since it relies on the number 3706 // of raw contacts. 3707 ContactsTableUtil.deleteContactIfSingleton(db, rawContactId); 3708 3709 db.delete(Tables.PRESENCE, 3710 PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 3711 int count = db.delete(Tables.RAW_CONTACTS, 3712 RawContacts._ID + "=" + rawContactId, null); 3713 3714 mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId); 3715 mTransactionContext.get().markRawContactChangedOrDeletedOrInserted(rawContactId); 3716 return count; 3717 } else { 3718 ContactsTableUtil.deleteContactIfSingleton(db, rawContactId); 3719 return markRawContactAsDeleted(db, rawContactId, callerIsSyncAdapter); 3720 } 3721 } 3722 3723 /** 3724 * Returns whether the given raw contact ID is local (i.e. has no account associated with it). 3725 */ 3726 private boolean rawContactIsLocal(long rawContactId) { 3727 final SQLiteDatabase db = mDbHelper.get().getReadableDatabase(); 3728 Cursor c = db.query(Tables.RAW_CONTACTS, Projections.LITERAL_ONE, 3729 RawContactsColumns.CONCRETE_ID + "=? AND " + 3730 RawContactsColumns.ACCOUNT_ID + "=" + Clauses.LOCAL_ACCOUNT_ID, 3731 new String[] {String.valueOf(rawContactId)}, null, null, null); 3732 try { 3733 return c.getCount() > 0; 3734 } finally { 3735 c.close(); 3736 } 3737 } 3738 3739 private int deleteStatusUpdates(String selection, String[] selectionArgs) { 3740 // delete from both tables: presence and status_updates 3741 // TODO should account type/name be appended to the where clause? 3742 if (VERBOSE_LOGGING) { 3743 Log.v(TAG, "deleting data from status_updates for " + selection); 3744 } 3745 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3746 db.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection), 3747 selectionArgs); 3748 return db.delete(Tables.PRESENCE, selection, selectionArgs); 3749 } 3750 3751 private int deleteStreamItems(Uri uri, ContentValues values, String selection, 3752 String[] selectionArgs) { 3753 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3754 int count = 0; 3755 final Cursor c = db.query(Views.STREAM_ITEMS, Projections.ID, 3756 selection, selectionArgs, null, null, null); 3757 try { 3758 c.moveToPosition(-1); 3759 while (c.moveToNext()) { 3760 count += deleteStreamItem(db, c.getLong(0)); 3761 } 3762 } finally { 3763 c.close(); 3764 } 3765 return count; 3766 } 3767 3768 private int deleteStreamItem(SQLiteDatabase db, long streamItemId) { 3769 deleteStreamItemPhotos(streamItemId); 3770 return db.delete(Tables.STREAM_ITEMS, StreamItems._ID + "=?", 3771 new String[]{String.valueOf(streamItemId)}); 3772 } 3773 3774 private int deleteStreamItemPhotos(Uri uri, ContentValues values, String selection, 3775 String[] selectionArgs) { 3776 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3777 return db.delete(Tables.STREAM_ITEM_PHOTOS, selection, selectionArgs); 3778 } 3779 3780 private int deleteStreamItemPhotos(long streamItemId) { 3781 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3782 // Note that this does not enforce the modifying account. 3783 return db.delete(Tables.STREAM_ITEM_PHOTOS, 3784 StreamItemPhotos.STREAM_ITEM_ID + "=?", 3785 new String[]{String.valueOf(streamItemId)}); 3786 } 3787 3788 private int markRawContactAsDeleted(SQLiteDatabase db, long rawContactId, 3789 boolean callerIsSyncAdapter) { 3790 mSyncToNetwork = true; 3791 3792 mValues.clear(); 3793 mValues.put(RawContacts.DELETED, 1); 3794 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 3795 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 3796 mValues.putNull(RawContacts.CONTACT_ID); 3797 mValues.put(RawContacts.DIRTY, 1); 3798 return updateRawContact(db, rawContactId, mValues, callerIsSyncAdapter); 3799 } 3800 3801 private int deleteDataUsage() { 3802 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3803 db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " + 3804 Contacts.TIMES_CONTACTED + "=0," + 3805 Contacts.LAST_TIME_CONTACTED + "=NULL" 3806 ); 3807 db.execSQL("UPDATE " + Tables.CONTACTS + " SET " + 3808 Contacts.TIMES_CONTACTED + "=0," + 3809 Contacts.LAST_TIME_CONTACTED + "=NULL" 3810 ); 3811 db.delete(Tables.DATA_USAGE_STAT, null, null); 3812 3813 return 1; 3814 } 3815 3816 @Override 3817 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 3818 String[] selectionArgs) { 3819 if (VERBOSE_LOGGING) { 3820 Log.v(TAG, "updateInTransaction: uri=" + uri + 3821 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 3822 " values=[" + values + "]"); 3823 } 3824 3825 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 3826 3827 int count = 0; 3828 3829 final int match = sUriMatcher.match(uri); 3830 if (match == SYNCSTATE_ID && selection == null) { 3831 long rowId = ContentUris.parseId(uri); 3832 Object data = values.get(ContactsContract.SyncState.DATA); 3833 mTransactionContext.get().syncStateUpdated(rowId, data); 3834 return 1; 3835 } 3836 flushTransactionalChanges(); 3837 final boolean callerIsSyncAdapter = 3838 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 3839 switch(match) { 3840 case SYNCSTATE: 3841 case PROFILE_SYNCSTATE: 3842 return mDbHelper.get().getSyncState().update(db, values, 3843 appendAccountToSelection(uri, selection), selectionArgs); 3844 3845 case SYNCSTATE_ID: { 3846 selection = appendAccountToSelection(uri, selection); 3847 String selectionWithId = 3848 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3849 + (selection == null ? "" : " AND (" + selection + ")"); 3850 return mDbHelper.get().getSyncState().update(db, values, 3851 selectionWithId, selectionArgs); 3852 } 3853 3854 case PROFILE_SYNCSTATE_ID: { 3855 selection = appendAccountToSelection(uri, selection); 3856 String selectionWithId = 3857 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 3858 + (selection == null ? "" : " AND (" + selection + ")"); 3859 return mProfileHelper.getSyncState().update(db, values, 3860 selectionWithId, selectionArgs); 3861 } 3862 3863 case CONTACTS: 3864 case PROFILE: { 3865 invalidateFastScrollingIndexCache(); 3866 count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter); 3867 break; 3868 } 3869 3870 case CONTACTS_ID: { 3871 invalidateFastScrollingIndexCache(); 3872 count = updateContactOptions(db, ContentUris.parseId(uri), values, 3873 callerIsSyncAdapter); 3874 break; 3875 } 3876 3877 case CONTACTS_LOOKUP: 3878 case CONTACTS_LOOKUP_ID: { 3879 invalidateFastScrollingIndexCache(); 3880 final List<String> pathSegments = uri.getPathSegments(); 3881 final int segmentCount = pathSegments.size(); 3882 if (segmentCount < 3) { 3883 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 3884 "Missing a lookup key", uri)); 3885 } 3886 final String lookupKey = pathSegments.get(2); 3887 final long contactId = lookupContactIdByLookupKey(db, lookupKey); 3888 count = updateContactOptions(db, contactId, values, callerIsSyncAdapter); 3889 break; 3890 } 3891 3892 case RAW_CONTACTS_ID_DATA: 3893 case PROFILE_RAW_CONTACTS_ID_DATA: { 3894 invalidateFastScrollingIndexCache(); 3895 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2; 3896 final String rawContactId = uri.getPathSegments().get(segment); 3897 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ") 3898 + (selection == null ? "" : " AND " + selection); 3899 3900 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter); 3901 3902 break; 3903 } 3904 3905 case DATA: 3906 case PROFILE_DATA: { 3907 invalidateFastScrollingIndexCache(); 3908 count = updateData(uri, values, appendAccountToSelection(uri, selection), 3909 selectionArgs, callerIsSyncAdapter); 3910 if (count > 0) { 3911 mSyncToNetwork |= !callerIsSyncAdapter; 3912 } 3913 break; 3914 } 3915 3916 case DATA_ID: 3917 case PHONES_ID: 3918 case EMAILS_ID: 3919 case CALLABLES_ID: 3920 case POSTALS_ID: { 3921 invalidateFastScrollingIndexCache(); 3922 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); 3923 if (count > 0) { 3924 mSyncToNetwork |= !callerIsSyncAdapter; 3925 } 3926 break; 3927 } 3928 3929 case RAW_CONTACTS: 3930 case PROFILE_RAW_CONTACTS: { 3931 invalidateFastScrollingIndexCache(); 3932 selection = appendAccountIdToSelection(uri, selection); 3933 count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter); 3934 break; 3935 } 3936 3937 case RAW_CONTACTS_ID: { 3938 invalidateFastScrollingIndexCache(); 3939 long rawContactId = ContentUris.parseId(uri); 3940 if (selection != null) { 3941 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 3942 count = updateRawContacts(values, RawContacts._ID + "=?" 3943 + " AND(" + selection + ")", selectionArgs, 3944 callerIsSyncAdapter); 3945 } else { 3946 mSelectionArgs1[0] = String.valueOf(rawContactId); 3947 count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1, 3948 callerIsSyncAdapter); 3949 } 3950 break; 3951 } 3952 3953 case GROUPS: { 3954 count = updateGroups(uri, values, appendAccountIdToSelection(uri, selection), 3955 selectionArgs, callerIsSyncAdapter); 3956 if (count > 0) { 3957 mSyncToNetwork |= !callerIsSyncAdapter; 3958 } 3959 break; 3960 } 3961 3962 case GROUPS_ID: { 3963 long groupId = ContentUris.parseId(uri); 3964 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId)); 3965 String selectionWithId = Groups._ID + "=? " 3966 + (selection == null ? "" : " AND " + selection); 3967 count = updateGroups(uri, values, selectionWithId, selectionArgs, 3968 callerIsSyncAdapter); 3969 if (count > 0) { 3970 mSyncToNetwork |= !callerIsSyncAdapter; 3971 } 3972 break; 3973 } 3974 3975 case AGGREGATION_EXCEPTIONS: { 3976 count = updateAggregationException(db, values); 3977 invalidateFastScrollingIndexCache(); 3978 break; 3979 } 3980 3981 case SETTINGS: { 3982 count = updateSettings(uri, values, appendAccountToSelection(uri, selection), 3983 selectionArgs); 3984 mSyncToNetwork |= !callerIsSyncAdapter; 3985 break; 3986 } 3987 3988 case STATUS_UPDATES: 3989 case PROFILE_STATUS_UPDATES: { 3990 count = updateStatusUpdate(uri, values, selection, selectionArgs); 3991 break; 3992 } 3993 3994 case STREAM_ITEMS: { 3995 count = updateStreamItems(uri, values, selection, selectionArgs); 3996 break; 3997 } 3998 3999 case STREAM_ITEMS_ID: { 4000 count = updateStreamItems(uri, values, StreamItems._ID + "=?", 4001 new String[]{uri.getLastPathSegment()}); 4002 break; 4003 } 4004 4005 case RAW_CONTACTS_ID_STREAM_ITEMS_ID: { 4006 String rawContactId = uri.getPathSegments().get(1); 4007 String streamItemId = uri.getLastPathSegment(); 4008 count = updateStreamItems(uri, values, 4009 StreamItems.RAW_CONTACT_ID + "=? AND " + StreamItems._ID + "=?", 4010 new String[]{rawContactId, streamItemId}); 4011 break; 4012 } 4013 4014 case STREAM_ITEMS_PHOTOS: { 4015 count = updateStreamItemPhotos(uri, values, selection, selectionArgs); 4016 break; 4017 } 4018 4019 case STREAM_ITEMS_ID_PHOTOS: { 4020 String streamItemId = uri.getPathSegments().get(1); 4021 count = updateStreamItemPhotos(uri, values, 4022 StreamItemPhotos.STREAM_ITEM_ID + "=?", new String[]{streamItemId}); 4023 break; 4024 } 4025 4026 case STREAM_ITEMS_ID_PHOTOS_ID: { 4027 String streamItemId = uri.getPathSegments().get(1); 4028 String streamItemPhotoId = uri.getPathSegments().get(3); 4029 count = updateStreamItemPhotos(uri, values, 4030 StreamItemPhotosColumns.CONCRETE_ID + "=? AND " + 4031 StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?", 4032 new String[]{streamItemPhotoId, streamItemId}); 4033 break; 4034 } 4035 4036 case DIRECTORIES: { 4037 mContactDirectoryManager.scanPackagesByUid(Binder.getCallingUid()); 4038 count = 1; 4039 break; 4040 } 4041 4042 case DATA_USAGE_FEEDBACK_ID: { 4043 if (handleDataUsageFeedback(uri)) { 4044 count = 1; 4045 } else { 4046 count = 0; 4047 } 4048 break; 4049 } 4050 4051 case PINNED_POSITION_UPDATE: { 4052 final boolean forceStarWhenPinning = uri.getBooleanQueryParameter( 4053 PinnedPositions.STAR_WHEN_PINNING, false); 4054 count = handlePinningUpdate(values, forceStarWhenPinning); 4055 break; 4056 } 4057 4058 default: { 4059 mSyncToNetwork = true; 4060 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 4061 } 4062 } 4063 4064 return count; 4065 } 4066 4067 private int updateStatusUpdate(Uri uri, ContentValues values, String selection, 4068 String[] selectionArgs) { 4069 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4070 // update status_updates table, if status is provided 4071 // TODO should account type/name be appended to the where clause? 4072 int updateCount = 0; 4073 ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values); 4074 if (settableValues.size() > 0) { 4075 updateCount = db.update(Tables.STATUS_UPDATES, 4076 settableValues, 4077 getWhereClauseForStatusUpdatesTable(selection), 4078 selectionArgs); 4079 } 4080 4081 // now update the Presence table 4082 settableValues = getSettableColumnsForPresenceTable(values); 4083 if (settableValues.size() > 0) { 4084 updateCount = db.update(Tables.PRESENCE, settableValues, 4085 selection, selectionArgs); 4086 } 4087 // TODO updateCount is not entirely a valid count of updated rows because 2 tables could 4088 // potentially get updated in this method. 4089 return updateCount; 4090 } 4091 4092 private int updateStreamItems(Uri uri, ContentValues values, String selection, 4093 String[] selectionArgs) { 4094 // Stream items can't be moved to a new raw contact. 4095 values.remove(StreamItems.RAW_CONTACT_ID); 4096 4097 // Don't attempt to update accounts params - they don't exist in the stream items table. 4098 values.remove(RawContacts.ACCOUNT_NAME); 4099 values.remove(RawContacts.ACCOUNT_TYPE); 4100 4101 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4102 4103 // If there's been no exception, the update should be fine. 4104 return db.update(Tables.STREAM_ITEMS, values, selection, selectionArgs); 4105 } 4106 4107 private int updateStreamItemPhotos(Uri uri, ContentValues values, String selection, 4108 String[] selectionArgs) { 4109 // Stream item photos can't be moved to a new stream item. 4110 values.remove(StreamItemPhotos.STREAM_ITEM_ID); 4111 4112 // Don't attempt to update accounts params - they don't exist in the stream item 4113 // photos table. 4114 values.remove(RawContacts.ACCOUNT_NAME); 4115 values.remove(RawContacts.ACCOUNT_TYPE); 4116 4117 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4118 4119 // Process the photo (since we're updating, it's valid for the photo to not be present). 4120 if (processStreamItemPhoto(values, true)) { 4121 // If there's been no exception, the update should be fine. 4122 return db.update(Tables.STREAM_ITEM_PHOTOS, values, selection, 4123 selectionArgs); 4124 } 4125 return 0; 4126 } 4127 4128 /** 4129 * Build a where clause to select the rows to be updated in status_updates table. 4130 */ 4131 private String getWhereClauseForStatusUpdatesTable(String selection) { 4132 mSb.setLength(0); 4133 mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE); 4134 mSb.append(selection); 4135 mSb.append(")"); 4136 return mSb.toString(); 4137 } 4138 4139 private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) { 4140 mValues.clear(); 4141 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values, 4142 StatusUpdates.STATUS); 4143 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values, 4144 StatusUpdates.STATUS_TIMESTAMP); 4145 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values, 4146 StatusUpdates.STATUS_RES_PACKAGE); 4147 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values, 4148 StatusUpdates.STATUS_LABEL); 4149 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values, 4150 StatusUpdates.STATUS_ICON); 4151 return mValues; 4152 } 4153 4154 private ContentValues getSettableColumnsForPresenceTable(ContentValues values) { 4155 mValues.clear(); 4156 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values, 4157 StatusUpdates.PRESENCE); 4158 ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values, 4159 StatusUpdates.CHAT_CAPABILITY); 4160 return mValues; 4161 } 4162 4163 private interface GroupAccountQuery { 4164 String TABLE = Views.GROUPS; 4165 4166 String[] COLUMNS = new String[] { 4167 Groups._ID, 4168 Groups.ACCOUNT_TYPE, 4169 Groups.ACCOUNT_NAME, 4170 Groups.DATA_SET, 4171 }; 4172 int ID = 0; 4173 int ACCOUNT_TYPE = 1; 4174 int ACCOUNT_NAME = 2; 4175 int DATA_SET = 3; 4176 } 4177 4178 private int updateGroups(Uri uri, ContentValues originalValues, String selectionWithId, 4179 String[] selectionArgs, boolean callerIsSyncAdapter) { 4180 mGroupIdCache.clear(); 4181 4182 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4183 final ContactsDatabaseHelper dbHelper = mDbHelper.get(); 4184 4185 final ContentValues updatedValues = new ContentValues(); 4186 updatedValues.putAll(originalValues); 4187 4188 if (!callerIsSyncAdapter && !updatedValues.containsKey(Groups.DIRTY)) { 4189 updatedValues.put(Groups.DIRTY, 1); 4190 } 4191 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 4192 mVisibleTouched = true; 4193 } 4194 4195 // Prepare for account change 4196 final boolean isAccountNameChanging = updatedValues.containsKey(Groups.ACCOUNT_NAME); 4197 final boolean isAccountTypeChanging = updatedValues.containsKey(Groups.ACCOUNT_TYPE); 4198 final boolean isDataSetChanging = updatedValues.containsKey(Groups.DATA_SET); 4199 final boolean isAccountChanging = isAccountNameChanging || isAccountTypeChanging 4200 || isDataSetChanging; 4201 final String updatedAccountName = updatedValues.getAsString(Groups.ACCOUNT_NAME); 4202 final String updatedAccountType = updatedValues.getAsString(Groups.ACCOUNT_TYPE); 4203 final String updatedDataSet = updatedValues.getAsString(Groups.DATA_SET); 4204 4205 updatedValues.remove(Groups.ACCOUNT_NAME); 4206 updatedValues.remove(Groups.ACCOUNT_TYPE); 4207 updatedValues.remove(Groups.DATA_SET); 4208 4209 // We later call requestSync() on all affected accounts. 4210 final Set<Account> affectedAccounts = Sets.newHashSet(); 4211 4212 // Look for all affected rows, and change them row by row. 4213 final Cursor c = db.query(GroupAccountQuery.TABLE, GroupAccountQuery.COLUMNS, 4214 selectionWithId, selectionArgs, null, null, null); 4215 int returnCount = 0; 4216 try { 4217 c.moveToPosition(-1); 4218 while (c.moveToNext()) { 4219 final long groupId = c.getLong(GroupAccountQuery.ID); 4220 4221 mSelectionArgs1[0] = Long.toString(groupId); 4222 4223 final String accountName = isAccountNameChanging 4224 ? updatedAccountName : c.getString(GroupAccountQuery.ACCOUNT_NAME); 4225 final String accountType = isAccountTypeChanging 4226 ? updatedAccountType : c.getString(GroupAccountQuery.ACCOUNT_TYPE); 4227 final String dataSet = isDataSetChanging 4228 ? updatedDataSet : c.getString(GroupAccountQuery.DATA_SET); 4229 4230 if (isAccountChanging) { 4231 final long accountId = dbHelper.getOrCreateAccountIdInTransaction( 4232 AccountWithDataSet.get(accountName, accountType, dataSet)); 4233 updatedValues.put(GroupsColumns.ACCOUNT_ID, accountId); 4234 } 4235 4236 // Finally do the actual update. 4237 final int count = db.update(Tables.GROUPS, updatedValues, 4238 GroupsColumns.CONCRETE_ID + "=?", mSelectionArgs1); 4239 4240 if ((count > 0) 4241 && !TextUtils.isEmpty(accountName) 4242 && !TextUtils.isEmpty(accountType)) { 4243 affectedAccounts.add(new Account(accountName, accountType)); 4244 } 4245 4246 returnCount += count; 4247 } 4248 } finally { 4249 c.close(); 4250 } 4251 4252 // TODO: This will not work for groups that have a data set specified, since the content 4253 // resolver will not be able to request a sync for the right source (unless it is updated 4254 // to key off account with data set). 4255 // i.e. requestSync only takes Account, not AccountWithDataSet. 4256 if (flagIsSet(updatedValues, Groups.SHOULD_SYNC)) { 4257 for (Account account : affectedAccounts) { 4258 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle()); 4259 } 4260 } 4261 return returnCount; 4262 } 4263 4264 private int updateSettings(Uri uri, ContentValues values, String selection, 4265 String[] selectionArgs) { 4266 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4267 final int count = db.update(Tables.SETTINGS, values, selection, selectionArgs); 4268 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 4269 mVisibleTouched = true; 4270 } 4271 return count; 4272 } 4273 4274 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs, 4275 boolean callerIsSyncAdapter) { 4276 if (values.containsKey(RawContacts.CONTACT_ID)) { 4277 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 4278 "in content values. Contact IDs are assigned automatically"); 4279 } 4280 4281 if (!callerIsSyncAdapter) { 4282 selection = DatabaseUtils.concatenateWhere(selection, 4283 RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0"); 4284 } 4285 4286 int count = 0; 4287 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4288 Cursor cursor = db.query(Views.RAW_CONTACTS, 4289 Projections.ID, selection, 4290 selectionArgs, null, null, null); 4291 try { 4292 while (cursor.moveToNext()) { 4293 long rawContactId = cursor.getLong(0); 4294 updateRawContact(db, rawContactId, values, callerIsSyncAdapter); 4295 count++; 4296 } 4297 } finally { 4298 cursor.close(); 4299 } 4300 4301 return count; 4302 } 4303 4304 private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values, 4305 boolean callerIsSyncAdapter) { 4306 final String selection = RawContactsColumns.CONCRETE_ID + " = ?"; 4307 mSelectionArgs1[0] = Long.toString(rawContactId); 4308 4309 final ContactsDatabaseHelper dbHelper = mDbHelper.get(); 4310 4311 final boolean requestUndoDelete = flagIsClear(values, RawContacts.DELETED); 4312 4313 final boolean isAccountNameChanging = values.containsKey(RawContacts.ACCOUNT_NAME); 4314 final boolean isAccountTypeChanging = values.containsKey(RawContacts.ACCOUNT_TYPE); 4315 final boolean isDataSetChanging = values.containsKey(RawContacts.DATA_SET); 4316 final boolean isAccountChanging = isAccountNameChanging || isAccountTypeChanging 4317 || isDataSetChanging; 4318 4319 int previousDeleted = 0; 4320 long accountId = 0; 4321 String oldAccountType = null; 4322 String oldAccountName = null; 4323 String oldDataSet = null; 4324 4325 if (requestUndoDelete || isAccountChanging) { 4326 Cursor cursor = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, 4327 selection, mSelectionArgs1, null, null, null); 4328 try { 4329 if (cursor.moveToFirst()) { 4330 previousDeleted = cursor.getInt(RawContactsQuery.DELETED); 4331 accountId = cursor.getLong(RawContactsQuery.ACCOUNT_ID); 4332 oldAccountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE); 4333 oldAccountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME); 4334 oldDataSet = cursor.getString(RawContactsQuery.DATA_SET); 4335 } 4336 } finally { 4337 cursor.close(); 4338 } 4339 if (isAccountChanging) { 4340 // We can't change the original ContentValues, as it'll be re-used over all 4341 // updateRawContact invocations in a transaction, so we need to create a new one. 4342 // (However we don't want to use mValues here, because mValues may be used in some 4343 // other methods that are called by this method.) 4344 final ContentValues originalValues = values; 4345 values = new ContentValues(); 4346 values.clear(); 4347 values.putAll(originalValues); 4348 4349 final AccountWithDataSet newAccountWithDataSet = AccountWithDataSet.get( 4350 isAccountNameChanging 4351 ? values.getAsString(RawContacts.ACCOUNT_NAME) : oldAccountName, 4352 isAccountTypeChanging 4353 ? values.getAsString(RawContacts.ACCOUNT_TYPE) : oldAccountType, 4354 isDataSetChanging 4355 ? values.getAsString(RawContacts.DATA_SET) : oldDataSet 4356 ); 4357 accountId = dbHelper.getOrCreateAccountIdInTransaction(newAccountWithDataSet); 4358 4359 values.put(RawContactsColumns.ACCOUNT_ID, accountId); 4360 4361 values.remove(RawContacts.ACCOUNT_NAME); 4362 values.remove(RawContacts.ACCOUNT_TYPE); 4363 values.remove(RawContacts.DATA_SET); 4364 } 4365 } 4366 if (requestUndoDelete) { 4367 values.put(ContactsContract.RawContacts.AGGREGATION_MODE, 4368 ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT); 4369 } 4370 4371 int count = db.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1); 4372 if (count != 0) { 4373 int aggregationMode = getIntValue(values, RawContacts.AGGREGATION_MODE, 4374 RawContacts.AGGREGATION_MODE_DEFAULT); 4375 // As per ContactsContract documentation, changing aggregation mode 4376 // to DEFAULT should not trigger aggregation 4377 if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) { 4378 mAggregator.get().markForAggregation(rawContactId, aggregationMode, false); 4379 } 4380 if (flagExists(values, RawContacts.STARRED)) { 4381 if (!callerIsSyncAdapter) { 4382 updateFavoritesMembership(rawContactId, 4383 flagIsSet(values, RawContacts.STARRED)); 4384 } 4385 mAggregator.get().updateStarred(rawContactId); 4386 mAggregator.get().updatePinned(rawContactId); 4387 } else { 4388 // if this raw contact is being associated with an account, then update the 4389 // favorites group membership based on whether or not this contact is starred. 4390 // If it is starred, add a group membership, if one doesn't already exist 4391 // otherwise delete any matching group memberships. 4392 if (!callerIsSyncAdapter && isAccountChanging) { 4393 boolean starred = 0 != DatabaseUtils.longForQuery(db, 4394 SELECTION_STARRED_FROM_RAW_CONTACTS, 4395 new String[]{Long.toString(rawContactId)}); 4396 updateFavoritesMembership(rawContactId, starred); 4397 } 4398 } 4399 4400 // if this raw contact is being associated with an account, then add a 4401 // group membership to the group marked as AutoAdd, if any. 4402 if (!callerIsSyncAdapter && isAccountChanging) { 4403 addAutoAddMembership(rawContactId); 4404 } 4405 4406 if (values.containsKey(RawContacts.SOURCE_ID)) { 4407 mAggregator.get().updateLookupKeyForRawContact(db, rawContactId); 4408 } 4409 if (flagExists(values, RawContacts.NAME_VERIFIED)) { 4410 // If setting NAME_VERIFIED for this raw contact, reset it for all 4411 // other raw contacts in the same aggregate 4412 if (flagIsSet(values, RawContacts.NAME_VERIFIED)) { 4413 mDbHelper.get().resetNameVerifiedForOtherRawContacts(rawContactId); 4414 } 4415 mAggregator.get().updateDisplayNameForRawContact(db, rawContactId); 4416 } 4417 if (requestUndoDelete && previousDeleted == 1) { 4418 // Note before the accounts refactoring, we used to use the *old* account here, 4419 // which doesn't make sense, so now we pass the *new* account. 4420 // (In practice it doesn't matter because there's probably no apps that undo-delete 4421 // and change accounts at the same time.) 4422 mTransactionContext.get().rawContactInserted(rawContactId, accountId); 4423 } 4424 mTransactionContext.get().markRawContactChangedOrDeletedOrInserted(rawContactId); 4425 } 4426 return count; 4427 } 4428 4429 private int updateData(Uri uri, ContentValues values, String selection, 4430 String[] selectionArgs, boolean callerIsSyncAdapter) { 4431 mValues.clear(); 4432 mValues.putAll(values); 4433 mValues.remove(Data._ID); 4434 mValues.remove(Data.RAW_CONTACT_ID); 4435 mValues.remove(Data.MIMETYPE); 4436 4437 String packageName = values.getAsString(Data.RES_PACKAGE); 4438 if (packageName != null) { 4439 mValues.remove(Data.RES_PACKAGE); 4440 mValues.put(DataColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName)); 4441 } 4442 4443 if (!callerIsSyncAdapter) { 4444 selection = DatabaseUtils.concatenateWhere(selection, 4445 Data.IS_READ_ONLY + "=0"); 4446 } 4447 4448 int count = 0; 4449 4450 // Note that the query will return data according to the access restrictions, 4451 // so we don't need to worry about updating data we don't have permission to read. 4452 Cursor c = queryLocal(uri, 4453 DataRowHandler.DataUpdateQuery.COLUMNS, 4454 selection, selectionArgs, null, -1 /* directory ID */, null); 4455 try { 4456 while(c.moveToNext()) { 4457 count += updateData(mValues, c, callerIsSyncAdapter); 4458 } 4459 } finally { 4460 c.close(); 4461 } 4462 4463 return count; 4464 } 4465 4466 private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { 4467 if (values.size() == 0) { 4468 return 0; 4469 } 4470 4471 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4472 4473 final String mimeType = c.getString(DataRowHandler.DataUpdateQuery.MIMETYPE); 4474 DataRowHandler rowHandler = getDataRowHandler(mimeType); 4475 boolean updated = 4476 rowHandler.update(db, mTransactionContext.get(), values, c, 4477 callerIsSyncAdapter); 4478 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 4479 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); 4480 } 4481 return updated ? 1 : 0; 4482 } 4483 4484 private int updateContactOptions(ContentValues values, String selection, 4485 String[] selectionArgs, boolean callerIsSyncAdapter) { 4486 int count = 0; 4487 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4488 4489 Cursor cursor = db.query(Views.CONTACTS, 4490 new String[] { Contacts._ID }, selection, selectionArgs, null, null, null); 4491 try { 4492 while (cursor.moveToNext()) { 4493 long contactId = cursor.getLong(0); 4494 4495 updateContactOptions(db, contactId, values, callerIsSyncAdapter); 4496 count++; 4497 } 4498 } finally { 4499 cursor.close(); 4500 } 4501 4502 return count; 4503 } 4504 4505 private int updateContactOptions(SQLiteDatabase db, long contactId, ContentValues values, 4506 boolean callerIsSyncAdapter) { 4507 4508 mValues.clear(); 4509 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 4510 values, Contacts.CUSTOM_RINGTONE); 4511 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 4512 values, Contacts.SEND_TO_VOICEMAIL); 4513 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 4514 values, Contacts.LAST_TIME_CONTACTED); 4515 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 4516 values, Contacts.TIMES_CONTACTED); 4517 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 4518 values, Contacts.STARRED); 4519 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.PINNED, 4520 values, Contacts.PINNED); 4521 4522 // Nothing to update - just return 4523 if (mValues.size() == 0) { 4524 return 0; 4525 } 4526 4527 boolean hasStarredValue = flagExists(mValues, RawContacts.STARRED); 4528 if (hasStarredValue) { 4529 // Mark dirty when changing starred to trigger sync 4530 mValues.put(RawContacts.DIRTY, 1); 4531 } 4532 4533 mSelectionArgs1[0] = String.valueOf(contactId); 4534 db.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?" 4535 + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1); 4536 4537 if (hasStarredValue && !callerIsSyncAdapter) { 4538 Cursor cursor = db.query(Views.RAW_CONTACTS, 4539 new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?", 4540 mSelectionArgs1, null, null, null); 4541 try { 4542 while (cursor.moveToNext()) { 4543 long rawContactId = cursor.getLong(0); 4544 updateFavoritesMembership(rawContactId, 4545 flagIsSet(mValues, RawContacts.STARRED)); 4546 } 4547 } finally { 4548 cursor.close(); 4549 } 4550 } 4551 4552 // Copy changeable values to prevent automatically managed fields from 4553 // being explicitly updated by clients. 4554 mValues.clear(); 4555 ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 4556 values, Contacts.CUSTOM_RINGTONE); 4557 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 4558 values, Contacts.SEND_TO_VOICEMAIL); 4559 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 4560 values, Contacts.LAST_TIME_CONTACTED); 4561 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 4562 values, Contacts.TIMES_CONTACTED); 4563 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED, 4564 values, Contacts.STARRED); 4565 ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.PINNED, 4566 values, Contacts.PINNED); 4567 mValues.put(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP, 4568 Clock.getInstance().currentTimeMillis()); 4569 4570 int rslt = db.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", 4571 mSelectionArgs1); 4572 4573 if (values.containsKey(Contacts.LAST_TIME_CONTACTED) && 4574 !values.containsKey(Contacts.TIMES_CONTACTED)) { 4575 db.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1); 4576 db.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1); 4577 } 4578 return rslt; 4579 } 4580 4581 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 4582 Integer exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 4583 Long rcId1 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID1); 4584 Long rcId2 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID2); 4585 if (exceptionType == null || rcId1 == null || rcId2 == null) { 4586 return 0; 4587 } 4588 4589 long rawContactId1; 4590 long rawContactId2; 4591 if (rcId1 < rcId2) { 4592 rawContactId1 = rcId1; 4593 rawContactId2 = rcId2; 4594 } else { 4595 rawContactId2 = rcId1; 4596 rawContactId1 = rcId2; 4597 } 4598 4599 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 4600 mSelectionArgs2[0] = String.valueOf(rawContactId1); 4601 mSelectionArgs2[1] = String.valueOf(rawContactId2); 4602 db.delete(Tables.AGGREGATION_EXCEPTIONS, 4603 AggregationExceptions.RAW_CONTACT_ID1 + "=? AND " 4604 + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2); 4605 } else { 4606 ContentValues exceptionValues = new ContentValues(3); 4607 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 4608 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 4609 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 4610 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 4611 exceptionValues); 4612 } 4613 4614 mAggregator.get().invalidateAggregationExceptionCache(); 4615 mAggregator.get().markForAggregation(rawContactId1, 4616 RawContacts.AGGREGATION_MODE_DEFAULT, true); 4617 mAggregator.get().markForAggregation(rawContactId2, 4618 RawContacts.AGGREGATION_MODE_DEFAULT, true); 4619 4620 mAggregator.get().aggregateContact(mTransactionContext.get(), db, rawContactId1); 4621 mAggregator.get().aggregateContact(mTransactionContext.get(), db, rawContactId2); 4622 4623 // The return value is fake - we just confirm that we made a change, not count actual 4624 // rows changed. 4625 return 1; 4626 } 4627 4628 @Override 4629 public void onAccountsUpdated(Account[] accounts) { 4630 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS); 4631 } 4632 4633 private static final String ACCOUNT_STRING_SEPARATOR_OUTER = "\u0001"; 4634 private static final String ACCOUNT_STRING_SEPARATOR_INNER = "\u0002"; 4635 4636 /** return serialized version of {@code accounts} */ 4637 @VisibleForTesting 4638 static String accountsToString(Set<Account> accounts) { 4639 final StringBuilder sb = new StringBuilder(); 4640 for (Account account : accounts) { 4641 if (sb.length() > 0) { 4642 sb.append(ACCOUNT_STRING_SEPARATOR_OUTER); 4643 } 4644 sb.append(account.name); 4645 sb.append(ACCOUNT_STRING_SEPARATOR_INNER); 4646 sb.append(account.type); 4647 } 4648 return sb.toString(); 4649 } 4650 4651 /** 4652 * de-serialize string returned by {@link #accountsToString} and return it. 4653 * If {@code accountsString} is malformed it'll throw {@link IllegalArgumentException}. 4654 */ 4655 @VisibleForTesting 4656 static Set<Account> stringToAccounts(String accountsString) { 4657 final Set<Account> ret = Sets.newHashSet(); 4658 if (accountsString.length() == 0) return ret; // no accounts 4659 try { 4660 for (String accountString : accountsString.split(ACCOUNT_STRING_SEPARATOR_OUTER)) { 4661 String[] nameAndType = accountString.split(ACCOUNT_STRING_SEPARATOR_INNER); 4662 ret.add(new Account(nameAndType[0], nameAndType[1])); 4663 } 4664 return ret; 4665 } catch (RuntimeException ex) { 4666 throw new IllegalArgumentException("Malformed string", ex); 4667 } 4668 } 4669 4670 /** 4671 * @return {@code true} if the given {@code currentSystemAccounts} are different from the 4672 * accounts we know, which are stored in the {@link DbProperties#KNOWN_ACCOUNTS} property. 4673 */ 4674 @VisibleForTesting 4675 boolean haveAccountsChanged(Account[] currentSystemAccounts) { 4676 final ContactsDatabaseHelper dbHelper = mDbHelper.get(); 4677 final Set<Account> knownAccountSet; 4678 try { 4679 knownAccountSet = stringToAccounts( 4680 dbHelper.getProperty(DbProperties.KNOWN_ACCOUNTS, "")); 4681 } catch (IllegalArgumentException e) { 4682 // Failed to get the last known accounts for an unknown reason. Let's just 4683 // treat as if accounts have changed. 4684 return true; 4685 } 4686 final Set<Account> currentAccounts = Sets.newHashSet(currentSystemAccounts); 4687 return !knownAccountSet.equals(currentAccounts); 4688 } 4689 4690 @VisibleForTesting 4691 void saveAccounts(Account[] systemAccounts) { 4692 final ContactsDatabaseHelper dbHelper = mDbHelper.get(); 4693 dbHelper.setProperty(DbProperties.KNOWN_ACCOUNTS, 4694 accountsToString(Sets.newHashSet(systemAccounts))); 4695 } 4696 4697 private boolean updateAccountsInBackground(Account[] systemAccounts) { 4698 if (!haveAccountsChanged(systemAccounts)) { 4699 return false; 4700 } 4701 if ("1".equals(SystemProperties.get(DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA))) { 4702 Log.w(TAG, "Accounts changed, but not removing stale data for " + 4703 DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA); 4704 return true; 4705 } 4706 Log.i(TAG, "Accounts changed"); 4707 4708 invalidateFastScrollingIndexCache(); 4709 4710 final ContactsDatabaseHelper dbHelper = mDbHelper.get(); 4711 final SQLiteDatabase db = dbHelper.getWritableDatabase(); 4712 db.beginTransaction(); 4713 4714 // WARNING: This method can be run in either contacts mode or profile mode. It is 4715 // absolutely imperative that no calls be made inside the following try block that can 4716 // interact with a specific contacts or profile DB. Otherwise it is quite possible for a 4717 // deadlock to occur. i.e. always use the current database in mDbHelper and do not access 4718 // mContactsHelper or mProfileHelper directly. 4719 // 4720 // The problem may be a bit more subtle if you also access something that stores the current 4721 // db instance in it's constructor. updateSearchIndexInTransaction relies on the 4722 // SearchIndexManager which upon construction, stores the current db. In this case, 4723 // SearchIndexManager always contains the contact DB. This is why the 4724 // updateSearchIndexInTransaction is protected with !isInProfileMode now. 4725 try { 4726 // First, remove stale rows from raw_contacts, groups, and related tables. 4727 4728 // All accounts that are used in raw_contacts and/or groups. 4729 final Set<AccountWithDataSet> knownAccountsWithDataSets 4730 = dbHelper.getAllAccountsWithDataSets(); 4731 4732 // Find the accounts that have been removed. 4733 final List<AccountWithDataSet> accountsWithDataSetsToDelete = Lists.newArrayList(); 4734 for (AccountWithDataSet knownAccountWithDataSet : knownAccountsWithDataSets) { 4735 if (knownAccountWithDataSet.isLocalAccount() 4736 || knownAccountWithDataSet.inSystemAccounts(systemAccounts)) { 4737 continue; 4738 } 4739 accountsWithDataSetsToDelete.add(knownAccountWithDataSet); 4740 } 4741 4742 if (!accountsWithDataSetsToDelete.isEmpty()) { 4743 for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) { 4744 Log.d(TAG, "removing data for removed account " + accountWithDataSet); 4745 final Long accountIdOrNull = dbHelper.getAccountIdOrNull(accountWithDataSet); 4746 4747 // getAccountIdOrNull() really shouldn't return null here, but just in case... 4748 if (accountIdOrNull != null) { 4749 final String accountId = Long.toString(accountIdOrNull); 4750 final String[] accountIdParams = 4751 new String[] {accountId}; 4752 db.execSQL( 4753 "DELETE FROM " + Tables.GROUPS + 4754 " WHERE " + GroupsColumns.ACCOUNT_ID + " = ?", 4755 accountIdParams); 4756 db.execSQL( 4757 "DELETE FROM " + Tables.PRESENCE + 4758 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" + 4759 "SELECT " + RawContacts._ID + 4760 " FROM " + Tables.RAW_CONTACTS + 4761 " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)", 4762 accountIdParams); 4763 db.execSQL( 4764 "DELETE FROM " + Tables.STREAM_ITEM_PHOTOS + 4765 " WHERE " + StreamItemPhotos.STREAM_ITEM_ID + " IN (" + 4766 "SELECT " + StreamItems._ID + 4767 " FROM " + Tables.STREAM_ITEMS + 4768 " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" + 4769 "SELECT " + RawContacts._ID + 4770 " FROM " + Tables.RAW_CONTACTS + 4771 " WHERE " + RawContactsColumns.ACCOUNT_ID + "=?))", 4772 accountIdParams); 4773 db.execSQL( 4774 "DELETE FROM " + Tables.STREAM_ITEMS + 4775 " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" + 4776 "SELECT " + RawContacts._ID + 4777 " FROM " + Tables.RAW_CONTACTS + 4778 " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)", 4779 accountIdParams); 4780 4781 // Delta api is only needed for regular contacts. 4782 if (!inProfileMode()) { 4783 // Contacts are deleted by a trigger on the raw_contacts table. 4784 // But we also need to insert the contact into the delete log. 4785 // This logic is being consolidated into the ContactsTableUtil. 4786 4787 // deleteContactIfSingleton() does not work in this case because raw 4788 // contacts will be deleted in a single batch below. Contacts with 4789 // multiple raw contacts in the same account will be missed. 4790 4791 // Find all contacts that do not have raw contacts in other accounts. 4792 // These should be deleted. 4793 Cursor cursor = db.rawQuery( 4794 "SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID + 4795 " FROM " + Tables.RAW_CONTACTS + 4796 " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" + 4797 " AND " + RawContactsColumns.CONCRETE_CONTACT_ID + 4798 " NOT IN (" + 4799 " SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID + 4800 " FROM " + Tables.RAW_CONTACTS + 4801 " WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1" 4802 + ")", accountIdParams); 4803 try { 4804 while (cursor.moveToNext()) { 4805 final long contactId = cursor.getLong(0); 4806 ContactsTableUtil.deleteContact(db, contactId); 4807 } 4808 } finally { 4809 MoreCloseables.closeQuietly(cursor); 4810 } 4811 4812 // If the contact was not deleted, it's last updated timestamp needs to 4813 // be refreshed since one of it's raw contacts got removed. 4814 // Find all contacts that will not be deleted (i.e. contacts with 4815 // raw contacts in other accounts) 4816 cursor = db.rawQuery( 4817 "SELECT DISTINCT " + RawContactsColumns.CONCRETE_CONTACT_ID + 4818 " FROM " + Tables.RAW_CONTACTS + 4819 " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" + 4820 " AND " + RawContactsColumns.CONCRETE_CONTACT_ID + 4821 " IN (" + 4822 " SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID + 4823 " FROM " + Tables.RAW_CONTACTS + 4824 " WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1" 4825 + ")", accountIdParams); 4826 try { 4827 while (cursor.moveToNext()) { 4828 final long contactId = cursor.getLong(0); 4829 ContactsTableUtil.updateContactLastUpdateByContactId(db, 4830 contactId); 4831 } 4832 } finally { 4833 MoreCloseables.closeQuietly(cursor); 4834 } 4835 } 4836 4837 db.execSQL( 4838 "DELETE FROM " + Tables.RAW_CONTACTS + 4839 " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?", 4840 accountIdParams); 4841 db.execSQL( 4842 "DELETE FROM " + Tables.ACCOUNTS + 4843 " WHERE " + AccountsColumns._ID + "=?", 4844 accountIdParams); 4845 } 4846 } 4847 4848 // Find all aggregated contacts that used to contain the raw contacts 4849 // we have just deleted and see if they are still referencing the deleted 4850 // names or photos. If so, fix up those contacts. 4851 HashSet<Long> orphanContactIds = Sets.newHashSet(); 4852 Cursor cursor = db.rawQuery("SELECT " + Contacts._ID + 4853 " FROM " + Tables.CONTACTS + 4854 " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " + 4855 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " + 4856 "(SELECT " + RawContacts._ID + 4857 " FROM " + Tables.RAW_CONTACTS + "))" + 4858 " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " + 4859 Contacts.PHOTO_ID + " NOT IN " + 4860 "(SELECT " + Data._ID + 4861 " FROM " + Tables.DATA + "))", null); 4862 try { 4863 while (cursor.moveToNext()) { 4864 orphanContactIds.add(cursor.getLong(0)); 4865 } 4866 } finally { 4867 cursor.close(); 4868 } 4869 4870 for (Long contactId : orphanContactIds) { 4871 mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId); 4872 } 4873 dbHelper.updateAllVisible(); 4874 4875 // Don't bother updating the search index if we're in profile mode - there is no 4876 // search index for the profile DB, and updating it for the contacts DB in this case 4877 // makes no sense and risks a deadlock. 4878 if (!inProfileMode()) { 4879 // TODO Fix it. It only updates index for contacts/raw_contacts that the 4880 // current transaction context knows updated, but here in this method we don't 4881 // update that information, so effectively it's no-op. 4882 // We can probably just schedule BACKGROUND_TASK_UPDATE_SEARCH_INDEX. 4883 // (But make sure it's not scheduled yet. We schedule this task in initialize() 4884 // too.) 4885 updateSearchIndexInTransaction(); 4886 } 4887 } 4888 4889 // Second, remove stale rows from Tables.SETTINGS and Tables.DIRECTORIES 4890 removeStaleAccountRows(Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE, 4891 systemAccounts); 4892 removeStaleAccountRows(Tables.DIRECTORIES, Directory.ACCOUNT_NAME, 4893 Directory.ACCOUNT_TYPE, systemAccounts); 4894 4895 // Third, remaining tasks that must be done in a transaction. 4896 // TODO: Should sync state take data set into consideration? 4897 dbHelper.getSyncState().onAccountsChanged(db, systemAccounts); 4898 4899 saveAccounts(systemAccounts); 4900 4901 db.setTransactionSuccessful(); 4902 } finally { 4903 db.endTransaction(); 4904 } 4905 mAccountWritability.clear(); 4906 4907 updateContactsAccountCount(systemAccounts); 4908 updateProviderStatus(); 4909 return true; 4910 } 4911 4912 private void updateContactsAccountCount(Account[] accounts) { 4913 int count = 0; 4914 for (Account account : accounts) { 4915 if (isContactsAccount(account)) { 4916 count++; 4917 } 4918 } 4919 mContactsAccountCount = count; 4920 } 4921 4922 protected boolean isContactsAccount(Account account) { 4923 final IContentService cs = ContentResolver.getContentService(); 4924 try { 4925 return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; 4926 } catch (RemoteException e) { 4927 Log.e(TAG, "Cannot obtain sync flag for account: " + account, e); 4928 return false; 4929 } 4930 } 4931 4932 public void onPackageChanged(String packageName) { 4933 scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName); 4934 } 4935 4936 public void removeStaleAccountRows(String table, String accountNameColumn, 4937 String accountTypeColumn, Account[] systemAccounts) { 4938 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 4939 final Cursor c = db.rawQuery( 4940 "SELECT DISTINCT " + accountNameColumn + 4941 "," + accountTypeColumn + 4942 " FROM " + table, null); 4943 try { 4944 c.moveToPosition(-1); 4945 while (c.moveToNext()) { 4946 final AccountWithDataSet accountWithDataSet = AccountWithDataSet.get( 4947 c.getString(0), c.getString(1), null); 4948 if (accountWithDataSet.isLocalAccount() 4949 || accountWithDataSet.inSystemAccounts(systemAccounts)) { 4950 // Account still exists. 4951 continue; 4952 } 4953 4954 db.execSQL("DELETE FROM " + table + 4955 " WHERE " + accountNameColumn + "=? AND " + 4956 accountTypeColumn + "=?", 4957 new String[] {accountWithDataSet.getAccountName(), 4958 accountWithDataSet.getAccountType()}); 4959 } 4960 } finally { 4961 c.close(); 4962 } 4963 } 4964 4965 @Override 4966 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 4967 String sortOrder) { 4968 return query(uri, projection, selection, selectionArgs, sortOrder, null); 4969 } 4970 4971 @Override 4972 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 4973 String sortOrder, CancellationSignal cancellationSignal) { 4974 if (VERBOSE_LOGGING) { 4975 Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) + 4976 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 4977 " order=[" + sortOrder + "]"); 4978 } 4979 4980 waitForAccess(mReadAccessLatch); 4981 4982 // Enforce stream items access check if applicable. 4983 enforceSocialStreamReadPermission(uri); 4984 4985 // Query the profile DB if appropriate. 4986 if (mapsToProfileDb(uri)) { 4987 switchToProfileMode(); 4988 return mProfileProvider.query(uri, projection, selection, selectionArgs, sortOrder, 4989 cancellationSignal); 4990 } 4991 4992 // Otherwise proceed with a normal query against the contacts DB. 4993 switchToContactMode(); 4994 String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY); 4995 if (directory == null) { 4996 return addSnippetExtrasToCursor(uri, 4997 queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1, 4998 cancellationSignal)); 4999 } else if (directory.equals("0")) { 5000 return addSnippetExtrasToCursor(uri, 5001 queryLocal(uri, projection, selection, selectionArgs, sortOrder, 5002 Directory.DEFAULT, cancellationSignal)); 5003 } else if (directory.equals("1")) { 5004 return addSnippetExtrasToCursor(uri, 5005 queryLocal(uri, projection, selection, selectionArgs, sortOrder, 5006 Directory.LOCAL_INVISIBLE, cancellationSignal)); 5007 } 5008 5009 DirectoryInfo directoryInfo = getDirectoryAuthority(directory); 5010 if (directoryInfo == null) { 5011 Log.e(TAG, "Invalid directory ID: " + uri); 5012 return null; 5013 } 5014 5015 Builder builder = new Uri.Builder(); 5016 builder.scheme(ContentResolver.SCHEME_CONTENT); 5017 builder.authority(directoryInfo.authority); 5018 builder.encodedPath(uri.getEncodedPath()); 5019 if (directoryInfo.accountName != null) { 5020 builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName); 5021 } 5022 if (directoryInfo.accountType != null) { 5023 builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType); 5024 } 5025 5026 String limit = getLimit(uri); 5027 if (limit != null) { 5028 builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit); 5029 } 5030 5031 Uri directoryUri = builder.build(); 5032 5033 if (projection == null) { 5034 projection = getDefaultProjection(uri); 5035 } 5036 5037 Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection, 5038 selectionArgs, sortOrder); 5039 5040 if (cursor == null) { 5041 return null; 5042 } 5043 5044 // Load the cursor contents into a memory cursor (backed by a cursor window) and close the 5045 // underlying cursor. 5046 try { 5047 MemoryCursor memCursor = new MemoryCursor(null, cursor.getColumnNames()); 5048 memCursor.fillFromCursor(cursor); 5049 return memCursor; 5050 } finally { 5051 cursor.close(); 5052 } 5053 } 5054 5055 private Cursor addSnippetExtrasToCursor(Uri uri, Cursor cursor) { 5056 5057 // If the cursor doesn't contain a snippet column, don't bother wrapping it. 5058 if (cursor.getColumnIndex(SearchSnippetColumns.SNIPPET) < 0) { 5059 return cursor; 5060 } 5061 5062 String query = uri.getLastPathSegment(); 5063 5064 // Snippet data is needed for the snippeting on the client side, so store it in the cursor 5065 if (cursor instanceof AbstractCursor && deferredSnippetingRequested(uri)){ 5066 Bundle oldExtras = cursor.getExtras(); 5067 Bundle extras = new Bundle(); 5068 if (oldExtras != null) { 5069 extras.putAll(oldExtras); 5070 } 5071 extras.putString(ContactsContract.DEFERRED_SNIPPETING_QUERY, query); 5072 5073 ((AbstractCursor) cursor).setExtras(extras); 5074 } 5075 return cursor; 5076 } 5077 5078 private Cursor addDeferredSnippetingExtra(Cursor cursor) { 5079 if (cursor instanceof AbstractCursor){ 5080 Bundle oldExtras = cursor.getExtras(); 5081 Bundle extras = new Bundle(); 5082 if (oldExtras != null) { 5083 extras.putAll(oldExtras); 5084 } 5085 extras.putBoolean(ContactsContract.DEFERRED_SNIPPETING, true); 5086 ((AbstractCursor) cursor).setExtras(extras); 5087 } 5088 return cursor; 5089 } 5090 5091 private static final class DirectoryQuery { 5092 public static final String[] COLUMNS = new String[] { 5093 Directory._ID, 5094 Directory.DIRECTORY_AUTHORITY, 5095 Directory.ACCOUNT_NAME, 5096 Directory.ACCOUNT_TYPE 5097 }; 5098 5099 public static final int DIRECTORY_ID = 0; 5100 public static final int AUTHORITY = 1; 5101 public static final int ACCOUNT_NAME = 2; 5102 public static final int ACCOUNT_TYPE = 3; 5103 } 5104 5105 /** 5106 * Reads and caches directory information for the database. 5107 */ 5108 private DirectoryInfo getDirectoryAuthority(String directoryId) { 5109 synchronized (mDirectoryCache) { 5110 if (!mDirectoryCacheValid) { 5111 mDirectoryCache.clear(); 5112 SQLiteDatabase db = mDbHelper.get().getReadableDatabase(); 5113 Cursor cursor = db.query(Tables.DIRECTORIES, 5114 DirectoryQuery.COLUMNS, 5115 null, null, null, null, null); 5116 try { 5117 while (cursor.moveToNext()) { 5118 DirectoryInfo info = new DirectoryInfo(); 5119 String id = cursor.getString(DirectoryQuery.DIRECTORY_ID); 5120 info.authority = cursor.getString(DirectoryQuery.AUTHORITY); 5121 info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 5122 info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 5123 mDirectoryCache.put(id, info); 5124 } 5125 } finally { 5126 cursor.close(); 5127 } 5128 mDirectoryCacheValid = true; 5129 } 5130 5131 return mDirectoryCache.get(directoryId); 5132 } 5133 } 5134 5135 public void resetDirectoryCache() { 5136 synchronized(mDirectoryCache) { 5137 mDirectoryCacheValid = false; 5138 } 5139 } 5140 5141 protected Cursor queryLocal(final Uri uri, final String[] projection, String selection, 5142 String[] selectionArgs, String sortOrder, final long directoryId, 5143 final CancellationSignal cancellationSignal) { 5144 5145 final SQLiteDatabase db = mDbHelper.get().getReadableDatabase(); 5146 5147 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 5148 String groupBy = null; 5149 String having = null; 5150 String limit = getLimit(uri); 5151 boolean snippetDeferred = false; 5152 5153 // The expression used in bundleLetterCountExtras() to get count. 5154 String addressBookIndexerCountExpression = null; 5155 5156 final int match = sUriMatcher.match(uri); 5157 switch (match) { 5158 case SYNCSTATE: 5159 case PROFILE_SYNCSTATE: 5160 return mDbHelper.get().getSyncState().query(db, projection, selection, 5161 selectionArgs, sortOrder); 5162 5163 case CONTACTS: { 5164 setTablesAndProjectionMapForContacts(qb, uri, projection); 5165 appendLocalDirectoryAndAccountSelectionIfNeeded(qb, directoryId, uri); 5166 break; 5167 } 5168 5169 case CONTACTS_ID: { 5170 long contactId = ContentUris.parseId(uri); 5171 setTablesAndProjectionMapForContacts(qb, uri, projection); 5172 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 5173 qb.appendWhere(Contacts._ID + "=?"); 5174 break; 5175 } 5176 5177 case CONTACTS_LOOKUP: 5178 case CONTACTS_LOOKUP_ID: { 5179 List<String> pathSegments = uri.getPathSegments(); 5180 int segmentCount = pathSegments.size(); 5181 if (segmentCount < 3) { 5182 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 5183 "Missing a lookup key", uri)); 5184 } 5185 5186 String lookupKey = pathSegments.get(2); 5187 if (segmentCount == 4) { 5188 long contactId = Long.parseLong(pathSegments.get(3)); 5189 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 5190 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 5191 5192 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 5193 projection, selection, selectionArgs, sortOrder, groupBy, limit, 5194 Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey, 5195 cancellationSignal); 5196 if (c != null) { 5197 return c; 5198 } 5199 } 5200 5201 setTablesAndProjectionMapForContacts(qb, uri, projection); 5202 selectionArgs = insertSelectionArg(selectionArgs, 5203 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 5204 qb.appendWhere(Contacts._ID + "=?"); 5205 break; 5206 } 5207 5208 case CONTACTS_LOOKUP_DATA: 5209 case CONTACTS_LOOKUP_ID_DATA: 5210 case CONTACTS_LOOKUP_PHOTO: 5211 case CONTACTS_LOOKUP_ID_PHOTO: { 5212 List<String> pathSegments = uri.getPathSegments(); 5213 int segmentCount = pathSegments.size(); 5214 if (segmentCount < 4) { 5215 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 5216 "Missing a lookup key", uri)); 5217 } 5218 String lookupKey = pathSegments.get(2); 5219 if (segmentCount == 5) { 5220 long contactId = Long.parseLong(pathSegments.get(3)); 5221 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 5222 setTablesAndProjectionMapForData(lookupQb, uri, projection, false); 5223 if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) { 5224 lookupQb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 5225 } 5226 lookupQb.appendWhere(" AND "); 5227 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 5228 projection, selection, selectionArgs, sortOrder, groupBy, limit, 5229 Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey, 5230 cancellationSignal); 5231 if (c != null) { 5232 return c; 5233 } 5234 5235 // TODO see if the contact exists but has no data rows (rare) 5236 } 5237 5238 setTablesAndProjectionMapForData(qb, uri, projection, false); 5239 long contactId = lookupContactIdByLookupKey(db, lookupKey); 5240 selectionArgs = insertSelectionArg(selectionArgs, 5241 String.valueOf(contactId)); 5242 if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) { 5243 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 5244 } 5245 qb.appendWhere(" AND " + Data.CONTACT_ID + "=?"); 5246 break; 5247 } 5248 5249 case CONTACTS_ID_STREAM_ITEMS: { 5250 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 5251 setTablesAndProjectionMapForStreamItems(qb); 5252 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 5253 qb.appendWhere(StreamItems.CONTACT_ID + "=?"); 5254 break; 5255 } 5256 5257 case CONTACTS_LOOKUP_STREAM_ITEMS: 5258 case CONTACTS_LOOKUP_ID_STREAM_ITEMS: { 5259 List<String> pathSegments = uri.getPathSegments(); 5260 int segmentCount = pathSegments.size(); 5261 if (segmentCount < 4) { 5262 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 5263 "Missing a lookup key", uri)); 5264 } 5265 String lookupKey = pathSegments.get(2); 5266 if (segmentCount == 5) { 5267 long contactId = Long.parseLong(pathSegments.get(3)); 5268 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 5269 setTablesAndProjectionMapForStreamItems(lookupQb); 5270 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 5271 projection, selection, selectionArgs, sortOrder, groupBy, limit, 5272 StreamItems.CONTACT_ID, contactId, 5273 StreamItems.CONTACT_LOOKUP_KEY, lookupKey, 5274 cancellationSignal); 5275 if (c != null) { 5276 return c; 5277 } 5278 } 5279 5280 setTablesAndProjectionMapForStreamItems(qb); 5281 long contactId = lookupContactIdByLookupKey(db, lookupKey); 5282 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 5283 qb.appendWhere(RawContacts.CONTACT_ID + "=?"); 5284 break; 5285 } 5286 5287 case CONTACTS_AS_VCARD: { 5288 final String lookupKey = Uri.encode(uri.getPathSegments().get(2)); 5289 long contactId = lookupContactIdByLookupKey(db, lookupKey); 5290 qb.setTables(Views.CONTACTS); 5291 qb.setProjectionMap(sContactsVCardProjectionMap); 5292 selectionArgs = insertSelectionArg(selectionArgs, 5293 String.valueOf(contactId)); 5294 qb.appendWhere(Contacts._ID + "=?"); 5295 break; 5296 } 5297 5298 case CONTACTS_AS_MULTI_VCARD: { 5299 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); 5300 String currentDateString = dateFormat.format(new Date()).toString(); 5301 return db.rawQuery( 5302 "SELECT" + 5303 " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," + 5304 " NULL AS " + OpenableColumns.SIZE, 5305 new String[] { currentDateString }); 5306 } 5307 5308 case CONTACTS_FILTER: { 5309 String filterParam = ""; 5310 boolean deferredSnipRequested = deferredSnippetingRequested(uri); 5311 if (uri.getPathSegments().size() > 2) { 5312 filterParam = uri.getLastPathSegment(); 5313 } 5314 5315 // If the query consists of a single word, we can do snippetizing after-the-fact for 5316 // a performance boost. Otherwise, we can't defer. 5317 snippetDeferred = isSingleWordQuery(filterParam) 5318 && deferredSnipRequested && snippetNeeded(projection); 5319 setTablesAndProjectionMapForContactsWithSnippet( 5320 qb, uri, projection, filterParam, directoryId, 5321 snippetDeferred); 5322 break; 5323 } 5324 5325 case CONTACTS_STREQUENT_FILTER: 5326 case CONTACTS_STREQUENT: { 5327 // Basically the resultant SQL should look like this: 5328 // (SQL for listing starred items) 5329 // UNION ALL 5330 // (SQL for listing frequently contacted items) 5331 // ORDER BY ... 5332 5333 final boolean phoneOnly = readBooleanQueryParameter( 5334 uri, ContactsContract.STREQUENT_PHONE_ONLY, false); 5335 if (match == CONTACTS_STREQUENT_FILTER && uri.getPathSegments().size() > 3) { 5336 String filterParam = uri.getLastPathSegment(); 5337 StringBuilder sb = new StringBuilder(); 5338 sb.append(Contacts._ID + " IN "); 5339 appendContactFilterAsNestedQuery(sb, filterParam); 5340 selection = DbQueryUtils.concatenateClauses(selection, sb.toString()); 5341 } 5342 5343 String[] subProjection = null; 5344 if (projection != null) { 5345 subProjection = new String[projection.length + 2]; 5346 System.arraycopy(projection, 0, subProjection, 0, projection.length); 5347 subProjection[projection.length + 0] = DataUsageStatColumns.TIMES_USED; 5348 subProjection[projection.length + 1] = DataUsageStatColumns.LAST_TIME_USED; 5349 } 5350 5351 // String that will store the query for starred contacts. For phone only queries, 5352 // these will return a list of all phone numbers that belong to starred contacts. 5353 final String starredInnerQuery; 5354 // String that will store the query for frequents. These JOINS can be very slow 5355 // if assembled in the wrong order. Be sure to test changes against huge databases. 5356 final String frequentInnerQuery; 5357 5358 if (phoneOnly) { 5359 final StringBuilder tableBuilder = new StringBuilder(); 5360 // In phone only mode, we need to look at view_data instead of 5361 // contacts/raw_contacts to obtain actual phone numbers. One problem is that 5362 // view_data is much larger than view_contacts, so our query might become much 5363 // slower. 5364 5365 // For starred phone numbers, we select only phone numbers that belong to 5366 // starred contacts, and then do an outer join against the data usage table, 5367 // to make sure that even if a starred number hasn't been previously used, 5368 // it is included in the list of strequent numbers. 5369 tableBuilder.append("(SELECT * FROM " + Views.DATA + " WHERE " 5370 + Contacts.STARRED + "=1)" + " AS " + Tables.DATA 5371 + " LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT 5372 + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" 5373 + DataColumns.CONCRETE_ID + " AND " 5374 + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" 5375 + DataUsageStatColumns.USAGE_TYPE_INT_CALL + ")"); 5376 appendContactPresenceJoin(tableBuilder, projection, RawContacts.CONTACT_ID); 5377 appendContactStatusUpdateJoin(tableBuilder, projection, 5378 ContactsColumns.LAST_STATUS_UPDATE_ID); 5379 qb.setTables(tableBuilder.toString()); 5380 qb.setProjectionMap(sStrequentPhoneOnlyProjectionMap); 5381 final long phoneMimeTypeId = 5382 mDbHelper.get().getMimeTypeId(Phone.CONTENT_ITEM_TYPE); 5383 final long sipMimeTypeId = 5384 mDbHelper.get().getMimeTypeId(SipAddress.CONTENT_ITEM_TYPE); 5385 5386 qb.appendWhere(DbQueryUtils.concatenateClauses( 5387 selection, 5388 "(" + Contacts.STARRED + "=1", 5389 DataColumns.MIMETYPE_ID + " IN (" + 5390 phoneMimeTypeId + ", " + sipMimeTypeId + ")) AND (" + 5391 RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")")); 5392 starredInnerQuery = qb.buildQuery(subProjection, null, null, 5393 null, Data.IS_SUPER_PRIMARY + " DESC," + SORT_BY_DATA_USAGE, null); 5394 5395 qb = new SQLiteQueryBuilder(); 5396 qb.setStrict(true); 5397 // Construct the query string for frequent phone numbers 5398 tableBuilder.setLength(0); 5399 // For frequent phone numbers, we start from data usage table and join 5400 // view_data to the table, assuming data usage table is quite smaller than 5401 // data rows (almost always it should be), and we don't want any phone 5402 // numbers not used by the user. This way sqlite is able to drop a number of 5403 // rows in view_data in the early stage of data lookup. 5404 tableBuilder.append(Tables.DATA_USAGE_STAT 5405 + " INNER JOIN " + Views.DATA + " " + Tables.DATA 5406 + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" 5407 + DataColumns.CONCRETE_ID + " AND " 5408 + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" 5409 + DataUsageStatColumns.USAGE_TYPE_INT_CALL + ")"); 5410 appendContactPresenceJoin(tableBuilder, projection, RawContacts.CONTACT_ID); 5411 appendContactStatusUpdateJoin(tableBuilder, projection, 5412 ContactsColumns.LAST_STATUS_UPDATE_ID); 5413 qb.setTables(tableBuilder.toString()); 5414 qb.setProjectionMap(sStrequentPhoneOnlyProjectionMap); 5415 qb.appendWhere(DbQueryUtils.concatenateClauses( 5416 selection, 5417 "(" + Contacts.STARRED + "=0 OR " + Contacts.STARRED + " IS NULL", 5418 DataColumns.MIMETYPE_ID + " IN (" + 5419 phoneMimeTypeId + ", " + sipMimeTypeId + ")) AND (" + 5420 RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")")); 5421 frequentInnerQuery = qb.buildQuery(subProjection, null, null, null, 5422 SORT_BY_DATA_USAGE, "25"); 5423 5424 } else { 5425 // Build the first query for starred contacts 5426 qb.setStrict(true); 5427 setTablesAndProjectionMapForContacts(qb, uri, projection, false); 5428 qb.setProjectionMap(sStrequentStarredProjectionMap); 5429 5430 starredInnerQuery = qb.buildQuery(subProjection, 5431 Contacts.STARRED + "=1", Contacts._ID, null, 5432 Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC", null); 5433 5434 // Reset the builder, and build the second query for frequents contacts 5435 qb = new SQLiteQueryBuilder(); 5436 qb.setStrict(true); 5437 5438 setTablesAndProjectionMapForContacts(qb, uri, projection, true); 5439 qb.setProjectionMap(sStrequentFrequentProjectionMap); 5440 qb.appendWhere(DbQueryUtils.concatenateClauses( 5441 selection, 5442 "(" + Contacts.STARRED + " =0 OR " + Contacts.STARRED + " IS NULL)")); 5443 // Note frequentInnerQuery is a grouping query, so the "IN default_directory" 5444 // selection needs to be in HAVING, not in WHERE. 5445 final String HAVING = 5446 RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY; 5447 frequentInnerQuery = qb.buildQuery(subProjection, 5448 null, Contacts._ID, HAVING, SORT_BY_DATA_USAGE, "25"); 5449 } 5450 5451 // We need to wrap the inner queries in an extra select, because they contain 5452 // their own SORT and LIMIT 5453 5454 // Phone numbers that were used more than 30 days ago are dropped from frequents 5455 final String frequentQuery = "SELECT * FROM (" + frequentInnerQuery + ") WHERE " + 5456 TIME_SINCE_LAST_USED_SEC + "<" + LAST_TIME_USED_30_DAYS_SEC; 5457 final String starredQuery = "SELECT * FROM (" + starredInnerQuery + ")"; 5458 5459 // Put them together 5460 final String unionQuery = 5461 qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, null, null); 5462 5463 // Here, we need to use selection / selectionArgs (supplied from users) "twice", 5464 // as we want them both for starred items and for frequently contacted items. 5465 // 5466 // e.g. if the user specify selection = "starred =?" and selectionArgs = "0", 5467 // the resultant SQL should be like: 5468 // SELECT ... WHERE starred =? AND ... 5469 // UNION ALL 5470 // SELECT ... WHERE starred =? AND ... 5471 String[] doubledSelectionArgs = null; 5472 if (selectionArgs != null) { 5473 final int length = selectionArgs.length; 5474 doubledSelectionArgs = new String[length * 2]; 5475 System.arraycopy(selectionArgs, 0, doubledSelectionArgs, 0, length); 5476 System.arraycopy(selectionArgs, 0, doubledSelectionArgs, length, length); 5477 } 5478 5479 Cursor cursor = db.rawQuery(unionQuery, doubledSelectionArgs); 5480 if (cursor != null) { 5481 cursor.setNotificationUri(getContext().getContentResolver(), 5482 ContactsContract.AUTHORITY_URI); 5483 } 5484 return cursor; 5485 } 5486 5487 case CONTACTS_FREQUENT: { 5488 setTablesAndProjectionMapForContacts(qb, uri, projection, true); 5489 qb.setProjectionMap(sStrequentFrequentProjectionMap); 5490 groupBy = Contacts._ID; 5491 having = Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY; 5492 if (!TextUtils.isEmpty(sortOrder)) { 5493 sortOrder = FREQUENT_ORDER_BY + ", " + sortOrder; 5494 } else { 5495 sortOrder = FREQUENT_ORDER_BY; 5496 } 5497 break; 5498 } 5499 5500 case CONTACTS_GROUP: { 5501 setTablesAndProjectionMapForContacts(qb, uri, projection); 5502 if (uri.getPathSegments().size() > 2) { 5503 qb.appendWhere(CONTACTS_IN_GROUP_SELECT); 5504 String groupMimeTypeId = String.valueOf( 5505 mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 5506 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5507 selectionArgs = insertSelectionArg(selectionArgs, groupMimeTypeId); 5508 } 5509 break; 5510 } 5511 5512 case PROFILE: { 5513 setTablesAndProjectionMapForContacts(qb, uri, projection); 5514 break; 5515 } 5516 5517 case PROFILE_ENTITIES: { 5518 setTablesAndProjectionMapForEntities(qb, uri, projection); 5519 break; 5520 } 5521 5522 case PROFILE_AS_VCARD: { 5523 qb.setTables(Views.CONTACTS); 5524 qb.setProjectionMap(sContactsVCardProjectionMap); 5525 break; 5526 } 5527 5528 case CONTACTS_ID_DATA: { 5529 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 5530 setTablesAndProjectionMapForData(qb, uri, projection, false); 5531 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 5532 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 5533 break; 5534 } 5535 5536 case CONTACTS_ID_PHOTO: { 5537 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 5538 setTablesAndProjectionMapForData(qb, uri, projection, false); 5539 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 5540 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 5541 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 5542 break; 5543 } 5544 5545 case CONTACTS_ID_ENTITIES: { 5546 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 5547 setTablesAndProjectionMapForEntities(qb, uri, projection); 5548 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId)); 5549 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?"); 5550 break; 5551 } 5552 5553 case CONTACTS_LOOKUP_ENTITIES: 5554 case CONTACTS_LOOKUP_ID_ENTITIES: { 5555 List<String> pathSegments = uri.getPathSegments(); 5556 int segmentCount = pathSegments.size(); 5557 if (segmentCount < 4) { 5558 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 5559 "Missing a lookup key", uri)); 5560 } 5561 String lookupKey = pathSegments.get(2); 5562 if (segmentCount == 5) { 5563 long contactId = Long.parseLong(pathSegments.get(3)); 5564 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 5565 setTablesAndProjectionMapForEntities(lookupQb, uri, projection); 5566 lookupQb.appendWhere(" AND "); 5567 5568 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 5569 projection, selection, selectionArgs, sortOrder, groupBy, limit, 5570 Contacts.Entity.CONTACT_ID, contactId, 5571 Contacts.Entity.LOOKUP_KEY, lookupKey, 5572 cancellationSignal); 5573 if (c != null) { 5574 return c; 5575 } 5576 } 5577 5578 setTablesAndProjectionMapForEntities(qb, uri, projection); 5579 selectionArgs = insertSelectionArg(selectionArgs, 5580 String.valueOf(lookupContactIdByLookupKey(db, lookupKey))); 5581 qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?"); 5582 break; 5583 } 5584 5585 case STREAM_ITEMS: { 5586 setTablesAndProjectionMapForStreamItems(qb); 5587 break; 5588 } 5589 5590 case STREAM_ITEMS_ID: { 5591 setTablesAndProjectionMapForStreamItems(qb); 5592 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5593 qb.appendWhere(StreamItems._ID + "=?"); 5594 break; 5595 } 5596 5597 case STREAM_ITEMS_LIMIT: { 5598 return buildSingleRowResult(projection, new String[] {StreamItems.MAX_ITEMS}, 5599 new Object[] {MAX_STREAM_ITEMS_PER_RAW_CONTACT}); 5600 } 5601 5602 case STREAM_ITEMS_PHOTOS: { 5603 setTablesAndProjectionMapForStreamItemPhotos(qb); 5604 break; 5605 } 5606 5607 case STREAM_ITEMS_ID_PHOTOS: { 5608 setTablesAndProjectionMapForStreamItemPhotos(qb); 5609 String streamItemId = uri.getPathSegments().get(1); 5610 selectionArgs = insertSelectionArg(selectionArgs, streamItemId); 5611 qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?"); 5612 break; 5613 } 5614 5615 case STREAM_ITEMS_ID_PHOTOS_ID: { 5616 setTablesAndProjectionMapForStreamItemPhotos(qb); 5617 String streamItemId = uri.getPathSegments().get(1); 5618 String streamItemPhotoId = uri.getPathSegments().get(3); 5619 selectionArgs = insertSelectionArg(selectionArgs, streamItemPhotoId); 5620 selectionArgs = insertSelectionArg(selectionArgs, streamItemId); 5621 qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=? AND " + 5622 StreamItemPhotosColumns.CONCRETE_ID + "=?"); 5623 break; 5624 } 5625 5626 case PHOTO_DIMENSIONS: { 5627 return buildSingleRowResult(projection, 5628 new String[] {DisplayPhoto.DISPLAY_MAX_DIM, DisplayPhoto.THUMBNAIL_MAX_DIM}, 5629 new Object[] {getMaxDisplayPhotoDim(), getMaxThumbnailDim()}); 5630 } 5631 5632 case PHONES: 5633 case CALLABLES: { 5634 final String mimeTypeIsPhoneExpression = 5635 DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone(); 5636 final String mimeTypeIsSipExpression = 5637 DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip(); 5638 setTablesAndProjectionMapForData(qb, uri, projection, false); 5639 if (match == CALLABLES) { 5640 qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression + 5641 ") OR (" + mimeTypeIsSipExpression + "))"); 5642 } else { 5643 qb.appendWhere(" AND " + mimeTypeIsPhoneExpression); 5644 } 5645 5646 final boolean removeDuplicates = readBooleanQueryParameter( 5647 uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false); 5648 if (removeDuplicates) { 5649 groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1; 5650 5651 // In this case, because we dedupe phone numbers, the address book indexer needs 5652 // to take it into account too. (Otherwise headers will appear in wrong 5653 // positions.) 5654 // So use count(distinct pair(CONTACT_ID, PHONE NUMBER)) instead of count(*). 5655 // But because there's no such thing as pair() on sqlite, we use 5656 // CONTACT_ID || ',' || PHONE NUMBER instead. 5657 // This only slows down the query by 14% with 10,000 contacts. 5658 addressBookIndexerCountExpression = "DISTINCT " 5659 + RawContacts.CONTACT_ID + "||','||" + Data.DATA1; 5660 } 5661 break; 5662 } 5663 5664 case PHONES_ID: 5665 case CALLABLES_ID: { 5666 final String mimeTypeIsPhoneExpression = 5667 DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone(); 5668 final String mimeTypeIsSipExpression = 5669 DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip(); 5670 setTablesAndProjectionMapForData(qb, uri, projection, false); 5671 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5672 if (match == CALLABLES_ID) { 5673 qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression + 5674 ") OR (" + mimeTypeIsSipExpression + "))"); 5675 } else { 5676 qb.appendWhere(" AND " + mimeTypeIsPhoneExpression); 5677 } 5678 qb.appendWhere(" AND " + Data._ID + "=?"); 5679 break; 5680 } 5681 5682 case PHONES_FILTER: 5683 case CALLABLES_FILTER: { 5684 final String mimeTypeIsPhoneExpression = 5685 DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone(); 5686 final String mimeTypeIsSipExpression = 5687 DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip(); 5688 5689 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE); 5690 final int typeInt = getDataUsageFeedbackType(typeParam, 5691 DataUsageStatColumns.USAGE_TYPE_INT_CALL); 5692 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt); 5693 if (match == CALLABLES_FILTER) { 5694 qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression + 5695 ") OR (" + mimeTypeIsSipExpression + "))"); 5696 } else { 5697 qb.appendWhere(" AND " + mimeTypeIsPhoneExpression); 5698 } 5699 5700 if (uri.getPathSegments().size() > 2) { 5701 final String filterParam = uri.getLastPathSegment(); 5702 final boolean searchDisplayName = uri.getBooleanQueryParameter( 5703 Phone.SEARCH_DISPLAY_NAME_KEY, true); 5704 final boolean searchPhoneNumber = uri.getBooleanQueryParameter( 5705 Phone.SEARCH_PHONE_NUMBER_KEY, true); 5706 5707 final StringBuilder sb = new StringBuilder(); 5708 sb.append(" AND ("); 5709 5710 boolean hasCondition = false; 5711 // This searches the name, nickname and organization fields. 5712 final String ftsMatchQuery = 5713 searchDisplayName 5714 ? SearchIndexManager.getFtsMatchQuery(filterParam, 5715 FtsQueryBuilder.UNSCOPED_NORMALIZING) 5716 : null; 5717 if (!TextUtils.isEmpty(ftsMatchQuery)) { 5718 sb.append(Data.RAW_CONTACT_ID + " IN " + 5719 "(SELECT " + RawContactsColumns.CONCRETE_ID + 5720 " FROM " + Tables.SEARCH_INDEX + 5721 " JOIN " + Tables.RAW_CONTACTS + 5722 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID 5723 + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" + 5724 " WHERE " + SearchIndexColumns.NAME + " MATCH '"); 5725 sb.append(ftsMatchQuery); 5726 sb.append("')"); 5727 hasCondition = true; 5728 } 5729 5730 if (searchPhoneNumber) { 5731 final String number = PhoneNumberUtils.normalizeNumber(filterParam); 5732 if (!TextUtils.isEmpty(number)) { 5733 if (hasCondition) { 5734 sb.append(" OR "); 5735 } 5736 sb.append(Data._ID + 5737 " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID 5738 + " FROM " + Tables.PHONE_LOOKUP 5739 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 5740 sb.append(number); 5741 sb.append("%')"); 5742 hasCondition = true; 5743 } 5744 5745 if (!TextUtils.isEmpty(filterParam) && match == CALLABLES_FILTER) { 5746 // If the request is via Callable uri, Sip addresses matching the filter 5747 // parameter should be returned. 5748 if (hasCondition) { 5749 sb.append(" OR "); 5750 } 5751 sb.append("("); 5752 sb.append(mimeTypeIsSipExpression); 5753 sb.append(" AND ((" + Data.DATA1 + " LIKE "); 5754 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); 5755 sb.append(") OR (" + Data.DATA1 + " LIKE "); 5756 // Users may want SIP URIs starting from "sip:" 5757 DatabaseUtils.appendEscapedSQLString(sb, "sip:"+ filterParam + '%'); 5758 sb.append(")))"); 5759 hasCondition = true; 5760 } 5761 } 5762 5763 if (!hasCondition) { 5764 // If it is neither a phone number nor a name, the query should return 5765 // an empty cursor. Let's ensure that. 5766 sb.append("0"); 5767 } 5768 sb.append(")"); 5769 qb.appendWhere(sb); 5770 } 5771 if (match == CALLABLES_FILTER) { 5772 // If the row is for a phone number that has a normalized form, we should use 5773 // the normalized one as PHONES_FILTER does, while we shouldn't do that 5774 // if the row is for a sip address. 5775 String isPhoneAndHasNormalized = "(" 5776 + mimeTypeIsPhoneExpression + " AND " 5777 + Phone.NORMALIZED_NUMBER + " IS NOT NULL)"; 5778 groupBy = "(CASE WHEN " + isPhoneAndHasNormalized 5779 + " THEN " + Phone.NORMALIZED_NUMBER 5780 + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID; 5781 } else { 5782 groupBy = "(CASE WHEN " + Phone.NORMALIZED_NUMBER 5783 + " IS NOT NULL THEN " + Phone.NORMALIZED_NUMBER 5784 + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID; 5785 } 5786 if (sortOrder == null) { 5787 final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri); 5788 if (!TextUtils.isEmpty(accountPromotionSortOrder)) { 5789 sortOrder = accountPromotionSortOrder + ", " + PHONE_FILTER_SORT_ORDER; 5790 } else { 5791 sortOrder = PHONE_FILTER_SORT_ORDER; 5792 } 5793 } 5794 break; 5795 } 5796 5797 case EMAILS: { 5798 setTablesAndProjectionMapForData(qb, uri, projection, false); 5799 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = " 5800 + mDbHelper.get().getMimeTypeIdForEmail()); 5801 5802 final boolean removeDuplicates = readBooleanQueryParameter( 5803 uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false); 5804 if (removeDuplicates) { 5805 groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1; 5806 5807 // See PHONES for more detail. 5808 addressBookIndexerCountExpression = "DISTINCT " 5809 + RawContacts.CONTACT_ID + "||','||" + Data.DATA1; 5810 } 5811 break; 5812 } 5813 5814 case EMAILS_ID: { 5815 setTablesAndProjectionMapForData(qb, uri, projection, false); 5816 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 5817 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = " 5818 + mDbHelper.get().getMimeTypeIdForEmail() 5819 + " AND " + Data._ID + "=?"); 5820 break; 5821 } 5822 5823 case EMAILS_LOOKUP: { 5824 setTablesAndProjectionMapForData(qb, uri, projection, false); 5825 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = " 5826 + mDbHelper.get().getMimeTypeIdForEmail()); 5827 if (uri.getPathSegments().size() > 2) { 5828 String email = uri.getLastPathSegment(); 5829 String address = mDbHelper.get().extractAddressFromEmailAddress(email); 5830 selectionArgs = insertSelectionArg(selectionArgs, address); 5831 qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)"); 5832 } 5833 // unless told otherwise, we'll return visible before invisible contacts 5834 if (sortOrder == null) { 5835 sortOrder = "(" + RawContacts.CONTACT_ID + " IN " + 5836 Tables.DEFAULT_DIRECTORY + ") DESC"; 5837 } 5838 break; 5839 } 5840 5841 case EMAILS_FILTER: { 5842 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE); 5843 final int typeInt = getDataUsageFeedbackType(typeParam, 5844 DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT); 5845 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt); 5846 String filterParam = null; 5847 5848 if (uri.getPathSegments().size() > 3) { 5849 filterParam = uri.getLastPathSegment(); 5850 if (TextUtils.isEmpty(filterParam)) { 5851 filterParam = null; 5852 } 5853 } 5854 5855 if (filterParam == null) { 5856 // If the filter is unspecified, return nothing 5857 qb.appendWhere(" AND 0"); 5858 } else { 5859 StringBuilder sb = new StringBuilder(); 5860 sb.append(" AND " + Data._ID + " IN ("); 5861 sb.append( 5862 "SELECT " + Data._ID + 5863 " FROM " + Tables.DATA + 5864 " WHERE " + DataColumns.MIMETYPE_ID + "="); 5865 sb.append(mDbHelper.get().getMimeTypeIdForEmail()); 5866 sb.append(" AND " + Data.DATA1 + " LIKE "); 5867 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); 5868 if (!filterParam.contains("@")) { 5869 sb.append( 5870 " UNION SELECT " + Data._ID + 5871 " FROM " + Tables.DATA + 5872 " WHERE +" + DataColumns.MIMETYPE_ID + "="); 5873 sb.append(mDbHelper.get().getMimeTypeIdForEmail()); 5874 sb.append(" AND " + Data.RAW_CONTACT_ID + " IN " + 5875 "(SELECT " + RawContactsColumns.CONCRETE_ID + 5876 " FROM " + Tables.SEARCH_INDEX + 5877 " JOIN " + Tables.RAW_CONTACTS + 5878 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID 5879 + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" + 5880 " WHERE " + SearchIndexColumns.NAME + " MATCH '"); 5881 final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery( 5882 filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING); 5883 sb.append(ftsMatchQuery); 5884 sb.append("')"); 5885 } 5886 sb.append(")"); 5887 qb.appendWhere(sb); 5888 } 5889 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 5890 if (sortOrder == null) { 5891 final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri); 5892 if (!TextUtils.isEmpty(accountPromotionSortOrder)) { 5893 sortOrder = accountPromotionSortOrder + ", " + EMAIL_FILTER_SORT_ORDER; 5894 } else { 5895 sortOrder = EMAIL_FILTER_SORT_ORDER; 5896 } 5897 5898 final String primaryAccountName = 5899 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME); 5900 if (!TextUtils.isEmpty(primaryAccountName)) { 5901 final int index = primaryAccountName.indexOf('@'); 5902 if (index != -1) { 5903 // Purposely include '@' in matching. 5904 final String domain = primaryAccountName.substring(index); 5905 final char escapeChar = '\\'; 5906 5907 final StringBuilder likeValue = new StringBuilder(); 5908 likeValue.append('%'); 5909 DbQueryUtils.escapeLikeValue(likeValue, domain, escapeChar); 5910 selectionArgs = appendSelectionArg(selectionArgs, likeValue.toString()); 5911 5912 // similar email domains is the last sort preference. 5913 sortOrder += ", (CASE WHEN " + Data.DATA1 + " like ? ESCAPE '" + 5914 escapeChar + "' THEN 0 ELSE 1 END)"; 5915 } 5916 } 5917 } 5918 break; 5919 } 5920 5921 case CONTACTABLES: 5922 case CONTACTABLES_FILTER: { 5923 setTablesAndProjectionMapForData(qb, uri, projection, false); 5924 5925 String filterParam = null; 5926 5927 final int uriPathSize = uri.getPathSegments().size(); 5928 if (uriPathSize > 3) { 5929 filterParam = uri.getLastPathSegment(); 5930 if (TextUtils.isEmpty(filterParam)) { 5931 filterParam = null; 5932 } 5933 } 5934 5935 // CONTACTABLES_FILTER but no query provided, return an empty cursor 5936 if (uriPathSize > 2 && filterParam == null) { 5937 qb.appendWhere(" AND 0"); 5938 break; 5939 } 5940 5941 if (uri.getBooleanQueryParameter(Contactables.VISIBLE_CONTACTS_ONLY, false)) { 5942 qb.appendWhere(" AND " + Data.CONTACT_ID + " in " + 5943 Tables.DEFAULT_DIRECTORY); 5944 } 5945 5946 final StringBuilder sb = new StringBuilder(); 5947 5948 // we only want data items that are either email addresses or phone numbers 5949 sb.append(" AND ("); 5950 sb.append(DataColumns.MIMETYPE_ID + " IN ("); 5951 sb.append(mDbHelper.get().getMimeTypeIdForEmail()); 5952 sb.append(","); 5953 sb.append(mDbHelper.get().getMimeTypeIdForPhone()); 5954 sb.append("))"); 5955 5956 // Rest of the query is only relevant if we are handling CONTACTABLES_FILTER 5957 if (uriPathSize < 3) { 5958 qb.appendWhere(sb); 5959 break; 5960 } 5961 5962 // but we want all the email addresses and phone numbers that belong to 5963 // all contacts that have any data items (or name) that match the query 5964 sb.append(" AND "); 5965 sb.append("(" + Data.CONTACT_ID + " IN ("); 5966 5967 // All contacts where the email address data1 column matches the query 5968 sb.append( 5969 "SELECT " + RawContacts.CONTACT_ID + 5970 " FROM " + Tables.DATA + " JOIN " + Tables.RAW_CONTACTS + 5971 " ON " + Tables.DATA + "." + Data.RAW_CONTACT_ID + "=" + 5972 Tables.RAW_CONTACTS + "." + RawContacts._ID + 5973 " WHERE (" + DataColumns.MIMETYPE_ID + "="); 5974 sb.append(mDbHelper.get().getMimeTypeIdForEmail()); 5975 5976 sb.append(" AND " + Data.DATA1 + " LIKE "); 5977 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%'); 5978 sb.append(")"); 5979 5980 // All contacts where the phone number matches the query (determined by checking 5981 // Tables.PHONE_LOOKUP 5982 final String number = PhoneNumberUtils.normalizeNumber(filterParam); 5983 if (!TextUtils.isEmpty(number)) { 5984 sb.append("UNION SELECT DISTINCT " + RawContacts.CONTACT_ID + 5985 " FROM " + Tables.PHONE_LOOKUP + " JOIN " + Tables.RAW_CONTACTS + 5986 " ON (" + Tables.PHONE_LOOKUP + "." + 5987 PhoneLookupColumns.RAW_CONTACT_ID + "=" + 5988 Tables.RAW_CONTACTS + "." + RawContacts._ID + ")" + 5989 " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 5990 sb.append(number); 5991 sb.append("%'"); 5992 } 5993 5994 // All contacts where the name matches the query (determined by checking 5995 // Tables.SEARCH_INDEX 5996 sb.append( 5997 " UNION SELECT " + Data.CONTACT_ID + 5998 " FROM " + Tables.DATA + " JOIN " + Tables.RAW_CONTACTS + 5999 " ON " + Tables.DATA + "." + Data.RAW_CONTACT_ID + "=" + 6000 Tables.RAW_CONTACTS + "." + RawContacts._ID + 6001 6002 " WHERE " + Data.RAW_CONTACT_ID + " IN " + 6003 6004 "(SELECT " + RawContactsColumns.CONCRETE_ID + 6005 " FROM " + Tables.SEARCH_INDEX + 6006 " JOIN " + Tables.RAW_CONTACTS + 6007 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID 6008 + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" + 6009 6010 " WHERE " + SearchIndexColumns.NAME + " MATCH '"); 6011 6012 final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery( 6013 filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING); 6014 sb.append(ftsMatchQuery); 6015 sb.append("')"); 6016 6017 sb.append("))"); 6018 qb.appendWhere(sb); 6019 6020 break; 6021 } 6022 6023 case POSTALS: { 6024 setTablesAndProjectionMapForData(qb, uri, projection, false); 6025 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = " 6026 + mDbHelper.get().getMimeTypeIdForStructuredPostal()); 6027 6028 final boolean removeDuplicates = readBooleanQueryParameter( 6029 uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false); 6030 if (removeDuplicates) { 6031 groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1; 6032 6033 // See PHONES for more detail. 6034 addressBookIndexerCountExpression = "DISTINCT " 6035 + RawContacts.CONTACT_ID + "||','||" + Data.DATA1; 6036 } 6037 break; 6038 } 6039 6040 case POSTALS_ID: { 6041 setTablesAndProjectionMapForData(qb, uri, projection, false); 6042 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 6043 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = " 6044 + mDbHelper.get().getMimeTypeIdForStructuredPostal()); 6045 qb.appendWhere(" AND " + Data._ID + "=?"); 6046 break; 6047 } 6048 6049 case RAW_CONTACTS: 6050 case PROFILE_RAW_CONTACTS: { 6051 setTablesAndProjectionMapForRawContacts(qb, uri); 6052 break; 6053 } 6054 6055 case RAW_CONTACTS_ID: 6056 case PROFILE_RAW_CONTACTS_ID: { 6057 long rawContactId = ContentUris.parseId(uri); 6058 setTablesAndProjectionMapForRawContacts(qb, uri); 6059 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 6060 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 6061 break; 6062 } 6063 6064 case RAW_CONTACTS_ID_DATA: 6065 case PROFILE_RAW_CONTACTS_ID_DATA: { 6066 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2; 6067 long rawContactId = Long.parseLong(uri.getPathSegments().get(segment)); 6068 setTablesAndProjectionMapForData(qb, uri, projection, false); 6069 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 6070 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?"); 6071 break; 6072 } 6073 6074 case RAW_CONTACTS_ID_STREAM_ITEMS: { 6075 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 6076 setTablesAndProjectionMapForStreamItems(qb); 6077 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 6078 qb.appendWhere(StreamItems.RAW_CONTACT_ID + "=?"); 6079 break; 6080 } 6081 6082 case RAW_CONTACTS_ID_STREAM_ITEMS_ID: { 6083 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 6084 long streamItemId = Long.parseLong(uri.getPathSegments().get(3)); 6085 setTablesAndProjectionMapForStreamItems(qb); 6086 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(streamItemId)); 6087 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 6088 qb.appendWhere(StreamItems.RAW_CONTACT_ID + "=? AND " + 6089 StreamItems._ID + "=?"); 6090 break; 6091 } 6092 6093 case PROFILE_RAW_CONTACTS_ID_ENTITIES: { 6094 long rawContactId = Long.parseLong(uri.getPathSegments().get(2)); 6095 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 6096 setTablesAndProjectionMapForRawEntities(qb, uri); 6097 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 6098 break; 6099 } 6100 6101 case DATA: 6102 case PROFILE_DATA: { 6103 final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE); 6104 final int typeInt = getDataUsageFeedbackType(usageType, USAGE_TYPE_ALL); 6105 setTablesAndProjectionMapForData(qb, uri, projection, false, typeInt); 6106 if (uri.getBooleanQueryParameter(Data.VISIBLE_CONTACTS_ONLY, false)) { 6107 qb.appendWhere(" AND " + Data.CONTACT_ID + " in " + 6108 Tables.DEFAULT_DIRECTORY); 6109 } 6110 break; 6111 } 6112 6113 case DATA_ID: 6114 case PROFILE_DATA_ID: { 6115 setTablesAndProjectionMapForData(qb, uri, projection, false); 6116 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 6117 qb.appendWhere(" AND " + Data._ID + "=?"); 6118 break; 6119 } 6120 6121 case PROFILE_PHOTO: { 6122 setTablesAndProjectionMapForData(qb, uri, projection, false); 6123 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 6124 break; 6125 } 6126 6127 case PHONE_LOOKUP: { 6128 // Phone lookup cannot be combined with a selection 6129 selection = null; 6130 selectionArgs = null; 6131 if (uri.getBooleanQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false)) { 6132 if (TextUtils.isEmpty(sortOrder)) { 6133 // Default the sort order to something reasonable so we get consistent 6134 // results when callers don't request an ordering 6135 sortOrder = Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; 6136 } 6137 6138 String sipAddress = uri.getPathSegments().size() > 1 6139 ? Uri.decode(uri.getLastPathSegment()) : ""; 6140 setTablesAndProjectionMapForData(qb, uri, null, false, true); 6141 StringBuilder sb = new StringBuilder(); 6142 selectionArgs = mDbHelper.get().buildSipContactQuery(sb, sipAddress); 6143 selection = sb.toString(); 6144 } else { 6145 // Use this flag to track whether sortOrder was originally empty 6146 boolean sortOrderIsEmpty = false; 6147 if (TextUtils.isEmpty(sortOrder)) { 6148 // Default the sort order to something reasonable so we get consistent 6149 // results when callers don't request an ordering 6150 sortOrder = " length(lookup.normalized_number) DESC"; 6151 sortOrderIsEmpty = true; 6152 } 6153 6154 String number = uri.getPathSegments().size() > 1 6155 ? uri.getLastPathSegment() : ""; 6156 String numberE164 = PhoneNumberUtils.formatNumberToE164(number, 6157 mDbHelper.get().getCurrentCountryIso()); 6158 String normalizedNumber = 6159 PhoneNumberUtils.normalizeNumber(number); 6160 mDbHelper.get().buildPhoneLookupAndContactQuery( 6161 qb, normalizedNumber, numberE164); 6162 qb.setProjectionMap(sPhoneLookupProjectionMap); 6163 6164 // Peek at the results of the first query (which attempts to use fully 6165 // normalized and internationalized numbers for comparison). If no results 6166 // were returned, fall back to using the SQLite function 6167 // phone_number_compare_loose. 6168 qb.setStrict(true); 6169 boolean foundResult = false; 6170 Cursor cursor = query(db, qb, projection, selection, selectionArgs, 6171 sortOrder, groupBy, null, limit, cancellationSignal); 6172 try { 6173 if (cursor.getCount() > 0) { 6174 foundResult = true; 6175 return cursor; 6176 } else { 6177 // Use fallback lookup method 6178 6179 qb = new SQLiteQueryBuilder(); 6180 6181 // use the raw number instead of the normalized number because 6182 // phone_number_compare_loose in SQLite works only with non-normalized 6183 // numbers 6184 mDbHelper.get().buildFallbackPhoneLookupAndContactQuery(qb, number); 6185 6186 qb.setProjectionMap(sPhoneLookupProjectionMap); 6187 } 6188 } finally { 6189 if (!foundResult) { 6190 // We'll be returning a different cursor, so close this one. 6191 cursor.close(); 6192 } 6193 } 6194 } 6195 break; 6196 } 6197 6198 case GROUPS: { 6199 qb.setTables(Views.GROUPS); 6200 qb.setProjectionMap(sGroupsProjectionMap); 6201 appendAccountIdFromParameter(qb, uri); 6202 break; 6203 } 6204 6205 case GROUPS_ID: { 6206 qb.setTables(Views.GROUPS); 6207 qb.setProjectionMap(sGroupsProjectionMap); 6208 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 6209 qb.appendWhere(Groups._ID + "=?"); 6210 break; 6211 } 6212 6213 case GROUPS_SUMMARY: { 6214 String tables = Views.GROUPS + " AS " + Tables.GROUPS; 6215 if (ContactsDatabaseHelper.isInProjection(projection, Groups.SUMMARY_COUNT)) { 6216 tables = tables + Joins.GROUP_MEMBER_COUNT; 6217 } 6218 if (ContactsDatabaseHelper.isInProjection(projection, 6219 Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT)) { 6220 // TODO Add join for this column too (and update the projection map) 6221 // TODO Also remove Groups.PARAM_RETURN_GROUP_COUNT_PER_ACCOUNT when it works. 6222 Log.w(TAG, Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT + " is not supported yet"); 6223 } 6224 qb.setTables(tables); 6225 qb.setProjectionMap(sGroupsSummaryProjectionMap); 6226 appendAccountIdFromParameter(qb, uri); 6227 groupBy = GroupsColumns.CONCRETE_ID; 6228 break; 6229 } 6230 6231 case AGGREGATION_EXCEPTIONS: { 6232 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 6233 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 6234 break; 6235 } 6236 6237 case AGGREGATION_SUGGESTIONS: { 6238 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 6239 String filter = null; 6240 if (uri.getPathSegments().size() > 3) { 6241 filter = uri.getPathSegments().get(3); 6242 } 6243 final int maxSuggestions; 6244 if (limit != null) { 6245 maxSuggestions = Integer.parseInt(limit); 6246 } else { 6247 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 6248 } 6249 6250 ArrayList<AggregationSuggestionParameter> parameters = null; 6251 List<String> query = uri.getQueryParameters("query"); 6252 if (query != null && !query.isEmpty()) { 6253 parameters = new ArrayList<AggregationSuggestionParameter>(query.size()); 6254 for (String parameter : query) { 6255 int offset = parameter.indexOf(':'); 6256 parameters.add(offset == -1 6257 ? new AggregationSuggestionParameter( 6258 AggregationSuggestions.PARAMETER_MATCH_NAME, 6259 parameter) 6260 : new AggregationSuggestionParameter( 6261 parameter.substring(0, offset), 6262 parameter.substring(offset + 1))); 6263 } 6264 } 6265 6266 setTablesAndProjectionMapForContacts(qb, uri, projection); 6267 6268 return mAggregator.get().queryAggregationSuggestions(qb, projection, contactId, 6269 maxSuggestions, filter, parameters); 6270 } 6271 6272 case SETTINGS: { 6273 qb.setTables(Tables.SETTINGS); 6274 qb.setProjectionMap(sSettingsProjectionMap); 6275 appendAccountFromParameter(qb, uri); 6276 6277 // When requesting specific columns, this query requires 6278 // late-binding of the GroupMembership MIME-type. 6279 final String groupMembershipMimetypeId = Long.toString(mDbHelper.get() 6280 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 6281 if (projection != null && projection.length != 0 && 6282 mDbHelper.get().isInProjection(projection, Settings.UNGROUPED_COUNT)) { 6283 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 6284 } 6285 if (projection != null && projection.length != 0 && 6286 mDbHelper.get().isInProjection( 6287 projection, Settings.UNGROUPED_WITH_PHONES)) { 6288 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 6289 } 6290 6291 break; 6292 } 6293 6294 case STATUS_UPDATES: 6295 case PROFILE_STATUS_UPDATES: { 6296 setTableAndProjectionMapForStatusUpdates(qb, projection); 6297 break; 6298 } 6299 6300 case STATUS_UPDATES_ID: { 6301 setTableAndProjectionMapForStatusUpdates(qb, projection); 6302 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 6303 qb.appendWhere(DataColumns.CONCRETE_ID + "=?"); 6304 break; 6305 } 6306 6307 case SEARCH_SUGGESTIONS: { 6308 return mGlobalSearchSupport.handleSearchSuggestionsQuery( 6309 db, uri, projection, limit, cancellationSignal); 6310 } 6311 6312 case SEARCH_SHORTCUT: { 6313 String lookupKey = uri.getLastPathSegment(); 6314 String filter = getQueryParameter( 6315 uri, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 6316 return mGlobalSearchSupport.handleSearchShortcutRefresh( 6317 db, projection, lookupKey, filter, cancellationSignal); 6318 } 6319 6320 case RAW_CONTACT_ENTITIES: 6321 case PROFILE_RAW_CONTACT_ENTITIES: { 6322 setTablesAndProjectionMapForRawEntities(qb, uri); 6323 break; 6324 } 6325 6326 case RAW_CONTACT_ID_ENTITY: { 6327 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 6328 setTablesAndProjectionMapForRawEntities(qb, uri); 6329 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId)); 6330 qb.appendWhere(" AND " + RawContacts._ID + "=?"); 6331 break; 6332 } 6333 6334 case PROVIDER_STATUS: { 6335 return buildSingleRowResult(projection, 6336 new String[] {ProviderStatus.STATUS, ProviderStatus.DATA1}, 6337 new Object[] {mProviderStatus, mEstimatedStorageRequirement}); 6338 } 6339 6340 case DIRECTORIES : { 6341 qb.setTables(Tables.DIRECTORIES); 6342 qb.setProjectionMap(sDirectoryProjectionMap); 6343 break; 6344 } 6345 6346 case DIRECTORIES_ID : { 6347 long id = ContentUris.parseId(uri); 6348 qb.setTables(Tables.DIRECTORIES); 6349 qb.setProjectionMap(sDirectoryProjectionMap); 6350 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id)); 6351 qb.appendWhere(Directory._ID + "=?"); 6352 break; 6353 } 6354 6355 case COMPLETE_NAME: { 6356 return completeName(uri, projection); 6357 } 6358 6359 case DELETED_CONTACTS: { 6360 qb.setTables(Tables.DELETED_CONTACTS); 6361 qb.setProjectionMap(sDeletedContactsProjectionMap); 6362 break; 6363 } 6364 6365 case DELETED_CONTACTS_ID: { 6366 String id = uri.getLastPathSegment(); 6367 qb.setTables(Tables.DELETED_CONTACTS); 6368 qb.setProjectionMap(sDeletedContactsProjectionMap); 6369 qb.appendWhere(ContactsContract.DeletedContacts.CONTACT_ID + "=?"); 6370 selectionArgs = insertSelectionArg(selectionArgs, id); 6371 break; 6372 } 6373 6374 default: 6375 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 6376 sortOrder, limit); 6377 } 6378 6379 qb.setStrict(true); 6380 6381 // Auto-rewrite SORT_KEY_{PRIMARY, ALTERNATIVE} sort orders. 6382 String localizedSortOrder = getLocalizedSortOrder(sortOrder); 6383 Cursor cursor = 6384 query(db, qb, projection, selection, selectionArgs, localizedSortOrder, groupBy, 6385 having, limit, cancellationSignal); 6386 6387 if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) { 6388 bundleFastScrollingIndexExtras(cursor, uri, db, qb, selection, 6389 selectionArgs, sortOrder, addressBookIndexerCountExpression, 6390 cancellationSignal); 6391 } 6392 if (snippetDeferred) { 6393 cursor = addDeferredSnippetingExtra(cursor); 6394 } 6395 6396 return cursor; 6397 } 6398 6399 6400 // Rewrites query sort orders using SORT_KEY_{PRIMARY, ALTERNATIVE} 6401 // to use PHONEBOOK_BUCKET_{PRIMARY, ALTERNATIVE} as primary key; all 6402 // other sort orders are returned unchanged. Preserves ordering 6403 // (eg 'DESC') if present. 6404 protected static String getLocalizedSortOrder(String sortOrder) { 6405 String localizedSortOrder = sortOrder; 6406 if (sortOrder != null) { 6407 String sortKey; 6408 String sortOrderSuffix = ""; 6409 int spaceIndex = sortOrder.indexOf(' '); 6410 if (spaceIndex != -1) { 6411 sortKey = sortOrder.substring(0, spaceIndex); 6412 sortOrderSuffix = sortOrder.substring(spaceIndex); 6413 } else { 6414 sortKey = sortOrder; 6415 } 6416 if (TextUtils.equals(sortKey, Contacts.SORT_KEY_PRIMARY)) { 6417 localizedSortOrder = ContactsColumns.PHONEBOOK_BUCKET_PRIMARY 6418 + sortOrderSuffix + ", " + sortOrder; 6419 } else if (TextUtils.equals(sortKey, Contacts.SORT_KEY_ALTERNATIVE)) { 6420 localizedSortOrder = ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE 6421 + sortOrderSuffix + ", " + sortOrder; 6422 } 6423 } 6424 return localizedSortOrder; 6425 } 6426 6427 6428 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 6429 String selection, String[] selectionArgs, String sortOrder, String groupBy, 6430 String having, String limit, CancellationSignal cancellationSignal) { 6431 if (projection != null && projection.length == 1 6432 && BaseColumns._COUNT.equals(projection[0])) { 6433 qb.setProjectionMap(sCountProjectionMap); 6434 } 6435 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, having, 6436 sortOrder, limit, cancellationSignal); 6437 if (c != null) { 6438 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 6439 } 6440 return c; 6441 } 6442 6443 6444 /** 6445 * Runs the query with the supplied contact ID and lookup ID. If the query succeeds, 6446 * it returns the resulting cursor, otherwise it returns null and the calling 6447 * method needs to resolve the lookup key and rerun the query. 6448 * @param cancellationSignal 6449 */ 6450 private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb, 6451 SQLiteDatabase db, Uri uri, 6452 String[] projection, String selection, String[] selectionArgs, 6453 String sortOrder, String groupBy, String limit, 6454 String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey, 6455 CancellationSignal cancellationSignal) { 6456 String[] args; 6457 if (selectionArgs == null) { 6458 args = new String[2]; 6459 } else { 6460 args = new String[selectionArgs.length + 2]; 6461 System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length); 6462 } 6463 args[0] = String.valueOf(contactId); 6464 args[1] = Uri.encode(lookupKey); 6465 lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?"); 6466 Cursor c = query(db, lookupQb, projection, selection, args, sortOrder, 6467 groupBy, null, limit, cancellationSignal); 6468 if (c.getCount() != 0) { 6469 return c; 6470 } 6471 6472 c.close(); 6473 return null; 6474 } 6475 6476 private void invalidateFastScrollingIndexCache() { 6477 // FastScrollingIndexCache is thread-safe, no need to synchronize here. 6478 mFastScrollingIndexCache.invalidate(); 6479 } 6480 6481 /** 6482 * Add the "fast scrolling index" bundle, generated by {@link #getFastScrollingIndexExtras}, 6483 * to a cursor as extras. It first checks {@link FastScrollingIndexCache} to see if we 6484 * already have a cached result. 6485 */ 6486 private void bundleFastScrollingIndexExtras(Cursor cursor, Uri queryUri, 6487 final SQLiteDatabase db, SQLiteQueryBuilder qb, String selection, 6488 String[] selectionArgs, String sortOrder, String countExpression, 6489 CancellationSignal cancellationSignal) { 6490 if (!(cursor instanceof AbstractCursor)) { 6491 Log.w(TAG, "Unable to bundle extras. Cursor is not AbstractCursor."); 6492 return; 6493 } 6494 Bundle b; 6495 // Note even though FastScrollingIndexCache is thread-safe, we really need to put the 6496 // put-get pair in a single synchronized block, so that even if multiple-threads request the 6497 // same index at the same time (which actually happens on the phone app) we only execute 6498 // the query once. 6499 // 6500 // This doesn't cause deadlock, because only reader threads get here but not writer 6501 // threads. (Writer threads may call invalidateFastScrollingIndexCache(), but it doesn't 6502 // synchronize on mFastScrollingIndexCache) 6503 // 6504 // All reader and writer threads share the single lock object internally in 6505 // FastScrollingIndexCache, but the lock scope is limited within each put(), get() and 6506 // invalidate() call, so it won't deadlock. 6507 6508 // Synchronizing on a non-static field is generally not a good idea, but nobody should 6509 // modify mFastScrollingIndexCache once initialized, and it shouldn't be null at this point. 6510 synchronized (mFastScrollingIndexCache) { 6511 // First, try the cache. 6512 mFastScrollingIndexCacheRequestCount++; 6513 b = mFastScrollingIndexCache.get(queryUri, selection, selectionArgs, sortOrder, 6514 countExpression); 6515 6516 if (b == null) { 6517 mFastScrollingIndexCacheMissCount++; 6518 // Not in the cache. Generate and put. 6519 final long start = System.currentTimeMillis(); 6520 6521 b = getFastScrollingIndexExtras(queryUri, db, qb, selection, selectionArgs, 6522 sortOrder, countExpression, cancellationSignal); 6523 6524 final long end = System.currentTimeMillis(); 6525 final int time = (int) (end - start); 6526 mTotalTimeFastScrollingIndexGenerate += time; 6527 if (VERBOSE_LOGGING) { 6528 Log.v(TAG, "getLetterCountExtraBundle took " + time + "ms"); 6529 } 6530 mFastScrollingIndexCache.put(queryUri, selection, selectionArgs, sortOrder, 6531 countExpression, b); 6532 } 6533 } 6534 ((AbstractCursor) cursor).setExtras(b); 6535 } 6536 6537 private static final class AddressBookIndexQuery { 6538 public static final String NAME = "name"; 6539 public static final String BUCKET = "bucket"; 6540 public static final String LABEL = "label"; 6541 public static final String COUNT = "count"; 6542 6543 public static final String[] COLUMNS = new String[] { 6544 NAME, BUCKET, LABEL, COUNT 6545 }; 6546 6547 public static final int COLUMN_NAME = 0; 6548 public static final int COLUMN_BUCKET = 1; 6549 public static final int COLUMN_LABEL = 2; 6550 public static final int COLUMN_COUNT = 3; 6551 6552 public static final String GROUP_BY = BUCKET + ", " + LABEL; 6553 public static final String ORDER_BY = 6554 BUCKET + ", " + NAME + " COLLATE " + PHONEBOOK_COLLATOR_NAME; 6555 } 6556 6557 /** 6558 * Computes counts by the address book index labels and returns it as {@link Bundle} which 6559 * will be appended to a {@link Cursor} as extras. 6560 */ 6561 private static Bundle getFastScrollingIndexExtras(final Uri queryUri, final SQLiteDatabase db, 6562 final SQLiteQueryBuilder qb, final String selection, final String[] selectionArgs, 6563 final String sortOrder, String countExpression, 6564 final CancellationSignal cancellationSignal) { 6565 String sortKey; 6566 6567 // The sort order suffix could be something like "DESC". 6568 // We want to preserve it in the query even though we will change 6569 // the sort column itself. 6570 String sortOrderSuffix = ""; 6571 if (sortOrder != null) { 6572 int spaceIndex = sortOrder.indexOf(' '); 6573 if (spaceIndex != -1) { 6574 sortKey = sortOrder.substring(0, spaceIndex); 6575 sortOrderSuffix = sortOrder.substring(spaceIndex); 6576 } else { 6577 sortKey = sortOrder; 6578 } 6579 } else { 6580 sortKey = Contacts.SORT_KEY_PRIMARY; 6581 } 6582 6583 String bucketKey; 6584 String labelKey; 6585 if (TextUtils.equals(sortKey, Contacts.SORT_KEY_PRIMARY)) { 6586 bucketKey = ContactsColumns.PHONEBOOK_BUCKET_PRIMARY; 6587 labelKey = ContactsColumns.PHONEBOOK_LABEL_PRIMARY; 6588 } else if (TextUtils.equals(sortKey, Contacts.SORT_KEY_ALTERNATIVE)) { 6589 bucketKey = ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE; 6590 labelKey = ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE; 6591 } else { 6592 return null; 6593 } 6594 6595 HashMap<String, String> projectionMap = Maps.newHashMap(); 6596 projectionMap.put(AddressBookIndexQuery.NAME, 6597 sortKey + " AS " + AddressBookIndexQuery.NAME); 6598 projectionMap.put(AddressBookIndexQuery.BUCKET, 6599 bucketKey + " AS " + AddressBookIndexQuery.BUCKET); 6600 projectionMap.put(AddressBookIndexQuery.LABEL, 6601 labelKey + " AS " + AddressBookIndexQuery.LABEL); 6602 6603 // If "what to count" is not specified, we just count all records. 6604 if (TextUtils.isEmpty(countExpression)) { 6605 countExpression = "*"; 6606 } 6607 6608 projectionMap.put(AddressBookIndexQuery.COUNT, 6609 "COUNT(" + countExpression + ") AS " + AddressBookIndexQuery.COUNT); 6610 qb.setProjectionMap(projectionMap); 6611 String orderBy = AddressBookIndexQuery.BUCKET + sortOrderSuffix 6612 + ", " + AddressBookIndexQuery.NAME + " COLLATE " 6613 + PHONEBOOK_COLLATOR_NAME + sortOrderSuffix; 6614 6615 Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs, 6616 AddressBookIndexQuery.GROUP_BY, null /* having */, 6617 orderBy, null, cancellationSignal); 6618 6619 try { 6620 int numLabels = indexCursor.getCount(); 6621 String labels[] = new String[numLabels]; 6622 int counts[] = new int[numLabels]; 6623 6624 for (int i = 0; i < numLabels; i++) { 6625 indexCursor.moveToNext(); 6626 labels[i] = indexCursor.getString(AddressBookIndexQuery.COLUMN_LABEL); 6627 counts[i] = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT); 6628 } 6629 6630 return FastScrollingIndexCache.buildExtraBundle(labels, counts); 6631 } finally { 6632 indexCursor.close(); 6633 } 6634 } 6635 6636 /** 6637 * Returns the contact Id for the contact identified by the lookupKey. 6638 * Robust against changes in the lookup key: if the key has changed, will 6639 * look up the contact by the raw contact IDs or name encoded in the lookup 6640 * key. 6641 */ 6642 public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 6643 ContactLookupKey key = new ContactLookupKey(); 6644 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 6645 6646 long contactId = -1; 6647 if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_PROFILE)) { 6648 // We should already be in a profile database context, so just look up a single contact. 6649 contactId = lookupSingleContactId(db); 6650 } 6651 6652 if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) { 6653 contactId = lookupContactIdBySourceIds(db, segments); 6654 if (contactId != -1) { 6655 return contactId; 6656 } 6657 } 6658 6659 boolean hasRawContactIds = 6660 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID); 6661 if (hasRawContactIds) { 6662 contactId = lookupContactIdByRawContactIds(db, segments); 6663 if (contactId != -1) { 6664 return contactId; 6665 } 6666 } 6667 6668 if (hasRawContactIds 6669 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) { 6670 contactId = lookupContactIdByDisplayNames(db, segments); 6671 } 6672 6673 return contactId; 6674 } 6675 6676 private long lookupSingleContactId(SQLiteDatabase db) { 6677 Cursor c = db.query(Tables.CONTACTS, new String[] {Contacts._ID}, 6678 null, null, null, null, null, "1"); 6679 try { 6680 if (c.moveToFirst()) { 6681 return c.getLong(0); 6682 } else { 6683 return -1; 6684 } 6685 } finally { 6686 c.close(); 6687 } 6688 } 6689 6690 private interface LookupBySourceIdQuery { 6691 String TABLE = Views.RAW_CONTACTS; 6692 6693 String COLUMNS[] = { 6694 RawContacts.CONTACT_ID, 6695 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 6696 RawContacts.ACCOUNT_NAME, 6697 RawContacts.SOURCE_ID 6698 }; 6699 6700 int CONTACT_ID = 0; 6701 int ACCOUNT_TYPE_AND_DATA_SET = 1; 6702 int ACCOUNT_NAME = 2; 6703 int SOURCE_ID = 3; 6704 } 6705 6706 private long lookupContactIdBySourceIds(SQLiteDatabase db, 6707 ArrayList<LookupKeySegment> segments) { 6708 StringBuilder sb = new StringBuilder(); 6709 sb.append(RawContacts.SOURCE_ID + " IN ("); 6710 for (int i = 0; i < segments.size(); i++) { 6711 LookupKeySegment segment = segments.get(i); 6712 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) { 6713 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 6714 sb.append(","); 6715 } 6716 } 6717 sb.setLength(sb.length() - 1); // Last comma 6718 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 6719 6720 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 6721 sb.toString(), null, null, null, null); 6722 try { 6723 while (c.moveToNext()) { 6724 String accountTypeAndDataSet = 6725 c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE_AND_DATA_SET); 6726 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 6727 int accountHashCode = 6728 ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName); 6729 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 6730 for (int i = 0; i < segments.size(); i++) { 6731 LookupKeySegment segment = segments.get(i); 6732 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID 6733 && accountHashCode == segment.accountHashCode 6734 && segment.key.equals(sourceId)) { 6735 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 6736 break; 6737 } 6738 } 6739 } 6740 } finally { 6741 c.close(); 6742 } 6743 6744 return getMostReferencedContactId(segments); 6745 } 6746 6747 private interface LookupByRawContactIdQuery { 6748 String TABLE = Views.RAW_CONTACTS; 6749 6750 String COLUMNS[] = { 6751 RawContacts.CONTACT_ID, 6752 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 6753 RawContacts.ACCOUNT_NAME, 6754 RawContacts._ID, 6755 }; 6756 6757 int CONTACT_ID = 0; 6758 int ACCOUNT_TYPE_AND_DATA_SET = 1; 6759 int ACCOUNT_NAME = 2; 6760 int ID = 3; 6761 } 6762 6763 private long lookupContactIdByRawContactIds(SQLiteDatabase db, 6764 ArrayList<LookupKeySegment> segments) { 6765 StringBuilder sb = new StringBuilder(); 6766 sb.append(RawContacts._ID + " IN ("); 6767 for (int i = 0; i < segments.size(); i++) { 6768 LookupKeySegment segment = segments.get(i); 6769 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 6770 sb.append(segment.rawContactId); 6771 sb.append(","); 6772 } 6773 } 6774 sb.setLength(sb.length() - 1); // Last comma 6775 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 6776 6777 Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS, 6778 sb.toString(), null, null, null, null); 6779 try { 6780 while (c.moveToNext()) { 6781 String accountTypeAndDataSet = c.getString( 6782 LookupByRawContactIdQuery.ACCOUNT_TYPE_AND_DATA_SET); 6783 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME); 6784 int accountHashCode = 6785 ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName); 6786 String rawContactId = c.getString(LookupByRawContactIdQuery.ID); 6787 for (int i = 0; i < segments.size(); i++) { 6788 LookupKeySegment segment = segments.get(i); 6789 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID 6790 && accountHashCode == segment.accountHashCode 6791 && segment.rawContactId.equals(rawContactId)) { 6792 segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID); 6793 break; 6794 } 6795 } 6796 } 6797 } finally { 6798 c.close(); 6799 } 6800 6801 return getMostReferencedContactId(segments); 6802 } 6803 6804 private interface LookupByDisplayNameQuery { 6805 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 6806 6807 String COLUMNS[] = { 6808 RawContacts.CONTACT_ID, 6809 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 6810 RawContacts.ACCOUNT_NAME, 6811 NameLookupColumns.NORMALIZED_NAME 6812 }; 6813 6814 int CONTACT_ID = 0; 6815 int ACCOUNT_TYPE_AND_DATA_SET = 1; 6816 int ACCOUNT_NAME = 2; 6817 int NORMALIZED_NAME = 3; 6818 } 6819 6820 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 6821 ArrayList<LookupKeySegment> segments) { 6822 StringBuilder sb = new StringBuilder(); 6823 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 6824 for (int i = 0; i < segments.size(); i++) { 6825 LookupKeySegment segment = segments.get(i); 6826 if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 6827 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) { 6828 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 6829 sb.append(","); 6830 } 6831 } 6832 sb.setLength(sb.length() - 1); // Last comma 6833 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 6834 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 6835 6836 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 6837 sb.toString(), null, null, null, null); 6838 try { 6839 while (c.moveToNext()) { 6840 String accountTypeAndDataSet = 6841 c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET); 6842 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 6843 int accountHashCode = 6844 ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName); 6845 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 6846 for (int i = 0; i < segments.size(); i++) { 6847 LookupKeySegment segment = segments.get(i); 6848 if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME 6849 || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) 6850 && accountHashCode == segment.accountHashCode 6851 && segment.key.equals(name)) { 6852 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 6853 break; 6854 } 6855 } 6856 } 6857 } finally { 6858 c.close(); 6859 } 6860 6861 return getMostReferencedContactId(segments); 6862 } 6863 6864 private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) { 6865 for (int i = 0; i < segments.size(); i++) { 6866 LookupKeySegment segment = segments.get(i); 6867 if (segment.lookupType == lookupType) { 6868 return true; 6869 } 6870 } 6871 6872 return false; 6873 } 6874 6875 /** 6876 * Returns the contact ID that is mentioned the highest number of times. 6877 */ 6878 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 6879 Collections.sort(segments); 6880 6881 long bestContactId = -1; 6882 int bestRefCount = 0; 6883 6884 long contactId = -1; 6885 int count = 0; 6886 6887 int segmentCount = segments.size(); 6888 for (int i = 0; i < segmentCount; i++) { 6889 LookupKeySegment segment = segments.get(i); 6890 if (segment.contactId != -1) { 6891 if (segment.contactId == contactId) { 6892 count++; 6893 } else { 6894 if (count > bestRefCount) { 6895 bestContactId = contactId; 6896 bestRefCount = count; 6897 } 6898 contactId = segment.contactId; 6899 count = 1; 6900 } 6901 } 6902 } 6903 if (count > bestRefCount) { 6904 return contactId; 6905 } else { 6906 return bestContactId; 6907 } 6908 } 6909 6910 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 6911 String[] projection) { 6912 setTablesAndProjectionMapForContacts(qb, uri, projection, false); 6913 } 6914 6915 /** 6916 * @param includeDataUsageStat true when the table should include DataUsageStat table. 6917 * Note that this uses INNER JOIN instead of LEFT OUTER JOIN, so some of data in Contacts 6918 * may be dropped. 6919 */ 6920 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, 6921 String[] projection, boolean includeDataUsageStat) { 6922 StringBuilder sb = new StringBuilder(); 6923 if (includeDataUsageStat) { 6924 sb.append(Views.DATA_USAGE_STAT + " AS " + Tables.DATA_USAGE_STAT); 6925 sb.append(" INNER JOIN "); 6926 } 6927 6928 sb.append(Views.CONTACTS); 6929 6930 // Just for frequently contacted contacts in Strequent Uri handling. 6931 if (includeDataUsageStat) { 6932 sb.append(" ON (" + 6933 DbQueryUtils.concatenateClauses( 6934 DataUsageStatColumns.CONCRETE_TIMES_USED + " > 0", 6935 RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID) + 6936 ")"); 6937 } 6938 6939 appendContactPresenceJoin(sb, projection, Contacts._ID); 6940 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 6941 qb.setTables(sb.toString()); 6942 qb.setProjectionMap(sContactsProjectionMap); 6943 } 6944 6945 /** 6946 * Finds name lookup records matching the supplied filter, picks one arbitrary match per 6947 * contact and joins that with other contacts tables. 6948 */ 6949 private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri, 6950 String[] projection, String filter, long directoryId, boolean deferSnippeting) { 6951 6952 StringBuilder sb = new StringBuilder(); 6953 sb.append(Views.CONTACTS); 6954 6955 if (filter != null) { 6956 filter = filter.trim(); 6957 } 6958 6959 if (TextUtils.isEmpty(filter) || (directoryId != -1 && directoryId != Directory.DEFAULT)) { 6960 sb.append(" JOIN (SELECT NULL AS " + SearchSnippetColumns.SNIPPET + " WHERE 0)"); 6961 } else { 6962 appendSearchIndexJoin(sb, uri, projection, filter, deferSnippeting); 6963 } 6964 appendContactPresenceJoin(sb, projection, Contacts._ID); 6965 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 6966 qb.setTables(sb.toString()); 6967 qb.setProjectionMap(sContactsProjectionWithSnippetMap); 6968 } 6969 6970 private void appendSearchIndexJoin( 6971 StringBuilder sb, Uri uri, String[] projection, String filter, 6972 boolean deferSnippeting) { 6973 6974 if (snippetNeeded(projection)) { 6975 String[] args = null; 6976 String snippetArgs = 6977 getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY); 6978 if (snippetArgs != null) { 6979 args = snippetArgs.split(","); 6980 } 6981 6982 String startMatch = args != null && args.length > 0 ? args[0] 6983 : DEFAULT_SNIPPET_ARG_START_MATCH; 6984 String endMatch = args != null && args.length > 1 ? args[1] 6985 : DEFAULT_SNIPPET_ARG_END_MATCH; 6986 String ellipsis = args != null && args.length > 2 ? args[2] 6987 : DEFAULT_SNIPPET_ARG_ELLIPSIS; 6988 int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3]) 6989 : DEFAULT_SNIPPET_ARG_MAX_TOKENS; 6990 6991 appendSearchIndexJoin( 6992 sb, filter, true, startMatch, endMatch, ellipsis, maxTokens, 6993 deferSnippeting); 6994 } else { 6995 appendSearchIndexJoin(sb, filter, false, null, null, null, 0, false); 6996 } 6997 } 6998 6999 public void appendSearchIndexJoin(StringBuilder sb, String filter, 7000 boolean snippetNeeded, String startMatch, String endMatch, String ellipsis, 7001 int maxTokens, boolean deferSnippeting) { 7002 boolean isEmailAddress = false; 7003 String emailAddress = null; 7004 boolean isPhoneNumber = false; 7005 String phoneNumber = null; 7006 String numberE164 = null; 7007 7008 7009 if (filter.indexOf('@') != -1) { 7010 emailAddress = mDbHelper.get().extractAddressFromEmailAddress(filter); 7011 isEmailAddress = !TextUtils.isEmpty(emailAddress); 7012 } else { 7013 isPhoneNumber = isPhoneNumber(filter); 7014 if (isPhoneNumber) { 7015 phoneNumber = PhoneNumberUtils.normalizeNumber(filter); 7016 numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber, 7017 mDbHelper.get().getCurrentCountryIso()); 7018 } 7019 } 7020 7021 final String SNIPPET_CONTACT_ID = "snippet_contact_id"; 7022 sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS " + SNIPPET_CONTACT_ID); 7023 if (snippetNeeded) { 7024 sb.append(", "); 7025 if (isEmailAddress) { 7026 sb.append("ifnull("); 7027 if (!deferSnippeting) { 7028 // Add the snippet marker only when we're really creating snippet. 7029 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 7030 sb.append("||"); 7031 } 7032 sb.append("(SELECT MIN(" + Email.ADDRESS + ")"); 7033 sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS); 7034 sb.append(" WHERE " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID); 7035 sb.append("=" + RawContacts.CONTACT_ID + " AND " + Email.ADDRESS + " LIKE "); 7036 DatabaseUtils.appendEscapedSQLString(sb, filter + "%"); 7037 sb.append(")"); 7038 if (!deferSnippeting) { 7039 sb.append("||"); 7040 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 7041 } 7042 sb.append(","); 7043 7044 if (deferSnippeting) { 7045 sb.append(SearchIndexColumns.CONTENT); 7046 } else { 7047 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 7048 } 7049 sb.append(")"); 7050 } else if (isPhoneNumber) { 7051 sb.append("ifnull("); 7052 if (!deferSnippeting) { 7053 // Add the snippet marker only when we're really creating snippet. 7054 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 7055 sb.append("||"); 7056 } 7057 sb.append("(SELECT MIN(" + Phone.NUMBER + ")"); 7058 sb.append(" FROM " + 7059 Tables.DATA_JOIN_RAW_CONTACTS + " JOIN " + Tables.PHONE_LOOKUP); 7060 sb.append(" ON " + DataColumns.CONCRETE_ID); 7061 sb.append("=" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID); 7062 sb.append(" WHERE " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID); 7063 sb.append("=" + RawContacts.CONTACT_ID); 7064 sb.append(" AND " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 7065 sb.append(phoneNumber); 7066 sb.append("%'"); 7067 if (!TextUtils.isEmpty(numberE164)) { 7068 sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '"); 7069 sb.append(numberE164); 7070 sb.append("%'"); 7071 } 7072 sb.append(")"); 7073 if (! deferSnippeting) { 7074 sb.append("||"); 7075 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 7076 } 7077 sb.append(","); 7078 7079 if (deferSnippeting) { 7080 sb.append(SearchIndexColumns.CONTENT); 7081 } else { 7082 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 7083 } 7084 sb.append(")"); 7085 } else { 7086 final String normalizedFilter = NameNormalizer.normalize(filter); 7087 if (!TextUtils.isEmpty(normalizedFilter)) { 7088 if (deferSnippeting) { 7089 sb.append(SearchIndexColumns.CONTENT); 7090 } else { 7091 sb.append("(CASE WHEN EXISTS (SELECT 1 FROM "); 7092 sb.append(Tables.RAW_CONTACTS + " AS rc INNER JOIN "); 7093 sb.append(Tables.NAME_LOOKUP + " AS nl ON (rc." + RawContacts._ID); 7094 sb.append("=nl." + NameLookupColumns.RAW_CONTACT_ID); 7095 sb.append(") WHERE nl." + NameLookupColumns.NORMALIZED_NAME); 7096 sb.append(" GLOB '" + normalizedFilter + "*' AND "); 7097 sb.append("nl." + NameLookupColumns.NAME_TYPE + "="); 7098 sb.append(NameLookupType.NAME_COLLATION_KEY + " AND "); 7099 sb.append(Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID); 7100 sb.append("=rc." + RawContacts.CONTACT_ID); 7101 sb.append(") THEN NULL ELSE "); 7102 appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens); 7103 sb.append(" END)"); 7104 } 7105 } else { 7106 sb.append("NULL"); 7107 } 7108 } 7109 sb.append(" AS " + SearchSnippetColumns.SNIPPET); 7110 } 7111 7112 sb.append(" FROM " + Tables.SEARCH_INDEX); 7113 sb.append(" WHERE "); 7114 sb.append(Tables.SEARCH_INDEX + " MATCH '"); 7115 if (isEmailAddress) { 7116 // we know that the emailAddress contains a @. This phrase search should be 7117 // scoped against "content:" only, but unfortunately SQLite doesn't support 7118 // phrases and scoped columns at once. This is fine in this case however, because: 7119 // - We can't erronously match against name, as name is all-hex (so the @ can't match) 7120 // - We can't match against tokens, because phone-numbers can't contain @ 7121 final String sanitizedEmailAddress = 7122 emailAddress == null ? "" : sanitizeMatch(emailAddress); 7123 sb.append("\""); 7124 sb.append(sanitizedEmailAddress); 7125 sb.append("*\""); 7126 } else if (isPhoneNumber) { 7127 // normalized version of the phone number (phoneNumber can only have + and digits) 7128 final String phoneNumberCriteria = " OR tokens:" + phoneNumber + "*"; 7129 7130 // international version of this number (numberE164 can only have + and digits) 7131 final String numberE164Criteria = 7132 (numberE164 != null && !TextUtils.equals(numberE164, phoneNumber)) 7133 ? " OR tokens:" + numberE164 + "*" 7134 : ""; 7135 7136 // combine all criteria 7137 final String commonCriteria = 7138 phoneNumberCriteria + numberE164Criteria; 7139 7140 // search in content 7141 sb.append(SearchIndexManager.getFtsMatchQuery(filter, 7142 FtsQueryBuilder.getDigitsQueryBuilder(commonCriteria))); 7143 } else { 7144 // general case: not a phone number, not an email-address 7145 sb.append(SearchIndexManager.getFtsMatchQuery(filter, 7146 FtsQueryBuilder.SCOPED_NAME_NORMALIZING)); 7147 } 7148 // Omit results in "Other Contacts". 7149 sb.append("' AND " + SNIPPET_CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")"); 7150 sb.append(" ON (" + Contacts._ID + "=" + SNIPPET_CONTACT_ID + ")"); 7151 } 7152 7153 private static String sanitizeMatch(String filter) { 7154 return filter.replace("'", "").replace("*", "").replace("-", "").replace("\"", ""); 7155 } 7156 7157 private void appendSnippetFunction( 7158 StringBuilder sb, String startMatch, String endMatch, String ellipsis, int maxTokens) { 7159 sb.append("snippet(" + Tables.SEARCH_INDEX + ","); 7160 DatabaseUtils.appendEscapedSQLString(sb, startMatch); 7161 sb.append(","); 7162 DatabaseUtils.appendEscapedSQLString(sb, endMatch); 7163 sb.append(","); 7164 DatabaseUtils.appendEscapedSQLString(sb, ellipsis); 7165 7166 // The index of the column used for the snippet, "content" 7167 sb.append(",1,"); 7168 sb.append(maxTokens); 7169 sb.append(")"); 7170 } 7171 7172 private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) { 7173 StringBuilder sb = new StringBuilder(); 7174 sb.append(Views.RAW_CONTACTS); 7175 qb.setTables(sb.toString()); 7176 qb.setProjectionMap(sRawContactsProjectionMap); 7177 appendAccountIdFromParameter(qb, uri); 7178 } 7179 7180 private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) { 7181 qb.setTables(Views.RAW_ENTITIES); 7182 qb.setProjectionMap(sRawEntityProjectionMap); 7183 appendAccountIdFromParameter(qb, uri); 7184 } 7185 7186 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 7187 String[] projection, boolean distinct) { 7188 setTablesAndProjectionMapForData(qb, uri, projection, distinct, false, null); 7189 } 7190 7191 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 7192 String[] projection, boolean distinct, boolean addSipLookupColumns) { 7193 setTablesAndProjectionMapForData(qb, uri, projection, distinct, addSipLookupColumns, null); 7194 } 7195 7196 /** 7197 * @param usageType when non-null {@link Tables#DATA_USAGE_STAT} is joined with the specified 7198 * type. 7199 */ 7200 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 7201 String[] projection, boolean distinct, Integer usageType) { 7202 setTablesAndProjectionMapForData(qb, uri, projection, distinct, false, usageType); 7203 } 7204 7205 private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri, 7206 String[] projection, boolean distinct, boolean addSipLookupColumns, Integer usageType) { 7207 StringBuilder sb = new StringBuilder(); 7208 sb.append(Views.DATA); 7209 sb.append(" data"); 7210 7211 appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID); 7212 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 7213 appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); 7214 appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); 7215 7216 appendDataUsageStatJoin(sb, usageType == null ? USAGE_TYPE_ALL : usageType, 7217 DataColumns.CONCRETE_ID); 7218 7219 qb.setTables(sb.toString()); 7220 7221 boolean useDistinct = distinct || !ContactsDatabaseHelper.isInProjection( 7222 projection, DISTINCT_DATA_PROHIBITING_COLUMNS); 7223 qb.setDistinct(useDistinct); 7224 7225 final ProjectionMap projectionMap; 7226 if (addSipLookupColumns) { 7227 projectionMap = useDistinct 7228 ? sDistinctDataSipLookupProjectionMap : sDataSipLookupProjectionMap; 7229 } else { 7230 projectionMap = useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap; 7231 } 7232 7233 qb.setProjectionMap(projectionMap); 7234 appendAccountIdFromParameter(qb, uri); 7235 } 7236 7237 private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb, 7238 String[] projection) { 7239 StringBuilder sb = new StringBuilder(); 7240 sb.append(Views.DATA); 7241 sb.append(" data"); 7242 appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID); 7243 appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID); 7244 7245 qb.setTables(sb.toString()); 7246 qb.setProjectionMap(sStatusUpdatesProjectionMap); 7247 } 7248 7249 private void setTablesAndProjectionMapForStreamItems(SQLiteQueryBuilder qb) { 7250 qb.setTables(Views.STREAM_ITEMS); 7251 qb.setProjectionMap(sStreamItemsProjectionMap); 7252 } 7253 7254 private void setTablesAndProjectionMapForStreamItemPhotos(SQLiteQueryBuilder qb) { 7255 qb.setTables(Tables.PHOTO_FILES 7256 + " JOIN " + Tables.STREAM_ITEM_PHOTOS + " ON (" 7257 + StreamItemPhotosColumns.CONCRETE_PHOTO_FILE_ID + "=" 7258 + PhotoFilesColumns.CONCRETE_ID 7259 + ") JOIN " + Tables.STREAM_ITEMS + " ON (" 7260 + StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=" 7261 + StreamItemsColumns.CONCRETE_ID + ")" 7262 + " JOIN " + Tables.RAW_CONTACTS + " ON (" 7263 + StreamItemsColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID 7264 + ")"); 7265 qb.setProjectionMap(sStreamItemPhotosProjectionMap); 7266 } 7267 7268 private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri, 7269 String[] projection) { 7270 StringBuilder sb = new StringBuilder(); 7271 sb.append(Views.ENTITIES); 7272 sb.append(" data"); 7273 7274 appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID); 7275 appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID); 7276 appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID); 7277 appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID); 7278 7279 qb.setTables(sb.toString()); 7280 qb.setProjectionMap(sEntityProjectionMap); 7281 appendAccountIdFromParameter(qb, uri); 7282 } 7283 7284 private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection, 7285 String lastStatusUpdateIdColumn) { 7286 if (ContactsDatabaseHelper.isInProjection(projection, 7287 Contacts.CONTACT_STATUS, 7288 Contacts.CONTACT_STATUS_RES_PACKAGE, 7289 Contacts.CONTACT_STATUS_ICON, 7290 Contacts.CONTACT_STATUS_LABEL, 7291 Contacts.CONTACT_STATUS_TIMESTAMP)) { 7292 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " " 7293 + ContactsStatusUpdatesColumns.ALIAS + 7294 " ON (" + lastStatusUpdateIdColumn + "=" 7295 + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")"); 7296 } 7297 } 7298 7299 private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection, 7300 String dataIdColumn) { 7301 if (ContactsDatabaseHelper.isInProjection(projection, 7302 StatusUpdates.STATUS, 7303 StatusUpdates.STATUS_RES_PACKAGE, 7304 StatusUpdates.STATUS_ICON, 7305 StatusUpdates.STATUS_LABEL, 7306 StatusUpdates.STATUS_TIMESTAMP)) { 7307 sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + 7308 " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "=" 7309 + dataIdColumn + ")"); 7310 } 7311 } 7312 7313 private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) { 7314 if (usageType != USAGE_TYPE_ALL) { 7315 sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT + 7316 " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="); 7317 sb.append(dataIdColumn); 7318 sb.append(" AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "="); 7319 sb.append(usageType); 7320 sb.append(")"); 7321 } else { 7322 sb.append( 7323 " LEFT OUTER JOIN " + 7324 "(SELECT " + 7325 DataUsageStatColumns.CONCRETE_DATA_ID + ", " + 7326 "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + 7327 ") as " + DataUsageStatColumns.TIMES_USED + ", " + 7328 "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED + 7329 ") as " + DataUsageStatColumns.LAST_TIME_USED + 7330 " FROM " + Tables.DATA_USAGE_STAT + " GROUP BY " + 7331 DataUsageStatColumns.DATA_ID + ") as " + Tables.DATA_USAGE_STAT 7332 ); 7333 sb.append(" ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="); 7334 sb.append(dataIdColumn); 7335 sb.append(")"); 7336 } 7337 } 7338 7339 private void appendContactPresenceJoin(StringBuilder sb, String[] projection, 7340 String contactIdColumn) { 7341 if (ContactsDatabaseHelper.isInProjection(projection, 7342 Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) { 7343 sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + 7344 " ON (" + contactIdColumn + " = " 7345 + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")"); 7346 } 7347 } 7348 7349 private void appendDataPresenceJoin(StringBuilder sb, String[] projection, 7350 String dataIdColumn) { 7351 if (ContactsDatabaseHelper.isInProjection( 7352 projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) { 7353 sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE + 7354 " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")"); 7355 } 7356 } 7357 7358 private void appendLocalDirectoryAndAccountSelectionIfNeeded(SQLiteQueryBuilder qb, 7359 long directoryId, Uri uri) { 7360 final StringBuilder sb = new StringBuilder(); 7361 if (directoryId == Directory.DEFAULT) { 7362 sb.append("(" + Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY + ")"); 7363 } else if (directoryId == Directory.LOCAL_INVISIBLE){ 7364 sb.append("(" + Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY + ")"); 7365 } else { 7366 sb.append("(1)"); 7367 } 7368 7369 final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri); 7370 // Accounts are valid by only checking one parameter, since we've 7371 // already ruled out partial accounts. 7372 final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName()); 7373 if (validAccount) { 7374 final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet); 7375 if (accountId == null) { 7376 // No such account. 7377 sb.setLength(0); 7378 sb.append("(1=2)"); 7379 } else { 7380 sb.append( 7381 " AND (" + Contacts._ID + " IN (" + 7382 "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS + 7383 " WHERE " + RawContactsColumns.ACCOUNT_ID + "=" + accountId.toString() + 7384 "))"); 7385 } 7386 } 7387 qb.appendWhere(sb.toString()); 7388 } 7389 7390 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 7391 final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri); 7392 7393 // Accounts are valid by only checking one parameter, since we've 7394 // already ruled out partial accounts. 7395 final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName()); 7396 if (validAccount) { 7397 String toAppend = "(" + RawContacts.ACCOUNT_NAME + "=" 7398 + DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountName()) + " AND " 7399 + RawContacts.ACCOUNT_TYPE + "=" 7400 + DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountType()); 7401 if (accountWithDataSet.getDataSet() == null) { 7402 toAppend += " AND " + RawContacts.DATA_SET + " IS NULL"; 7403 } else { 7404 toAppend += " AND " + RawContacts.DATA_SET + "=" + 7405 DatabaseUtils.sqlEscapeString(accountWithDataSet.getDataSet()); 7406 } 7407 toAppend += ")"; 7408 qb.appendWhere(toAppend); 7409 } else { 7410 qb.appendWhere("1"); 7411 } 7412 } 7413 7414 private void appendAccountIdFromParameter(SQLiteQueryBuilder qb, Uri uri) { 7415 final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri); 7416 7417 // Accounts are valid by only checking one parameter, since we've 7418 // already ruled out partial accounts. 7419 final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName()); 7420 if (validAccount) { 7421 final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet); 7422 if (accountId == null) { 7423 // No such account. 7424 qb.appendWhere("(1=2)"); 7425 } else { 7426 qb.appendWhere( 7427 "(" + RawContactsColumns.ACCOUNT_ID + "=" + accountId.toString() + ")"); 7428 } 7429 } else { 7430 qb.appendWhere("1"); 7431 } 7432 } 7433 7434 private AccountWithDataSet getAccountWithDataSetFromUri(Uri uri) { 7435 final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME); 7436 final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE); 7437 final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET); 7438 7439 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 7440 if (partialUri) { 7441 // Throw when either account is incomplete 7442 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 7443 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri)); 7444 } 7445 return AccountWithDataSet.get(accountName, accountType, dataSet); 7446 } 7447 7448 private String appendAccountToSelection(Uri uri, String selection) { 7449 final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri); 7450 7451 // Accounts are valid by only checking one parameter, since we've 7452 // already ruled out partial accounts. 7453 final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName()); 7454 if (validAccount) { 7455 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="); 7456 selectionSb.append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountName())); 7457 selectionSb.append(" AND " + RawContacts.ACCOUNT_TYPE + "="); 7458 selectionSb.append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountType())); 7459 if (accountWithDataSet.getDataSet() == null) { 7460 selectionSb.append(" AND " + RawContacts.DATA_SET + " IS NULL"); 7461 } else { 7462 selectionSb.append(" AND " + RawContacts.DATA_SET + "=") 7463 .append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getDataSet())); 7464 } 7465 if (!TextUtils.isEmpty(selection)) { 7466 selectionSb.append(" AND ("); 7467 selectionSb.append(selection); 7468 selectionSb.append(')'); 7469 } 7470 return selectionSb.toString(); 7471 } else { 7472 return selection; 7473 } 7474 } 7475 7476 private String appendAccountIdToSelection(Uri uri, String selection) { 7477 final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri); 7478 7479 // Accounts are valid by only checking one parameter, since we've 7480 // already ruled out partial accounts. 7481 final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName()); 7482 if (validAccount) { 7483 final StringBuilder selectionSb = new StringBuilder(); 7484 7485 final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet); 7486 if (accountId == null) { 7487 // No such account in the accounts table. This means, there's no rows to be 7488 // selected. 7489 // Note even in this case, we still need to append the original selection, because 7490 // it may have query parameters. If we remove these we'll get the # of parameters 7491 // mismatch exception. 7492 selectionSb.append("(1=2)"); 7493 } else { 7494 selectionSb.append(RawContactsColumns.ACCOUNT_ID + "="); 7495 selectionSb.append(Long.toString(accountId)); 7496 } 7497 7498 if (!TextUtils.isEmpty(selection)) { 7499 selectionSb.append(" AND ("); 7500 selectionSb.append(selection); 7501 selectionSb.append(')'); 7502 } 7503 return selectionSb.toString(); 7504 } else { 7505 return selection; 7506 } 7507 } 7508 7509 /** 7510 * Gets the value of the "limit" URI query parameter. 7511 * 7512 * @return A string containing a non-negative integer, or <code>null</code> if 7513 * the parameter is not set, or is set to an invalid value. 7514 */ 7515 private String getLimit(Uri uri) { 7516 String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY); 7517 if (limitParam == null) { 7518 return null; 7519 } 7520 // make sure that the limit is a non-negative integer 7521 try { 7522 int l = Integer.parseInt(limitParam); 7523 if (l < 0) { 7524 Log.w(TAG, "Invalid limit parameter: " + limitParam); 7525 return null; 7526 } 7527 return String.valueOf(l); 7528 } catch (NumberFormatException ex) { 7529 Log.w(TAG, "Invalid limit parameter: " + limitParam); 7530 return null; 7531 } 7532 } 7533 7534 @Override 7535 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 7536 boolean success = false; 7537 try { 7538 if (mode.equals("r")) { 7539 waitForAccess(mReadAccessLatch); 7540 } else { 7541 waitForAccess(mWriteAccessLatch); 7542 } 7543 final AssetFileDescriptor ret; 7544 if (mapsToProfileDb(uri)) { 7545 switchToProfileMode(); 7546 ret = mProfileProvider.openAssetFile(uri, mode); 7547 } else { 7548 switchToContactMode(); 7549 ret = openAssetFileLocal(uri, mode); 7550 } 7551 success = true; 7552 return ret; 7553 } finally { 7554 if (VERBOSE_LOGGING) { 7555 Log.v(TAG, "openAssetFile uri=" + uri + " mode=" + mode + " success=" + success); 7556 } 7557 } 7558 } 7559 7560 public AssetFileDescriptor openAssetFileLocal(Uri uri, String mode) 7561 throws FileNotFoundException { 7562 // In some cases to implement this, we will need to do further queries 7563 // on the content provider. We have already done the permission check for 7564 // access to the uri given here, so we don't need to do further checks on 7565 // the queries we will do to populate it. Also this makes sure that when 7566 // we go through any app ops checks for those queries that the calling uid 7567 // and package names match at that point. 7568 final long ident = Binder.clearCallingIdentity(); 7569 try { 7570 return openAssetFileInner(uri, mode); 7571 } finally { 7572 Binder.restoreCallingIdentity(ident); 7573 } 7574 } 7575 7576 private AssetFileDescriptor openAssetFileInner(Uri uri, String mode) 7577 throws FileNotFoundException { 7578 7579 final boolean writing = mode.contains("w"); 7580 7581 final SQLiteDatabase db = mDbHelper.get().getDatabase(writing); 7582 7583 int match = sUriMatcher.match(uri); 7584 switch (match) { 7585 case CONTACTS_ID_PHOTO: { 7586 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 7587 return openPhotoAssetFile(db, uri, mode, 7588 Data._ID + "=" + Contacts.PHOTO_ID + " AND " + 7589 RawContacts.CONTACT_ID + "=?", 7590 new String[]{String.valueOf(contactId)}); 7591 } 7592 7593 case CONTACTS_ID_DISPLAY_PHOTO: { 7594 if (!mode.equals("r")) { 7595 throw new IllegalArgumentException( 7596 "Display photos retrieved by contact ID can only be read."); 7597 } 7598 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 7599 Cursor c = db.query(Tables.CONTACTS, 7600 new String[]{Contacts.PHOTO_FILE_ID}, 7601 Contacts._ID + "=?", new String[]{String.valueOf(contactId)}, 7602 null, null, null); 7603 try { 7604 if (c.moveToFirst()) { 7605 long photoFileId = c.getLong(0); 7606 return openDisplayPhotoForRead(photoFileId); 7607 } else { 7608 // No contact for this ID. 7609 throw new FileNotFoundException(uri.toString()); 7610 } 7611 } finally { 7612 c.close(); 7613 } 7614 } 7615 7616 case PROFILE_DISPLAY_PHOTO: { 7617 if (!mode.equals("r")) { 7618 throw new IllegalArgumentException( 7619 "Display photos retrieved by contact ID can only be read."); 7620 } 7621 Cursor c = db.query(Tables.CONTACTS, 7622 new String[]{Contacts.PHOTO_FILE_ID}, null, null, null, null, null); 7623 try { 7624 if (c.moveToFirst()) { 7625 long photoFileId = c.getLong(0); 7626 return openDisplayPhotoForRead(photoFileId); 7627 } else { 7628 // No profile record. 7629 throw new FileNotFoundException(uri.toString()); 7630 } 7631 } finally { 7632 c.close(); 7633 } 7634 } 7635 7636 case CONTACTS_LOOKUP_PHOTO: 7637 case CONTACTS_LOOKUP_ID_PHOTO: 7638 case CONTACTS_LOOKUP_DISPLAY_PHOTO: 7639 case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO: { 7640 if (!mode.equals("r")) { 7641 throw new IllegalArgumentException( 7642 "Photos retrieved by contact lookup key can only be read."); 7643 } 7644 List<String> pathSegments = uri.getPathSegments(); 7645 int segmentCount = pathSegments.size(); 7646 if (segmentCount < 4) { 7647 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage( 7648 "Missing a lookup key", uri)); 7649 } 7650 7651 boolean forDisplayPhoto = (match == CONTACTS_LOOKUP_ID_DISPLAY_PHOTO 7652 || match == CONTACTS_LOOKUP_DISPLAY_PHOTO); 7653 String lookupKey = pathSegments.get(2); 7654 String[] projection = new String[]{Contacts.PHOTO_ID, Contacts.PHOTO_FILE_ID}; 7655 if (segmentCount == 5) { 7656 long contactId = Long.parseLong(pathSegments.get(3)); 7657 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 7658 setTablesAndProjectionMapForContacts(lookupQb, uri, projection); 7659 Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri, 7660 projection, null, null, null, null, null, 7661 Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey, null); 7662 if (c != null) { 7663 try { 7664 c.moveToFirst(); 7665 if (forDisplayPhoto) { 7666 long photoFileId = 7667 c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID)); 7668 return openDisplayPhotoForRead(photoFileId); 7669 } else { 7670 long photoId = c.getLong(c.getColumnIndex(Contacts.PHOTO_ID)); 7671 return openPhotoAssetFile(db, uri, mode, 7672 Data._ID + "=?", new String[]{String.valueOf(photoId)}); 7673 } 7674 } finally { 7675 c.close(); 7676 } 7677 } 7678 } 7679 7680 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 7681 setTablesAndProjectionMapForContacts(qb, uri, projection); 7682 long contactId = lookupContactIdByLookupKey(db, lookupKey); 7683 Cursor c = qb.query(db, projection, Contacts._ID + "=?", 7684 new String[]{String.valueOf(contactId)}, null, null, null); 7685 try { 7686 c.moveToFirst(); 7687 if (forDisplayPhoto) { 7688 long photoFileId = c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID)); 7689 return openDisplayPhotoForRead(photoFileId); 7690 } else { 7691 long photoId = c.getLong(c.getColumnIndex(Contacts.PHOTO_ID)); 7692 return openPhotoAssetFile(db, uri, mode, 7693 Data._ID + "=?", new String[]{String.valueOf(photoId)}); 7694 } 7695 } finally { 7696 c.close(); 7697 } 7698 } 7699 7700 case RAW_CONTACTS_ID_DISPLAY_PHOTO: { 7701 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 7702 boolean writeable = !mode.equals("r"); 7703 7704 // Find the primary photo data record for this raw contact. 7705 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 7706 String[] projection = new String[]{Data._ID, Photo.PHOTO_FILE_ID}; 7707 setTablesAndProjectionMapForData(qb, uri, projection, false); 7708 long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE); 7709 Cursor c = qb.query(db, projection, 7710 Data.RAW_CONTACT_ID + "=? AND " + DataColumns.MIMETYPE_ID + "=?", 7711 new String[]{String.valueOf(rawContactId), String.valueOf(photoMimetypeId)}, 7712 null, null, Data.IS_PRIMARY + " DESC"); 7713 long dataId = 0; 7714 long photoFileId = 0; 7715 try { 7716 if (c.getCount() >= 1) { 7717 c.moveToFirst(); 7718 dataId = c.getLong(0); 7719 photoFileId = c.getLong(1); 7720 } 7721 } finally { 7722 c.close(); 7723 } 7724 7725 // If writeable, open a writeable file descriptor that we can monitor. 7726 // When the caller finishes writing content, we'll process the photo and 7727 // update the data record. 7728 if (writeable) { 7729 return openDisplayPhotoForWrite(rawContactId, dataId, uri, mode); 7730 } else { 7731 return openDisplayPhotoForRead(photoFileId); 7732 } 7733 } 7734 7735 case DISPLAY_PHOTO_ID: { 7736 long photoFileId = ContentUris.parseId(uri); 7737 if (!mode.equals("r")) { 7738 throw new IllegalArgumentException( 7739 "Display photos retrieved by key can only be read."); 7740 } 7741 return openDisplayPhotoForRead(photoFileId); 7742 } 7743 7744 case DATA_ID: { 7745 long dataId = Long.parseLong(uri.getPathSegments().get(1)); 7746 long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE); 7747 return openPhotoAssetFile(db, uri, mode, 7748 Data._ID + "=? AND " + DataColumns.MIMETYPE_ID + "=" + photoMimetypeId, 7749 new String[]{String.valueOf(dataId)}); 7750 } 7751 7752 case PROFILE_AS_VCARD: { 7753 // When opening a contact as file, we pass back contents as a 7754 // vCard-encoded stream. We build into a local buffer first, 7755 // then pipe into MemoryFile once the exact size is known. 7756 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 7757 outputRawContactsAsVCard(uri, localStream, null, null); 7758 return buildAssetFileDescriptor(localStream); 7759 } 7760 7761 case CONTACTS_AS_VCARD: { 7762 // When opening a contact as file, we pass back contents as a 7763 // vCard-encoded stream. We build into a local buffer first, 7764 // then pipe into MemoryFile once the exact size is known. 7765 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 7766 outputRawContactsAsVCard(uri, localStream, null, null); 7767 return buildAssetFileDescriptor(localStream); 7768 } 7769 7770 case CONTACTS_AS_MULTI_VCARD: { 7771 final String lookupKeys = uri.getPathSegments().get(2); 7772 final String[] loopupKeyList = lookupKeys.split(":"); 7773 final StringBuilder inBuilder = new StringBuilder(); 7774 Uri queryUri = Contacts.CONTENT_URI; 7775 int index = 0; 7776 7777 // SQLite has limits on how many parameters can be used 7778 // so the IDs are concatenated to a query string here instead 7779 for (String lookupKey : loopupKeyList) { 7780 if (index == 0) { 7781 inBuilder.append("("); 7782 } else { 7783 inBuilder.append(","); 7784 } 7785 // TODO: Figure out what to do if the profile contact is in the list. 7786 long contactId = lookupContactIdByLookupKey(db, lookupKey); 7787 inBuilder.append(contactId); 7788 index++; 7789 } 7790 inBuilder.append(')'); 7791 final String selection = Contacts._ID + " IN " + inBuilder.toString(); 7792 7793 // When opening a contact as file, we pass back contents as a 7794 // vCard-encoded stream. We build into a local buffer first, 7795 // then pipe into MemoryFile once the exact size is known. 7796 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 7797 outputRawContactsAsVCard(queryUri, localStream, selection, null); 7798 return buildAssetFileDescriptor(localStream); 7799 } 7800 7801 default: 7802 throw new FileNotFoundException(mDbHelper.get().exceptionMessage( 7803 "File does not exist", uri)); 7804 } 7805 } 7806 7807 private AssetFileDescriptor openPhotoAssetFile(SQLiteDatabase db, Uri uri, String mode, 7808 String selection, String[] selectionArgs) 7809 throws FileNotFoundException { 7810 if (!"r".equals(mode)) { 7811 throw new FileNotFoundException(mDbHelper.get().exceptionMessage("Mode " + mode 7812 + " not supported.", uri)); 7813 } 7814 7815 String sql = 7816 "SELECT " + Photo.PHOTO + " FROM " + Views.DATA + 7817 " WHERE " + selection; 7818 try { 7819 return makeAssetFileDescriptor( 7820 DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs)); 7821 } catch (SQLiteDoneException e) { 7822 // this will happen if the DB query returns no rows (i.e. contact does not exist) 7823 throw new FileNotFoundException(uri.toString()); 7824 } 7825 } 7826 7827 /** 7828 * Opens a display photo from the photo store for reading. 7829 * @param photoFileId The display photo file ID 7830 * @return An asset file descriptor that allows the file to be read. 7831 * @throws FileNotFoundException If no photo file for the given ID exists. 7832 */ 7833 private AssetFileDescriptor openDisplayPhotoForRead(long photoFileId) 7834 throws FileNotFoundException { 7835 PhotoStore.Entry entry = mPhotoStore.get().get(photoFileId); 7836 if (entry != null) { 7837 try { 7838 return makeAssetFileDescriptor( 7839 ParcelFileDescriptor.open(new File(entry.path), 7840 ParcelFileDescriptor.MODE_READ_ONLY), 7841 entry.size); 7842 } catch (FileNotFoundException fnfe) { 7843 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); 7844 throw fnfe; 7845 } 7846 } else { 7847 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS); 7848 throw new FileNotFoundException("No photo file found for ID " + photoFileId); 7849 } 7850 } 7851 7852 /** 7853 * Opens a file descriptor for a photo to be written. When the caller completes writing 7854 * to the file (closing the output stream), the image will be parsed out and processed. 7855 * If processing succeeds, the given raw contact ID's primary photo record will be 7856 * populated with the inserted image (if no primary photo record exists, the data ID can 7857 * be left as 0, and a new data record will be inserted). 7858 * @param rawContactId Raw contact ID this photo entry should be associated with. 7859 * @param dataId Data ID for a photo mimetype that will be updated with the inserted 7860 * image. May be set to 0, in which case the inserted image will trigger creation 7861 * of a new primary photo image data row for the raw contact. 7862 * @param uri The URI being used to access this file. 7863 * @param mode Read/write mode string. 7864 * @return An asset file descriptor the caller can use to write an image file for the 7865 * raw contact. 7866 */ 7867 private AssetFileDescriptor openDisplayPhotoForWrite(long rawContactId, long dataId, Uri uri, 7868 String mode) { 7869 try { 7870 ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe(); 7871 PipeMonitor pipeMonitor = new PipeMonitor(rawContactId, dataId, pipeFds[0]); 7872 pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[]) null); 7873 return new AssetFileDescriptor(pipeFds[1], 0, AssetFileDescriptor.UNKNOWN_LENGTH); 7874 } catch (IOException ioe) { 7875 Log.e(TAG, "Could not create temp image file in mode " + mode); 7876 return null; 7877 } 7878 } 7879 7880 /** 7881 * Async task that monitors the given file descriptor (the read end of a pipe) for 7882 * the writer finishing. If the data from the pipe contains a valid image, the image 7883 * is either inserted into the given raw contact or updated in the given data row. 7884 */ 7885 private class PipeMonitor extends AsyncTask<Object, Object, Object> { 7886 private final ParcelFileDescriptor mDescriptor; 7887 private final long mRawContactId; 7888 private final long mDataId; 7889 private PipeMonitor(long rawContactId, long dataId, ParcelFileDescriptor descriptor) { 7890 mRawContactId = rawContactId; 7891 mDataId = dataId; 7892 mDescriptor = descriptor; 7893 } 7894 7895 @Override 7896 protected Object doInBackground(Object... params) { 7897 AutoCloseInputStream is = new AutoCloseInputStream(mDescriptor); 7898 try { 7899 Bitmap b = BitmapFactory.decodeStream(is); 7900 if (b != null) { 7901 waitForAccess(mWriteAccessLatch); 7902 PhotoProcessor processor = new PhotoProcessor(b, getMaxDisplayPhotoDim(), 7903 getMaxThumbnailDim()); 7904 7905 // Store the compressed photo in the photo store. 7906 PhotoStore photoStore = ContactsContract.isProfileId(mRawContactId) 7907 ? mProfilePhotoStore 7908 : mContactsPhotoStore; 7909 long photoFileId = photoStore.insert(processor); 7910 7911 // Depending on whether we already had a data row to attach the photo 7912 // to, do an update or insert. 7913 if (mDataId != 0) { 7914 // Update the data record with the new photo. 7915 ContentValues updateValues = new ContentValues(); 7916 7917 // Signal that photo processing has already been handled. 7918 updateValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true); 7919 7920 if (photoFileId != 0) { 7921 updateValues.put(Photo.PHOTO_FILE_ID, photoFileId); 7922 } 7923 updateValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes()); 7924 update(ContentUris.withAppendedId(Data.CONTENT_URI, mDataId), 7925 updateValues, null, null); 7926 } else { 7927 // Insert a new primary data record with the photo. 7928 ContentValues insertValues = new ContentValues(); 7929 7930 // Signal that photo processing has already been handled. 7931 insertValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true); 7932 7933 insertValues.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 7934 insertValues.put(Data.IS_PRIMARY, 1); 7935 if (photoFileId != 0) { 7936 insertValues.put(Photo.PHOTO_FILE_ID, photoFileId); 7937 } 7938 insertValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes()); 7939 insert(RawContacts.CONTENT_URI.buildUpon() 7940 .appendPath(String.valueOf(mRawContactId)) 7941 .appendPath(RawContacts.Data.CONTENT_DIRECTORY).build(), 7942 insertValues); 7943 } 7944 7945 } 7946 } catch (IOException e) { 7947 throw new RuntimeException(e); 7948 } finally { 7949 IoUtils.closeQuietly(is); 7950 } 7951 return null; 7952 } 7953 } 7954 7955 private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; 7956 7957 /** 7958 * Returns an {@link AssetFileDescriptor} backed by the 7959 * contents of the given {@link ByteArrayOutputStream}. 7960 */ 7961 private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { 7962 try { 7963 stream.flush(); 7964 7965 final byte[] byteData = stream.toByteArray(); 7966 return makeAssetFileDescriptor( 7967 ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME), 7968 byteData.length); 7969 } catch (IOException e) { 7970 Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString()); 7971 return null; 7972 } 7973 } 7974 7975 private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) { 7976 return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH); 7977 } 7978 7979 private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) { 7980 return fd != null ? new AssetFileDescriptor(fd, 0, length) : null; 7981 } 7982 7983 /** 7984 * Output {@link RawContacts} matching the requested selection in the vCard 7985 * format to the given {@link OutputStream}. This method returns silently if 7986 * any errors encountered. 7987 */ 7988 private void outputRawContactsAsVCard(Uri uri, OutputStream stream, 7989 String selection, String[] selectionArgs) { 7990 final Context context = this.getContext(); 7991 int vcardconfig = VCardConfig.VCARD_TYPE_DEFAULT; 7992 if(uri.getBooleanQueryParameter( 7993 Contacts.QUERY_PARAMETER_VCARD_NO_PHOTO, false)) { 7994 vcardconfig |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; 7995 } 7996 final VCardComposer composer = 7997 new VCardComposer(context, vcardconfig, false); 7998 Writer writer = null; 7999 final Uri rawContactsUri; 8000 if (mapsToProfileDb(uri)) { 8001 // Pre-authorize the URI, since the caller would have already gone through the 8002 // permission check to get here, but the pre-authorization at the top level wouldn't 8003 // carry over to the raw contact. 8004 rawContactsUri = preAuthorizeUri(RawContactsEntity.PROFILE_CONTENT_URI); 8005 } else { 8006 rawContactsUri = RawContactsEntity.CONTENT_URI; 8007 } 8008 try { 8009 writer = new BufferedWriter(new OutputStreamWriter(stream)); 8010 if (!composer.init(uri, selection, selectionArgs, null, rawContactsUri)) { 8011 Log.w(TAG, "Failed to init VCardComposer"); 8012 return; 8013 } 8014 8015 while (!composer.isAfterLast()) { 8016 writer.write(composer.createOneEntry()); 8017 } 8018 } catch (IOException e) { 8019 Log.e(TAG, "IOException: " + e); 8020 } finally { 8021 composer.terminate(); 8022 if (writer != null) { 8023 try { 8024 writer.close(); 8025 } catch (IOException e) { 8026 Log.w(TAG, "IOException during closing output stream: " + e); 8027 } 8028 } 8029 } 8030 } 8031 8032 @Override 8033 public String getType(Uri uri) { 8034 final int match = sUriMatcher.match(uri); 8035 switch (match) { 8036 case CONTACTS: 8037 return Contacts.CONTENT_TYPE; 8038 case CONTACTS_LOOKUP: 8039 case CONTACTS_ID: 8040 case CONTACTS_LOOKUP_ID: 8041 case PROFILE: 8042 return Contacts.CONTENT_ITEM_TYPE; 8043 case CONTACTS_AS_VCARD: 8044 case CONTACTS_AS_MULTI_VCARD: 8045 case PROFILE_AS_VCARD: 8046 return Contacts.CONTENT_VCARD_TYPE; 8047 case CONTACTS_ID_PHOTO: 8048 case CONTACTS_LOOKUP_PHOTO: 8049 case CONTACTS_LOOKUP_ID_PHOTO: 8050 case CONTACTS_ID_DISPLAY_PHOTO: 8051 case CONTACTS_LOOKUP_DISPLAY_PHOTO: 8052 case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO: 8053 case RAW_CONTACTS_ID_DISPLAY_PHOTO: 8054 case DISPLAY_PHOTO_ID: 8055 return "image/jpeg"; 8056 case RAW_CONTACTS: 8057 case PROFILE_RAW_CONTACTS: 8058 return RawContacts.CONTENT_TYPE; 8059 case RAW_CONTACTS_ID: 8060 case PROFILE_RAW_CONTACTS_ID: 8061 return RawContacts.CONTENT_ITEM_TYPE; 8062 case DATA: 8063 case PROFILE_DATA: 8064 return Data.CONTENT_TYPE; 8065 case DATA_ID: 8066 // We need db access for this. 8067 waitForAccess(mReadAccessLatch); 8068 8069 long id = ContentUris.parseId(uri); 8070 if (ContactsContract.isProfileId(id)) { 8071 return mProfileHelper.getDataMimeType(id); 8072 } else { 8073 return mContactsHelper.getDataMimeType(id); 8074 } 8075 case PHONES: 8076 return Phone.CONTENT_TYPE; 8077 case PHONES_ID: 8078 return Phone.CONTENT_ITEM_TYPE; 8079 case PHONE_LOOKUP: 8080 return PhoneLookup.CONTENT_TYPE; 8081 case EMAILS: 8082 return Email.CONTENT_TYPE; 8083 case EMAILS_ID: 8084 return Email.CONTENT_ITEM_TYPE; 8085 case POSTALS: 8086 return StructuredPostal.CONTENT_TYPE; 8087 case POSTALS_ID: 8088 return StructuredPostal.CONTENT_ITEM_TYPE; 8089 case AGGREGATION_EXCEPTIONS: 8090 return AggregationExceptions.CONTENT_TYPE; 8091 case AGGREGATION_EXCEPTION_ID: 8092 return AggregationExceptions.CONTENT_ITEM_TYPE; 8093 case SETTINGS: 8094 return Settings.CONTENT_TYPE; 8095 case AGGREGATION_SUGGESTIONS: 8096 return Contacts.CONTENT_TYPE; 8097 case SEARCH_SUGGESTIONS: 8098 return SearchManager.SUGGEST_MIME_TYPE; 8099 case SEARCH_SHORTCUT: 8100 return SearchManager.SHORTCUT_MIME_TYPE; 8101 case DIRECTORIES: 8102 return Directory.CONTENT_TYPE; 8103 case DIRECTORIES_ID: 8104 return Directory.CONTENT_ITEM_TYPE; 8105 case STREAM_ITEMS: 8106 return StreamItems.CONTENT_TYPE; 8107 case STREAM_ITEMS_ID: 8108 return StreamItems.CONTENT_ITEM_TYPE; 8109 case STREAM_ITEMS_ID_PHOTOS: 8110 return StreamItems.StreamItemPhotos.CONTENT_TYPE; 8111 case STREAM_ITEMS_ID_PHOTOS_ID: 8112 return StreamItems.StreamItemPhotos.CONTENT_ITEM_TYPE; 8113 case STREAM_ITEMS_PHOTOS: 8114 throw new UnsupportedOperationException("Not supported for write-only URI " + uri); 8115 default: 8116 waitForAccess(mReadAccessLatch); 8117 return mLegacyApiSupport.getType(uri); 8118 } 8119 } 8120 8121 public String[] getDefaultProjection(Uri uri) { 8122 final int match = sUriMatcher.match(uri); 8123 switch (match) { 8124 case CONTACTS: 8125 case CONTACTS_LOOKUP: 8126 case CONTACTS_ID: 8127 case CONTACTS_LOOKUP_ID: 8128 case AGGREGATION_SUGGESTIONS: 8129 case PROFILE: 8130 return sContactsProjectionMap.getColumnNames(); 8131 8132 case CONTACTS_ID_ENTITIES: 8133 case PROFILE_ENTITIES: 8134 return sEntityProjectionMap.getColumnNames(); 8135 8136 case CONTACTS_AS_VCARD: 8137 case CONTACTS_AS_MULTI_VCARD: 8138 case PROFILE_AS_VCARD: 8139 return sContactsVCardProjectionMap.getColumnNames(); 8140 8141 case RAW_CONTACTS: 8142 case RAW_CONTACTS_ID: 8143 case PROFILE_RAW_CONTACTS: 8144 case PROFILE_RAW_CONTACTS_ID: 8145 return sRawContactsProjectionMap.getColumnNames(); 8146 8147 case DATA_ID: 8148 case PHONES: 8149 case PHONES_ID: 8150 case EMAILS: 8151 case EMAILS_ID: 8152 case POSTALS: 8153 case POSTALS_ID: 8154 case PROFILE_DATA: 8155 return sDataProjectionMap.getColumnNames(); 8156 8157 case PHONE_LOOKUP: 8158 return sPhoneLookupProjectionMap.getColumnNames(); 8159 8160 case AGGREGATION_EXCEPTIONS: 8161 case AGGREGATION_EXCEPTION_ID: 8162 return sAggregationExceptionsProjectionMap.getColumnNames(); 8163 8164 case SETTINGS: 8165 return sSettingsProjectionMap.getColumnNames(); 8166 8167 case DIRECTORIES: 8168 case DIRECTORIES_ID: 8169 return sDirectoryProjectionMap.getColumnNames(); 8170 8171 default: 8172 return null; 8173 } 8174 } 8175 8176 private class StructuredNameLookupBuilder extends NameLookupBuilder { 8177 8178 public StructuredNameLookupBuilder(NameSplitter splitter) { 8179 super(splitter); 8180 } 8181 8182 @Override 8183 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 8184 String name) { 8185 mDbHelper.get().insertNameLookup(rawContactId, dataId, lookupType, name); 8186 } 8187 8188 @Override 8189 protected String[] getCommonNicknameClusters(String normalizedName) { 8190 return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); 8191 } 8192 } 8193 8194 public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 8195 sb.append("(" + 8196 "SELECT DISTINCT " + RawContacts.CONTACT_ID + 8197 " FROM " + Tables.RAW_CONTACTS + 8198 " JOIN " + Tables.NAME_LOOKUP + 8199 " ON(" + RawContactsColumns.CONCRETE_ID + "=" 8200 + NameLookupColumns.RAW_CONTACT_ID + ")" + 8201 " WHERE normalized_name GLOB '"); 8202 sb.append(NameNormalizer.normalize(filterParam)); 8203 sb.append("*' AND " + NameLookupColumns.NAME_TYPE + 8204 " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))"); 8205 } 8206 8207 public boolean isPhoneNumber(String query) { 8208 if (TextUtils.isEmpty(query)) { 8209 return false; 8210 } 8211 // assume a phone number if it has at least 1 digit 8212 return countPhoneNumberDigits(query) > 0; 8213 } 8214 8215 /** 8216 * Returns the number of digitis in a phone number ignoring special characters such as '-'. 8217 * If the string is not a valid phone number, 0 is returned. 8218 */ 8219 public static int countPhoneNumberDigits(String query) { 8220 int numDigits = 0; 8221 int len = query.length(); 8222 for (int i = 0; i < len; i++) { 8223 char c = query.charAt(i); 8224 if (Character.isDigit(c)) { 8225 numDigits ++; 8226 } else if (c == '*' || c == '#' || c == 'N' || c == '.' || c == ';' 8227 || c == '-' || c == '(' || c == ')' || c == ' ') { 8228 // carry on 8229 } else if (c == '+' && numDigits == 0) { 8230 // plus before any digits is ok 8231 } else { 8232 return 0; // not a phone number 8233 } 8234 } 8235 return numDigits; 8236 } 8237 8238 /** 8239 * Takes components of a name from the query parameters and returns a cursor with those 8240 * components as well as all missing components. There is no database activity involved 8241 * in this so the call can be made on the UI thread. 8242 */ 8243 private Cursor completeName(Uri uri, String[] projection) { 8244 if (projection == null) { 8245 projection = sDataProjectionMap.getColumnNames(); 8246 } 8247 8248 ContentValues values = new ContentValues(); 8249 DataRowHandlerForStructuredName handler = (DataRowHandlerForStructuredName) 8250 getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE); 8251 8252 copyQueryParamsToContentValues(values, uri, 8253 StructuredName.DISPLAY_NAME, 8254 StructuredName.PREFIX, 8255 StructuredName.GIVEN_NAME, 8256 StructuredName.MIDDLE_NAME, 8257 StructuredName.FAMILY_NAME, 8258 StructuredName.SUFFIX, 8259 StructuredName.PHONETIC_NAME, 8260 StructuredName.PHONETIC_FAMILY_NAME, 8261 StructuredName.PHONETIC_MIDDLE_NAME, 8262 StructuredName.PHONETIC_GIVEN_NAME 8263 ); 8264 8265 handler.fixStructuredNameComponents(values, values); 8266 8267 MatrixCursor cursor = new MatrixCursor(projection); 8268 Object[] row = new Object[projection.length]; 8269 for (int i = 0; i < projection.length; i++) { 8270 row[i] = values.get(projection[i]); 8271 } 8272 cursor.addRow(row); 8273 return cursor; 8274 } 8275 8276 private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) { 8277 for (String column : columns) { 8278 String param = uri.getQueryParameter(column); 8279 if (param != null) { 8280 values.put(column, param); 8281 } 8282 } 8283 } 8284 8285 8286 /** 8287 * Inserts an argument at the beginning of the selection arg list. 8288 */ 8289 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 8290 if (selectionArgs == null) { 8291 return new String[] {arg}; 8292 } else { 8293 int newLength = selectionArgs.length + 1; 8294 String[] newSelectionArgs = new String[newLength]; 8295 newSelectionArgs[0] = arg; 8296 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 8297 return newSelectionArgs; 8298 } 8299 } 8300 8301 private String[] appendSelectionArg(String[] selectionArgs, String arg) { 8302 if (selectionArgs == null) { 8303 return new String[]{arg}; 8304 } else { 8305 int newLength = selectionArgs.length + 1; 8306 String[] newSelectionArgs = new String[newLength]; 8307 newSelectionArgs[newLength] = arg; 8308 System.arraycopy(selectionArgs, 0, newSelectionArgs, 0, selectionArgs.length - 1); 8309 return newSelectionArgs; 8310 } 8311 } 8312 8313 protected Account getDefaultAccount() { 8314 AccountManager accountManager = AccountManager.get(getContext()); 8315 try { 8316 Account[] accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE); 8317 if (accounts != null && accounts.length > 0) { 8318 return accounts[0]; 8319 } 8320 } catch (Throwable e) { 8321 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 8322 } 8323 return null; 8324 } 8325 8326 /** 8327 * Returns true if the specified account type and data set is writable. 8328 */ 8329 public boolean isWritableAccountWithDataSet(String accountTypeAndDataSet) { 8330 if (accountTypeAndDataSet == null) { 8331 return true; 8332 } 8333 8334 Boolean writable = mAccountWritability.get(accountTypeAndDataSet); 8335 if (writable != null) { 8336 return writable; 8337 } 8338 8339 IContentService contentService = ContentResolver.getContentService(); 8340 try { 8341 // TODO(dsantoro): Need to update this logic to allow for sub-accounts. 8342 for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) { 8343 if (ContactsContract.AUTHORITY.equals(sync.authority) && 8344 accountTypeAndDataSet.equals(sync.accountType)) { 8345 writable = sync.supportsUploading(); 8346 break; 8347 } 8348 } 8349 } catch (RemoteException e) { 8350 Log.e(TAG, "Could not acquire sync adapter types"); 8351 } 8352 8353 if (writable == null) { 8354 writable = false; 8355 } 8356 8357 mAccountWritability.put(accountTypeAndDataSet, writable); 8358 return writable; 8359 } 8360 8361 8362 /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter, 8363 boolean defaultValue) { 8364 8365 // Manually parse the query, which is much faster than calling uri.getQueryParameter 8366 String query = uri.getEncodedQuery(); 8367 if (query == null) { 8368 return defaultValue; 8369 } 8370 8371 int index = query.indexOf(parameter); 8372 if (index == -1) { 8373 return defaultValue; 8374 } 8375 8376 index += parameter.length(); 8377 8378 return !matchQueryParameter(query, index, "=0", false) 8379 && !matchQueryParameter(query, index, "=false", true); 8380 } 8381 8382 private static boolean matchQueryParameter(String query, int index, String value, 8383 boolean ignoreCase) { 8384 int length = value.length(); 8385 return query.regionMatches(ignoreCase, index, value, 0, length) 8386 && (query.length() == index + length || query.charAt(index + length) == '&'); 8387 } 8388 8389 /** 8390 * A fast re-implementation of {@link Uri#getQueryParameter} 8391 */ 8392 /* package */ static String getQueryParameter(Uri uri, String parameter) { 8393 String query = uri.getEncodedQuery(); 8394 if (query == null) { 8395 return null; 8396 } 8397 8398 int queryLength = query.length(); 8399 int parameterLength = parameter.length(); 8400 8401 String value; 8402 int index = 0; 8403 while (true) { 8404 index = query.indexOf(parameter, index); 8405 if (index == -1) { 8406 return null; 8407 } 8408 8409 // Should match against the whole parameter instead of its suffix. 8410 // e.g. The parameter "param" must not be found in "some_param=val". 8411 if (index > 0) { 8412 char prevChar = query.charAt(index - 1); 8413 if (prevChar != '?' && prevChar != '&') { 8414 // With "some_param=val1¶m=val2", we should find second "param" occurrence. 8415 index += parameterLength; 8416 continue; 8417 } 8418 } 8419 8420 index += parameterLength; 8421 8422 if (queryLength == index) { 8423 return null; 8424 } 8425 8426 if (query.charAt(index) == '=') { 8427 index++; 8428 break; 8429 } 8430 } 8431 8432 int ampIndex = query.indexOf('&', index); 8433 if (ampIndex == -1) { 8434 value = query.substring(index); 8435 } else { 8436 value = query.substring(index, ampIndex); 8437 } 8438 8439 return Uri.decode(value); 8440 } 8441 8442 protected boolean isAggregationUpgradeNeeded() { 8443 if (!mContactAggregator.isEnabled()) { 8444 return false; 8445 } 8446 8447 int version = Integer.parseInt(mContactsHelper.getProperty( 8448 DbProperties.AGGREGATION_ALGORITHM, "1")); 8449 return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION; 8450 } 8451 8452 protected void upgradeAggregationAlgorithmInBackground() { 8453 Log.i(TAG, "Upgrading aggregation algorithm"); 8454 8455 final long start = SystemClock.elapsedRealtime(); 8456 setProviderStatus(ProviderStatus.STATUS_UPGRADING); 8457 8458 // Re-aggregate all visible raw contacts. 8459 try { 8460 int count = 0; 8461 SQLiteDatabase db = null; 8462 boolean success = false; 8463 boolean transactionStarted = false; 8464 try { 8465 // Re-aggregation is only for the contacts DB. 8466 switchToContactMode(); 8467 db = mContactsHelper.getWritableDatabase(); 8468 8469 // Start the actual process. 8470 db.beginTransaction(); 8471 transactionStarted = true; 8472 8473 count = mContactAggregator.markAllVisibleForAggregation(db); 8474 mContactAggregator.aggregateInTransaction(mTransactionContext.get(), db); 8475 8476 updateSearchIndexInTransaction(); 8477 8478 updateAggregationAlgorithmVersion(); 8479 8480 db.setTransactionSuccessful(); 8481 8482 success = true; 8483 } finally { 8484 mTransactionContext.get().clearAll(); 8485 if (transactionStarted) { 8486 db.endTransaction(); 8487 } 8488 final long end = SystemClock.elapsedRealtime(); 8489 Log.i(TAG, "Aggregation algorithm upgraded for " + count + " raw contacts" 8490 + (success ? (" in " + (end - start) + "ms") : " failed")); 8491 } 8492 } catch (RuntimeException e) { 8493 Log.e(TAG, "Failed to upgrade aggregation algorithm; continuing anyway.", e); 8494 8495 // Got some exception during re-aggregation. Re-aggregation isn't that important, so 8496 // just bump the aggregation algorithm version and let the provider start normally. 8497 try { 8498 final SQLiteDatabase db = mContactsHelper.getWritableDatabase(); 8499 db.beginTransaction(); 8500 try { 8501 updateAggregationAlgorithmVersion(); 8502 db.setTransactionSuccessful(); 8503 } finally { 8504 db.endTransaction(); 8505 } 8506 } catch (RuntimeException e2) { 8507 // Couldn't even update the algorithm version... There's really nothing we can do 8508 // here, so just go ahead and start the provider. Next time the provider starts 8509 // it'll try re-aggregation again, which may or may not succeed. 8510 Log.e(TAG, "Failed to bump aggregation algorithm version; continuing anyway.", e2); 8511 } 8512 } finally { // Need one more finally because endTransaction() may fail. 8513 setProviderStatus(ProviderStatus.STATUS_NORMAL); 8514 } 8515 } 8516 8517 private void updateAggregationAlgorithmVersion() { 8518 mContactsHelper.setProperty(DbProperties.AGGREGATION_ALGORITHM, 8519 String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION)); 8520 } 8521 8522 @VisibleForTesting 8523 boolean isPhone() { 8524 if (!mIsPhoneInitialized) { 8525 mIsPhone = new TelephonyManager(getContext()).isVoiceCapable(); 8526 mIsPhoneInitialized = true; 8527 } 8528 return mIsPhone; 8529 } 8530 8531 boolean isVoiceCapable() { 8532 // this copied from com.android.phone.PhoneApp.onCreate(): 8533 8534 // "voice capable" flag. 8535 // This flag currently comes from a resource (which is 8536 // overrideable on a per-product basis): 8537 return getContext().getResources() 8538 .getBoolean(com.android.internal.R.bool.config_voice_capable); 8539 // ...but this might eventually become a PackageManager "system 8540 // feature" instead, in which case we'd do something like: 8541 // return 8542 // getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_VOICE_CALLS); 8543 } 8544 8545 /** 8546 * Handles pinning update information from clients. 8547 * 8548 * @param values ContentValues containing key-value pairs where keys correspond to 8549 * the contactId for which to update the pinnedPosition, and the value is the actual 8550 * pinned position (a positive integer). 8551 * @return The number of contacts that had their pinned positions updated. 8552 */ 8553 private int handlePinningUpdate(ContentValues values, boolean forceStarWhenPinning) { 8554 if (values.size() == 0) return 0; 8555 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 8556 final String[] args; 8557 if (forceStarWhenPinning) { 8558 args = new String[3]; 8559 } else { 8560 args = new String[2]; 8561 } 8562 8563 final StringBuilder sb = new StringBuilder(); 8564 8565 sb.append("UPDATE " + Tables.CONTACTS + " SET " + Contacts.PINNED + "=?2"); 8566 if (forceStarWhenPinning) { 8567 sb.append("," + Contacts.STARRED + "=?3"); 8568 } 8569 sb.append(" WHERE " + Contacts._ID + " =?1;"); 8570 final String contactSQL = sb.toString(); 8571 8572 sb.setLength(0); 8573 sb.append("UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.PINNED + "=?2"); 8574 if (forceStarWhenPinning) { 8575 sb.append("," + RawContacts.STARRED + "=?3"); 8576 } 8577 sb.append(" WHERE " + RawContacts.CONTACT_ID + " =?1;"); 8578 final String rawContactSQL = sb.toString(); 8579 8580 int count = 0; 8581 for (String id : values.keySet()) { 8582 count++; 8583 final long contactId; 8584 try { 8585 contactId = Integer.valueOf(id); 8586 } catch (NumberFormatException e) { 8587 throw new IllegalArgumentException("contactId must be a positive integer. Found: " 8588 + id); 8589 } 8590 8591 // If contact is to be undemoted, go through a separate un-demotion process 8592 final String undemote = values.getAsString(id); 8593 if (PinnedPositions.UNDEMOTE.equals(undemote)) { 8594 undemoteContact(db, contactId); 8595 continue; 8596 } 8597 8598 final Integer pinnedPosition = values.getAsInteger(id); 8599 if (pinnedPosition == null) { 8600 throw new IllegalArgumentException("Pinned position must be an integer."); 8601 } 8602 args[0] = String.valueOf(contactId); 8603 args[1] = String.valueOf(pinnedPosition); 8604 if (forceStarWhenPinning) { 8605 args[2] = (pinnedPosition == PinnedPositions.UNPINNED || 8606 pinnedPosition == PinnedPositions.DEMOTED ? "0" : "1"); 8607 } 8608 db.execSQL(contactSQL, args); 8609 8610 db.execSQL(rawContactSQL, args); 8611 } 8612 return count; 8613 } 8614 8615 private void undemoteContact(SQLiteDatabase db, long id) { 8616 final String[] arg = new String[1]; 8617 arg[0] = String.valueOf(id); 8618 db.execSQL(UNDEMOTE_CONTACT, arg); 8619 db.execSQL(UNDEMOTE_RAW_CONTACT, arg); 8620 } 8621 8622 private boolean handleDataUsageFeedback(Uri uri) { 8623 final long currentTimeMillis = Clock.getInstance().currentTimeMillis(); 8624 final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE); 8625 final String[] ids = uri.getLastPathSegment().trim().split(","); 8626 final ArrayList<Long> dataIds = new ArrayList<Long>(ids.length); 8627 8628 for (String id : ids) { 8629 dataIds.add(Long.valueOf(id)); 8630 } 8631 final boolean successful; 8632 if (TextUtils.isEmpty(usageType)) { 8633 Log.w(TAG, "Method for data usage feedback isn't specified. Ignoring."); 8634 successful = false; 8635 } else { 8636 successful = updateDataUsageStat(dataIds, usageType, currentTimeMillis) > 0; 8637 } 8638 8639 // Handle old API. This doesn't affect the result of this entire method. 8640 final StringBuilder rawContactIdSelect = new StringBuilder(); 8641 rawContactIdSelect.append("SELECT " + Data.RAW_CONTACT_ID + " FROM " + Tables.DATA + 8642 " WHERE " + Data._ID + " IN ("); 8643 for (int i = 0; i < ids.length; i++) { 8644 if (i > 0) rawContactIdSelect.append(","); 8645 rawContactIdSelect.append(ids[i]); 8646 } 8647 rawContactIdSelect.append(")"); 8648 8649 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 8650 8651 mSelectionArgs1[0] = String.valueOf(currentTimeMillis); 8652 8653 db.execSQL("UPDATE " + Tables.RAW_CONTACTS + 8654 " SET " + RawContacts.LAST_TIME_CONTACTED + "=?" + 8655 "," + RawContacts.TIMES_CONTACTED + "=" + 8656 "ifnull(" + RawContacts.TIMES_CONTACTED + ",0) + 1" + 8657 " WHERE " + RawContacts._ID + " IN (" + rawContactIdSelect.toString() + ")" 8658 , mSelectionArgs1); 8659 db.execSQL("UPDATE " + Tables.CONTACTS + 8660 " SET " + Contacts.LAST_TIME_CONTACTED + "=?1" + 8661 "," + Contacts.TIMES_CONTACTED + "=" + 8662 "ifnull(" + Contacts.TIMES_CONTACTED + ",0) + 1" + 8663 "," + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=?1" + 8664 " WHERE " + Contacts._ID + " IN (SELECT " + RawContacts.CONTACT_ID + 8665 " FROM " + Tables.RAW_CONTACTS + 8666 " WHERE " + RawContacts._ID + " IN (" + rawContactIdSelect.toString() + "))" 8667 , mSelectionArgs1); 8668 8669 return successful; 8670 } 8671 8672 private interface DataUsageStatQuery { 8673 String TABLE = Tables.DATA_USAGE_STAT; 8674 8675 String[] COLUMNS = new String[] { 8676 DataUsageStatColumns._ID, 8677 }; 8678 int ID = 0; 8679 8680 String SELECTION = DataUsageStatColumns.DATA_ID + " =? AND " 8681 + DataUsageStatColumns.USAGE_TYPE_INT + " =?"; 8682 } 8683 8684 /** 8685 * Update {@link Tables#DATA_USAGE_STAT}. 8686 * 8687 * @return the number of rows affected. 8688 */ 8689 @VisibleForTesting 8690 /* package */ int updateDataUsageStat( 8691 List<Long> dataIds, String type, long currentTimeMillis) { 8692 8693 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase(); 8694 8695 final String typeString = String.valueOf(getDataUsageFeedbackType(type, null)); 8696 final String currentTimeMillisString = String.valueOf(currentTimeMillis); 8697 8698 for (long dataId : dataIds) { 8699 final String dataIdString = String.valueOf(dataId); 8700 mSelectionArgs2[0] = dataIdString; 8701 mSelectionArgs2[1] = typeString; 8702 final Cursor cursor = db.query(DataUsageStatQuery.TABLE, 8703 DataUsageStatQuery.COLUMNS, DataUsageStatQuery.SELECTION, 8704 mSelectionArgs2, null, null, null); 8705 try { 8706 if (cursor.moveToFirst()) { 8707 final long id = cursor.getLong(DataUsageStatQuery.ID); 8708 8709 mSelectionArgs2[0] = currentTimeMillisString; 8710 mSelectionArgs2[1] = String.valueOf(id); 8711 8712 db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT + 8713 " SET " + DataUsageStatColumns.TIMES_USED + "=" + 8714 "ifnull(" + DataUsageStatColumns.TIMES_USED +",0)+1" + 8715 "," + DataUsageStatColumns.LAST_TIME_USED + "=?" + 8716 " WHERE " + DataUsageStatColumns._ID + "=?", 8717 mSelectionArgs2); 8718 } else { 8719 mSelectionArgs4[0] = dataIdString; 8720 mSelectionArgs4[1] = typeString; 8721 mSelectionArgs4[2] = "1"; // times used 8722 mSelectionArgs4[3] = currentTimeMillisString; 8723 db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT + 8724 "(" + DataUsageStatColumns.DATA_ID + 8725 "," + DataUsageStatColumns.USAGE_TYPE_INT + 8726 "," + DataUsageStatColumns.TIMES_USED + 8727 "," + DataUsageStatColumns.LAST_TIME_USED + 8728 ") VALUES (?,?,?,?)", 8729 mSelectionArgs4); 8730 } 8731 } finally { 8732 cursor.close(); 8733 } 8734 } 8735 8736 return dataIds.size(); 8737 } 8738 8739 /** 8740 * Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.) 8741 * associated with a primary account. The primary account should be supplied from applications 8742 * with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and 8743 * {@link ContactsContract#PRIMARY_ACCOUNT_TYPE}. Null will be returned when the primary 8744 * account isn't available. 8745 */ 8746 private String getAccountPromotionSortOrder(Uri uri) { 8747 final String primaryAccountName = 8748 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME); 8749 final String primaryAccountType = 8750 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE); 8751 8752 // Data rows associated with primary account should be promoted. 8753 if (!TextUtils.isEmpty(primaryAccountName)) { 8754 StringBuilder sb = new StringBuilder(); 8755 sb.append("(CASE WHEN " + RawContacts.ACCOUNT_NAME + "="); 8756 DatabaseUtils.appendEscapedSQLString(sb, primaryAccountName); 8757 if (!TextUtils.isEmpty(primaryAccountType)) { 8758 sb.append(" AND " + RawContacts.ACCOUNT_TYPE + "="); 8759 DatabaseUtils.appendEscapedSQLString(sb, primaryAccountType); 8760 } 8761 sb.append(" THEN 0 ELSE 1 END)"); 8762 return sb.toString(); 8763 } else { 8764 return null; 8765 } 8766 } 8767 8768 /** 8769 * Checks the URI for a deferred snippeting request 8770 * @return a boolean indicating if a deferred snippeting request is in the RI 8771 */ 8772 private boolean deferredSnippetingRequested(Uri uri) { 8773 String deferredSnippeting = 8774 getQueryParameter(uri, SearchSnippetColumns.DEFERRED_SNIPPETING_KEY); 8775 return !TextUtils.isEmpty(deferredSnippeting) && deferredSnippeting.equals("1"); 8776 } 8777 8778 /** 8779 * Checks if query is a single word or not. 8780 * @return a boolean indicating if the query is one word or not 8781 */ 8782 private boolean isSingleWordQuery(String query) { 8783 // Split can remove empty trailing tokens but cannot remove starting empty tokens so we 8784 // have to loop. 8785 String[] tokens = query.split(QUERY_TOKENIZER_REGEX, 0); 8786 int count = 0; 8787 for (String token : tokens) { 8788 if (!"".equals(token)) { 8789 count++; 8790 } 8791 } 8792 return count == 1; 8793 } 8794 8795 /** 8796 * Checks the projection for a SNIPPET column indicating that a snippet is needed 8797 * @return a boolean indicating if a snippet is needed or not. 8798 */ 8799 private boolean snippetNeeded(String [] projection) { 8800 return ContactsDatabaseHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET); 8801 } 8802 8803 /** 8804 * Create a single row cursor for a simple, informational queries, such as 8805 * {@link ProviderStatus#CONTENT_URI}. 8806 */ 8807 @VisibleForTesting 8808 static Cursor buildSingleRowResult(String[] projection, String[] availableColumns, 8809 Object[] data) { 8810 Preconditions.checkArgument(availableColumns.length == data.length); 8811 if (projection == null) { 8812 projection = availableColumns; 8813 } 8814 final MatrixCursor c = new MatrixCursor(projection, 1); 8815 final RowBuilder row = c.newRow(); 8816 8817 // It's O(n^2), but it's okay because we only have a few columns. 8818 for (int i = 0; i < c.getColumnCount(); i++) { 8819 final String column = c.getColumnName(i); 8820 8821 boolean found = false; 8822 for (int j = 0; j < availableColumns.length; j++) { 8823 if (availableColumns[j].equals(column)) { 8824 row.add(data[j]); 8825 found = true; 8826 break; 8827 } 8828 } 8829 if (!found) { 8830 throw new IllegalArgumentException("Invalid column " + projection[i]); 8831 } 8832 } 8833 return c; 8834 } 8835 8836 /** 8837 * @return the currently active {@link ContactsDatabaseHelper} for the current thread. 8838 */ 8839 @NeededForTesting 8840 public ContactsDatabaseHelper getThreadActiveDatabaseHelperForTest() { 8841 return mDbHelper.get(); 8842 } 8843 8844 @Override 8845 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 8846 pw.print("FastScrollingIndex stats:\n"); 8847 pw.printf("request=%d miss=%d (%d%%) avg time=%dms\n", 8848 mFastScrollingIndexCacheRequestCount, 8849 mFastScrollingIndexCacheMissCount, 8850 safeDiv(mFastScrollingIndexCacheMissCount * 100, 8851 mFastScrollingIndexCacheRequestCount), 8852 safeDiv(mTotalTimeFastScrollingIndexGenerate, mFastScrollingIndexCacheMissCount) 8853 ); 8854 } 8855 8856 private static final long safeDiv(long dividend, long divisor) { 8857 return (divisor == 0) ? 0 : dividend / divisor; 8858 } 8859 8860 private static final int getDataUsageFeedbackType(String type, Integer defaultType) { 8861 if (DataUsageFeedback.USAGE_TYPE_CALL.equals(type)) { 8862 return DataUsageStatColumns.USAGE_TYPE_INT_CALL; // 0 8863 } 8864 if (DataUsageFeedback.USAGE_TYPE_LONG_TEXT.equals(type)) { 8865 return DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT; // 1 8866 } 8867 if (DataUsageFeedback.USAGE_TYPE_SHORT_TEXT.equals(type)) { 8868 return DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT; // 2 8869 } 8870 if (defaultType != null) { 8871 return defaultType; 8872 } 8873 throw new IllegalArgumentException("Invalid usage type " + type); 8874 } 8875 8876 /** Use only for debug logging */ 8877 @Override 8878 public String toString() { 8879 return "ContactsProvider2"; 8880 } 8881 8882 @NeededForTesting 8883 public void switchToProfileModeForTest() { 8884 switchToProfileMode(); 8885 } 8886} 8887