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