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