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