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