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