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