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