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