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