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