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