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