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