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