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