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