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