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