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