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