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