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