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