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