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