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