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