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