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