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