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