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