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