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