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