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