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