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