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