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