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