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