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