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