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