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