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