ContactsProvider2.java revision 1443b159afd6149659383ff5fd7b7958d6c077cc
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 17package com.android.providers.contacts; 18 19import com.android.internal.content.SyncStateContentProviderHelper; 20import com.android.providers.contacts.ContactLookupKey.LookupKeySegment; 21import com.android.providers.contacts.OpenHelper.AggregatedPresenceColumns; 22import com.android.providers.contacts.OpenHelper.AggregationExceptionColumns; 23import com.android.providers.contacts.OpenHelper.Clauses; 24import com.android.providers.contacts.OpenHelper.ContactsColumns; 25import com.android.providers.contacts.OpenHelper.DataColumns; 26import com.android.providers.contacts.OpenHelper.DisplayNameSources; 27import com.android.providers.contacts.OpenHelper.GroupsColumns; 28import com.android.providers.contacts.OpenHelper.MimetypesColumns; 29import com.android.providers.contacts.OpenHelper.NameLookupColumns; 30import com.android.providers.contacts.OpenHelper.NameLookupType; 31import com.android.providers.contacts.OpenHelper.PackagesColumns; 32import com.android.providers.contacts.OpenHelper.PhoneColumns; 33import com.android.providers.contacts.OpenHelper.PhoneLookupColumns; 34import com.android.providers.contacts.OpenHelper.PresenceColumns; 35import com.android.providers.contacts.OpenHelper.RawContactsColumns; 36import com.android.providers.contacts.OpenHelper.SettingsColumns; 37import com.android.providers.contacts.OpenHelper.Tables; 38import com.google.android.collect.Lists; 39import com.google.android.collect.Sets; 40import com.google.android.collect.Maps; 41 42import android.accounts.Account; 43import android.accounts.AccountManager; 44import android.accounts.OnAccountsUpdatedListener; 45import android.app.SearchManager; 46import android.content.ContentProviderOperation; 47import android.content.ContentProviderResult; 48import android.content.ContentUris; 49import android.content.ContentValues; 50import android.content.Context; 51import android.content.Entity; 52import android.content.EntityIterator; 53import android.content.OperationApplicationException; 54import android.content.SharedPreferences; 55import android.content.UriMatcher; 56import android.content.SharedPreferences.Editor; 57import android.content.res.AssetFileDescriptor; 58import android.database.Cursor; 59import android.database.DatabaseUtils; 60import android.database.sqlite.SQLiteContentHelper; 61import android.database.sqlite.SQLiteCursor; 62import android.database.sqlite.SQLiteDatabase; 63import android.database.sqlite.SQLiteQueryBuilder; 64import android.database.sqlite.SQLiteStatement; 65import android.net.Uri; 66import android.os.MemoryFile; 67import android.os.RemoteException; 68import android.pim.vcard.VCardComposer; 69import android.preference.PreferenceManager; 70import android.provider.BaseColumns; 71import android.provider.ContactsContract; 72import android.provider.LiveFolders; 73import android.provider.OpenableColumns; 74import android.provider.SyncStateContract; 75import android.provider.ContactsContract.AggregationExceptions; 76import android.provider.ContactsContract.Contacts; 77import android.provider.ContactsContract.Data; 78import android.provider.ContactsContract.Groups; 79import android.provider.ContactsContract.PhoneLookup; 80import android.provider.ContactsContract.Presence; 81import android.provider.ContactsContract.RawContacts; 82import android.provider.ContactsContract.Settings; 83import android.provider.ContactsContract.CommonDataKinds.BaseTypes; 84import android.provider.ContactsContract.CommonDataKinds.Email; 85import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 86import android.provider.ContactsContract.CommonDataKinds.Im; 87import android.provider.ContactsContract.CommonDataKinds.Nickname; 88import android.provider.ContactsContract.CommonDataKinds.Organization; 89import android.provider.ContactsContract.CommonDataKinds.Phone; 90import android.provider.ContactsContract.CommonDataKinds.Photo; 91import android.provider.ContactsContract.CommonDataKinds.StructuredName; 92import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 93import android.telephony.PhoneNumberUtils; 94import android.text.TextUtils; 95import android.util.Log; 96 97import java.io.ByteArrayOutputStream; 98import java.io.FileNotFoundException; 99import java.io.IOException; 100import java.io.OutputStream; 101import java.util.ArrayList; 102import java.util.Collections; 103import java.util.HashMap; 104import java.util.List; 105import java.util.Locale; 106import java.util.Set; 107import java.util.HashSet; 108import java.util.Map; 109import java.util.concurrent.CountDownLatch; 110 111/** 112 * Contacts content provider. The contract between this provider and applications 113 * is defined in {@link ContactsContract}. 114 */ 115public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdatedListener { 116 117 private static final String TAG = "ContactsProvider"; 118 119 private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 120 121 // TODO: carefully prevent all incoming nested queries; they can be gaping security holes 122 // TODO: check for restricted flag during insert(), update(), and delete() calls 123 124 /** Default for the maximum number of returned aggregation suggestions. */ 125 private static final int DEFAULT_MAX_SUGGESTIONS = 5; 126 127 /** 128 * Shared preference key for the legacy contact import version. The need for a version 129 * as opposed to a boolean flag is that if we discover bugs in the contact import process, 130 * we can trigger re-import by incrementing the import version. 131 */ 132 private static final String PREF_CONTACTS_IMPORTED = "contacts_imported_v1"; 133 private static final int PREF_CONTACTS_IMPORT_VERSION = 1; 134 135 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 136 137 private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, " 138 + Contacts.TIMES_CONTACTED + " DESC, " 139 + Contacts.DISPLAY_NAME + " ASC"; 140 private static final String STREQUENT_LIMIT = 141 "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE " 142 + Contacts.STARRED + "=1) + 25"; 143 144 private static final int CONTACTS = 1000; 145 private static final int CONTACTS_ID = 1001; 146 private static final int CONTACTS_LOOKUP = 1002; 147 private static final int CONTACTS_LOOKUP_ID = 1003; 148 private static final int CONTACTS_DATA = 1004; 149 private static final int CONTACTS_FILTER = 1005; 150 private static final int CONTACTS_STREQUENT = 1006; 151 private static final int CONTACTS_STREQUENT_FILTER = 1007; 152 private static final int CONTACTS_GROUP = 1008; 153 private static final int CONTACTS_PHOTO = 1009; 154 155 private static final int RAW_CONTACTS = 2002; 156 private static final int RAW_CONTACTS_ID = 2003; 157 private static final int RAW_CONTACTS_DATA = 2004; 158 159 private static final int DATA = 3000; 160 private static final int DATA_ID = 3001; 161 private static final int PHONES = 3002; 162 private static final int PHONES_FILTER = 3003; 163 private static final int EMAILS = 3004; 164 private static final int EMAILS_LOOKUP = 3005; 165 private static final int EMAILS_FILTER = 3006; 166 private static final int POSTALS = 3007; 167 168 private static final int PHONE_LOOKUP = 4000; 169 170 private static final int AGGREGATION_EXCEPTIONS = 6000; 171 private static final int AGGREGATION_EXCEPTION_ID = 6001; 172 173 private static final int PRESENCE = 7000; 174 private static final int PRESENCE_ID = 7001; 175 176 private static final int AGGREGATION_SUGGESTIONS = 8000; 177 178 private static final int SETTINGS = 9000; 179 180 private static final int GROUPS = 10000; 181 private static final int GROUPS_ID = 10001; 182 private static final int GROUPS_SUMMARY = 10003; 183 184 private static final int SYNCSTATE = 11000; 185 private static final int SYNCSTATE_ID = 11001; 186 187 private static final int SEARCH_SUGGESTIONS = 12001; 188 private static final int SEARCH_SHORTCUT = 12002; 189 190 private static final int DATA_WITH_PRESENCE = 13000; 191 192 private static final int LIVE_FOLDERS_CONTACTS = 14000; 193 private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001; 194 private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002; 195 private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003; 196 197 private interface ContactsQuery { 198 public static final String TABLE = Tables.RAW_CONTACTS; 199 200 public static final String[] PROJECTION = new String[] { 201 RawContactsColumns.CONCRETE_ID, 202 RawContacts.ACCOUNT_NAME, 203 RawContacts.ACCOUNT_TYPE, 204 }; 205 206 public static final int RAW_CONTACT_ID = 0; 207 public static final int ACCOUNT_NAME = 1; 208 public static final int ACCOUNT_TYPE = 2; 209 } 210 211 private interface DataContactsQuery { 212 public static final String TABLE = Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS; 213 214 public static final String[] PROJECTION = new String[] { 215 RawContactsColumns.CONCRETE_ID, 216 DataColumns.CONCRETE_ID, 217 ContactsColumns.CONCRETE_ID, 218 MimetypesColumns.CONCRETE_ID, 219 }; 220 221 public static final int RAW_CONTACT_ID = 0; 222 public static final int DATA_ID = 1; 223 public static final int CONTACT_ID = 2; 224 public static final int MIMETYPE_ID = 3; 225 } 226 227 private interface DisplayNameQuery { 228 public static final String TABLE = Tables.DATA_JOIN_MIMETYPES; 229 230 public static final String[] COLUMNS = new String[] { 231 MimetypesColumns.MIMETYPE, 232 Data.IS_PRIMARY, 233 Data.DATA2, 234 StructuredName.DISPLAY_NAME, 235 }; 236 237 public static final int MIMETYPE = 0; 238 public static final int IS_PRIMARY = 1; 239 public static final int DATA2 = 2; 240 public static final int DISPLAY_NAME = 3; 241 } 242 243 private interface DataDeleteQuery { 244 public static final String TABLE = Tables.DATA_JOIN_MIMETYPES; 245 246 public static final String[] CONCRETE_COLUMNS = new String[] { 247 DataColumns.CONCRETE_ID, 248 MimetypesColumns.MIMETYPE, 249 Data.RAW_CONTACT_ID, 250 Data.IS_PRIMARY, 251 Data.DATA2, 252 }; 253 254 public static final String[] COLUMNS = new String[] { 255 Data._ID, 256 MimetypesColumns.MIMETYPE, 257 Data.RAW_CONTACT_ID, 258 Data.IS_PRIMARY, 259 Data.DATA2, 260 }; 261 262 public static final int _ID = 0; 263 public static final int MIMETYPE = 1; 264 public static final int RAW_CONTACT_ID = 2; 265 public static final int IS_PRIMARY = 3; 266 public static final int DATA2 = 4; 267 } 268 269 private interface DataUpdateQuery { 270 String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE }; 271 272 int _ID = 0; 273 int RAW_CONTACT_ID = 1; 274 int MIMETYPE = 2; 275 } 276 277 private static final HashMap<String, Integer> sDisplayNameSources; 278 static { 279 sDisplayNameSources = new HashMap<String, Integer>(); 280 sDisplayNameSources.put(StructuredName.CONTENT_ITEM_TYPE, 281 DisplayNameSources.STRUCTURED_NAME); 282 sDisplayNameSources.put(Organization.CONTENT_ITEM_TYPE, 283 DisplayNameSources.ORGANIZATION); 284 sDisplayNameSources.put(Phone.CONTENT_ITEM_TYPE, 285 DisplayNameSources.PHONE); 286 sDisplayNameSources.put(Email.CONTENT_ITEM_TYPE, 287 DisplayNameSources.EMAIL); 288 } 289 290 public static final String DEFAULT_ACCOUNT_TYPE = "com.google.GAIA"; 291 public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google"; 292 293 /** Contains just BaseColumns._COUNT */ 294 private static final HashMap<String, String> sCountProjectionMap; 295 /** Contains just the contacts columns */ 296 private static final HashMap<String, String> sContactsProjectionMap; 297 /** Contains contacts and presence columns */ 298 private static final HashMap<String, String> sContactsWithPresenceProjectionMap; 299 /** Contains just the raw contacts columns */ 300 private static final HashMap<String, String> sRawContactsProjectionMap; 301 /** Contains columns from the data view */ 302 private static final HashMap<String, String> sDataProjectionMap; 303 /** Contains columns from the data view */ 304 private static final HashMap<String, String> sDistinctDataProjectionMap; 305 /** Contains the data and contacts columns, for joined tables */ 306 private static final HashMap<String, String> sPhoneLookupProjectionMap; 307 /** Contains the just the {@link Groups} columns */ 308 private static final HashMap<String, String> sGroupsProjectionMap; 309 /** Contains {@link Groups} columns along with summary details */ 310 private static final HashMap<String, String> sGroupsSummaryProjectionMap; 311 /** Contains the agg_exceptions columns */ 312 private static final HashMap<String, String> sAggregationExceptionsProjectionMap; 313 /** Contains the agg_exceptions columns */ 314 private static final HashMap<String, String> sSettingsProjectionMap; 315 /** Contains Presence columns */ 316 private static final HashMap<String, String> sPresenceProjectionMap; 317 /** Contains Presence columns */ 318 private static final HashMap<String, String> sDataWithPresenceProjectionMap; 319 /** Contains Live Folders columns */ 320 private static final HashMap<String, String> sLiveFoldersProjectionMap; 321 322 /** Sql where statement for filtering on groups. */ 323 private static final String sContactsInGroupSelect; 324 325 /** Precompiled sql statement for setting a data record to the primary. */ 326 private SQLiteStatement mSetPrimaryStatement; 327 /** Precompiled sql statement for setting a data record to the super primary. */ 328 private SQLiteStatement mSetSuperPrimaryStatement; 329 /** Precompiled sql statement for incrementing times contacted for an contact */ 330 private SQLiteStatement mLastTimeContactedUpdate; 331 /** Precompiled sql statement for updating a contact display name */ 332 private SQLiteStatement mRawContactDisplayNameUpdate; 333 /** Precompiled sql statement for marking a raw contact as dirty */ 334 private SQLiteStatement mRawContactDirtyUpdate; 335 /** Precompiled sql statement for setting an aggregated presence */ 336 private SQLiteStatement mAggregatedPresenceReplace; 337 /** Precompiled sql statement for updating an aggregated presence status */ 338 private SQLiteStatement mAggregatedPresenceStatusUpdate; 339 340 static { 341 // Contacts URI matching table 342 final UriMatcher matcher = sUriMatcher; 343 matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS); 344 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 345 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA); 346 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", 347 AGGREGATION_SUGGESTIONS); 348 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO); 349 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER); 350 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP); 351 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID); 352 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT); 353 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", 354 CONTACTS_STREQUENT_FILTER); 355 matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP); 356 357 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS); 358 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID); 359 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA); 360 361 matcher.addURI(ContactsContract.AUTHORITY, "data", DATA); 362 matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID); 363 matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES); 364 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER); 365 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER); 366 matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS); 367 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP); 368 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER); 369 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER); 370 matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS); 371 372 matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS); 373 matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID); 374 matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY); 375 376 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE); 377 matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#", 378 SYNCSTATE_ID); 379 380 matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP); 381 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", 382 AGGREGATION_EXCEPTIONS); 383 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", 384 AGGREGATION_EXCEPTION_ID); 385 386 matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS); 387 388 matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE); 389 matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID); 390 391 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, 392 SEARCH_SUGGESTIONS); 393 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 394 SEARCH_SUGGESTIONS); 395 matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#", 396 SEARCH_SHORTCUT); 397 398 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts", 399 LIVE_FOLDERS_CONTACTS); 400 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*", 401 LIVE_FOLDERS_CONTACTS_GROUP_NAME); 402 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones", 403 LIVE_FOLDERS_CONTACTS_WITH_PHONES); 404 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites", 405 LIVE_FOLDERS_CONTACTS_FAVORITES); 406 407 // Private API 408 matcher.addURI(ContactsContract.AUTHORITY, "data_with_presence", DATA_WITH_PRESENCE); 409 } 410 411 static { 412 sCountProjectionMap = new HashMap<String, String>(); 413 sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)"); 414 415 sContactsProjectionMap = new HashMap<String, String>(); 416 sContactsProjectionMap.put(Contacts._ID, Contacts._ID); 417 sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 418 sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 419 sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 420 sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 421 sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); 422 sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 423 sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 424 sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER); 425 sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 426 sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 427 sContactsProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME 428 + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME); 429 sContactsProjectionMap.put(OpenableColumns.SIZE, "0 AS " + OpenableColumns.SIZE); 430 431 sContactsWithPresenceProjectionMap = new HashMap<String, String>(); 432 sContactsWithPresenceProjectionMap.putAll(sContactsProjectionMap); 433 sContactsWithPresenceProjectionMap.put(Contacts.PRESENCE_STATUS, 434 Presence.PRESENCE_STATUS + " AS " + Contacts.PRESENCE_STATUS); 435 sContactsWithPresenceProjectionMap.put(Contacts.PRESENCE_CUSTOM_STATUS, 436 Presence.PRESENCE_CUSTOM_STATUS + " AS " + Contacts.PRESENCE_CUSTOM_STATUS); 437 438 sRawContactsProjectionMap = new HashMap<String, String>(); 439 sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID); 440 sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 441 sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 442 sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 443 sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 444 sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); 445 sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); 446 sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED); 447 sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED); 448 sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED, 449 RawContacts.LAST_TIME_CONTACTED); 450 sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE); 451 sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL); 452 sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED); 453 sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE); 454 sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1); 455 sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2); 456 sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3); 457 sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4); 458 459 sDataProjectionMap = new HashMap<String, String>(); 460 sDataProjectionMap.put(Data._ID, Data._ID); 461 sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID); 462 sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); 463 sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 464 sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 465 sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 466 sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); 467 sDataProjectionMap.put(Data.DATA1, Data.DATA1); 468 sDataProjectionMap.put(Data.DATA2, Data.DATA2); 469 sDataProjectionMap.put(Data.DATA3, Data.DATA3); 470 sDataProjectionMap.put(Data.DATA4, Data.DATA4); 471 sDataProjectionMap.put(Data.DATA5, Data.DATA5); 472 sDataProjectionMap.put(Data.DATA6, Data.DATA6); 473 sDataProjectionMap.put(Data.DATA7, Data.DATA7); 474 sDataProjectionMap.put(Data.DATA8, Data.DATA8); 475 sDataProjectionMap.put(Data.DATA9, Data.DATA9); 476 sDataProjectionMap.put(Data.DATA10, Data.DATA10); 477 sDataProjectionMap.put(Data.DATA11, Data.DATA11); 478 sDataProjectionMap.put(Data.DATA12, Data.DATA12); 479 sDataProjectionMap.put(Data.DATA13, Data.DATA13); 480 sDataProjectionMap.put(Data.DATA14, Data.DATA14); 481 sDataProjectionMap.put(Data.DATA15, Data.DATA15); 482 sDataProjectionMap.put(Data.SYNC1, Data.SYNC1); 483 sDataProjectionMap.put(Data.SYNC2, Data.SYNC2); 484 sDataProjectionMap.put(Data.SYNC3, Data.SYNC3); 485 sDataProjectionMap.put(Data.SYNC4, Data.SYNC4); 486 sDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 487 sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 488 sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 489 sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 490 sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); 491 sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); 492 sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 493 sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 494 sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 495 sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 496 sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 497 sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 498 sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 499 sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 500 sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID); 501 502 // Projection map for data grouped by contact (not raw contact) and some data field(s) 503 sDistinctDataProjectionMap = new HashMap<String, String>(); 504 sDistinctDataProjectionMap.put(Data._ID, 505 "MIN(" + Data._ID + ") AS " + Data._ID); 506 sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); 507 sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 508 sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 509 sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 510 sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); 511 sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1); 512 sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2); 513 sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3); 514 sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4); 515 sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5); 516 sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6); 517 sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7); 518 sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8); 519 sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9); 520 sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10); 521 sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11); 522 sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12); 523 sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13); 524 sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14); 525 sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15); 526 sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1); 527 sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2); 528 sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3); 529 sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4); 530 sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 531 sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 532 sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 533 sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 534 sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 535 sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 536 sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 537 sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 538 sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, 539 GroupMembership.GROUP_SOURCE_ID); 540 541 sPhoneLookupProjectionMap = new HashMap<String, String>(); 542 sPhoneLookupProjectionMap.put(PhoneLookup._ID, 543 ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID); 544 sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY, 545 Contacts.LOOKUP_KEY + " AS " + PhoneLookup.LOOKUP_KEY); 546 sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME, 547 ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME); 548 sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED, 549 ContactsColumns.CONCRETE_LAST_TIME_CONTACTED 550 + " AS " + PhoneLookup.LAST_TIME_CONTACTED); 551 sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED, 552 ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED); 553 sPhoneLookupProjectionMap.put(PhoneLookup.STARRED, 554 ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED); 555 sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP, 556 Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP); 557 sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID, 558 Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID); 559 sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE, 560 ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE); 561 sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER, 562 Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER); 563 sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL, 564 ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL 565 + " AS " + PhoneLookup.SEND_TO_VOICEMAIL); 566 sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER, 567 Phone.NUMBER + " AS " + PhoneLookup.NUMBER); 568 sPhoneLookupProjectionMap.put(PhoneLookup.TYPE, 569 Phone.TYPE + " AS " + PhoneLookup.TYPE); 570 sPhoneLookupProjectionMap.put(PhoneLookup.LABEL, 571 Phone.LABEL + " AS " + PhoneLookup.LABEL); 572 573 HashMap<String, String> columns; 574 575 // Groups projection map 576 columns = new HashMap<String, String>(); 577 columns.put(Groups._ID, "groups._id AS _id"); 578 columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME); 579 columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE); 580 columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID); 581 columns.put(Groups.DIRTY, Groups.DIRTY); 582 columns.put(Groups.VERSION, Groups.VERSION); 583 columns.put(Groups.RES_PACKAGE, PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE); 584 columns.put(Groups.TITLE, Groups.TITLE); 585 columns.put(Groups.TITLE_RES, Groups.TITLE_RES); 586 columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE); 587 columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID); 588 columns.put(Groups.DELETED, Groups.DELETED); 589 columns.put(Groups.NOTES, Groups.NOTES); 590 columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC); 591 columns.put(Groups.SYNC1, Tables.GROUPS + "." + Groups.SYNC1 + " AS " + Groups.SYNC1); 592 columns.put(Groups.SYNC2, Tables.GROUPS + "." + Groups.SYNC2 + " AS " + Groups.SYNC2); 593 columns.put(Groups.SYNC3, Tables.GROUPS + "." + Groups.SYNC3 + " AS " + Groups.SYNC3); 594 columns.put(Groups.SYNC4, Tables.GROUPS + "." + Groups.SYNC4 + " AS " + Groups.SYNC4); 595 sGroupsProjectionMap = columns; 596 597 // RawContacts and groups projection map 598 columns = new HashMap<String, String>(); 599 columns.putAll(sGroupsProjectionMap); 600 columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID 601 + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " 602 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 603 + ") AS " + Groups.SUMMARY_COUNT); 604 columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT " 605 + ContactsColumns.CONCRETE_ID + ") FROM " 606 + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " 607 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 608 + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES); 609 sGroupsSummaryProjectionMap = columns; 610 611 // Aggregate exception projection map 612 columns = new HashMap<String, String>(); 613 columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id"); 614 columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE); 615 columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1); 616 columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2); 617 sAggregationExceptionsProjectionMap = columns; 618 619 // Settings projection map 620 columns = new HashMap<String, String>(); 621 columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME); 622 columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE); 623 columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE); 624 columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC); 625 columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC 626 + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN(" 627 + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE " 628 + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME 629 + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "=" 630 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS " 631 + Settings.ANY_UNSYNCED); 632 columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM " 633 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY " 634 + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS 635 + ")) AS " + Settings.UNGROUPED_COUNT); 636 columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM " 637 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE " 638 + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID 639 + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS " 640 + Settings.UNGROUPED_WITH_PHONES); 641 sSettingsProjectionMap = columns; 642 643 columns = new HashMap<String, String>(); 644 columns.put(Presence._ID, Presence._ID); 645 columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID); 646 columns.put(Presence.DATA_ID, Presence.DATA_ID); 647 columns.put(Presence.IM_ACCOUNT, Presence.IM_ACCOUNT); 648 columns.put(Presence.IM_HANDLE, Presence.IM_HANDLE); 649 columns.put(Presence.PROTOCOL, Presence.PROTOCOL); 650 columns.put(Presence.CUSTOM_PROTOCOL, Presence.CUSTOM_PROTOCOL); 651 columns.put(Presence.PRESENCE_STATUS, Presence.PRESENCE_STATUS); 652 columns.put(Presence.PRESENCE_CUSTOM_STATUS, Presence.PRESENCE_CUSTOM_STATUS); 653 sPresenceProjectionMap = columns; 654 655 sDataWithPresenceProjectionMap = new HashMap<String, String>(); 656 sDataWithPresenceProjectionMap.putAll(sDataProjectionMap); 657 sDataWithPresenceProjectionMap.put(Presence.PRESENCE_STATUS, 658 Presence.PRESENCE_STATUS); 659 sDataWithPresenceProjectionMap.put(Presence.PRESENCE_CUSTOM_STATUS, 660 Presence.PRESENCE_CUSTOM_STATUS); 661 662 // Live folder projection 663 sLiveFoldersProjectionMap = new HashMap<String, String>(); 664 sLiveFoldersProjectionMap.put(LiveFolders._ID, 665 Contacts._ID + " AS " + LiveFolders._ID); 666 sLiveFoldersProjectionMap.put(LiveFolders.NAME, 667 Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME); 668 669 // TODO: Put contact photo back when we have a way to display a default icon 670 // for contacts without a photo 671 // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP, 672 // Photos.DATA + " AS " + LiveFolders.ICON_BITMAP); 673 674 sContactsInGroupSelect = Contacts._ID + " IN " 675 + "(SELECT " + RawContacts.CONTACT_ID 676 + " FROM " + Tables.RAW_CONTACTS 677 + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN " 678 + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID 679 + " FROM " + Tables.DATA_JOIN_MIMETYPES 680 + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 681 + "' AND " + GroupMembership.GROUP_ROW_ID + "=" 682 + "(SELECT " + Tables.GROUPS + "." + Groups._ID 683 + " FROM " + Tables.GROUPS 684 + " WHERE " + Groups.TITLE + "=?)))"; 685 } 686 687 /** 688 * Handles inserts and update for a specific Data type. 689 */ 690 private abstract class DataRowHandler { 691 692 protected final String mMimetype; 693 protected long mMimetypeId; 694 695 public DataRowHandler(String mimetype) { 696 mMimetype = mimetype; 697 } 698 699 protected long getMimeTypeId() { 700 if (mMimetypeId == 0) { 701 mMimetypeId = mOpenHelper.getMimeTypeId(mMimetype); 702 } 703 return mMimetypeId; 704 } 705 706 /** 707 * Inserts a row into the {@link Data} table. 708 */ 709 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 710 final long dataId = db.insert(Tables.DATA, null, values); 711 712 Integer primary = values.getAsInteger(Data.IS_PRIMARY); 713 if (primary != null && primary != 0) { 714 setIsPrimary(rawContactId, dataId, getMimeTypeId()); 715 } 716 717 return dataId; 718 } 719 720 /** 721 * Validates data and updates a {@link Data} row using the cursor, which contains 722 * the current data. 723 */ 724 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 725 boolean callerIsSyncAdapter) { 726 long dataId = c.getLong(DataUpdateQuery._ID); 727 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 728 729 if (values.containsKey(Data.IS_SUPER_PRIMARY)) { 730 long mimeTypeId = getMimeTypeId(); 731 setIsSuperPrimary(rawContactId, dataId, mimeTypeId); 732 setIsPrimary(rawContactId, dataId, mimeTypeId); 733 734 // Now that we've taken care of setting these, remove them from "values". 735 values.remove(Data.IS_SUPER_PRIMARY); 736 values.remove(Data.IS_PRIMARY); 737 } else if (values.containsKey(Data.IS_PRIMARY)) { 738 setIsPrimary(rawContactId, dataId, getMimeTypeId()); 739 740 // Now that we've taken care of setting this, remove it from "values". 741 values.remove(Data.IS_PRIMARY); 742 } 743 744 if (values.size() > 0) { 745 mDb.update(Tables.DATA, values, Data._ID + " = " + dataId, null); 746 } 747 748 if (!callerIsSyncAdapter) { 749 setRawContactDirty(rawContactId); 750 } 751 } 752 753 public int delete(SQLiteDatabase db, Cursor c) { 754 long dataId = c.getLong(DataDeleteQuery._ID); 755 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 756 boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0; 757 int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null); 758 if (count != 0 && primary) { 759 fixPrimary(db, rawContactId); 760 } 761 return count; 762 } 763 764 private void fixPrimary(SQLiteDatabase db, long rawContactId) { 765 long newPrimaryId = findNewPrimaryDataId(db, rawContactId); 766 if (newPrimaryId != -1) { 767 setIsPrimary(rawContactId, newPrimaryId, getMimeTypeId()); 768 } 769 } 770 771 protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) { 772 long primaryId = -1; 773 int primaryType = -1; 774 Cursor c = queryData(db, rawContactId); 775 try { 776 while (c.moveToNext()) { 777 long dataId = c.getLong(DataDeleteQuery._ID); 778 int type = c.getInt(DataDeleteQuery.DATA2); 779 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) { 780 primaryId = dataId; 781 primaryType = type; 782 } 783 } 784 } finally { 785 c.close(); 786 } 787 return primaryId; 788 } 789 790 /** 791 * Returns the rank of a specific record type to be used in determining the primary 792 * row. Lower number represents higher priority. 793 */ 794 protected int getTypeRank(int type) { 795 return 0; 796 } 797 798 protected Cursor queryData(SQLiteDatabase db, long rawContactId) { 799 return db.query(DataDeleteQuery.TABLE, DataDeleteQuery.CONCRETE_COLUMNS, 800 Data.RAW_CONTACT_ID + "=" + rawContactId + 801 " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'", 802 null, null, null, null); 803 } 804 805 protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 806 String bestDisplayName = null; 807 int bestDisplayNameSource = DisplayNameSources.UNDEFINED; 808 809 Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS, 810 Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null); 811 try { 812 while (c.moveToNext()) { 813 String mimeType = c.getString(DisplayNameQuery.MIMETYPE); 814 boolean primary; 815 String name; 816 817 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 818 name = c.getString(DisplayNameQuery.DISPLAY_NAME); 819 primary = true; 820 } else { 821 name = c.getString(DisplayNameQuery.DATA2); 822 primary = (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0); 823 } 824 825 if (name != null) { 826 Integer source = sDisplayNameSources.get(mimeType); 827 if (source != null 828 && (source > bestDisplayNameSource 829 || (source == bestDisplayNameSource && primary))) { 830 bestDisplayNameSource = source; 831 bestDisplayName = name; 832 } 833 } 834 } 835 836 } finally { 837 c.close(); 838 } 839 840 setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource); 841 if (!isNewRawContact(rawContactId)) { 842 mContactAggregator.updateDisplayName(db, rawContactId); 843 } 844 } 845 846 public boolean isAggregationRequired() { 847 return true; 848 } 849 850 /** 851 * Return set of values, using current values at given {@link Data#_ID} 852 * as baseline, but augmented with any updates. 853 */ 854 public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId, 855 ContentValues update) { 856 final ContentValues values = new ContentValues(); 857 final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=" + dataId, 858 null, null, null, null); 859 try { 860 if (cursor.moveToFirst()) { 861 for (int i = 0; i < cursor.getColumnCount(); i++) { 862 final String key = cursor.getColumnName(i); 863 values.put(key, cursor.getString(i)); 864 } 865 } 866 } finally { 867 cursor.close(); 868 } 869 values.putAll(update); 870 return values; 871 } 872 } 873 874 public class CustomDataRowHandler extends DataRowHandler { 875 876 public CustomDataRowHandler(String mimetype) { 877 super(mimetype); 878 } 879 } 880 881 public class StructuredNameRowHandler extends DataRowHandler { 882 private final NameSplitter mSplitter; 883 884 public StructuredNameRowHandler(NameSplitter splitter) { 885 super(StructuredName.CONTENT_ITEM_TYPE); 886 mSplitter = splitter; 887 } 888 889 @Override 890 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 891 fixStructuredNameComponents(values, values); 892 893 long dataId = super.insert(db, rawContactId, values); 894 895 String givenName = values.getAsString(StructuredName.GIVEN_NAME); 896 String familyName = values.getAsString(StructuredName.FAMILY_NAME); 897 mOpenHelper.insertNameLookupForStructuredName(rawContactId, dataId, givenName, 898 familyName); 899 fixRawContactDisplayName(db, rawContactId); 900 return dataId; 901 } 902 903 @Override 904 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 905 boolean callerIsSyncAdapter) { 906 final long dataId = c.getLong(DataUpdateQuery._ID); 907 final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 908 909 final ContentValues augmented = getAugmentedValues(db, dataId, values); 910 fixStructuredNameComponents(augmented, values); 911 912 super.update(db, values, c, callerIsSyncAdapter); 913 914 boolean hasGivenName = values.containsKey(StructuredName.GIVEN_NAME); 915 boolean hasFamilyName = values.containsKey(StructuredName.FAMILY_NAME); 916 if (hasGivenName || hasFamilyName) { 917 String givenName = augmented.getAsString(StructuredName.GIVEN_NAME); 918 String familyName = augmented.getAsString(StructuredName.FAMILY_NAME); 919 mOpenHelper.deleteNameLookup(dataId); 920 mOpenHelper.insertNameLookupForStructuredName(rawContactId, dataId, givenName, 921 familyName); 922 } 923 fixRawContactDisplayName(db, rawContactId); 924 } 925 926 @Override 927 public int delete(SQLiteDatabase db, Cursor c) { 928 long dataId = c.getLong(DataDeleteQuery._ID); 929 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 930 931 int count = super.delete(db, c); 932 933 mOpenHelper.deleteNameLookup(dataId); 934 fixRawContactDisplayName(db, rawContactId); 935 return count; 936 } 937 938 /** 939 * Specific list of structured fields. 940 */ 941 private final String[] STRUCTURED_FIELDS = new String[] { 942 StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME, 943 StructuredName.FAMILY_NAME, StructuredName.SUFFIX 944 }; 945 946 /** 947 * Parses the supplied display name, but only if the incoming values do 948 * not already contain structured name parts. Also, if the display name 949 * is not provided, generate one by concatenating first name and last 950 * name. 951 */ 952 private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) { 953 final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME); 954 955 final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); 956 final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); 957 958 if (touchedUnstruct && !touchedStruct) { 959 NameSplitter.Name name = new NameSplitter.Name(); 960 mSplitter.split(name, unstruct); 961 name.toValues(update); 962 } else if (!touchedUnstruct && touchedStruct) { 963 NameSplitter.Name name = new NameSplitter.Name(); 964 name.fromValues(augmented); 965 final String joined = mSplitter.join(name); 966 update.put(StructuredName.DISPLAY_NAME, joined); 967 } 968 } 969 } 970 971 public class StructuredPostalRowHandler extends DataRowHandler { 972 private PostalSplitter mSplitter; 973 974 public StructuredPostalRowHandler(PostalSplitter splitter) { 975 super(StructuredPostal.CONTENT_ITEM_TYPE); 976 mSplitter = splitter; 977 } 978 979 @Override 980 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 981 fixStructuredPostalComponents(values, values); 982 return super.insert(db, rawContactId, values); 983 } 984 985 @Override 986 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 987 boolean callerIsSyncAdapter) { 988 final long dataId = c.getLong(DataUpdateQuery._ID); 989 final ContentValues augmented = getAugmentedValues(db, dataId, values); 990 fixStructuredPostalComponents(augmented, values); 991 super.update(db, values, c, callerIsSyncAdapter); 992 } 993 994 /** 995 * Specific list of structured fields. 996 */ 997 private final String[] STRUCTURED_FIELDS = new String[] { 998 StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD, 999 StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE, 1000 StructuredPostal.COUNTRY, 1001 }; 1002 1003 /** 1004 * Prepares the given {@link StructuredPostal} row, building 1005 * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured 1006 * values when missing. When structured components are missing, the 1007 * unstructured value is assigned to {@link StructuredPostal#STREET}. 1008 */ 1009 private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) { 1010 final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS); 1011 1012 final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); 1013 final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); 1014 1015 final PostalSplitter.Postal postal = new PostalSplitter.Postal(); 1016 1017 if (touchedUnstruct && !touchedStruct) { 1018 mSplitter.split(postal, unstruct); 1019 postal.toValues(update); 1020 } else if (!touchedUnstruct && touchedStruct) { 1021 postal.fromValues(augmented); 1022 final String joined = mSplitter.join(postal); 1023 update.put(StructuredPostal.FORMATTED_ADDRESS, joined); 1024 } 1025 } 1026 } 1027 1028 public class CommonDataRowHandler extends DataRowHandler { 1029 1030 private final String mTypeColumn; 1031 private final String mLabelColumn; 1032 1033 public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) { 1034 super(mimetype); 1035 mTypeColumn = typeColumn; 1036 mLabelColumn = labelColumn; 1037 } 1038 1039 @Override 1040 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1041 enforceTypeAndLabel(values, values); 1042 return super.insert(db, rawContactId, values); 1043 } 1044 1045 @Override 1046 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1047 boolean callerIsSyncAdapter) { 1048 final long dataId = c.getLong(DataUpdateQuery._ID); 1049 final ContentValues augmented = getAugmentedValues(db, dataId, values); 1050 enforceTypeAndLabel(augmented, values); 1051 super.update(db, values, c, callerIsSyncAdapter); 1052 } 1053 1054 /** 1055 * If the given {@link ContentValues} defines {@link #mTypeColumn}, 1056 * enforce that {@link #mLabelColumn} only appears when type is 1057 * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise. 1058 */ 1059 private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) { 1060 final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn)); 1061 final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn)); 1062 1063 if (hasLabel && !hasType) { 1064 // When label exists, assert that some type is defined 1065 throw new IllegalArgumentException(mTypeColumn + " must be specified when " 1066 + mLabelColumn + " is defined."); 1067 } 1068 } 1069 } 1070 1071 public class OrganizationDataRowHandler extends CommonDataRowHandler { 1072 1073 public OrganizationDataRowHandler() { 1074 super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL); 1075 } 1076 1077 @Override 1078 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1079 long id = super.insert(db, rawContactId, values); 1080 fixRawContactDisplayName(db, rawContactId); 1081 return id; 1082 } 1083 1084 @Override 1085 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1086 boolean callerIsSyncAdapter) { 1087 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1088 1089 super.update(db, values, c, callerIsSyncAdapter); 1090 1091 fixRawContactDisplayName(db, rawContactId); 1092 } 1093 1094 @Override 1095 public int delete(SQLiteDatabase db, Cursor c) { 1096 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1097 1098 int count = super.delete(db, c); 1099 fixRawContactDisplayName(db, rawContactId); 1100 return count; 1101 } 1102 1103 @Override 1104 protected int getTypeRank(int type) { 1105 switch (type) { 1106 case Organization.TYPE_WORK: return 0; 1107 case Organization.TYPE_CUSTOM: return 1; 1108 case Organization.TYPE_OTHER: return 2; 1109 default: return 1000; 1110 } 1111 } 1112 1113 @Override 1114 public boolean isAggregationRequired() { 1115 return false; 1116 } 1117 } 1118 1119 public class EmailDataRowHandler extends CommonDataRowHandler { 1120 1121 public EmailDataRowHandler() { 1122 super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL); 1123 } 1124 1125 @Override 1126 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1127 String address = values.getAsString(Email.DATA); 1128 1129 long dataId = super.insert(db, rawContactId, values); 1130 1131 fixRawContactDisplayName(db, rawContactId); 1132 mOpenHelper.insertNameLookupForEmail(rawContactId, dataId, address); 1133 return dataId; 1134 } 1135 1136 @Override 1137 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1138 boolean callerIsSyncAdapter) { 1139 long dataId = c.getLong(DataUpdateQuery._ID); 1140 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1141 String address = values.getAsString(Email.DATA); 1142 1143 super.update(db, values, c, callerIsSyncAdapter); 1144 1145 mOpenHelper.deleteNameLookup(dataId); 1146 mOpenHelper.insertNameLookupForEmail(rawContactId, dataId, address); 1147 fixRawContactDisplayName(db, rawContactId); 1148 } 1149 1150 @Override 1151 public int delete(SQLiteDatabase db, Cursor c) { 1152 long dataId = c.getLong(DataDeleteQuery._ID); 1153 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1154 1155 int count = super.delete(db, c); 1156 1157 mOpenHelper.deleteNameLookup(dataId); 1158 fixRawContactDisplayName(db, rawContactId); 1159 return count; 1160 } 1161 1162 @Override 1163 protected int getTypeRank(int type) { 1164 switch (type) { 1165 case Email.TYPE_HOME: return 0; 1166 case Email.TYPE_WORK: return 1; 1167 case Email.TYPE_CUSTOM: return 2; 1168 case Email.TYPE_OTHER: return 3; 1169 default: return 1000; 1170 } 1171 } 1172 } 1173 1174 public class NicknameDataRowHandler extends CommonDataRowHandler { 1175 1176 public NicknameDataRowHandler() { 1177 super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL); 1178 } 1179 1180 @Override 1181 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1182 String nickname = values.getAsString(Nickname.NAME); 1183 1184 long dataId = super.insert(db, rawContactId, values); 1185 1186 fixRawContactDisplayName(db, rawContactId); 1187 mOpenHelper.insertNameLookupForNickname(rawContactId, dataId, nickname); 1188 return dataId; 1189 } 1190 1191 @Override 1192 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1193 boolean callerIsSyncAdapter) { 1194 long dataId = c.getLong(DataUpdateQuery._ID); 1195 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1196 String nickname = values.getAsString(Nickname.NAME); 1197 1198 super.update(db, values, c, callerIsSyncAdapter); 1199 1200 mOpenHelper.deleteNameLookup(dataId); 1201 mOpenHelper.insertNameLookupForNickname(rawContactId, dataId, nickname); 1202 fixRawContactDisplayName(db, rawContactId); 1203 } 1204 1205 @Override 1206 public int delete(SQLiteDatabase db, Cursor c) { 1207 long dataId = c.getLong(DataDeleteQuery._ID); 1208 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1209 1210 int count = super.delete(db, c); 1211 1212 mOpenHelper.deleteNameLookup(dataId); 1213 fixRawContactDisplayName(db, rawContactId); 1214 return count; 1215 } 1216 } 1217 1218 public class PhoneDataRowHandler extends CommonDataRowHandler { 1219 1220 public PhoneDataRowHandler() { 1221 super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL); 1222 } 1223 1224 @Override 1225 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1226 long dataId; 1227 if (values.containsKey(Phone.NUMBER)) { 1228 String number = values.getAsString(Phone.NUMBER); 1229 String normalizedNumber = computeNormalizedNumber(number, values); 1230 1231 dataId = super.insert(db, rawContactId, values); 1232 1233 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); 1234 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1235 fixRawContactDisplayName(db, rawContactId); 1236 } else { 1237 dataId = super.insert(db, rawContactId, values); 1238 } 1239 return dataId; 1240 } 1241 1242 @Override 1243 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1244 boolean callerIsSyncAdapter) { 1245 long dataId = c.getLong(DataUpdateQuery._ID); 1246 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1247 if (values.containsKey(Phone.NUMBER)) { 1248 String number = values.getAsString(Phone.NUMBER); 1249 String normalizedNumber = computeNormalizedNumber(number, values); 1250 1251 super.update(db, values, c, callerIsSyncAdapter); 1252 1253 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); 1254 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1255 fixRawContactDisplayName(db, rawContactId); 1256 } else { 1257 super.update(db, values, c, callerIsSyncAdapter); 1258 } 1259 } 1260 1261 @Override 1262 public int delete(SQLiteDatabase db, Cursor c) { 1263 long dataId = c.getLong(DataDeleteQuery._ID); 1264 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1265 1266 int count = super.delete(db, c); 1267 1268 updatePhoneLookup(db, rawContactId, dataId, null, null); 1269 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1270 fixRawContactDisplayName(db, rawContactId); 1271 return count; 1272 } 1273 1274 private String computeNormalizedNumber(String number, ContentValues values) { 1275 String normalizedNumber = null; 1276 if (number != null) { 1277 normalizedNumber = PhoneNumberUtils.getStrippedReversed(number); 1278 } 1279 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber); 1280 return normalizedNumber; 1281 } 1282 1283 private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId, 1284 String number, String normalizedNumber) { 1285 if (number != null) { 1286 ContentValues phoneValues = new ContentValues(); 1287 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId); 1288 phoneValues.put(PhoneLookupColumns.DATA_ID, dataId); 1289 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber); 1290 db.replace(Tables.PHONE_LOOKUP, null, phoneValues); 1291 } else { 1292 db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=" + dataId, null); 1293 } 1294 } 1295 1296 @Override 1297 protected int getTypeRank(int type) { 1298 switch (type) { 1299 case Phone.TYPE_MOBILE: return 0; 1300 case Phone.TYPE_WORK: return 1; 1301 case Phone.TYPE_HOME: return 2; 1302 case Phone.TYPE_PAGER: return 3; 1303 case Phone.TYPE_CUSTOM: return 4; 1304 case Phone.TYPE_OTHER: return 5; 1305 case Phone.TYPE_FAX_WORK: return 6; 1306 case Phone.TYPE_FAX_HOME: return 7; 1307 default: return 1000; 1308 } 1309 } 1310 } 1311 1312 public class GroupMembershipRowHandler extends DataRowHandler { 1313 1314 public GroupMembershipRowHandler() { 1315 super(GroupMembership.CONTENT_ITEM_TYPE); 1316 } 1317 1318 @Override 1319 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1320 resolveGroupSourceIdInValues(rawContactId, db, values, true); 1321 return super.insert(db, rawContactId, values); 1322 } 1323 1324 @Override 1325 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1326 boolean callerIsSyncAdapter) { 1327 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1328 resolveGroupSourceIdInValues(rawContactId, db, values, false); 1329 super.update(db, values, c, callerIsSyncAdapter); 1330 } 1331 1332 private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db, 1333 ContentValues values, boolean isInsert) { 1334 boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID); 1335 boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID); 1336 if (containsGroupSourceId && containsGroupId) { 1337 throw new IllegalArgumentException( 1338 "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID " 1339 + "and GroupMembership.GROUP_ROW_ID"); 1340 } 1341 1342 if (!containsGroupSourceId && !containsGroupId) { 1343 if (isInsert) { 1344 throw new IllegalArgumentException( 1345 "you must set exactly one of GroupMembership.GROUP_SOURCE_ID " 1346 + "and GroupMembership.GROUP_ROW_ID"); 1347 } else { 1348 return; 1349 } 1350 } 1351 1352 if (containsGroupSourceId) { 1353 final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID); 1354 final long groupId = getOrMakeGroup(db, rawContactId, sourceId); 1355 values.remove(GroupMembership.GROUP_SOURCE_ID); 1356 values.put(GroupMembership.GROUP_ROW_ID, groupId); 1357 } 1358 } 1359 1360 @Override 1361 public boolean isAggregationRequired() { 1362 return false; 1363 } 1364 } 1365 1366 public class PhotoDataRowHandler extends DataRowHandler { 1367 1368 public PhotoDataRowHandler() { 1369 super(Photo.CONTENT_ITEM_TYPE); 1370 } 1371 1372 @Override 1373 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1374 long dataId = super.insert(db, rawContactId, values); 1375 if (!isNewRawContact(rawContactId)) { 1376 mContactAggregator.updatePhotoId(db, rawContactId); 1377 } 1378 return dataId; 1379 } 1380 1381 @Override 1382 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1383 boolean callerIsSyncAdapter) { 1384 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1385 super.update(db, values, c, callerIsSyncAdapter); 1386 mContactAggregator.updatePhotoId(db, rawContactId); 1387 } 1388 1389 @Override 1390 public int delete(SQLiteDatabase db, Cursor c) { 1391 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1392 int count = super.delete(db, c); 1393 mContactAggregator.updatePhotoId(db, rawContactId); 1394 return count; 1395 } 1396 1397 @Override 1398 public boolean isAggregationRequired() { 1399 return false; 1400 } 1401 } 1402 1403 1404 private HashMap<String, DataRowHandler> mDataRowHandlers; 1405 private final ContactAggregationScheduler mAggregationScheduler; 1406 private OpenHelper mOpenHelper; 1407 1408 private NameSplitter mNameSplitter; 1409 private PostalSplitter mPostalSplitter; 1410 1411 private ContactAggregator mContactAggregator; 1412 private LegacyApiSupport mLegacyApiSupport; 1413 private GlobalSearchSupport mGlobalSearchSupport; 1414 1415 private ContentValues mValues = new ContentValues(); 1416 1417 private volatile CountDownLatch mAccessLatch; 1418 private boolean mImportMode; 1419 1420 private boolean mScheduleAggregation; 1421 private HashSet<Long> mInsertedRawContacts = Sets.newHashSet(); 1422 private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet(); 1423 private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap(); 1424 1425 private boolean mSyncToNetwork; 1426 1427 public ContactsProvider2() { 1428 this(new ContactAggregationScheduler()); 1429 } 1430 1431 /** 1432 * Constructor for testing. 1433 */ 1434 /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) { 1435 mAggregationScheduler = scheduler; 1436 } 1437 1438 @Override 1439 public boolean onCreate() { 1440 super.onCreate(); 1441 1442 final Context context = getContext(); 1443 mOpenHelper = (OpenHelper)getOpenHelper(); 1444 mGlobalSearchSupport = new GlobalSearchSupport(this); 1445 mLegacyApiSupport = new LegacyApiSupport(context, mOpenHelper, this, mGlobalSearchSupport); 1446 mContactAggregator = new ContactAggregator(this, mOpenHelper, mAggregationScheduler); 1447 1448 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1449 1450 mSetPrimaryStatement = db.compileStatement( 1451 "UPDATE " + Tables.DATA + 1452 " SET " + Data.IS_PRIMARY + "=(_id=?)" + 1453 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1454 " AND " + Data.RAW_CONTACT_ID + "=?"); 1455 1456 mSetSuperPrimaryStatement = db.compileStatement( 1457 "UPDATE " + Tables.DATA + 1458 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" + 1459 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1460 " AND " + Data.RAW_CONTACT_ID + " IN (" + 1461 "SELECT " + RawContacts._ID + 1462 " FROM " + Tables.RAW_CONTACTS + 1463 " WHERE " + RawContacts.CONTACT_ID + " =(" + 1464 "SELECT " + RawContacts.CONTACT_ID + 1465 " FROM " + Tables.RAW_CONTACTS + 1466 " WHERE " + RawContacts._ID + "=?))"); 1467 1468 mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET " 1469 + RawContacts.TIMES_CONTACTED + "=" + RawContacts.TIMES_CONTACTED + "+1," 1470 + RawContacts.LAST_TIME_CONTACTED + "=? WHERE " + RawContacts.CONTACT_ID + "=?"); 1471 1472 mRawContactDisplayNameUpdate = db.compileStatement( 1473 "UPDATE " + Tables.RAW_CONTACTS + 1474 " SET " + RawContactsColumns.DISPLAY_NAME + "=?," 1475 + RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" + 1476 " WHERE " + RawContacts._ID + "=?"); 1477 1478 mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET " 1479 + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?"); 1480 1481 mAggregatedPresenceReplace = db.compileStatement( 1482 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "(" 1483 + AggregatedPresenceColumns.CONTACT_ID + ", " 1484 + Presence.PRESENCE_STATUS 1485 + ") VALUES (?, (SELECT MAX(" + Presence.PRESENCE_STATUS + ")" 1486 + " FROM " + Tables.PRESENCE + "," + Tables.RAW_CONTACTS 1487 + " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=" 1488 + RawContactsColumns.CONCRETE_ID 1489 + " AND " + RawContacts.CONTACT_ID + "=?))"); 1490 1491 mAggregatedPresenceStatusUpdate = db.compileStatement( 1492 "UPDATE " + Tables.AGGREGATED_PRESENCE 1493 + " SET " + Presence.PRESENCE_CUSTOM_STATUS + "=? " 1494 + " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?"); 1495 1496 final Locale locale = Locale.getDefault(); 1497 mNameSplitter = new NameSplitter( 1498 context.getString(com.android.internal.R.string.common_name_prefixes), 1499 context.getString(com.android.internal.R.string.common_last_name_prefixes), 1500 context.getString(com.android.internal.R.string.common_name_suffixes), 1501 context.getString(com.android.internal.R.string.common_name_conjunctions), 1502 locale); 1503 mPostalSplitter = new PostalSplitter(locale); 1504 1505 mDataRowHandlers = new HashMap<String, DataRowHandler>(); 1506 1507 mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler()); 1508 mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE, 1509 new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL)); 1510 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler( 1511 StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL)); 1512 mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler()); 1513 mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler()); 1514 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler()); 1515 mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE, 1516 new StructuredNameRowHandler(mNameSplitter)); 1517 mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, 1518 new StructuredPostalRowHandler(mPostalSplitter)); 1519 mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler()); 1520 mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler()); 1521 1522 if (isLegacyContactImportNeeded()) { 1523 importLegacyContactsAsync(); 1524 } 1525 1526 verifyAccounts(); 1527 1528 return (db != null); 1529 } 1530 1531 protected void verifyAccounts() { 1532 AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false); 1533 onAccountsUpdated(AccountManager.get(getContext()).getAccounts()); 1534 } 1535 1536 /* Visible for testing */ 1537 @Override 1538 protected OpenHelper getOpenHelper(final Context context) { 1539 return OpenHelper.getInstance(context); 1540 } 1541 1542 /* package */ ContactAggregationScheduler getContactAggregationScheduler() { 1543 return mAggregationScheduler; 1544 } 1545 1546 /* package */ NameSplitter getNameSplitter() { 1547 return mNameSplitter; 1548 } 1549 1550 protected boolean isLegacyContactImportNeeded() { 1551 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1552 return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION; 1553 } 1554 1555 protected LegacyContactImporter getLegacyContactImporter() { 1556 return new LegacyContactImporter(getContext(), this); 1557 } 1558 1559 /** 1560 * Imports legacy contacts in a separate thread. As long as the import process is running 1561 * all other access to the contacts is blocked. 1562 */ 1563 private void importLegacyContactsAsync() { 1564 mAccessLatch = new CountDownLatch(1); 1565 1566 Thread importThread = new Thread("LegacyContactImport") { 1567 @Override 1568 public void run() { 1569 if (importLegacyContacts()) { 1570 1571 /* 1572 * When the import process is done, we can unlock the provider and 1573 * start aggregating the imported contacts asynchronously. 1574 */ 1575 mAccessLatch.countDown(); 1576 mAccessLatch = null; 1577 scheduleContactAggregation(); 1578 } 1579 } 1580 }; 1581 1582 importThread.start(); 1583 } 1584 1585 private boolean importLegacyContacts() { 1586 LegacyContactImporter importer = getLegacyContactImporter(); 1587 if (importLegacyContacts(importer)) { 1588 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1589 Editor editor = prefs.edit(); 1590 editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION); 1591 editor.commit(); 1592 return true; 1593 } else { 1594 return false; 1595 } 1596 } 1597 1598 /* Visible for testing */ 1599 /* package */ boolean importLegacyContacts(LegacyContactImporter importer) { 1600 mContactAggregator.setEnabled(false); 1601 mImportMode = true; 1602 try { 1603 importer.importContacts(); 1604 mContactAggregator.setEnabled(true); 1605 return true; 1606 } catch (Throwable e) { 1607 Log.e(TAG, "Legacy contact import failed", e); 1608 return false; 1609 } finally { 1610 mImportMode = false; 1611 } 1612 } 1613 1614 @Override 1615 protected void finalize() throws Throwable { 1616 if (mContactAggregator != null) { 1617 mContactAggregator.quit(); 1618 } 1619 1620 super.finalize(); 1621 } 1622 1623 /** 1624 * Wipes all data from the contacts database. 1625 */ 1626 /* package */ void wipeData() { 1627 mOpenHelper.wipeData(); 1628 } 1629 1630 /** 1631 * While importing and aggregating contacts, this content provider will 1632 * block all attempts to change contacts data. In particular, it will hold 1633 * up all contact syncs. As soon as the import process is complete, all 1634 * processes waiting to write to the provider are unblocked and can proceed 1635 * to compete for the database transaction monitor. 1636 */ 1637 private void waitForAccess() { 1638 CountDownLatch latch = mAccessLatch; 1639 if (latch != null) { 1640 while (true) { 1641 try { 1642 latch.await(); 1643 mAccessLatch = null; 1644 return; 1645 } catch (InterruptedException e) { 1646 Thread.currentThread().interrupt(); 1647 } 1648 } 1649 } 1650 } 1651 1652 @Override 1653 public Uri insert(Uri uri, ContentValues values) { 1654 waitForAccess(); 1655 return super.insert(uri, values); 1656 } 1657 1658 @Override 1659 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1660 waitForAccess(); 1661 return super.update(uri, values, selection, selectionArgs); 1662 } 1663 1664 @Override 1665 public int delete(Uri uri, String selection, String[] selectionArgs) { 1666 waitForAccess(); 1667 return super.delete(uri, selection, selectionArgs); 1668 } 1669 1670 @Override 1671 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1672 throws OperationApplicationException { 1673 waitForAccess(); 1674 return super.applyBatch(operations); 1675 } 1676 1677 @Override 1678 protected void onBeginTransaction() { 1679 if (VERBOSE_LOGGING) { 1680 Log.v(TAG, "onBeginTransaction"); 1681 } 1682 super.onBeginTransaction(); 1683 clearTransactionalChanges(); 1684 } 1685 1686 private void clearTransactionalChanges() { 1687 mInsertedRawContacts.clear(); 1688 mUpdatedRawContacts.clear(); 1689 mUpdatedSyncStates.clear(); 1690 } 1691 1692 @Override 1693 protected void beforeTransactionCommit() { 1694 if (VERBOSE_LOGGING) { 1695 Log.v(TAG, "beforeTransactionCommit"); 1696 } 1697 super.beforeTransactionCommit(); 1698 flushTransactionalChanges(); 1699 } 1700 1701 private void flushTransactionalChanges() { 1702 if (VERBOSE_LOGGING) { 1703 Log.v(TAG, "flushTransactionChanges"); 1704 } 1705 for (long rawContactId : mInsertedRawContacts) { 1706 mContactAggregator.insertContact(mDb, rawContactId); 1707 } 1708 1709 String ids; 1710 if (!mUpdatedRawContacts.isEmpty()) { 1711 ids = buildIdsString(mUpdatedRawContacts); 1712 mDb.execSQL("UPDATE raw_contacts SET version = version + 1 WHERE _id in " + ids, 1713 new Object[]{}); 1714 } 1715 1716 for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) { 1717 long id = entry.getKey(); 1718 mOpenHelper.getSyncState().update(mDb, id, entry.getValue()); 1719 } 1720 1721 clearTransactionalChanges(); 1722 } 1723 1724 private String buildIdsString(HashSet<Long> ids) { 1725 StringBuilder idsBuilder = null; 1726 for (long id : ids) { 1727 if (idsBuilder == null) { 1728 idsBuilder = new StringBuilder(); 1729 idsBuilder.append("("); 1730 } else { 1731 idsBuilder.append(","); 1732 } 1733 idsBuilder.append(id); 1734 } 1735 idsBuilder.append(")"); 1736 return idsBuilder.toString(); 1737 } 1738 1739 @Override 1740 protected void onEndTransaction() { 1741 if (mScheduleAggregation) { 1742 mScheduleAggregation = false; 1743 scheduleContactAggregation(); 1744 } 1745 super.onEndTransaction(); 1746 } 1747 1748 @Override 1749 protected void notifyChange() { 1750 notifyChange(mSyncToNetwork); 1751 mSyncToNetwork = false; 1752 } 1753 1754 protected void notifyChange(boolean syncToNetwork) { 1755 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null, 1756 syncToNetwork); 1757 } 1758 1759 protected void scheduleContactAggregation() { 1760 mContactAggregator.run(); 1761 } 1762 1763 private boolean isNewRawContact(long rawContactId) { 1764 return mInsertedRawContacts.contains(rawContactId); 1765 } 1766 1767 private DataRowHandler getDataRowHandler(final String mimeType) { 1768 DataRowHandler handler = mDataRowHandlers.get(mimeType); 1769 if (handler == null) { 1770 handler = new CustomDataRowHandler(mimeType); 1771 mDataRowHandlers.put(mimeType, handler); 1772 } 1773 return handler; 1774 } 1775 1776 @Override 1777 protected Uri insertInTransaction(Uri uri, ContentValues values) { 1778 if (VERBOSE_LOGGING) { 1779 Log.v(TAG, "insertInTransaction: " + uri); 1780 } 1781 1782 final boolean callerIsSyncAdapter = 1783 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 1784 1785 final int match = sUriMatcher.match(uri); 1786 long id = 0; 1787 1788 switch (match) { 1789 case SYNCSTATE: 1790 id = mOpenHelper.getSyncState().insert(mDb, values); 1791 break; 1792 1793 case CONTACTS: { 1794 insertContact(values); 1795 break; 1796 } 1797 1798 case RAW_CONTACTS: { 1799 final Account account = readAccountFromQueryParams(uri); 1800 id = insertRawContact(values, account); 1801 mSyncToNetwork |= !callerIsSyncAdapter; 1802 break; 1803 } 1804 1805 case RAW_CONTACTS_DATA: { 1806 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 1807 id = insertData(values, callerIsSyncAdapter); 1808 mSyncToNetwork |= !callerIsSyncAdapter; 1809 break; 1810 } 1811 1812 case DATA: { 1813 id = insertData(values, callerIsSyncAdapter); 1814 mSyncToNetwork |= !callerIsSyncAdapter; 1815 break; 1816 } 1817 1818 case GROUPS: { 1819 final Account account = readAccountFromQueryParams(uri); 1820 id = insertGroup(values, account, callerIsSyncAdapter); 1821 mSyncToNetwork |= !callerIsSyncAdapter; 1822 break; 1823 } 1824 1825 case SETTINGS: { 1826 id = insertSettings(values); 1827 break; 1828 } 1829 1830 case PRESENCE: { 1831 id = insertPresence(values); 1832 break; 1833 } 1834 1835 default: 1836 mSyncToNetwork = true; 1837 return mLegacyApiSupport.insert(uri, values); 1838 } 1839 1840 if (id < 0) { 1841 return null; 1842 } 1843 1844 return ContentUris.withAppendedId(uri, id); 1845 } 1846 1847 /** 1848 * If account is non-null then store it in the values. If the account is already 1849 * specified in the values then it must be consistent with the account, if it is non-null. 1850 * @param values the ContentValues to read from and update 1851 * @param account the explicitly provided Account 1852 * @return false if the accounts are inconsistent 1853 */ 1854 private boolean resolveAccount(ContentValues values, Account account) { 1855 // If either is specified then both must be specified. 1856 final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME); 1857 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 1858 if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) { 1859 final Account valuesAccount = new Account(accountName, accountType); 1860 if (account != null && !valuesAccount.equals(account)) { 1861 return false; 1862 } 1863 account = valuesAccount; 1864 } 1865 if (account != null) { 1866 values.put(RawContacts.ACCOUNT_NAME, account.name); 1867 values.put(RawContacts.ACCOUNT_TYPE, account.type); 1868 } 1869 return true; 1870 } 1871 1872 /** 1873 * Inserts an item in the contacts table 1874 * 1875 * @param values the values for the new row 1876 * @return the row ID of the newly created row 1877 */ 1878 private long insertContact(ContentValues values) { 1879 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 1880 } 1881 1882 /** 1883 * Inserts an item in the contacts table 1884 * 1885 * @param values the values for the new row 1886 * @param account the account this contact should be associated with. may be null. 1887 * @return the row ID of the newly created row 1888 */ 1889 private long insertRawContact(ContentValues values, Account account) { 1890 ContentValues overriddenValues = new ContentValues(values); 1891 overriddenValues.putNull(RawContacts.CONTACT_ID); 1892 if (!resolveAccount(overriddenValues, account)) { 1893 return -1; 1894 } 1895 1896 if (values.containsKey(RawContacts.DELETED) 1897 && values.getAsInteger(RawContacts.DELETED) != 0) { 1898 overriddenValues.put(RawContacts.AGGREGATION_MODE, 1899 RawContacts.AGGREGATION_MODE_DISABLED); 1900 } 1901 1902 long rawContactId = 1903 mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues); 1904 mContactAggregator.markNewForAggregation(rawContactId); 1905 1906 // Trigger creation of a Contact based on this RawContact at the end of transaction 1907 mInsertedRawContacts.add(rawContactId); 1908 return rawContactId; 1909 } 1910 1911 /** 1912 * Inserts an item in the data table 1913 * 1914 * @param values the values for the new row 1915 * @return the row ID of the newly created row 1916 */ 1917 private long insertData(ContentValues values, boolean callerIsSyncAdapter) { 1918 long id = 0; 1919 mValues.clear(); 1920 mValues.putAll(values); 1921 1922 long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 1923 1924 // Replace package with internal mapping 1925 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 1926 if (packageName != null) { 1927 mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 1928 } 1929 mValues.remove(Data.RES_PACKAGE); 1930 1931 // Replace mimetype with internal mapping 1932 final String mimeType = mValues.getAsString(Data.MIMETYPE); 1933 if (TextUtils.isEmpty(mimeType)) { 1934 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 1935 } 1936 1937 mValues.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType)); 1938 mValues.remove(Data.MIMETYPE); 1939 1940 DataRowHandler rowHandler = getDataRowHandler(mimeType); 1941 id = rowHandler.insert(mDb, rawContactId, mValues); 1942 if (!callerIsSyncAdapter) { 1943 setRawContactDirty(rawContactId); 1944 } 1945 mUpdatedRawContacts.add(rawContactId); 1946 1947 if (rowHandler.isAggregationRequired()) { 1948 triggerAggregation(rawContactId); 1949 } 1950 return id; 1951 } 1952 1953 private void triggerAggregation(long rawContactId) { 1954 if (!mContactAggregator.isEnabled()) { 1955 return; 1956 } 1957 1958 int aggregationMode = mOpenHelper.getAggregationMode(rawContactId); 1959 switch (aggregationMode) { 1960 case RawContacts.AGGREGATION_MODE_DISABLED: 1961 break; 1962 1963 case RawContacts.AGGREGATION_MODE_DEFAULT: { 1964 mContactAggregator.markForAggregation(rawContactId); 1965 mScheduleAggregation = true; 1966 break; 1967 } 1968 1969 case RawContacts.AGGREGATION_MODE_SUSPENDED: { 1970 long contactId = mOpenHelper.getContactId(rawContactId); 1971 1972 if (contactId != 0) { 1973 mContactAggregator.updateAggregateData(contactId); 1974 } 1975 break; 1976 } 1977 1978 case RawContacts.AGGREGATION_MODE_IMMEDIATE: { 1979 long contactId = mOpenHelper.getContactId(rawContactId); 1980 mContactAggregator.aggregateContact(mDb, rawContactId, contactId); 1981 break; 1982 } 1983 } 1984 } 1985 1986 /** 1987 * Returns the group id of the group with sourceId and the same account as rawContactId. 1988 * If the group doesn't already exist then it is first created, 1989 * @param db SQLiteDatabase to use for this operation 1990 * @param rawContactId the contact this group is associated with 1991 * @param sourceId the sourceIf of the group to query or create 1992 * @return the group id of the existing or created group 1993 * @throws IllegalArgumentException if the contact is not associated with an account 1994 * @throws IllegalStateException if a group needs to be created but the creation failed 1995 */ 1996 private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) { 1997 Account account = null; 1998 Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "=" 1999 + rawContactId, null, null, null, null); 2000 try { 2001 if (c.moveToNext()) { 2002 final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME); 2003 final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE); 2004 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 2005 account = new Account(accountName, accountType); 2006 } 2007 } 2008 } finally { 2009 c.close(); 2010 } 2011 if (account == null) { 2012 throw new IllegalArgumentException("if the groupmembership only " 2013 + "has a sourceid the the contact must be associate with " 2014 + "an account"); 2015 } 2016 2017 // look up the group that contains this sourceId and has the same account name and type 2018 // as the contact refered to by rawContactId 2019 c = db.query(Tables.GROUPS, new String[]{RawContacts._ID}, 2020 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID, 2021 new String[]{sourceId, account.name, account.type}, null, null, null); 2022 try { 2023 if (c.moveToNext()) { 2024 return c.getLong(0); 2025 } else { 2026 ContentValues groupValues = new ContentValues(); 2027 groupValues.put(Groups.ACCOUNT_NAME, account.name); 2028 groupValues.put(Groups.ACCOUNT_TYPE, account.type); 2029 groupValues.put(Groups.SOURCE_ID, sourceId); 2030 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues); 2031 if (groupId < 0) { 2032 throw new IllegalStateException("unable to create a new group with " 2033 + "this sourceid: " + groupValues); 2034 } 2035 return groupId; 2036 } 2037 } finally { 2038 c.close(); 2039 } 2040 } 2041 2042 /** 2043 * Delete data row by row so that fixing of primaries etc work correctly. 2044 */ 2045 private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) { 2046 int count = 0; 2047 2048 // Note that the query will return data according to the access restrictions, 2049 // so we don't need to worry about deleting data we don't have permission to read. 2050 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null); 2051 try { 2052 while(c.moveToNext()) { 2053 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 2054 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 2055 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2056 count += rowHandler.delete(mDb, c); 2057 if (!callerIsSyncAdapter) { 2058 setRawContactDirty(rawContactId); 2059 if (rowHandler.isAggregationRequired()) { 2060 triggerAggregation(rawContactId); 2061 } 2062 } 2063 } 2064 } finally { 2065 c.close(); 2066 } 2067 2068 return count; 2069 } 2070 2071 /** 2072 * Delete a data row provided that it is one of the allowed mime types. 2073 */ 2074 public int deleteData(long dataId, String[] allowedMimeTypes) { 2075 2076 // Note that the query will return data according to the access restrictions, 2077 // so we don't need to worry about deleting data we don't have permission to read. 2078 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=" + dataId, null, 2079 null); 2080 2081 try { 2082 if (!c.moveToFirst()) { 2083 return 0; 2084 } 2085 2086 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 2087 boolean valid = false; 2088 for (int i = 0; i < allowedMimeTypes.length; i++) { 2089 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 2090 valid = true; 2091 break; 2092 } 2093 } 2094 2095 if (!valid) { 2096 throw new IllegalArgumentException("Data type mismatch: expected " 2097 + Lists.newArrayList(allowedMimeTypes)); 2098 } 2099 2100 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2101 int count = rowHandler.delete(mDb, c); 2102 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 2103 if (rowHandler.isAggregationRequired()) { 2104 triggerAggregation(rawContactId); 2105 } 2106 return count; 2107 } finally { 2108 c.close(); 2109 } 2110 } 2111 2112 /** 2113 * Inserts an item in the groups table 2114 */ 2115 private long insertGroup(ContentValues values, Account account, boolean callerIsSyncAdapter) { 2116 ContentValues overriddenValues = new ContentValues(values); 2117 if (!resolveAccount(overriddenValues, account)) { 2118 return -1; 2119 } 2120 2121 // Replace package with internal mapping 2122 final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE); 2123 if (packageName != null) { 2124 overriddenValues.put(GroupsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 2125 } 2126 overriddenValues.remove(Groups.RES_PACKAGE); 2127 2128 if (!callerIsSyncAdapter) { 2129 overriddenValues.put(Groups.DIRTY, 1); 2130 } 2131 2132 long result = mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues); 2133 2134 if (overriddenValues.containsKey(Groups.GROUP_VISIBLE)) { 2135 mOpenHelper.updateAllVisible(); 2136 } 2137 2138 return result; 2139 } 2140 2141 private long insertSettings(ContentValues values) { 2142 final long id = mDb.insert(Tables.SETTINGS, null, values); 2143 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2144 mOpenHelper.updateAllVisible(); 2145 } 2146 return id; 2147 } 2148 2149 /** 2150 * Inserts a presence update. 2151 */ 2152 public long insertPresence(ContentValues values) { 2153 final String handle = values.getAsString(Presence.IM_HANDLE); 2154 if (TextUtils.isEmpty(handle) || !values.containsKey(Presence.PROTOCOL)) { 2155 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 2156 } 2157 2158 final long protocol = values.getAsLong(Presence.PROTOCOL); 2159 String customProtocol = null; 2160 2161 if (protocol == Im.PROTOCOL_CUSTOM) { 2162 customProtocol = values.getAsString(Presence.CUSTOM_PROTOCOL); 2163 if (TextUtils.isEmpty(customProtocol)) { 2164 throw new IllegalArgumentException( 2165 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 2166 } 2167 } 2168 2169 // TODO: generalize to allow other providers to match against email 2170 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 2171 2172 StringBuilder selection = new StringBuilder(); 2173 String[] selectionArgs; 2174 if (matchEmail) { 2175 selection.append( 2176 "((" + MimetypesColumns.MIMETYPE + "='" + Im.CONTENT_ITEM_TYPE + "'" 2177 + " AND " + Im.PROTOCOL + "=?" 2178 + " AND " + Im.DATA + "=?"); 2179 if (customProtocol != null) { 2180 selection.append(" AND " + Im.CUSTOM_PROTOCOL + "="); 2181 DatabaseUtils.appendEscapedSQLString(selection, customProtocol); 2182 } 2183 selection.append(") OR (" 2184 + MimetypesColumns.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "'" 2185 + " AND " + Email.DATA + "=?" 2186 + "))"); 2187 selectionArgs = new String[] { String.valueOf(protocol), handle, handle }; 2188 } else { 2189 selection.append( 2190 MimetypesColumns.MIMETYPE + "='" + Im.CONTENT_ITEM_TYPE + "'" 2191 + " AND " + Im.PROTOCOL + "=?" 2192 + " AND " + Im.DATA + "=?"); 2193 if (customProtocol != null) { 2194 selection.append(" AND " + Im.CUSTOM_PROTOCOL + "="); 2195 DatabaseUtils.appendEscapedSQLString(selection, customProtocol); 2196 } 2197 2198 selectionArgs = new String[] { String.valueOf(protocol), handle }; 2199 } 2200 2201 if (values.containsKey(Presence.DATA_ID)) { 2202 selection.append(" AND " + DataColumns.CONCRETE_ID + "=") 2203 .append(values.getAsLong(Presence.DATA_ID)); 2204 } 2205 2206 selection.append(" AND ").append(getContactsRestrictions()); 2207 2208 long dataId = -1; 2209 long rawContactId = -1; 2210 long contactId = -1; 2211 2212 Cursor cursor = null; 2213 try { 2214 cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 2215 selection.toString(), selectionArgs, null, null, null); 2216 if (cursor.moveToFirst()) { 2217 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 2218 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 2219 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 2220 } else { 2221 // No contact found, return a null URI 2222 return -1; 2223 } 2224 } finally { 2225 if (cursor != null) { 2226 cursor.close(); 2227 } 2228 } 2229 2230 values.put(Presence.DATA_ID, dataId); 2231 values.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 2232 2233 // Insert the presence update 2234 long presenceId = mDb.replace(Tables.PRESENCE, null, values); 2235 2236 if (contactId != -1) { 2237 if (values.containsKey(Presence.PRESENCE_STATUS)) { 2238 mAggregatedPresenceReplace.bindLong(1, contactId); 2239 mAggregatedPresenceReplace.bindLong(2, contactId); 2240 mAggregatedPresenceReplace.execute(); 2241 } 2242 String status = values.getAsString(Presence.PRESENCE_CUSTOM_STATUS); 2243 if (status != null) { 2244 mAggregatedPresenceStatusUpdate.bindString(1, status); 2245 mAggregatedPresenceStatusUpdate.bindLong(2, contactId); 2246 mAggregatedPresenceStatusUpdate.execute(); 2247 } 2248 } 2249 return presenceId; 2250 } 2251 2252 @Override 2253 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 2254 if (VERBOSE_LOGGING) { 2255 Log.v(TAG, "deleteInTransaction: " + uri); 2256 } 2257 flushTransactionalChanges(); 2258 final boolean callerIsSyncAdapter = 2259 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2260 final int match = sUriMatcher.match(uri); 2261 switch (match) { 2262 case SYNCSTATE: 2263 return mOpenHelper.getSyncState().delete(mDb, selection, selectionArgs); 2264 2265 case SYNCSTATE_ID: 2266 String selectionWithId = 2267 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 2268 + (selection == null ? "" : " AND (" + selection + ")"); 2269 return mOpenHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs); 2270 2271 case CONTACTS: { 2272 // TODO 2273 return 0; 2274 } 2275 2276 case CONTACTS_ID: { 2277 long contactId = ContentUris.parseId(uri); 2278 return deleteContact(contactId); 2279 } 2280 2281 case CONTACTS_LOOKUP: 2282 case CONTACTS_LOOKUP_ID: { 2283 final List<String> pathSegments = uri.getPathSegments(); 2284 final int segmentCount = pathSegments.size(); 2285 if (segmentCount < 3) { 2286 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 2287 } 2288 final String lookupKey = pathSegments.get(2); 2289 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 2290 return deleteContact(contactId); 2291 } 2292 2293 case RAW_CONTACTS: { 2294 int numDeletes = 0; 2295 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2296 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2297 try { 2298 while (c.moveToNext()) { 2299 final long rawContactId = c.getLong(0); 2300 numDeletes += deleteRawContact(rawContactId, callerIsSyncAdapter); 2301 } 2302 } finally { 2303 c.close(); 2304 } 2305 return numDeletes; 2306 } 2307 2308 case RAW_CONTACTS_ID: { 2309 final long rawContactId = ContentUris.parseId(uri); 2310 return deleteRawContact(rawContactId, callerIsSyncAdapter); 2311 } 2312 2313 case DATA: { 2314 mSyncToNetwork |= !callerIsSyncAdapter; 2315 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 2316 callerIsSyncAdapter); 2317 } 2318 2319 case DATA_ID: { 2320 long dataId = ContentUris.parseId(uri); 2321 mSyncToNetwork |= !callerIsSyncAdapter; 2322 return deleteData(Data._ID + "=" + dataId, null, callerIsSyncAdapter); 2323 } 2324 2325 case GROUPS_ID: { 2326 mSyncToNetwork |= !callerIsSyncAdapter; 2327 return deleteGroup(ContentUris.parseId(uri), callerIsSyncAdapter); 2328 } 2329 2330 case GROUPS: { 2331 int numDeletes = 0; 2332 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 2333 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2334 try { 2335 while (c.moveToNext()) { 2336 numDeletes += deleteGroup(c.getLong(0), callerIsSyncAdapter); 2337 } 2338 } finally { 2339 c.close(); 2340 } 2341 if (numDeletes > 0) { 2342 mSyncToNetwork |= !callerIsSyncAdapter; 2343 } 2344 return numDeletes; 2345 } 2346 2347 case SETTINGS: { 2348 return deleteSettings(selection, selectionArgs); 2349 } 2350 2351 case PRESENCE: { 2352 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 2353 } 2354 2355 default: { 2356 mSyncToNetwork = true; 2357 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 2358 } 2359 } 2360 } 2361 2362 private boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) { 2363 final String flag = uri.getQueryParameter(name); 2364 return flag == null 2365 ? defaultValue 2366 : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase())); 2367 } 2368 2369 private int deleteGroup(long groupId, boolean callerIsSyncAdapter) { 2370 final long groupMembershipMimetypeId = mOpenHelper 2371 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 2372 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 2373 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 2374 + groupId, null); 2375 2376 try { 2377 if (callerIsSyncAdapter) { 2378 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 2379 } else { 2380 mValues.clear(); 2381 mValues.put(Groups.DELETED, 1); 2382 mValues.put(Groups.DIRTY, 1); 2383 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 2384 } 2385 } finally { 2386 mOpenHelper.updateAllVisible(); 2387 } 2388 } 2389 2390 private int deleteSettings(String selection, String[] selectionArgs) { 2391 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 2392 if (count > 0) { 2393 mOpenHelper.updateAllVisible(); 2394 } 2395 return count; 2396 } 2397 2398 private int deleteContact(long contactId) { 2399 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2400 RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null); 2401 try { 2402 while (c.moveToNext()) { 2403 long rawContactId = c.getLong(0); 2404 markRawContactAsDeleted(rawContactId); 2405 } 2406 } finally { 2407 c.close(); 2408 } 2409 2410 return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); 2411 } 2412 2413 public int deleteRawContact(long rawContactId, boolean callerIsSyncAdapter) { 2414 if (callerIsSyncAdapter) { 2415 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 2416 return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 2417 } else { 2418 mOpenHelper.removeContactIfSingleton(rawContactId); 2419 return markRawContactAsDeleted(rawContactId); 2420 } 2421 } 2422 2423 private int markRawContactAsDeleted(long rawContactId) { 2424 mSyncToNetwork = true; 2425 2426 mValues.clear(); 2427 mValues.put(RawContacts.DELETED, 1); 2428 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2429 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 2430 mValues.putNull(RawContacts.CONTACT_ID); 2431 mValues.put(RawContacts.DIRTY, 1); 2432 return updateRawContact(rawContactId, mValues); 2433 } 2434 2435 private static Account readAccountFromQueryParams(Uri uri) { 2436 final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 2437 final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 2438 if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) { 2439 return null; 2440 } 2441 return new Account(name, type); 2442 } 2443 2444 @Override 2445 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 2446 String[] selectionArgs) { 2447 if (VERBOSE_LOGGING) { 2448 Log.v(TAG, "updateInTransaction: " + uri); 2449 } 2450 2451 int count = 0; 2452 2453 final int match = sUriMatcher.match(uri); 2454 if (match == SYNCSTATE_ID && selection == null) { 2455 long rowId = ContentUris.parseId(uri); 2456 Object data = values.get(ContactsContract.SyncStateColumns.DATA); 2457 mUpdatedSyncStates.put(rowId, data); 2458 return 1; 2459 } 2460 flushTransactionalChanges(); 2461 final boolean callerIsSyncAdapter = 2462 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false); 2463 switch(match) { 2464 case SYNCSTATE: 2465 return mOpenHelper.getSyncState().update(mDb, values, 2466 appendAccountToSelection(uri, selection), selectionArgs); 2467 2468 case SYNCSTATE_ID: { 2469 selection = appendAccountToSelection(uri, selection); 2470 String selectionWithId = 2471 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 2472 + (selection == null ? "" : " AND (" + selection + ")"); 2473 return mOpenHelper.getSyncState().update(mDb, values, 2474 selectionWithId, selectionArgs); 2475 } 2476 2477 case CONTACTS: { 2478 count = updateContactOptions(values, selection, selectionArgs); 2479 break; 2480 } 2481 2482 case CONTACTS_ID: { 2483 count = updateContactOptions(ContentUris.parseId(uri), values); 2484 break; 2485 } 2486 2487 case CONTACTS_LOOKUP: 2488 case CONTACTS_LOOKUP_ID: { 2489 final List<String> pathSegments = uri.getPathSegments(); 2490 final int segmentCount = pathSegments.size(); 2491 if (segmentCount < 3) { 2492 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 2493 } 2494 final String lookupKey = pathSegments.get(2); 2495 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 2496 count = updateContactOptions(contactId, values); 2497 break; 2498 } 2499 2500 case DATA: { 2501 count = updateData(uri, values, appendAccountToSelection(uri, selection), 2502 selectionArgs, callerIsSyncAdapter); 2503 if (count > 0) { 2504 mSyncToNetwork |= !callerIsSyncAdapter; 2505 } 2506 break; 2507 } 2508 2509 case DATA_ID: { 2510 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter); 2511 if (count > 0) { 2512 mSyncToNetwork |= !callerIsSyncAdapter; 2513 } 2514 break; 2515 } 2516 2517 case RAW_CONTACTS: { 2518 count = updateRawContacts(values, selection, selectionArgs); 2519 break; 2520 } 2521 2522 case RAW_CONTACTS_ID: { 2523 long rawContactId = ContentUris.parseId(uri); 2524 if (selection != null) { 2525 count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId 2526 + " AND(" + selection + ")", selectionArgs); 2527 } else { 2528 count = updateRawContact(rawContactId, values); 2529 } 2530 break; 2531 } 2532 2533 case GROUPS: { 2534 count = updateGroups(values, appendAccountToSelection(uri, selection), 2535 selectionArgs, callerIsSyncAdapter); 2536 if (count > 0) { 2537 mSyncToNetwork |= !callerIsSyncAdapter; 2538 } 2539 break; 2540 } 2541 2542 case GROUPS_ID: { 2543 long groupId = ContentUris.parseId(uri); 2544 String selectionWithId = (Groups._ID + "=" + groupId + " ") 2545 + (selection == null ? "" : " AND " + selection); 2546 count = updateGroups(values, selectionWithId, selectionArgs, callerIsSyncAdapter); 2547 if (count > 0) { 2548 mSyncToNetwork |= !callerIsSyncAdapter; 2549 } 2550 break; 2551 } 2552 2553 case AGGREGATION_EXCEPTIONS: { 2554 count = updateAggregationException(mDb, values); 2555 break; 2556 } 2557 2558 case SETTINGS: { 2559 count = updateSettings(values, selection, selectionArgs); 2560 break; 2561 } 2562 2563 default: { 2564 mSyncToNetwork = true; 2565 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 2566 } 2567 } 2568 2569 return count; 2570 } 2571 2572 private int updateGroups(ContentValues values, String selectionWithId, 2573 String[] selectionArgs, boolean callerIsSyncAdapter) { 2574 2575 ContentValues updatedValues; 2576 if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) { 2577 updatedValues = mValues; 2578 updatedValues.clear(); 2579 updatedValues.putAll(values); 2580 updatedValues.put(Groups.DIRTY, 1); 2581 } else { 2582 updatedValues = values; 2583 } 2584 2585 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 2586 2587 // If changing visibility, then update contacts 2588 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 2589 mOpenHelper.updateAllVisible(); 2590 } 2591 return count; 2592 } 2593 2594 private int updateSettings(ContentValues values, String selection, String[] selectionArgs) { 2595 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 2596 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2597 mOpenHelper.updateAllVisible(); 2598 } 2599 return count; 2600 } 2601 2602 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) { 2603 if (values.containsKey(RawContacts.CONTACT_ID)) { 2604 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 2605 "in content values. Contact IDs are assigned automatically"); 2606 } 2607 2608 int count = 0; 2609 Cursor cursor = mDb.query(mOpenHelper.getRawContactView(), 2610 new String[] { RawContacts._ID }, selection, 2611 selectionArgs, null, null, null); 2612 try { 2613 while (cursor.moveToNext()) { 2614 long rawContactId = cursor.getLong(0); 2615 updateRawContact(rawContactId, values); 2616 count++; 2617 } 2618 } finally { 2619 cursor.close(); 2620 } 2621 2622 return count; 2623 } 2624 2625 private int updateRawContact(long rawContactId, ContentValues values) { 2626 int count = mDb.update(Tables.RAW_CONTACTS, values, RawContacts._ID + " = " + rawContactId, 2627 null); 2628 if (count != 0) { 2629 if (values.containsKey(RawContacts.STARRED)) { 2630 mContactAggregator.updateStarred(rawContactId); 2631 } 2632 if (values.containsKey(RawContacts.SOURCE_ID)) { 2633 mContactAggregator.updateLookupKey(mDb, rawContactId); 2634 } 2635 } 2636 return count; 2637 } 2638 2639 private int updateData(Uri uri, ContentValues values, String selection, 2640 String[] selectionArgs, boolean callerIsSyncAdapter) { 2641 mValues.clear(); 2642 mValues.putAll(values); 2643 mValues.remove(Data._ID); 2644 mValues.remove(Data.RAW_CONTACT_ID); 2645 mValues.remove(Data.MIMETYPE); 2646 2647 String packageName = values.getAsString(Data.RES_PACKAGE); 2648 if (packageName != null) { 2649 mValues.remove(Data.RES_PACKAGE); 2650 mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 2651 } 2652 2653 boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY); 2654 boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY); 2655 2656 // Remove primary or super primary values being set to 0. This is disallowed by the 2657 // content provider. 2658 if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { 2659 containsIsSuperPrimary = false; 2660 mValues.remove(Data.IS_SUPER_PRIMARY); 2661 } 2662 if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) { 2663 containsIsPrimary = false; 2664 mValues.remove(Data.IS_PRIMARY); 2665 } 2666 2667 int count = 0; 2668 2669 // Note that the query will return data according to the access restrictions, 2670 // so we don't need to worry about updating data we don't have permission to read. 2671 Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null); 2672 try { 2673 while(c.moveToNext()) { 2674 count += updateData(mValues, c, callerIsSyncAdapter); 2675 } 2676 } finally { 2677 c.close(); 2678 } 2679 2680 return count; 2681 } 2682 2683 private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) { 2684 if (values.size() == 0) { 2685 return 0; 2686 } 2687 2688 final String mimeType = c.getString(DataUpdateQuery.MIMETYPE); 2689 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2690 rowHandler.update(mDb, values, c, callerIsSyncAdapter); 2691 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 2692 if (rowHandler.isAggregationRequired()) { 2693 triggerAggregation(rawContactId); 2694 } 2695 2696 return 1; 2697 } 2698 2699 private int updateContactOptions(ContentValues values, String selection, 2700 String[] selectionArgs) { 2701 int count = 0; 2702 Cursor cursor = mDb.query(mOpenHelper.getContactView(), 2703 new String[] { Contacts._ID }, selection, 2704 selectionArgs, null, null, null); 2705 try { 2706 while (cursor.moveToNext()) { 2707 long contactId = cursor.getLong(0); 2708 updateContactOptions(contactId, values); 2709 count++; 2710 } 2711 } finally { 2712 cursor.close(); 2713 } 2714 2715 return count; 2716 } 2717 2718 private int updateContactOptions(long contactId, ContentValues values) { 2719 2720 mValues.clear(); 2721 OpenHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 2722 values, Contacts.CUSTOM_RINGTONE); 2723 OpenHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 2724 values, Contacts.SEND_TO_VOICEMAIL); 2725 OpenHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 2726 values, Contacts.LAST_TIME_CONTACTED); 2727 OpenHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 2728 values, Contacts.TIMES_CONTACTED); 2729 OpenHelper.copyLongValue(mValues, RawContacts.STARRED, 2730 values, Contacts.STARRED); 2731 2732 // Nothing to update - just return 2733 if (mValues.size() == 0) { 2734 return 0; 2735 } 2736 2737 if (mValues.containsKey(RawContacts.STARRED)) { 2738 // Mark dirty when changing starred to trigger sync 2739 mValues.put(RawContacts.DIRTY, 1); 2740 } 2741 2742 mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=" + contactId, null); 2743 2744 // Copy changeable values to prevent automatically managed fields from 2745 // being explicitly updated by clients. 2746 mValues.clear(); 2747 OpenHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE, 2748 values, Contacts.CUSTOM_RINGTONE); 2749 OpenHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL, 2750 values, Contacts.SEND_TO_VOICEMAIL); 2751 OpenHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED, 2752 values, Contacts.LAST_TIME_CONTACTED); 2753 OpenHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED, 2754 values, Contacts.TIMES_CONTACTED); 2755 OpenHelper.copyLongValue(mValues, RawContacts.STARRED, 2756 values, Contacts.STARRED); 2757 2758 return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=" + contactId, null); 2759 } 2760 2761 public void updateContactTime(long contactId, long lastTimeContacted) { 2762 mLastTimeContactedUpdate.bindLong(1, lastTimeContacted); 2763 mLastTimeContactedUpdate.bindLong(2, contactId); 2764 mLastTimeContactedUpdate.execute(); 2765 } 2766 2767 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 2768 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 2769 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 2770 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 2771 2772 long rawContactId1, rawContactId2; 2773 if (rcId1 < rcId2) { 2774 rawContactId1 = rcId1; 2775 rawContactId2 = rcId2; 2776 } else { 2777 rawContactId2 = rcId1; 2778 rawContactId1 = rcId2; 2779 } 2780 2781 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 2782 db.delete(Tables.AGGREGATION_EXCEPTIONS, 2783 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND " 2784 + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null); 2785 } else { 2786 ContentValues exceptionValues = new ContentValues(3); 2787 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 2788 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 2789 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 2790 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 2791 exceptionValues); 2792 } 2793 2794 mContactAggregator.markForAggregation(rawContactId1); 2795 mContactAggregator.markForAggregation(rawContactId2); 2796 2797 long contactId1 = mOpenHelper.getContactId(rawContactId1); 2798 mContactAggregator.aggregateContact(db, rawContactId1, contactId1); 2799 2800 long contactId2 = mOpenHelper.getContactId(rawContactId2); 2801 mContactAggregator.aggregateContact(db, rawContactId2, contactId2); 2802 2803 // The return value is fake - we just confirm that we made a change, not count actual 2804 // rows changed. 2805 return 1; 2806 } 2807 2808 public void onAccountsUpdated(Account[] accounts) { 2809 mDb = mOpenHelper.getWritableDatabase(); 2810 if (mDb == null) return; 2811 2812 Set<Account> validAccounts = Sets.newHashSet(); 2813 for (Account account : accounts) { 2814 validAccounts.add(new Account(account.name, account.type)); 2815 } 2816 ArrayList<Account> accountsToDelete = new ArrayList<Account>(); 2817 2818 mDb.beginTransaction(); 2819 try { 2820 // Find all the accounts the contacts DB knows about, mark the ones that aren't in the 2821 // valid set for deletion. 2822 Cursor c = mDb.rawQuery("SELECT DISTINCT account_name, account_type from " 2823 + Tables.RAW_CONTACTS, null); 2824 while (c.moveToNext()) { 2825 if (c.getString(0) != null && c.getString(1) != null) { 2826 Account currAccount = new Account(c.getString(0), c.getString(1)); 2827 if (!validAccounts.contains(currAccount)) { 2828 accountsToDelete.add(currAccount); 2829 } 2830 } 2831 } 2832 c.close(); 2833 2834 for (Account account : accountsToDelete) { 2835 String[] params = new String[]{account.name, account.type}; 2836 mDb.execSQL("DELETE FROM " + Tables.GROUPS 2837 + " WHERE account_name = ? AND account_type = ?", params); 2838 mDb.execSQL("DELETE FROM " + Tables.PRESENCE 2839 + " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (SELECT " 2840 + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS 2841 + " WHERE account_name = ? AND account_type = ?)", params); 2842 mDb.execSQL("DELETE FROM " + Tables.RAW_CONTACTS 2843 + " WHERE account_name = ? AND account_type = ?", params); 2844 } 2845 mOpenHelper.getSyncState().onAccountsChanged(mDb, accounts); 2846 mDb.setTransactionSuccessful(); 2847 } finally { 2848 mDb.endTransaction(); 2849 } 2850 } 2851 2852 /** 2853 * Test all against {@link TextUtils#isEmpty(CharSequence)}. 2854 */ 2855 private static boolean areAllEmpty(ContentValues values, String[] keys) { 2856 for (String key : keys) { 2857 if (!TextUtils.isEmpty(values.getAsString(key))) { 2858 return false; 2859 } 2860 } 2861 return true; 2862 } 2863 2864 @Override 2865 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 2866 String sortOrder) { 2867 if (VERBOSE_LOGGING) { 2868 Log.v(TAG, "query: " + uri); 2869 } 2870 2871 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 2872 2873 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2874 String groupBy = null; 2875 String limit = getLimit(uri); 2876 2877 // TODO: Consider writing a test case for RestrictionExceptions when you 2878 // write a new query() block to make sure it protects restricted data. 2879 final int match = sUriMatcher.match(uri); 2880 switch (match) { 2881 case SYNCSTATE: 2882 return mOpenHelper.getSyncState().query(db, projection, selection, selectionArgs, 2883 sortOrder); 2884 2885 case CONTACTS: { 2886 setTablesAndProjectionMapForContacts(qb, projection); 2887 break; 2888 } 2889 2890 case CONTACTS_ID: { 2891 long contactId = ContentUris.parseId(uri); 2892 setTablesAndProjectionMapForContacts(qb, projection); 2893 qb.appendWhere(Contacts._ID + "=" + contactId); 2894 break; 2895 } 2896 2897 case CONTACTS_LOOKUP: 2898 case CONTACTS_LOOKUP_ID: { 2899 List<String> pathSegments = uri.getPathSegments(); 2900 int segmentCount = pathSegments.size(); 2901 if (segmentCount < 3) { 2902 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 2903 } 2904 String lookupKey = pathSegments.get(2); 2905 if (segmentCount == 4) { 2906 long contactId = Long.parseLong(pathSegments.get(3)); 2907 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 2908 setTablesAndProjectionMapForContacts(lookupQb, projection); 2909 lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " + 2910 Contacts.LOOKUP_KEY + "="); 2911 lookupQb.appendWhereEscapeString(lookupKey); 2912 Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder, 2913 groupBy, limit); 2914 if (c.getCount() != 0) { 2915 return c; 2916 } 2917 2918 c.close(); 2919 } 2920 2921 setTablesAndProjectionMapForContacts(qb, projection); 2922 qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey)); 2923 break; 2924 } 2925 2926 case CONTACTS_FILTER: { 2927 setTablesAndProjectionMapForContacts(qb, projection); 2928 if (uri.getPathSegments().size() > 2) { 2929 String filterParam = uri.getLastPathSegment(); 2930 StringBuilder sb = new StringBuilder(); 2931 sb.append(Contacts._ID + " IN "); 2932 appendContactFilterAsNestedQuery(sb, filterParam); 2933 qb.appendWhere(sb.toString()); 2934 } 2935 break; 2936 } 2937 2938 case CONTACTS_STREQUENT_FILTER: 2939 case CONTACTS_STREQUENT: { 2940 String filterSql = null; 2941 if (match == CONTACTS_STREQUENT_FILTER 2942 && uri.getPathSegments().size() > 3) { 2943 String filterParam = uri.getLastPathSegment(); 2944 StringBuilder sb = new StringBuilder(); 2945 sb.append(Contacts._ID + " IN "); 2946 appendContactFilterAsNestedQuery(sb, filterParam); 2947 filterSql = sb.toString(); 2948 } 2949 2950 setTablesAndProjectionMapForContacts(qb, projection); 2951 2952 // Build the first query for starred 2953 if (filterSql != null) { 2954 qb.appendWhere(filterSql); 2955 } 2956 final String starredQuery = qb.buildQuery(projection, Contacts.STARRED + "=1", 2957 null, Contacts._ID, null, null, null); 2958 2959 // Build the second query for frequent 2960 qb = new SQLiteQueryBuilder(); 2961 setTablesAndProjectionMapForContacts(qb, projection); 2962 if (filterSql != null) { 2963 qb.appendWhere(filterSql); 2964 } 2965 final String frequentQuery = qb.buildQuery(projection, 2966 Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED 2967 + " = 0 OR " + Contacts.STARRED + " IS NULL)", 2968 null, Contacts._ID, null, null, null); 2969 2970 // Put them together 2971 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 2972 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 2973 Cursor c = db.rawQuery(query, null); 2974 if (c != null) { 2975 c.setNotificationUri(getContext().getContentResolver(), 2976 ContactsContract.AUTHORITY_URI); 2977 } 2978 return c; 2979 } 2980 2981 case CONTACTS_GROUP: { 2982 setTablesAndProjectionMapForContacts(qb, projection); 2983 if (uri.getPathSegments().size() > 2) { 2984 qb.appendWhere(sContactsInGroupSelect); 2985 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 2986 } 2987 break; 2988 } 2989 2990 case CONTACTS_DATA: { 2991 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 2992 2993 qb.setTables(mOpenHelper.getDataView()); 2994 qb.setProjectionMap(sDataProjectionMap); 2995 appendAccountFromParameter(qb, uri); 2996 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId); 2997 break; 2998 } 2999 3000 case CONTACTS_PHOTO: { 3001 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3002 3003 qb.setTables(mOpenHelper.getDataView()); 3004 qb.setProjectionMap(sDataProjectionMap); 3005 appendAccountFromParameter(qb, uri); 3006 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId); 3007 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 3008 break; 3009 } 3010 3011 case PHONES: { 3012 qb.setTables(mOpenHelper.getDataView()); 3013 qb.setProjectionMap(sDataProjectionMap); 3014 qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3015 break; 3016 } 3017 3018 case PHONES_FILTER: { 3019 qb.setTables(mOpenHelper.getDataView()); 3020 qb.setProjectionMap(sDistinctDataProjectionMap); 3021 qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 3022 if (uri.getPathSegments().size() > 2) { 3023 String filterParam = uri.getLastPathSegment(); 3024 StringBuilder sb = new StringBuilder(); 3025 sb.append("("); 3026 3027 boolean orNeeded = false; 3028 String normalizedName = NameNormalizer.normalize(filterParam); 3029 if (normalizedName.length() > 0) { 3030 sb.append(Data.RAW_CONTACT_ID + " IN "); 3031 appendRawContactsByNormalizedNameFilter(sb, normalizedName, null); 3032 orNeeded = true; 3033 } 3034 3035 if (isPhoneNumber(filterParam)) { 3036 if (orNeeded) { 3037 sb.append(" OR "); 3038 } 3039 String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam); 3040 String reversed = PhoneNumberUtils.getStrippedReversed(number); 3041 sb.append(Data._ID + 3042 " IN (SELECT " + PhoneLookupColumns.DATA_ID 3043 + " FROM " + Tables.PHONE_LOOKUP 3044 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%"); 3045 sb.append(reversed); 3046 sb.append("')"); 3047 } 3048 sb.append(")"); 3049 qb.appendWhere(" AND " + sb); 3050 } 3051 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 3052 break; 3053 } 3054 3055 case EMAILS: { 3056 qb.setTables(mOpenHelper.getDataView()); 3057 qb.setProjectionMap(sDataProjectionMap); 3058 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3059 break; 3060 } 3061 3062 case EMAILS_LOOKUP: { 3063 qb.setTables(mOpenHelper.getDataView()); 3064 qb.setProjectionMap(sDataProjectionMap); 3065 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3066 if (uri.getPathSegments().size() > 2) { 3067 qb.appendWhere(" AND " + Email.DATA + "="); 3068 qb.appendWhereEscapeString(uri.getLastPathSegment()); 3069 } 3070 break; 3071 } 3072 3073 case EMAILS_FILTER: { 3074 qb.setTables(mOpenHelper.getDataView()); 3075 qb.setProjectionMap(sDistinctDataProjectionMap); 3076 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 3077 if (uri.getPathSegments().size() > 2) { 3078 String filterParam = uri.getLastPathSegment(); 3079 StringBuilder sb = new StringBuilder(); 3080 sb.append("("); 3081 3082 String normalizedName = NameNormalizer.normalize(filterParam); 3083 if (normalizedName.length() > 0) { 3084 sb.append(Data.RAW_CONTACT_ID + " IN "); 3085 appendRawContactsByNormalizedNameFilter(sb, normalizedName, null); 3086 sb.append(" OR "); 3087 } 3088 3089 sb.append(Email.DATA + " LIKE "); 3090 sb.append(DatabaseUtils.sqlEscapeString(filterParam + '%')); 3091 sb.append(")"); 3092 qb.appendWhere(" AND " + sb); 3093 } 3094 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 3095 break; 3096 } 3097 3098 case POSTALS: { 3099 qb.setTables(mOpenHelper.getDataView()); 3100 qb.setProjectionMap(sDataProjectionMap); 3101 qb.appendWhere(Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 3102 break; 3103 } 3104 3105 case RAW_CONTACTS: { 3106 qb.setTables(mOpenHelper.getRawContactView()); 3107 qb.setProjectionMap(sRawContactsProjectionMap); 3108 break; 3109 } 3110 3111 case RAW_CONTACTS_ID: { 3112 long rawContactId = ContentUris.parseId(uri); 3113 qb.setTables(mOpenHelper.getRawContactView()); 3114 qb.setProjectionMap(sRawContactsProjectionMap); 3115 qb.appendWhere(RawContacts._ID + "=" + rawContactId); 3116 break; 3117 } 3118 3119 case RAW_CONTACTS_DATA: { 3120 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 3121 qb.setTables(mOpenHelper.getDataView()); 3122 qb.setProjectionMap(sDataProjectionMap); 3123 qb.appendWhere(Data.RAW_CONTACT_ID + "=" + rawContactId); 3124 break; 3125 } 3126 3127 case DATA: { 3128 qb.setTables(mOpenHelper.getDataView()); 3129 qb.setProjectionMap(sDataProjectionMap); 3130 appendAccountFromParameter(qb, uri); 3131 break; 3132 } 3133 3134 case DATA_ID: { 3135 qb.setTables(mOpenHelper.getDataView()); 3136 qb.setProjectionMap(sDataProjectionMap); 3137 qb.appendWhere(Data._ID + "=" + ContentUris.parseId(uri)); 3138 break; 3139 } 3140 3141 case DATA_WITH_PRESENCE: { 3142 qb.setTables(mOpenHelper.getDataView() + " data" 3143 + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE 3144 + " ON (" + AggregatedPresenceColumns.CONTACT_ID + "=" 3145 + RawContacts.CONTACT_ID + ")"); 3146 qb.setProjectionMap(sDataWithPresenceProjectionMap); 3147 appendAccountFromParameter(qb, uri); 3148 break; 3149 } 3150 3151 case PHONE_LOOKUP: { 3152 3153 if (TextUtils.isEmpty(sortOrder)) { 3154 // Default the sort order to something reasonable so we get consistent 3155 // results when callers don't request an ordering 3156 sortOrder = RawContactsColumns.CONCRETE_ID; 3157 } 3158 3159 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 3160 mOpenHelper.buildPhoneLookupAndContactQuery(qb, number); 3161 qb.setProjectionMap(sPhoneLookupProjectionMap); 3162 3163 // Phone lookup cannot be combined with a selection 3164 selection = null; 3165 selectionArgs = null; 3166 break; 3167 } 3168 3169 case GROUPS: { 3170 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3171 qb.setProjectionMap(sGroupsProjectionMap); 3172 break; 3173 } 3174 3175 case GROUPS_ID: { 3176 long groupId = ContentUris.parseId(uri); 3177 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3178 qb.setProjectionMap(sGroupsProjectionMap); 3179 qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId); 3180 break; 3181 } 3182 3183 case GROUPS_SUMMARY: { 3184 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3185 qb.setProjectionMap(sGroupsSummaryProjectionMap); 3186 groupBy = GroupsColumns.CONCRETE_ID; 3187 break; 3188 } 3189 3190 case AGGREGATION_EXCEPTIONS: { 3191 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 3192 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 3193 break; 3194 } 3195 3196 case AGGREGATION_SUGGESTIONS: { 3197 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3198 final int maxSuggestions; 3199 if (limit != null) { 3200 maxSuggestions = Integer.parseInt(limit); 3201 } else { 3202 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 3203 } 3204 3205 setTablesAndProjectionMapForContacts(qb, projection); 3206 3207 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId, 3208 maxSuggestions); 3209 } 3210 3211 case SETTINGS: { 3212 qb.setTables(Tables.SETTINGS); 3213 qb.setProjectionMap(sSettingsProjectionMap); 3214 3215 // When requesting specific columns, this query requires 3216 // late-binding of the GroupMembership MIME-type. 3217 final String groupMembershipMimetypeId = Long.toString(mOpenHelper 3218 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 3219 if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 3220 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3221 } 3222 if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 3223 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3224 } 3225 3226 break; 3227 } 3228 3229 case PRESENCE: { 3230 qb.setTables(Tables.PRESENCE); 3231 qb.setProjectionMap(sPresenceProjectionMap); 3232 break; 3233 } 3234 3235 case PRESENCE_ID: { 3236 qb.setTables(Tables.PRESENCE); 3237 qb.setProjectionMap(sPresenceProjectionMap); 3238 qb.appendWhere(Presence._ID + "=" + ContentUris.parseId(uri)); 3239 break; 3240 } 3241 3242 case SEARCH_SUGGESTIONS: { 3243 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); 3244 } 3245 3246 case SEARCH_SHORTCUT: { 3247 long contactId = ContentUris.parseId(uri); 3248 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection); 3249 } 3250 3251 case LIVE_FOLDERS_CONTACTS: 3252 qb.setTables(mOpenHelper.getContactView()); 3253 qb.setProjectionMap(sLiveFoldersProjectionMap); 3254 break; 3255 3256 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 3257 qb.setTables(mOpenHelper.getContactView()); 3258 qb.setProjectionMap(sLiveFoldersProjectionMap); 3259 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 3260 break; 3261 3262 case LIVE_FOLDERS_CONTACTS_FAVORITES: 3263 qb.setTables(mOpenHelper.getContactView()); 3264 qb.setProjectionMap(sLiveFoldersProjectionMap); 3265 qb.appendWhere(Contacts.STARRED + "=1"); 3266 break; 3267 3268 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 3269 qb.setTables(mOpenHelper.getContactView()); 3270 qb.setProjectionMap(sLiveFoldersProjectionMap); 3271 qb.appendWhere(sContactsInGroupSelect); 3272 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3273 break; 3274 3275 default: 3276 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 3277 sortOrder, limit); 3278 } 3279 3280 return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 3281 } 3282 3283 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 3284 String selection, String[] selectionArgs, String sortOrder, String groupBy, 3285 String limit) { 3286 if (projection != null && projection.length == 1 3287 && BaseColumns._COUNT.equals(projection[0])) { 3288 qb.setProjectionMap(sCountProjectionMap); 3289 } 3290 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 3291 sortOrder, limit); 3292 if (c != null) { 3293 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 3294 } 3295 return c; 3296 } 3297 3298 private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 3299 ContactLookupKey key = new ContactLookupKey(); 3300 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 3301 3302 long contactId = lookupContactIdBySourceIds(db, segments); 3303 if (contactId == -1) { 3304 contactId = lookupContactIdByDisplayNames(db, segments); 3305 } 3306 3307 return contactId; 3308 } 3309 3310 private interface LookupBySourceIdQuery { 3311 String TABLE = Tables.RAW_CONTACTS; 3312 3313 String COLUMNS[] = { 3314 RawContacts.CONTACT_ID, 3315 RawContacts.ACCOUNT_TYPE, 3316 RawContacts.ACCOUNT_NAME, 3317 RawContacts.SOURCE_ID 3318 }; 3319 3320 int CONTACT_ID = 0; 3321 int ACCOUNT_TYPE = 1; 3322 int ACCOUNT_NAME = 2; 3323 int SOURCE_ID = 3; 3324 } 3325 3326 private long lookupContactIdBySourceIds(SQLiteDatabase db, 3327 ArrayList<LookupKeySegment> segments) { 3328 int sourceIdCount = 0; 3329 for (int i = 0; i < segments.size(); i++) { 3330 LookupKeySegment segment = segments.get(i); 3331 if (segment.sourceIdLookup) { 3332 sourceIdCount++; 3333 } 3334 } 3335 3336 if (sourceIdCount == 0) { 3337 return -1; 3338 } 3339 3340 // First try sync ids 3341 StringBuilder sb = new StringBuilder(); 3342 sb.append(RawContacts.SOURCE_ID + " IN ("); 3343 for (int i = 0; i < segments.size(); i++) { 3344 LookupKeySegment segment = segments.get(i); 3345 if (segment.sourceIdLookup) { 3346 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 3347 sb.append(","); 3348 } 3349 } 3350 sb.setLength(sb.length() - 1); // Last comma 3351 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 3352 3353 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 3354 sb.toString(), null, null, null, null); 3355 try { 3356 while (c.moveToNext()) { 3357 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); 3358 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 3359 int accountHashCode = 3360 ContactLookupKey.getAccountHashCode(accountType, accountName); 3361 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 3362 for (int i = 0; i < segments.size(); i++) { 3363 LookupKeySegment segment = segments.get(i); 3364 if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode 3365 && segment.key.equals(sourceId)) { 3366 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 3367 break; 3368 } 3369 } 3370 } 3371 } finally { 3372 c.close(); 3373 } 3374 3375 return getMostReferencedContactId(segments); 3376 } 3377 3378 private interface LookupByDisplayNameQuery { 3379 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 3380 3381 String COLUMNS[] = { 3382 RawContacts.CONTACT_ID, 3383 RawContacts.ACCOUNT_TYPE, 3384 RawContacts.ACCOUNT_NAME, 3385 NameLookupColumns.NORMALIZED_NAME 3386 }; 3387 3388 int CONTACT_ID = 0; 3389 int ACCOUNT_TYPE = 1; 3390 int ACCOUNT_NAME = 2; 3391 int NORMALIZED_NAME = 3; 3392 } 3393 3394 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 3395 ArrayList<LookupKeySegment> segments) { 3396 int displayNameCount = 0; 3397 for (int i = 0; i < segments.size(); i++) { 3398 LookupKeySegment segment = segments.get(i); 3399 if (!segment.sourceIdLookup) { 3400 displayNameCount++; 3401 } 3402 } 3403 3404 if (displayNameCount == 0) { 3405 return -1; 3406 } 3407 3408 // First try sync ids 3409 StringBuilder sb = new StringBuilder(); 3410 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 3411 for (int i = 0; i < segments.size(); i++) { 3412 LookupKeySegment segment = segments.get(i); 3413 if (!segment.sourceIdLookup) { 3414 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 3415 sb.append(","); 3416 } 3417 } 3418 sb.setLength(sb.length() - 1); // Last comma 3419 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 3420 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 3421 3422 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 3423 sb.toString(), null, null, null, null); 3424 try { 3425 while (c.moveToNext()) { 3426 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); 3427 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 3428 int accountHashCode = 3429 ContactLookupKey.getAccountHashCode(accountType, accountName); 3430 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 3431 for (int i = 0; i < segments.size(); i++) { 3432 LookupKeySegment segment = segments.get(i); 3433 if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode 3434 && segment.key.equals(name)) { 3435 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 3436 break; 3437 } 3438 } 3439 } 3440 } finally { 3441 c.close(); 3442 } 3443 3444 return getMostReferencedContactId(segments); 3445 } 3446 3447 /** 3448 * Returns the contact ID that is mentioned the highest number of times. 3449 */ 3450 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 3451 Collections.sort(segments); 3452 3453 long bestContactId = -1; 3454 int bestRefCount = 0; 3455 3456 long contactId = -1; 3457 int count = 0; 3458 3459 int segmentCount = segments.size(); 3460 for (int i = 0; i < segmentCount; i++) { 3461 LookupKeySegment segment = segments.get(i); 3462 if (segment.contactId != -1) { 3463 if (segment.contactId == contactId) { 3464 count++; 3465 } else { 3466 if (count > bestRefCount) { 3467 bestContactId = contactId; 3468 bestRefCount = count; 3469 } 3470 contactId = segment.contactId; 3471 count = 1; 3472 } 3473 } 3474 } 3475 if (count > bestRefCount) { 3476 return contactId; 3477 } else { 3478 return bestContactId; 3479 } 3480 } 3481 3482 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, String[] projection) { 3483 String contactView = mOpenHelper.getContactView(); 3484 boolean needsPresence = mOpenHelper.isInProjection(projection, Contacts.PRESENCE_STATUS, 3485 Contacts.PRESENCE_CUSTOM_STATUS); 3486 if (!needsPresence) { 3487 qb.setTables(contactView); 3488 qb.setProjectionMap(sContactsProjectionMap); 3489 } else { 3490 qb.setTables(contactView + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + " ON (" 3491 + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ") "); 3492 qb.setProjectionMap(sContactsWithPresenceProjectionMap); 3493 3494 } 3495 } 3496 3497 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 3498 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3499 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3500 if (!TextUtils.isEmpty(accountName)) { 3501 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 3502 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3503 + RawContacts.ACCOUNT_TYPE + "=" 3504 + DatabaseUtils.sqlEscapeString(accountType)); 3505 } else { 3506 qb.appendWhere("1"); 3507 } 3508 } 3509 3510 private String appendAccountToSelection(Uri uri, String selection) { 3511 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3512 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3513 if (!TextUtils.isEmpty(accountName)) { 3514 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 3515 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3516 + RawContacts.ACCOUNT_TYPE + "=" 3517 + DatabaseUtils.sqlEscapeString(accountType)); 3518 if (!TextUtils.isEmpty(selection)) { 3519 selectionSb.append(" AND ("); 3520 selectionSb.append(selection); 3521 selectionSb.append(')'); 3522 } 3523 return selectionSb.toString(); 3524 } else { 3525 return selection; 3526 } 3527 } 3528 3529 /** 3530 * Gets the value of the "limit" URI query parameter. 3531 * 3532 * @return A string containing a non-negative integer, or <code>null</code> if 3533 * the parameter is not set, or is set to an invalid value. 3534 */ 3535 private String getLimit(Uri url) { 3536 String limitParam = url.getQueryParameter("limit"); 3537 if (limitParam == null) { 3538 return null; 3539 } 3540 // make sure that the limit is a non-negative integer 3541 try { 3542 int l = Integer.parseInt(limitParam); 3543 if (l < 0) { 3544 Log.w(TAG, "Invalid limit parameter: " + limitParam); 3545 return null; 3546 } 3547 return String.valueOf(l); 3548 } catch (NumberFormatException ex) { 3549 Log.w(TAG, "Invalid limit parameter: " + limitParam); 3550 return null; 3551 } 3552 } 3553 3554 /** 3555 * Returns true if all the characters are meaningful as digits 3556 * in a phone number -- letters, digits, and a few punctuation marks. 3557 */ 3558 private boolean isPhoneNumber(CharSequence cons) { 3559 int len = cons.length(); 3560 3561 for (int i = 0; i < len; i++) { 3562 char c = cons.charAt(i); 3563 3564 if ((c >= '0') && (c <= '9')) { 3565 continue; 3566 } 3567 if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+') 3568 || (c == '#') || (c == '*')) { 3569 continue; 3570 } 3571 if ((c >= 'A') && (c <= 'Z')) { 3572 continue; 3573 } 3574 if ((c >= 'a') && (c <= 'z')) { 3575 continue; 3576 } 3577 3578 return false; 3579 } 3580 3581 return true; 3582 } 3583 3584 String getContactsRestrictions() { 3585 if (mOpenHelper.hasRestrictedAccess()) { 3586 return "1"; 3587 } else { 3588 return RawContacts.IS_RESTRICTED + "=0"; 3589 } 3590 } 3591 3592 public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { 3593 if (mOpenHelper.hasRestrictedAccess()) { 3594 return "1"; 3595 } else { 3596 return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS 3597 + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; 3598 } 3599 } 3600 3601 @Override 3602 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 3603 int match = sUriMatcher.match(uri); 3604 switch (match) { 3605 case CONTACTS_PHOTO: { 3606 if (!"r".equals(mode)) { 3607 throw new FileNotFoundException("Mode " + mode + " not supported."); 3608 } 3609 3610 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3611 3612 String sql = 3613 "SELECT " + Photo.PHOTO + " FROM " + mOpenHelper.getDataView() + 3614 " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID 3615 + " AND " + RawContacts.CONTACT_ID + "=" + contactId; 3616 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 3617 return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null); 3618 } 3619 3620 case CONTACTS_LOOKUP: 3621 case CONTACTS_LOOKUP_ID: { 3622 // TODO: optimize lookup when direct id provided 3623 final String lookupKey = uri.getPathSegments().get(2); 3624 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey); 3625 final String selection = RawContacts.CONTACT_ID + "=" + contactId; 3626 3627 // When opening a contact as file, we pass back contents as a 3628 // vCard-encoded stream. We build into a local buffer first, 3629 // then pipe into MemoryFile once the exact size is known. 3630 final ByteArrayOutputStream localStream = new ByteArrayOutputStream(); 3631 outputRawContactsAsVCard(localStream, selection, null); 3632 return buildAssetFileDescriptor(localStream); 3633 } 3634 3635 default: 3636 throw new FileNotFoundException("No file at: " + uri); 3637 } 3638 } 3639 3640 private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile"; 3641 private static final String VCARD_TYPE_DEFAULT = "default"; 3642 3643 /** 3644 * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the 3645 * contents of the given {@link ByteArrayOutputStream}. 3646 */ 3647 private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) { 3648 AssetFileDescriptor fd = null; 3649 try { 3650 stream.flush(); 3651 3652 final byte[] byteData = stream.toByteArray(); 3653 final int size = byteData.length; 3654 3655 final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size); 3656 memoryFile.writeBytes(byteData, 0, 0, size); 3657 memoryFile.deactivate(); 3658 3659 fd = AssetFileDescriptor.fromMemoryFile(memoryFile); 3660 } catch (IOException e) { 3661 Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString()); 3662 } 3663 return fd; 3664 } 3665 3666 /** 3667 * Output {@link RawContacts} matching the requested selection in the vCard 3668 * format to the given {@link OutputStream}. This method returns silently if 3669 * any errors encountered. 3670 */ 3671 private void outputRawContactsAsVCard(OutputStream stream, String selection, 3672 String[] selectionArgs) { 3673 final Context context = this.getContext(); 3674 final VCardComposer composer = new VCardComposer(context, VCARD_TYPE_DEFAULT, false); 3675 composer.addHandler(composer.new HandlerForOutputStream(stream)); 3676 3677 // TODO: enforce the callers security clause is used 3678 if (!composer.init(selection, selectionArgs)) 3679 return; 3680 3681 while (!composer.isAfterLast()) { 3682 if (!composer.createOneEntry()) { 3683 Log.w(TAG, "Failed to output a contact."); 3684 } 3685 } 3686 composer.terminate(); 3687 } 3688 3689 /** 3690 * An implementation of EntityIterator that joins the contacts and data tables 3691 * and consumes all the data rows for a contact in order to build the Entity for a contact. 3692 */ 3693 private static class RawContactsEntityIterator implements EntityIterator { 3694 private final Cursor mEntityCursor; 3695 private volatile boolean mIsClosed; 3696 3697 private static final String[] DATA_KEYS = new String[]{ 3698 Data.DATA1, 3699 Data.DATA2, 3700 Data.DATA3, 3701 Data.DATA4, 3702 Data.DATA5, 3703 Data.DATA6, 3704 Data.DATA7, 3705 Data.DATA8, 3706 Data.DATA9, 3707 Data.DATA10, 3708 Data.DATA11, 3709 Data.DATA12, 3710 Data.DATA13, 3711 Data.DATA14, 3712 Data.DATA15, 3713 Data.SYNC1, 3714 Data.SYNC2, 3715 Data.SYNC3, 3716 Data.SYNC4}; 3717 3718 private static final String[] PROJECTION = new String[]{ 3719 RawContacts.ACCOUNT_NAME, 3720 RawContacts.ACCOUNT_TYPE, 3721 RawContacts.SOURCE_ID, 3722 RawContacts.VERSION, 3723 RawContacts.DIRTY, 3724 Data._ID, 3725 Data.RES_PACKAGE, 3726 Data.MIMETYPE, 3727 Data.DATA1, 3728 Data.DATA2, 3729 Data.DATA3, 3730 Data.DATA4, 3731 Data.DATA5, 3732 Data.DATA6, 3733 Data.DATA7, 3734 Data.DATA8, 3735 Data.DATA9, 3736 Data.DATA10, 3737 Data.DATA11, 3738 Data.DATA12, 3739 Data.DATA13, 3740 Data.DATA14, 3741 Data.DATA15, 3742 Data.SYNC1, 3743 Data.SYNC2, 3744 Data.SYNC3, 3745 Data.SYNC4, 3746 Data.RAW_CONTACT_ID, 3747 Data.IS_PRIMARY, 3748 Data.IS_SUPER_PRIMARY, 3749 Data.DATA_VERSION, 3750 GroupMembership.GROUP_SOURCE_ID, 3751 RawContacts.SYNC1, 3752 RawContacts.SYNC2, 3753 RawContacts.SYNC3, 3754 RawContacts.SYNC4, 3755 RawContacts.DELETED, 3756 RawContacts.CONTACT_ID, 3757 RawContacts.STARRED}; 3758 3759 private static final int COLUMN_ACCOUNT_NAME = 0; 3760 private static final int COLUMN_ACCOUNT_TYPE = 1; 3761 private static final int COLUMN_SOURCE_ID = 2; 3762 private static final int COLUMN_VERSION = 3; 3763 private static final int COLUMN_DIRTY = 4; 3764 private static final int COLUMN_DATA_ID = 5; 3765 private static final int COLUMN_RES_PACKAGE = 6; 3766 private static final int COLUMN_MIMETYPE = 7; 3767 private static final int COLUMN_DATA1 = 8; 3768 private static final int COLUMN_RAW_CONTACT_ID = 27; 3769 private static final int COLUMN_IS_PRIMARY = 28; 3770 private static final int COLUMN_IS_SUPER_PRIMARY = 29; 3771 private static final int COLUMN_DATA_VERSION = 30; 3772 private static final int COLUMN_GROUP_SOURCE_ID = 31; 3773 private static final int COLUMN_SYNC1 = 32; 3774 private static final int COLUMN_SYNC2 = 33; 3775 private static final int COLUMN_SYNC3 = 34; 3776 private static final int COLUMN_SYNC4 = 35; 3777 private static final int COLUMN_DELETED = 36; 3778 private static final int COLUMN_CONTACT_ID = 37; 3779 private static final int COLUMN_STARRED = 38; 3780 3781 public RawContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri, 3782 String selection, String[] selectionArgs, String sortOrder) { 3783 mIsClosed = false; 3784 3785 final String updatedSortOrder = (sortOrder == null) 3786 ? Data.RAW_CONTACT_ID 3787 : (Data.RAW_CONTACT_ID + "," + sortOrder); 3788 3789 final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase(); 3790 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3791 qb.setTables(Tables.CONTACT_ENTITIES); 3792 if (contactsIdString != null) { 3793 qb.appendWhere(Data.RAW_CONTACT_ID + "=" + contactsIdString); 3794 } 3795 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3796 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3797 if (!TextUtils.isEmpty(accountName)) { 3798 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 3799 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3800 + RawContacts.ACCOUNT_TYPE + "=" 3801 + DatabaseUtils.sqlEscapeString(accountType)); 3802 } 3803 mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs, 3804 null, null, updatedSortOrder); 3805 mEntityCursor.moveToFirst(); 3806 } 3807 3808 public void reset() throws RemoteException { 3809 if (mIsClosed) { 3810 throw new IllegalStateException("calling reset() when the iterator is closed"); 3811 } 3812 mEntityCursor.moveToFirst(); 3813 } 3814 3815 public void close() { 3816 if (mIsClosed) { 3817 throw new IllegalStateException("closing when already closed"); 3818 } 3819 mIsClosed = true; 3820 mEntityCursor.close(); 3821 } 3822 3823 public boolean hasNext() throws RemoteException { 3824 if (mIsClosed) { 3825 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 3826 } 3827 3828 return !mEntityCursor.isAfterLast(); 3829 } 3830 3831 public Entity next() throws RemoteException { 3832 if (mIsClosed) { 3833 throw new IllegalStateException("calling next() when the iterator is closed"); 3834 } 3835 if (!hasNext()) { 3836 throw new IllegalStateException("you may only call next() if hasNext() is true"); 3837 } 3838 3839 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 3840 3841 final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID); 3842 3843 // we expect the cursor is already at the row we need to read from 3844 ContentValues contactValues = new ContentValues(); 3845 contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 3846 contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 3847 contactValues.put(RawContacts._ID, rawContactId); 3848 contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY)); 3849 contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION)); 3850 contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 3851 contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1)); 3852 contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2)); 3853 contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3)); 3854 contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4)); 3855 contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED)); 3856 contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID)); 3857 contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED)); 3858 Entity contact = new Entity(contactValues); 3859 3860 // read data rows until the contact id changes 3861 do { 3862 if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) { 3863 break; 3864 } 3865 // add the data to to the contact 3866 ContentValues dataValues = new ContentValues(); 3867 dataValues.put(Data._ID, c.getString(COLUMN_DATA_ID)); 3868 dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE)); 3869 dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE)); 3870 dataValues.put(Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY)); 3871 dataValues.put(Data.IS_SUPER_PRIMARY, c.getString(COLUMN_IS_SUPER_PRIMARY)); 3872 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 3873 if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) { 3874 dataValues.put(GroupMembership.GROUP_SOURCE_ID, 3875 c.getString(COLUMN_GROUP_SOURCE_ID)); 3876 } 3877 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 3878 for (int i = 0; i < DATA_KEYS.length; i++) { 3879 final int columnIndex = i + COLUMN_DATA1; 3880 String key = DATA_KEYS[i]; 3881 if (c.isNull(columnIndex)) { 3882 // don't put anything 3883 } else if (c.isLong(columnIndex)) { 3884 dataValues.put(key, c.getLong(columnIndex)); 3885 } else if (c.isFloat(columnIndex)) { 3886 dataValues.put(key, c.getFloat(columnIndex)); 3887 } else if (c.isString(columnIndex)) { 3888 dataValues.put(key, c.getString(columnIndex)); 3889 } else if (c.isBlob(columnIndex)) { 3890 dataValues.put(key, c.getBlob(columnIndex)); 3891 } 3892 } 3893 contact.addSubValue(Data.CONTENT_URI, dataValues); 3894 } while (mEntityCursor.moveToNext()); 3895 3896 return contact; 3897 } 3898 } 3899 3900 /** 3901 * An implementation of EntityIterator that joins the contacts and data tables 3902 * and consumes all the data rows for a contact in order to build the Entity for a contact. 3903 */ 3904 private static class GroupsEntityIterator implements EntityIterator { 3905 private final Cursor mEntityCursor; 3906 private volatile boolean mIsClosed; 3907 3908 private static final String[] PROJECTION = new String[]{ 3909 Groups._ID, 3910 Groups.ACCOUNT_NAME, 3911 Groups.ACCOUNT_TYPE, 3912 Groups.SOURCE_ID, 3913 Groups.DIRTY, 3914 Groups.VERSION, 3915 Groups.RES_PACKAGE, 3916 Groups.TITLE, 3917 Groups.TITLE_RES, 3918 Groups.GROUP_VISIBLE, 3919 Groups.SYNC1, 3920 Groups.SYNC2, 3921 Groups.SYNC3, 3922 Groups.SYNC4, 3923 Groups.SYSTEM_ID, 3924 Groups.NOTES, 3925 Groups.DELETED}; 3926 3927 private static final int COLUMN_ID = 0; 3928 private static final int COLUMN_ACCOUNT_NAME = 1; 3929 private static final int COLUMN_ACCOUNT_TYPE = 2; 3930 private static final int COLUMN_SOURCE_ID = 3; 3931 private static final int COLUMN_DIRTY = 4; 3932 private static final int COLUMN_VERSION = 5; 3933 private static final int COLUMN_RES_PACKAGE = 6; 3934 private static final int COLUMN_TITLE = 7; 3935 private static final int COLUMN_TITLE_RES = 8; 3936 private static final int COLUMN_GROUP_VISIBLE = 9; 3937 private static final int COLUMN_SYNC1 = 10; 3938 private static final int COLUMN_SYNC2 = 11; 3939 private static final int COLUMN_SYNC3 = 12; 3940 private static final int COLUMN_SYNC4 = 13; 3941 private static final int COLUMN_SYSTEM_ID = 14; 3942 private static final int COLUMN_NOTES = 15; 3943 private static final int COLUMN_DELETED = 16; 3944 3945 public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri, 3946 String selection, String[] selectionArgs, String sortOrder) { 3947 mIsClosed = false; 3948 3949 final String updatedSortOrder = (sortOrder == null) 3950 ? Groups._ID 3951 : (Groups._ID + "," + sortOrder); 3952 3953 final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase(); 3954 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3955 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3956 qb.setProjectionMap(sGroupsProjectionMap); 3957 if (groupIdString != null) { 3958 qb.appendWhere(Groups._ID + "=" + groupIdString); 3959 } 3960 final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME); 3961 final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE); 3962 if (!TextUtils.isEmpty(accountName)) { 3963 qb.appendWhere(Groups.ACCOUNT_NAME + "=" 3964 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3965 + Groups.ACCOUNT_TYPE + "=" 3966 + DatabaseUtils.sqlEscapeString(accountType)); 3967 } 3968 mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs, 3969 null, null, updatedSortOrder); 3970 mEntityCursor.moveToFirst(); 3971 } 3972 3973 public void close() { 3974 if (mIsClosed) { 3975 throw new IllegalStateException("closing when already closed"); 3976 } 3977 mIsClosed = true; 3978 mEntityCursor.close(); 3979 } 3980 3981 public boolean hasNext() throws RemoteException { 3982 if (mIsClosed) { 3983 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 3984 } 3985 3986 return !mEntityCursor.isAfterLast(); 3987 } 3988 3989 public void reset() throws RemoteException { 3990 if (mIsClosed) { 3991 throw new IllegalStateException("calling reset() when the iterator is closed"); 3992 } 3993 mEntityCursor.moveToFirst(); 3994 } 3995 3996 public Entity next() throws RemoteException { 3997 if (mIsClosed) { 3998 throw new IllegalStateException("calling next() when the iterator is closed"); 3999 } 4000 if (!hasNext()) { 4001 throw new IllegalStateException("you may only call next() if hasNext() is true"); 4002 } 4003 4004 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 4005 4006 final long groupId = c.getLong(COLUMN_ID); 4007 4008 // we expect the cursor is already at the row we need to read from 4009 ContentValues groupValues = new ContentValues(); 4010 groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 4011 groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 4012 groupValues.put(Groups._ID, groupId); 4013 groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY)); 4014 groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION)); 4015 groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 4016 groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE)); 4017 groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE)); 4018 groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES)); 4019 groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE)); 4020 groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1)); 4021 groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2)); 4022 groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3)); 4023 groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4)); 4024 groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID)); 4025 groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED)); 4026 groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES)); 4027 Entity group = new Entity(groupValues); 4028 4029 mEntityCursor.moveToNext(); 4030 4031 return group; 4032 } 4033 } 4034 4035 @Override 4036 public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, 4037 String sortOrder) { 4038 waitForAccess(); 4039 4040 final int match = sUriMatcher.match(uri); 4041 switch (match) { 4042 case RAW_CONTACTS: 4043 case RAW_CONTACTS_ID: 4044 String contactsIdString = null; 4045 if (match == RAW_CONTACTS_ID) { 4046 contactsIdString = uri.getPathSegments().get(1); 4047 } 4048 4049 return new RawContactsEntityIterator(this, contactsIdString, 4050 uri, selection, selectionArgs, sortOrder); 4051 case GROUPS: 4052 case GROUPS_ID: 4053 String idString = null; 4054 if (match == GROUPS_ID) { 4055 idString = uri.getPathSegments().get(1); 4056 } 4057 4058 return new GroupsEntityIterator(this, idString, 4059 uri, selection, selectionArgs, sortOrder); 4060 default: 4061 throw new UnsupportedOperationException("Unknown uri: " + uri); 4062 } 4063 } 4064 4065 @Override 4066 public String getType(Uri uri) { 4067 final int match = sUriMatcher.match(uri); 4068 switch (match) { 4069 case CONTACTS: 4070 case CONTACTS_LOOKUP: 4071 return Contacts.CONTENT_TYPE; 4072 case CONTACTS_ID: 4073 case CONTACTS_LOOKUP_ID: 4074 return Contacts.CONTENT_ITEM_TYPE; 4075 case RAW_CONTACTS: 4076 return RawContacts.CONTENT_TYPE; 4077 case RAW_CONTACTS_ID: 4078 return RawContacts.CONTENT_ITEM_TYPE; 4079 case DATA_ID: 4080 return mOpenHelper.getDataMimeType(ContentUris.parseId(uri)); 4081 case AGGREGATION_EXCEPTIONS: 4082 return AggregationExceptions.CONTENT_TYPE; 4083 case AGGREGATION_EXCEPTION_ID: 4084 return AggregationExceptions.CONTENT_ITEM_TYPE; 4085 case SETTINGS: 4086 return Settings.CONTENT_TYPE; 4087 case AGGREGATION_SUGGESTIONS: 4088 return Contacts.CONTENT_TYPE; 4089 case SEARCH_SUGGESTIONS: 4090 return SearchManager.SUGGEST_MIME_TYPE; 4091 case SEARCH_SHORTCUT: 4092 return SearchManager.SHORTCUT_MIME_TYPE; 4093 default: 4094 return mLegacyApiSupport.getType(uri); 4095 } 4096 } 4097 4098 private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) { 4099 if (displayName != null) { 4100 mRawContactDisplayNameUpdate.bindString(1, displayName); 4101 } else { 4102 mRawContactDisplayNameUpdate.bindNull(1); 4103 } 4104 mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource); 4105 mRawContactDisplayNameUpdate.bindLong(3, rawContactId); 4106 mRawContactDisplayNameUpdate.execute(); 4107 } 4108 4109 /** 4110 * Sets the {@link RawContacts#DIRTY} for the specified raw contact. 4111 */ 4112 private void setRawContactDirty(long rawContactId) { 4113 mRawContactDirtyUpdate.bindLong(1, rawContactId); 4114 mRawContactDirtyUpdate.execute(); 4115 } 4116 4117 /* 4118 * Sets the given dataId record in the "data" table to primary, and resets all data records of 4119 * the same mimetype and under the same contact to not be primary. 4120 * 4121 * @param dataId the id of the data record to be set to primary. 4122 */ 4123 private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) { 4124 mSetPrimaryStatement.bindLong(1, dataId); 4125 mSetPrimaryStatement.bindLong(2, mimeTypeId); 4126 mSetPrimaryStatement.bindLong(3, rawContactId); 4127 mSetPrimaryStatement.execute(); 4128 } 4129 4130 /* 4131 * Sets the given dataId record in the "data" table to "super primary", and resets all data 4132 * records of the same mimetype and under the same aggregate to not be "super primary". 4133 * 4134 * @param dataId the id of the data record to be set to primary. 4135 */ 4136 private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) { 4137 mSetSuperPrimaryStatement.bindLong(1, dataId); 4138 mSetSuperPrimaryStatement.bindLong(2, mimeTypeId); 4139 mSetSuperPrimaryStatement.bindLong(3, rawContactId); 4140 mSetSuperPrimaryStatement.execute(); 4141 } 4142 4143 private void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 4144 sb.append("(SELECT DISTINCT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS 4145 + " JOIN name_lookup ON(" + RawContactsColumns.CONCRETE_ID + "=raw_contact_id)" 4146 + " WHERE normalized_name GLOB '"); 4147 sb.append(NameNormalizer.normalize(filterParam)); 4148 sb.append("*')"); 4149 } 4150 4151 public String getRawContactsByFilterAsNestedQuery(String filterParam) { 4152 StringBuilder sb = new StringBuilder(); 4153 appendRawContactsByFilterAsNestedQuery(sb, filterParam, null); 4154 return sb.toString(); 4155 } 4156 4157 public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam, 4158 String limit) { 4159 appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit); 4160 } 4161 4162 private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, 4163 String limit) { 4164 sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '"); 4165 sb.append(normalizedName); 4166 sb.append("*'"); 4167 if (limit != null) { 4168 sb.append(" LIMIT ").append(limit); 4169 } 4170 sb.append(")"); 4171 } 4172 4173 /** 4174 * Inserts an argument at the beginning of the selection arg list. 4175 */ 4176 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 4177 if (selectionArgs == null) { 4178 return new String[] {arg}; 4179 } else { 4180 int newLength = selectionArgs.length + 1; 4181 String[] newSelectionArgs = new String[newLength]; 4182 newSelectionArgs[0] = arg; 4183 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 4184 return newSelectionArgs; 4185 } 4186 } 4187 4188 protected Account getDefaultAccount() { 4189 AccountManager accountManager = AccountManager.get(getContext()); 4190 try { 4191 Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE, 4192 new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult(); 4193 if (accounts != null && accounts.length > 0) { 4194 return accounts[0]; 4195 } 4196 } catch (Throwable e) { 4197 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 4198 } 4199 return null; 4200 } 4201} 4202