ContactsProvider2.java revision df9db5e99572ce9760eb265683134c1f3293928f
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_ID: { 2250 long contactId = ContentUris.parseId(uri); 2251 2252 // Remove references to the contact first 2253 ContentValues values = new ContentValues(); 2254 values.putNull(RawContacts.CONTACT_ID); 2255 mDb.update(Tables.RAW_CONTACTS, values, 2256 RawContacts.CONTACT_ID + "=" + contactId, null); 2257 2258 return mDb.delete(Tables.CONTACTS, BaseColumns._ID + "=" + contactId, null); 2259 } 2260 2261 case RAW_CONTACTS: { 2262 final boolean permanently = 2263 readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false); 2264 int numDeletes = 0; 2265 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2266 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2267 try { 2268 while (c.moveToNext()) { 2269 final long rawContactId = c.getLong(0); 2270 numDeletes += deleteRawContact(rawContactId, permanently); 2271 } 2272 } finally { 2273 c.close(); 2274 } 2275 return numDeletes; 2276 } 2277 2278 case RAW_CONTACTS_ID: { 2279 final boolean permanently = 2280 readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false); 2281 final long rawContactId = ContentUris.parseId(uri); 2282 return deleteRawContact(rawContactId, permanently); 2283 } 2284 2285 case DATA: { 2286 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 2287 shouldMarkRawContactAsDirty(uri)); 2288 } 2289 2290 case DATA_ID: { 2291 long dataId = ContentUris.parseId(uri); 2292 return deleteData(Data._ID + "=" + dataId, null, shouldMarkRawContactAsDirty(uri)); 2293 } 2294 2295 case GROUPS_ID: { 2296 boolean markAsDirty = shouldMarkGroupAsDirty(uri); 2297 final boolean deletePermanently = 2298 readBooleanQueryParameter(uri, Groups.DELETE_PERMANENTLY, false); 2299 return deleteGroup(ContentUris.parseId(uri), markAsDirty, deletePermanently); 2300 } 2301 2302 case GROUPS: { 2303 boolean markAsDirty = shouldMarkGroupAsDirty(uri); 2304 final boolean permanently = 2305 readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false); 2306 int numDeletes = 0; 2307 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 2308 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2309 try { 2310 while (c.moveToNext()) { 2311 numDeletes += deleteGroup(c.getLong(0), markAsDirty, permanently); 2312 } 2313 } finally { 2314 c.close(); 2315 } 2316 return numDeletes; 2317 } 2318 2319 case SETTINGS: { 2320 return deleteSettings(selection, selectionArgs); 2321 } 2322 2323 case PRESENCE: { 2324 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 2325 } 2326 2327 default: 2328 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 2329 } 2330 } 2331 2332 private boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) { 2333 final String flag = uri.getQueryParameter(name); 2334 return flag == null 2335 ? defaultValue 2336 : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase())); 2337 } 2338 2339 private int deleteGroup(long groupId, boolean markAsDirty, boolean permanently) { 2340 final long groupMembershipMimetypeId = mOpenHelper 2341 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 2342 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 2343 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 2344 + groupId, null); 2345 2346 try { 2347 if (permanently) { 2348 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 2349 } else { 2350 mValues.clear(); 2351 mValues.put(Groups.DELETED, 1); 2352 if (markAsDirty) { 2353 mValues.put(Groups.DIRTY, 1); 2354 } 2355 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 2356 } 2357 } finally { 2358 mOpenHelper.updateAllVisible(); 2359 } 2360 } 2361 2362 private int deleteSettings(String selection, String[] selectionArgs) { 2363 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 2364 if (count > 0) { 2365 mOpenHelper.updateAllVisible(); 2366 } 2367 return count; 2368 } 2369 2370 public int deleteRawContact(long rawContactId, boolean permanently) { 2371 if (permanently) { 2372 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 2373 return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 2374 } else { 2375 mOpenHelper.removeContactIfSingleton(rawContactId); 2376 2377 mValues.clear(); 2378 mValues.put(RawContacts.DELETED, 1); 2379 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2380 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 2381 mValues.putNull(RawContacts.CONTACT_ID); 2382 mValues.put(RawContacts.DIRTY, 1); 2383 return updateRawContact(rawContactId, mValues); 2384 } 2385 } 2386 2387 private static Account readAccountFromQueryParams(Uri uri) { 2388 final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 2389 final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 2390 if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) { 2391 return null; 2392 } 2393 return new Account(name, type); 2394 } 2395 2396 @Override 2397 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 2398 String[] selectionArgs) { 2399 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2400 Log.v(TAG, "updateInTransaction: " + uri); 2401 } 2402 2403 int count = 0; 2404 2405 final int match = sUriMatcher.match(uri); 2406 if (match == SYNCSTATE_ID && selection == null) { 2407 long rowId = ContentUris.parseId(uri); 2408 Object data = values.get(ContactsContract.SyncStateColumns.DATA); 2409 mUpdatedSyncStates.put(rowId, data); 2410 return 1; 2411 } 2412 flushTransactionalChanges(); 2413 switch(match) { 2414 case SYNCSTATE: 2415 return mOpenHelper.getSyncState().update(mDb, values, 2416 appendAccountToSelection(uri, selection), selectionArgs); 2417 2418 case SYNCSTATE_ID: { 2419 selection = appendAccountToSelection(uri, selection); 2420 String selectionWithId = 2421 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 2422 + (selection == null ? "" : " AND (" + selection + ")"); 2423 return mOpenHelper.getSyncState().update(mDb, values, 2424 selectionWithId, selectionArgs); 2425 } 2426 2427 // TODO(emillar): We will want to disallow editing the contacts table at some point. 2428 case CONTACTS: { 2429 count = mDb.update(Tables.CONTACTS, values, 2430 appendAccountToSelection(uri, selection), selectionArgs); 2431 break; 2432 } 2433 2434 case CONTACTS_ID: { 2435 count = updateContactData(ContentUris.parseId(uri), values); 2436 break; 2437 } 2438 2439 case DATA: { 2440 count = updateData(uri, values, appendAccountToSelection(uri, selection), 2441 selectionArgs, shouldMarkRawContactAsDirty(uri)); 2442 break; 2443 } 2444 2445 case DATA_ID: { 2446 count = updateData(uri, values, selection, selectionArgs, 2447 shouldMarkRawContactAsDirty(uri)); 2448 break; 2449 } 2450 2451 case RAW_CONTACTS: { 2452 count = updateRawContacts(values, selection, selectionArgs); 2453 break; 2454 } 2455 2456 case RAW_CONTACTS_ID: { 2457 long rawContactId = ContentUris.parseId(uri); 2458 if (selection != null) { 2459 count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId 2460 + " AND(" + selection + ")", selectionArgs); 2461 } else { 2462 count = updateRawContact(rawContactId, values); 2463 } 2464 break; 2465 } 2466 2467 case GROUPS: { 2468 count = updateGroups(values, appendAccountToSelection(uri, selection), 2469 selectionArgs, shouldMarkGroupAsDirty(uri)); 2470 break; 2471 } 2472 2473 case GROUPS_ID: { 2474 long groupId = ContentUris.parseId(uri); 2475 String selectionWithId = (Groups._ID + "=" + groupId + " ") 2476 + (selection == null ? "" : " AND " + selection); 2477 count = updateGroups(values, selectionWithId, selectionArgs, 2478 shouldMarkGroupAsDirty(uri)); 2479 break; 2480 } 2481 2482 case AGGREGATION_EXCEPTIONS: { 2483 count = updateAggregationException(mDb, values); 2484 break; 2485 } 2486 2487 case SETTINGS: { 2488 count = updateSettings(values, selection, selectionArgs); 2489 break; 2490 } 2491 2492 default: 2493 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 2494 } 2495 2496 return count; 2497 } 2498 2499 private int updateGroups(ContentValues values, String selectionWithId, 2500 String[] selectionArgs, boolean markAsDirty) { 2501 2502 ContentValues updatedValues; 2503 if (markAsDirty && !values.containsKey(Groups.DIRTY)) { 2504 updatedValues = mValues; 2505 updatedValues.clear(); 2506 updatedValues.putAll(values); 2507 updatedValues.put(Groups.DIRTY, 1); 2508 } else { 2509 updatedValues = values; 2510 } 2511 2512 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 2513 2514 // If changing visibility, then update contacts 2515 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 2516 mOpenHelper.updateAllVisible(); 2517 } 2518 return count; 2519 } 2520 2521 private int updateSettings(ContentValues values, String selection, String[] selectionArgs) { 2522 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 2523 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2524 mOpenHelper.updateAllVisible(); 2525 } 2526 return count; 2527 } 2528 2529 private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) { 2530 if (values.containsKey(RawContacts.CONTACT_ID)) { 2531 throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " + 2532 "in content values. Contact IDs are assigned automatically"); 2533 } 2534 2535 int count = 0; 2536 Cursor cursor = mDb.query(mOpenHelper.getRawContactView(), 2537 new String[] { RawContacts.CONTACT_ID }, selection, 2538 selectionArgs, null, null, null); 2539 try { 2540 while (cursor.moveToNext()) { 2541 long rawContactId = cursor.getLong(0); 2542 updateRawContact(rawContactId, values); 2543 count++; 2544 } 2545 } finally { 2546 cursor.close(); 2547 } 2548 2549 return count; 2550 } 2551 2552 private int updateRawContact(long rawContactId, ContentValues values) { 2553 int count = mDb.update(Tables.RAW_CONTACTS, values, RawContacts._ID + " = " + rawContactId, 2554 null); 2555 if (count != 0) { 2556 if (values.containsKey(RawContacts.ACCOUNT_TYPE) 2557 || values.containsKey(RawContacts.ACCOUNT_NAME) 2558 || values.containsKey(RawContacts.SOURCE_ID)) { 2559 triggerAggregation(rawContactId); 2560 } 2561 2562 if (values.containsKey(RawContacts.STARRED)) { 2563 mContactAggregator.updateStarred(rawContactId); 2564 } 2565 if (values.containsKey(RawContacts.SOURCE_ID)) { 2566 mContactAggregator.updateLookupKey(mDb, rawContactId); 2567 } 2568 } 2569 return count; 2570 } 2571 2572 private int updateData(Uri uri, ContentValues values, String selection, 2573 String[] selectionArgs, boolean markRawContactAsDirty) { 2574 mValues.clear(); 2575 mValues.putAll(values); 2576 mValues.remove(Data._ID); 2577 mValues.remove(Data.RAW_CONTACT_ID); 2578 mValues.remove(Data.MIMETYPE); 2579 2580 String packageName = values.getAsString(Data.RES_PACKAGE); 2581 if (packageName != null) { 2582 mValues.remove(Data.RES_PACKAGE); 2583 mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 2584 } 2585 2586 boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY); 2587 boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY); 2588 2589 // Remove primary or super primary values being set to 0. This is disallowed by the 2590 // content provider. 2591 if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { 2592 containsIsSuperPrimary = false; 2593 mValues.remove(Data.IS_SUPER_PRIMARY); 2594 } 2595 if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) { 2596 containsIsPrimary = false; 2597 mValues.remove(Data.IS_PRIMARY); 2598 } 2599 2600 int count = 0; 2601 2602 // Note that the query will return data according to the access restrictions, 2603 // so we don't need to worry about updating data we don't have permission to read. 2604 Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null); 2605 try { 2606 while(c.moveToNext()) { 2607 count += updateData(mValues, c, markRawContactAsDirty); 2608 } 2609 } finally { 2610 c.close(); 2611 } 2612 2613 return count; 2614 } 2615 2616 private int updateData(ContentValues values, Cursor c, boolean markRawContactAsDirty) { 2617 if (values.size() == 0) { 2618 return 0; 2619 } 2620 2621 final String mimeType = c.getString(DataUpdateQuery.MIMETYPE); 2622 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2623 rowHandler.update(mDb, values, c, markRawContactAsDirty); 2624 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 2625 if (rowHandler.isAggregationRequired()) { 2626 triggerAggregation(rawContactId); 2627 } 2628 2629 return 1; 2630 } 2631 2632 private int updateContactData(long contactId, ContentValues values) { 2633 2634 // First update all constituent contacts 2635 ContentValues optionValues = new ContentValues(5); 2636 OpenHelper.copyStringValue(optionValues, RawContacts.CUSTOM_RINGTONE, 2637 values, Contacts.CUSTOM_RINGTONE); 2638 OpenHelper.copyLongValue(optionValues, RawContacts.SEND_TO_VOICEMAIL, 2639 values, Contacts.SEND_TO_VOICEMAIL); 2640 OpenHelper.copyLongValue(optionValues, RawContacts.LAST_TIME_CONTACTED, 2641 values, Contacts.LAST_TIME_CONTACTED); 2642 OpenHelper.copyLongValue(optionValues, RawContacts.TIMES_CONTACTED, 2643 values, Contacts.TIMES_CONTACTED); 2644 OpenHelper.copyLongValue(optionValues, RawContacts.STARRED, 2645 values, Contacts.STARRED); 2646 2647 // Nothing to update - just return 2648 if (optionValues.size() == 0) { 2649 return 0; 2650 } 2651 2652 if (optionValues.containsKey(RawContacts.STARRED)) { 2653 // Mark dirty when changing starred to trigger sync 2654 optionValues.put(RawContacts.DIRTY, 1); 2655 } 2656 2657 mDb.update(Tables.RAW_CONTACTS, optionValues, 2658 RawContacts.CONTACT_ID + "=" + contactId, null); 2659 return mDb.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null); 2660 } 2661 2662 public void updateContactTime(long contactId, long lastTimeContacted) { 2663 mLastTimeContactedUpdate.bindLong(1, lastTimeContacted); 2664 mLastTimeContactedUpdate.bindLong(2, contactId); 2665 mLastTimeContactedUpdate.execute(); 2666 } 2667 2668 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 2669 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 2670 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 2671 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 2672 2673 long rawContactId1, rawContactId2; 2674 if (rcId1 < rcId2) { 2675 rawContactId1 = rcId1; 2676 rawContactId2 = rcId2; 2677 } else { 2678 rawContactId2 = rcId1; 2679 rawContactId1 = rcId2; 2680 } 2681 2682 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 2683 db.delete(Tables.AGGREGATION_EXCEPTIONS, 2684 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND " 2685 + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null); 2686 } else { 2687 ContentValues exceptionValues = new ContentValues(3); 2688 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 2689 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 2690 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 2691 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 2692 exceptionValues); 2693 } 2694 2695 long contactId1 = mOpenHelper.getContactId(rawContactId1); 2696 mContactAggregator.aggregateContact(db, rawContactId1, contactId1); 2697 2698 long contactId2 = mOpenHelper.getContactId(rawContactId2); 2699 mContactAggregator.aggregateContact(db, rawContactId2, contactId2); 2700 2701 // The return value is fake - we just confirm that we made a change, not count actual 2702 // rows changed. 2703 return 1; 2704 } 2705 2706 public void onAccountsUpdated(Account[] accounts) { 2707 mDb = mOpenHelper.getWritableDatabase(); 2708 if (mDb == null) return; 2709 2710 Set<Account> validAccounts = Sets.newHashSet(); 2711 for (Account account : accounts) { 2712 validAccounts.add(new Account(account.name, account.type)); 2713 } 2714 ArrayList<Account> accountsToDelete = new ArrayList<Account>(); 2715 2716 mDb.beginTransaction(); 2717 try { 2718 // Find all the accounts the contacts DB knows about, mark the ones that aren't in the 2719 // valid set for deletion. 2720 Cursor c = mDb.rawQuery("SELECT DISTINCT account_name, account_type from " 2721 + Tables.RAW_CONTACTS, null); 2722 while (c.moveToNext()) { 2723 if (c.getString(0) != null && c.getString(1) != null) { 2724 Account currAccount = new Account(c.getString(0), c.getString(1)); 2725 if (!validAccounts.contains(currAccount)) { 2726 accountsToDelete.add(currAccount); 2727 } 2728 } 2729 } 2730 c.close(); 2731 2732 for (Account account : accountsToDelete) { 2733 String[] params = new String[]{account.name, account.type}; 2734 mDb.execSQL("DELETE FROM " + Tables.GROUPS 2735 + " WHERE account_name = ? AND account_type = ?", params); 2736 mDb.execSQL("DELETE FROM " + Tables.PRESENCE 2737 + " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (SELECT " 2738 + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS 2739 + " WHERE account_name = ? AND account_type = ?)", params); 2740 mDb.execSQL("DELETE FROM " + Tables.RAW_CONTACTS 2741 + " WHERE account_name = ? AND account_type = ?", params); 2742 } 2743 mDb.setTransactionSuccessful(); 2744 } finally { 2745 mDb.endTransaction(); 2746 } 2747 } 2748 2749 /** 2750 * Test all against {@link TextUtils#isEmpty(CharSequence)}. 2751 */ 2752 private static boolean areAllEmpty(ContentValues values, String[] keys) { 2753 for (String key : keys) { 2754 if (!TextUtils.isEmpty(values.getAsString(key))) { 2755 return false; 2756 } 2757 } 2758 return true; 2759 } 2760 2761 @Override 2762 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 2763 String sortOrder) { 2764 2765 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 2766 2767 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2768 String groupBy = null; 2769 String limit = getLimit(uri); 2770 2771 // TODO: Consider writing a test case for RestrictionExceptions when you 2772 // write a new query() block to make sure it protects restricted data. 2773 final int match = sUriMatcher.match(uri); 2774 switch (match) { 2775 case SYNCSTATE: 2776 return mOpenHelper.getSyncState().query(db, projection, selection, selectionArgs, 2777 sortOrder); 2778 2779 case CONTACTS: { 2780 setTablesAndProjectionMapForContacts(qb, projection); 2781 break; 2782 } 2783 2784 case CONTACTS_ID: { 2785 long contactId = ContentUris.parseId(uri); 2786 setTablesAndProjectionMapForContacts(qb, projection); 2787 qb.appendWhere(Contacts._ID + "=" + contactId); 2788 break; 2789 } 2790 2791 case CONTACTS_LOOKUP: 2792 case CONTACTS_LOOKUP_ID: { 2793 List<String> pathSegments = uri.getPathSegments(); 2794 int segmentCount = pathSegments.size(); 2795 if (segmentCount < 3) { 2796 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 2797 } 2798 String lookupKey = pathSegments.get(2); 2799 if (segmentCount == 4) { 2800 long contactId = Long.parseLong(pathSegments.get(3)); 2801 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 2802 setTablesAndProjectionMapForContacts(lookupQb, projection); 2803 lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " + 2804 Contacts.LOOKUP_KEY + "="); 2805 lookupQb.appendWhereEscapeString(lookupKey); 2806 Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder, 2807 groupBy, limit); 2808 if (c.getCount() != 0) { 2809 return c; 2810 } 2811 2812 c.close(); 2813 } 2814 2815 setTablesAndProjectionMapForContacts(qb, projection); 2816 qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey)); 2817 break; 2818 } 2819 2820 case CONTACTS_FILTER: { 2821 setTablesAndProjectionMapForContacts(qb, projection); 2822 if (uri.getPathSegments().size() > 2) { 2823 String filterParam = uri.getLastPathSegment(); 2824 StringBuilder sb = new StringBuilder(); 2825 sb.append(Contacts._ID + " IN "); 2826 appendContactFilterAsNestedQuery(sb, filterParam); 2827 qb.appendWhere(sb.toString()); 2828 } 2829 break; 2830 } 2831 2832 case CONTACTS_STREQUENT_FILTER: 2833 case CONTACTS_STREQUENT: { 2834 String filterSql = null; 2835 if (match == CONTACTS_STREQUENT_FILTER 2836 && uri.getPathSegments().size() > 3) { 2837 String filterParam = uri.getLastPathSegment(); 2838 StringBuilder sb = new StringBuilder(); 2839 sb.append(Contacts._ID + " IN "); 2840 appendContactFilterAsNestedQuery(sb, filterParam); 2841 filterSql = sb.toString(); 2842 } 2843 2844 setTablesAndProjectionMapForContacts(qb, projection); 2845 2846 // Build the first query for starred 2847 if (filterSql != null) { 2848 qb.appendWhere(filterSql); 2849 } 2850 final String starredQuery = qb.buildQuery(projection, Contacts.STARRED + "=1", 2851 null, Contacts._ID, null, null, null); 2852 2853 // Build the second query for frequent 2854 qb = new SQLiteQueryBuilder(); 2855 setTablesAndProjectionMapForContacts(qb, projection); 2856 if (filterSql != null) { 2857 qb.appendWhere(filterSql); 2858 } 2859 final String frequentQuery = qb.buildQuery(projection, 2860 Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED 2861 + " = 0 OR " + Contacts.STARRED + " IS NULL)", 2862 null, Contacts._ID, null, null, null); 2863 2864 // Put them together 2865 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 2866 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 2867 Cursor c = db.rawQuery(query, null); 2868 if (c != null) { 2869 c.setNotificationUri(getContext().getContentResolver(), 2870 ContactsContract.AUTHORITY_URI); 2871 } 2872 return c; 2873 } 2874 2875 case CONTACTS_GROUP: { 2876 setTablesAndProjectionMapForContacts(qb, projection); 2877 if (uri.getPathSegments().size() > 2) { 2878 qb.appendWhere(sContactsInGroupSelect); 2879 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 2880 } 2881 break; 2882 } 2883 2884 case CONTACTS_DATA: { 2885 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 2886 2887 qb.setTables(mOpenHelper.getDataView()); 2888 qb.setProjectionMap(sDataProjectionMap); 2889 appendAccountFromParameter(qb, uri); 2890 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId); 2891 break; 2892 } 2893 2894 case CONTACTS_PHOTO: { 2895 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 2896 2897 qb.setTables(mOpenHelper.getDataView()); 2898 qb.setProjectionMap(sDataProjectionMap); 2899 appendAccountFromParameter(qb, uri); 2900 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId); 2901 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 2902 break; 2903 } 2904 2905 case PHONES: { 2906 qb.setTables(mOpenHelper.getDataView()); 2907 qb.setProjectionMap(sDataProjectionMap); 2908 qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 2909 break; 2910 } 2911 2912 case PHONES_FILTER: { 2913 qb.setTables(mOpenHelper.getDataView()); 2914 qb.setProjectionMap(sDistinctDataProjectionMap); 2915 qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 2916 if (uri.getPathSegments().size() > 2) { 2917 String filterParam = uri.getLastPathSegment(); 2918 StringBuilder sb = new StringBuilder(); 2919 sb.append("("); 2920 2921 boolean orNeeded = false; 2922 String normalizedName = NameNormalizer.normalize(filterParam); 2923 if (normalizedName.length() > 0) { 2924 sb.append(Data.RAW_CONTACT_ID + " IN "); 2925 appendRawContactsByNormalizedNameFilter(sb, normalizedName, null); 2926 orNeeded = true; 2927 } 2928 2929 if (isPhoneNumber(filterParam)) { 2930 if (orNeeded) { 2931 sb.append(" OR "); 2932 } 2933 String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam); 2934 String reversed = PhoneNumberUtils.getStrippedReversed(number); 2935 sb.append(Data._ID + 2936 " IN (SELECT " + PhoneLookupColumns.DATA_ID 2937 + " FROM " + Tables.PHONE_LOOKUP 2938 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%"); 2939 sb.append(reversed); 2940 sb.append("')"); 2941 } 2942 sb.append(")"); 2943 qb.appendWhere(" AND " + sb); 2944 } 2945 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 2946 break; 2947 } 2948 2949 case EMAILS: { 2950 qb.setTables(mOpenHelper.getDataView()); 2951 qb.setProjectionMap(sDataProjectionMap); 2952 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 2953 break; 2954 } 2955 2956 case EMAILS_LOOKUP: { 2957 qb.setTables(mOpenHelper.getDataView()); 2958 qb.setProjectionMap(sDataProjectionMap); 2959 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 2960 if (uri.getPathSegments().size() > 2) { 2961 qb.appendWhere(" AND " + Email.DATA + "="); 2962 qb.appendWhereEscapeString(uri.getLastPathSegment()); 2963 } 2964 break; 2965 } 2966 2967 case EMAILS_FILTER: { 2968 qb.setTables(mOpenHelper.getDataView()); 2969 qb.setProjectionMap(sDistinctDataProjectionMap); 2970 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 2971 if (uri.getPathSegments().size() > 2) { 2972 String filterParam = uri.getLastPathSegment(); 2973 StringBuilder sb = new StringBuilder(); 2974 sb.append("("); 2975 2976 String normalizedName = NameNormalizer.normalize(filterParam); 2977 if (normalizedName.length() > 0) { 2978 sb.append(Data.RAW_CONTACT_ID + " IN "); 2979 appendRawContactsByNormalizedNameFilter(sb, normalizedName, null); 2980 sb.append(" OR "); 2981 } 2982 2983 sb.append(Email.DATA + " LIKE "); 2984 sb.append(DatabaseUtils.sqlEscapeString(filterParam)); 2985 sb.append(")"); 2986 qb.appendWhere(" AND " + sb); 2987 } 2988 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 2989 break; 2990 } 2991 2992 case POSTALS: { 2993 qb.setTables(mOpenHelper.getDataView()); 2994 qb.setProjectionMap(sDataProjectionMap); 2995 qb.appendWhere(Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 2996 break; 2997 } 2998 2999 case RAW_CONTACTS: { 3000 qb.setTables(mOpenHelper.getRawContactView()); 3001 qb.setProjectionMap(sRawContactsProjectionMap); 3002 break; 3003 } 3004 3005 case RAW_CONTACTS_ID: { 3006 long rawContactId = ContentUris.parseId(uri); 3007 qb.setTables(mOpenHelper.getRawContactView()); 3008 qb.setProjectionMap(sRawContactsProjectionMap); 3009 qb.appendWhere(RawContacts._ID + "=" + rawContactId); 3010 break; 3011 } 3012 3013 case RAW_CONTACTS_DATA: { 3014 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 3015 qb.setTables(mOpenHelper.getDataView()); 3016 qb.setProjectionMap(sDataProjectionMap); 3017 qb.appendWhere(Data.RAW_CONTACT_ID + "=" + rawContactId); 3018 break; 3019 } 3020 3021 case DATA: { 3022 qb.setTables(mOpenHelper.getDataView()); 3023 qb.setProjectionMap(sDataProjectionMap); 3024 appendAccountFromParameter(qb, uri); 3025 break; 3026 } 3027 3028 case DATA_ID: { 3029 qb.setTables(mOpenHelper.getDataView()); 3030 qb.setProjectionMap(sDataProjectionMap); 3031 qb.appendWhere(Data._ID + "=" + ContentUris.parseId(uri)); 3032 break; 3033 } 3034 3035 case DATA_WITH_PRESENCE: { 3036 qb.setTables(mOpenHelper.getDataView() + " data" 3037 + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE 3038 + " ON (" + AggregatedPresenceColumns.CONTACT_ID + "=" 3039 + RawContacts.CONTACT_ID + ")"); 3040 qb.setProjectionMap(sDataWithPresenceProjectionMap); 3041 appendAccountFromParameter(qb, uri); 3042 break; 3043 } 3044 3045 case PHONE_LOOKUP: { 3046 3047 if (TextUtils.isEmpty(sortOrder)) { 3048 // Default the sort order to something reasonable so we get consistent 3049 // results when callers don't request an ordering 3050 sortOrder = RawContactsColumns.CONCRETE_ID; 3051 } 3052 3053 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 3054 mOpenHelper.buildPhoneLookupAndContactQuery(qb, number); 3055 qb.setProjectionMap(sPhoneLookupProjectionMap); 3056 3057 // Phone lookup cannot be combined with a selection 3058 selection = null; 3059 selectionArgs = null; 3060 break; 3061 } 3062 3063 case GROUPS: { 3064 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3065 qb.setProjectionMap(sGroupsProjectionMap); 3066 break; 3067 } 3068 3069 case GROUPS_ID: { 3070 long groupId = ContentUris.parseId(uri); 3071 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3072 qb.setProjectionMap(sGroupsProjectionMap); 3073 qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId); 3074 break; 3075 } 3076 3077 case GROUPS_SUMMARY: { 3078 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3079 qb.setProjectionMap(sGroupsSummaryProjectionMap); 3080 groupBy = GroupsColumns.CONCRETE_ID; 3081 break; 3082 } 3083 3084 case AGGREGATION_EXCEPTIONS: { 3085 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 3086 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 3087 break; 3088 } 3089 3090 case AGGREGATION_SUGGESTIONS: { 3091 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3092 final int maxSuggestions; 3093 if (limit != null) { 3094 maxSuggestions = Integer.parseInt(limit); 3095 } else { 3096 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 3097 } 3098 3099 return mContactAggregator.queryAggregationSuggestions(contactId, projection, 3100 sContactsProjectionMap, maxSuggestions); 3101 } 3102 3103 case SETTINGS: { 3104 qb.setTables(Tables.SETTINGS); 3105 qb.setProjectionMap(sSettingsProjectionMap); 3106 3107 // When requesting specific columns, this query requires 3108 // late-binding of the GroupMembership MIME-type. 3109 final String groupMembershipMimetypeId = Long.toString(mOpenHelper 3110 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 3111 if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 3112 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3113 } 3114 if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 3115 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3116 } 3117 3118 break; 3119 } 3120 3121 case PRESENCE: { 3122 qb.setTables(Tables.PRESENCE); 3123 qb.setProjectionMap(sPresenceProjectionMap); 3124 break; 3125 } 3126 3127 case PRESENCE_ID: { 3128 qb.setTables(Tables.PRESENCE); 3129 qb.setProjectionMap(sPresenceProjectionMap); 3130 qb.appendWhere(Presence._ID + "=" + ContentUris.parseId(uri)); 3131 break; 3132 } 3133 3134 case SEARCH_SUGGESTIONS: { 3135 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); 3136 } 3137 3138 case SEARCH_SHORTCUT: { 3139 long contactId = ContentUris.parseId(uri); 3140 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection); 3141 } 3142 3143 case LIVE_FOLDERS_CONTACTS: 3144 qb.setTables(mOpenHelper.getContactView()); 3145 qb.setProjectionMap(sLiveFoldersProjectionMap); 3146 break; 3147 3148 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 3149 qb.setTables(mOpenHelper.getContactView()); 3150 qb.setProjectionMap(sLiveFoldersProjectionMap); 3151 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 3152 break; 3153 3154 case LIVE_FOLDERS_CONTACTS_FAVORITES: 3155 qb.setTables(mOpenHelper.getContactView()); 3156 qb.setProjectionMap(sLiveFoldersProjectionMap); 3157 qb.appendWhere(Contacts.STARRED + "=1"); 3158 break; 3159 3160 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 3161 qb.setTables(mOpenHelper.getContactView()); 3162 qb.setProjectionMap(sLiveFoldersProjectionMap); 3163 qb.appendWhere(sContactsInGroupSelect); 3164 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3165 break; 3166 3167 default: 3168 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 3169 sortOrder, limit); 3170 } 3171 3172 return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 3173 } 3174 3175 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 3176 String selection, String[] selectionArgs, String sortOrder, String groupBy, 3177 String limit) { 3178 if (projection != null && projection.length == 1 3179 && BaseColumns._COUNT.equals(projection[0])) { 3180 qb.setProjectionMap(sCountProjectionMap); 3181 } 3182 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 3183 sortOrder, limit); 3184 if (c != null) { 3185 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 3186 } 3187 return c; 3188 } 3189 3190 private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 3191 ContactLookupKey key = new ContactLookupKey(); 3192 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 3193 3194 long contactId = lookupContactIdBySourceIds(db, segments); 3195 if (contactId == -1) { 3196 contactId = lookupContactIdByDisplayNames(db, segments); 3197 } 3198 3199 return contactId; 3200 } 3201 3202 private interface LookupBySourceIdQuery { 3203 String TABLE = Tables.RAW_CONTACTS; 3204 3205 String COLUMNS[] = { 3206 RawContacts.CONTACT_ID, 3207 RawContacts.ACCOUNT_TYPE, 3208 RawContacts.ACCOUNT_NAME, 3209 RawContacts.SOURCE_ID 3210 }; 3211 3212 int CONTACT_ID = 0; 3213 int ACCOUNT_TYPE = 1; 3214 int ACCOUNT_NAME = 2; 3215 int SOURCE_ID = 3; 3216 } 3217 3218 private long lookupContactIdBySourceIds(SQLiteDatabase db, 3219 ArrayList<LookupKeySegment> segments) { 3220 int sourceIdCount = 0; 3221 for (int i = 0; i < segments.size(); i++) { 3222 LookupKeySegment segment = segments.get(i); 3223 if (segment.sourceIdLookup) { 3224 sourceIdCount++; 3225 } 3226 } 3227 3228 if (sourceIdCount == 0) { 3229 return -1; 3230 } 3231 3232 // First try sync ids 3233 StringBuilder sb = new StringBuilder(); 3234 sb.append(RawContacts.SOURCE_ID + " IN ("); 3235 for (int i = 0; i < segments.size(); i++) { 3236 LookupKeySegment segment = segments.get(i); 3237 if (segment.sourceIdLookup) { 3238 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 3239 sb.append(","); 3240 } 3241 } 3242 sb.setLength(sb.length() - 1); // Last comma 3243 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 3244 3245 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 3246 sb.toString(), null, null, null, null); 3247 try { 3248 while (c.moveToNext()) { 3249 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); 3250 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 3251 int accountHashCode = 3252 ContactLookupKey.getAccountHashCode(accountType, accountName); 3253 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 3254 for (int i = 0; i < segments.size(); i++) { 3255 LookupKeySegment segment = segments.get(i); 3256 if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode 3257 && segment.key.equals(sourceId)) { 3258 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 3259 break; 3260 } 3261 } 3262 } 3263 } finally { 3264 c.close(); 3265 } 3266 3267 return getMostReferencedContactId(segments); 3268 } 3269 3270 private interface LookupByDisplayNameQuery { 3271 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 3272 3273 String COLUMNS[] = { 3274 RawContacts.CONTACT_ID, 3275 RawContacts.ACCOUNT_TYPE, 3276 RawContacts.ACCOUNT_NAME, 3277 NameLookupColumns.NORMALIZED_NAME 3278 }; 3279 3280 int CONTACT_ID = 0; 3281 int ACCOUNT_TYPE = 1; 3282 int ACCOUNT_NAME = 2; 3283 int NORMALIZED_NAME = 3; 3284 } 3285 3286 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 3287 ArrayList<LookupKeySegment> segments) { 3288 int displayNameCount = 0; 3289 for (int i = 0; i < segments.size(); i++) { 3290 LookupKeySegment segment = segments.get(i); 3291 if (!segment.sourceIdLookup) { 3292 displayNameCount++; 3293 } 3294 } 3295 3296 if (displayNameCount == 0) { 3297 return -1; 3298 } 3299 3300 // First try sync ids 3301 StringBuilder sb = new StringBuilder(); 3302 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 3303 for (int i = 0; i < segments.size(); i++) { 3304 LookupKeySegment segment = segments.get(i); 3305 if (!segment.sourceIdLookup) { 3306 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 3307 sb.append(","); 3308 } 3309 } 3310 sb.setLength(sb.length() - 1); // Last comma 3311 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 3312 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 3313 3314 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 3315 sb.toString(), null, null, null, null); 3316 try { 3317 while (c.moveToNext()) { 3318 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); 3319 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 3320 int accountHashCode = 3321 ContactLookupKey.getAccountHashCode(accountType, accountName); 3322 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 3323 for (int i = 0; i < segments.size(); i++) { 3324 LookupKeySegment segment = segments.get(i); 3325 if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode 3326 && segment.key.equals(name)) { 3327 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 3328 break; 3329 } 3330 } 3331 } 3332 } finally { 3333 c.close(); 3334 } 3335 3336 return getMostReferencedContactId(segments); 3337 } 3338 3339 /** 3340 * Returns the contact ID that is mentioned the highest number of times. 3341 */ 3342 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 3343 Collections.sort(segments); 3344 3345 long bestContactId = -1; 3346 int bestRefCount = 0; 3347 3348 long contactId = -1; 3349 int count = 0; 3350 3351 int segmentCount = segments.size(); 3352 for (int i = 0; i < segmentCount; i++) { 3353 LookupKeySegment segment = segments.get(i); 3354 if (segment.contactId != -1) { 3355 if (segment.contactId == contactId) { 3356 count++; 3357 } else { 3358 if (count > bestRefCount) { 3359 bestContactId = contactId; 3360 bestRefCount = count; 3361 } 3362 contactId = segment.contactId; 3363 count = 1; 3364 } 3365 } 3366 } 3367 if (count > bestRefCount) { 3368 return contactId; 3369 } else { 3370 return bestContactId; 3371 } 3372 } 3373 3374 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, String[] projection) { 3375 String contactView = mOpenHelper.getContactView(); 3376 boolean needsPresence = mOpenHelper.isInProjection(projection, Contacts.PRESENCE_STATUS, 3377 Contacts.PRESENCE_CUSTOM_STATUS); 3378 if (!needsPresence) { 3379 qb.setTables(contactView); 3380 qb.setProjectionMap(sContactsProjectionMap); 3381 } else { 3382 qb.setTables(contactView + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + " ON (" 3383 + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ") "); 3384 qb.setProjectionMap(sContactsWithPresenceProjectionMap); 3385 3386 } 3387 } 3388 3389 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 3390 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3391 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3392 if (!TextUtils.isEmpty(accountName)) { 3393 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 3394 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3395 + RawContacts.ACCOUNT_TYPE + "=" 3396 + DatabaseUtils.sqlEscapeString(accountType)); 3397 } else { 3398 qb.appendWhere("1"); 3399 } 3400 } 3401 3402 private String appendAccountToSelection(Uri uri, String selection) { 3403 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3404 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3405 if (!TextUtils.isEmpty(accountName)) { 3406 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 3407 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3408 + RawContacts.ACCOUNT_TYPE + "=" 3409 + DatabaseUtils.sqlEscapeString(accountType)); 3410 if (!TextUtils.isEmpty(selection)) { 3411 selectionSb.append(" AND ("); 3412 selectionSb.append(selection); 3413 selectionSb.append(')'); 3414 } 3415 return selectionSb.toString(); 3416 } else { 3417 return selection; 3418 } 3419 } 3420 3421 /** 3422 * Gets the value of the "limit" URI query parameter. 3423 * 3424 * @return A string containing a non-negative integer, or <code>null</code> if 3425 * the parameter is not set, or is set to an invalid value. 3426 */ 3427 private String getLimit(Uri url) { 3428 String limitParam = url.getQueryParameter("limit"); 3429 if (limitParam == null) { 3430 return null; 3431 } 3432 // make sure that the limit is a non-negative integer 3433 try { 3434 int l = Integer.parseInt(limitParam); 3435 if (l < 0) { 3436 Log.w(TAG, "Invalid limit parameter: " + limitParam); 3437 return null; 3438 } 3439 return String.valueOf(l); 3440 } catch (NumberFormatException ex) { 3441 Log.w(TAG, "Invalid limit parameter: " + limitParam); 3442 return null; 3443 } 3444 } 3445 3446 /** 3447 * Returns true if all the characters are meaningful as digits 3448 * in a phone number -- letters, digits, and a few punctuation marks. 3449 */ 3450 private boolean isPhoneNumber(CharSequence cons) { 3451 int len = cons.length(); 3452 3453 for (int i = 0; i < len; i++) { 3454 char c = cons.charAt(i); 3455 3456 if ((c >= '0') && (c <= '9')) { 3457 continue; 3458 } 3459 if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+') 3460 || (c == '#') || (c == '*')) { 3461 continue; 3462 } 3463 if ((c >= 'A') && (c <= 'Z')) { 3464 continue; 3465 } 3466 if ((c >= 'a') && (c <= 'z')) { 3467 continue; 3468 } 3469 3470 return false; 3471 } 3472 3473 return true; 3474 } 3475 3476 String getContactsRestrictions() { 3477 if (mOpenHelper.hasRestrictedAccess()) { 3478 return "1"; 3479 } else { 3480 return RawContacts.IS_RESTRICTED + "=0"; 3481 } 3482 } 3483 3484 public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { 3485 if (mOpenHelper.hasRestrictedAccess()) { 3486 return "1"; 3487 } else { 3488 return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS 3489 + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; 3490 } 3491 } 3492 3493 @Override 3494 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 3495 int match = sUriMatcher.match(uri); 3496 switch (match) { 3497 case CONTACTS_PHOTO: 3498 if (!"r".equals(mode)) { 3499 throw new FileNotFoundException("Mode " + mode + " not supported."); 3500 } 3501 3502 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3503 3504 String sql = 3505 "SELECT " + Photo.PHOTO + " FROM " + mOpenHelper.getDataView() + 3506 " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID 3507 + " AND " + RawContacts.CONTACT_ID + "=" + contactId; 3508 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 3509 return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null); 3510 3511 default: 3512 throw new FileNotFoundException("No file at: " + uri); 3513 } 3514 } 3515 3516 3517 3518 /** 3519 * An implementation of EntityIterator that joins the contacts and data tables 3520 * and consumes all the data rows for a contact in order to build the Entity for a contact. 3521 */ 3522 private static class ContactsEntityIterator implements EntityIterator { 3523 private final Cursor mEntityCursor; 3524 private volatile boolean mIsClosed; 3525 3526 private static final String[] DATA_KEYS = new String[]{ 3527 Data.DATA1, 3528 Data.DATA2, 3529 Data.DATA3, 3530 Data.DATA4, 3531 Data.DATA5, 3532 Data.DATA6, 3533 Data.DATA7, 3534 Data.DATA8, 3535 Data.DATA9, 3536 Data.DATA10, 3537 Data.DATA11, 3538 Data.DATA12, 3539 Data.DATA13, 3540 Data.DATA14, 3541 Data.DATA15, 3542 Data.SYNC1, 3543 Data.SYNC2, 3544 Data.SYNC3, 3545 Data.SYNC4}; 3546 3547 private static final String[] PROJECTION = new String[]{ 3548 RawContacts.ACCOUNT_NAME, 3549 RawContacts.ACCOUNT_TYPE, 3550 RawContacts.SOURCE_ID, 3551 RawContacts.VERSION, 3552 RawContacts.DIRTY, 3553 Data._ID, 3554 Data.RES_PACKAGE, 3555 Data.MIMETYPE, 3556 Data.DATA1, 3557 Data.DATA2, 3558 Data.DATA3, 3559 Data.DATA4, 3560 Data.DATA5, 3561 Data.DATA6, 3562 Data.DATA7, 3563 Data.DATA8, 3564 Data.DATA9, 3565 Data.DATA10, 3566 Data.DATA11, 3567 Data.DATA12, 3568 Data.DATA13, 3569 Data.DATA14, 3570 Data.DATA15, 3571 Data.SYNC1, 3572 Data.SYNC2, 3573 Data.SYNC3, 3574 Data.SYNC4, 3575 Data.RAW_CONTACT_ID, 3576 Data.IS_PRIMARY, 3577 Data.IS_SUPER_PRIMARY, 3578 Data.DATA_VERSION, 3579 GroupMembership.GROUP_SOURCE_ID, 3580 RawContacts.SYNC1, 3581 RawContacts.SYNC2, 3582 RawContacts.SYNC3, 3583 RawContacts.SYNC4, 3584 RawContacts.DELETED, 3585 RawContacts.CONTACT_ID, 3586 RawContacts.STARRED}; 3587 3588 private static final int COLUMN_ACCOUNT_NAME = 0; 3589 private static final int COLUMN_ACCOUNT_TYPE = 1; 3590 private static final int COLUMN_SOURCE_ID = 2; 3591 private static final int COLUMN_VERSION = 3; 3592 private static final int COLUMN_DIRTY = 4; 3593 private static final int COLUMN_DATA_ID = 5; 3594 private static final int COLUMN_RES_PACKAGE = 6; 3595 private static final int COLUMN_MIMETYPE = 7; 3596 private static final int COLUMN_DATA1 = 8; 3597 private static final int COLUMN_RAW_CONTACT_ID = 27; 3598 private static final int COLUMN_IS_PRIMARY = 28; 3599 private static final int COLUMN_IS_SUPER_PRIMARY = 29; 3600 private static final int COLUMN_DATA_VERSION = 30; 3601 private static final int COLUMN_GROUP_SOURCE_ID = 31; 3602 private static final int COLUMN_SYNC1 = 32; 3603 private static final int COLUMN_SYNC2 = 33; 3604 private static final int COLUMN_SYNC3 = 34; 3605 private static final int COLUMN_SYNC4 = 35; 3606 private static final int COLUMN_DELETED = 36; 3607 private static final int COLUMN_CONTACT_ID = 37; 3608 private static final int COLUMN_STARRED = 38; 3609 3610 public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri, 3611 String selection, String[] selectionArgs, String sortOrder) { 3612 mIsClosed = false; 3613 3614 final String updatedSortOrder = (sortOrder == null) 3615 ? Data.RAW_CONTACT_ID 3616 : (Data.RAW_CONTACT_ID + "," + sortOrder); 3617 3618 final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase(); 3619 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3620 qb.setTables(Tables.CONTACT_ENTITIES); 3621 if (contactsIdString != null) { 3622 qb.appendWhere(Data.RAW_CONTACT_ID + "=" + contactsIdString); 3623 } 3624 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3625 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3626 if (!TextUtils.isEmpty(accountName)) { 3627 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 3628 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3629 + RawContacts.ACCOUNT_TYPE + "=" 3630 + DatabaseUtils.sqlEscapeString(accountType)); 3631 } 3632 mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs, 3633 null, null, updatedSortOrder); 3634 mEntityCursor.moveToFirst(); 3635 } 3636 3637 public void reset() throws RemoteException { 3638 if (mIsClosed) { 3639 throw new IllegalStateException("calling reset() when the iterator is closed"); 3640 } 3641 mEntityCursor.moveToFirst(); 3642 } 3643 3644 public void close() { 3645 if (mIsClosed) { 3646 throw new IllegalStateException("closing when already closed"); 3647 } 3648 mIsClosed = true; 3649 mEntityCursor.close(); 3650 } 3651 3652 public boolean hasNext() throws RemoteException { 3653 if (mIsClosed) { 3654 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 3655 } 3656 3657 return !mEntityCursor.isAfterLast(); 3658 } 3659 3660 public Entity next() throws RemoteException { 3661 if (mIsClosed) { 3662 throw new IllegalStateException("calling next() when the iterator is closed"); 3663 } 3664 if (!hasNext()) { 3665 throw new IllegalStateException("you may only call next() if hasNext() is true"); 3666 } 3667 3668 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 3669 3670 final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID); 3671 3672 // we expect the cursor is already at the row we need to read from 3673 ContentValues contactValues = new ContentValues(); 3674 contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 3675 contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 3676 contactValues.put(RawContacts._ID, rawContactId); 3677 contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY)); 3678 contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION)); 3679 contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 3680 contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1)); 3681 contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2)); 3682 contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3)); 3683 contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4)); 3684 contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED)); 3685 contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID)); 3686 contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED)); 3687 Entity contact = new Entity(contactValues); 3688 3689 // read data rows until the contact id changes 3690 do { 3691 if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) { 3692 break; 3693 } 3694 // add the data to to the contact 3695 ContentValues dataValues = new ContentValues(); 3696 dataValues.put(Data._ID, c.getString(COLUMN_DATA_ID)); 3697 dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE)); 3698 dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE)); 3699 dataValues.put(Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY)); 3700 dataValues.put(Data.IS_SUPER_PRIMARY, c.getString(COLUMN_IS_SUPER_PRIMARY)); 3701 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 3702 if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) { 3703 dataValues.put(GroupMembership.GROUP_SOURCE_ID, 3704 c.getString(COLUMN_GROUP_SOURCE_ID)); 3705 } 3706 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 3707 for (int i = 0; i < DATA_KEYS.length; i++) { 3708 final int columnIndex = i + COLUMN_DATA1; 3709 String key = DATA_KEYS[i]; 3710 if (c.isNull(columnIndex)) { 3711 // don't put anything 3712 } else if (c.isLong(columnIndex)) { 3713 dataValues.put(key, c.getLong(columnIndex)); 3714 } else if (c.isFloat(columnIndex)) { 3715 dataValues.put(key, c.getFloat(columnIndex)); 3716 } else if (c.isString(columnIndex)) { 3717 dataValues.put(key, c.getString(columnIndex)); 3718 } else if (c.isBlob(columnIndex)) { 3719 dataValues.put(key, c.getBlob(columnIndex)); 3720 } 3721 } 3722 contact.addSubValue(Data.CONTENT_URI, dataValues); 3723 } while (mEntityCursor.moveToNext()); 3724 3725 return contact; 3726 } 3727 } 3728 3729 /** 3730 * An implementation of EntityIterator that joins the contacts and data tables 3731 * and consumes all the data rows for a contact in order to build the Entity for a contact. 3732 */ 3733 private static class GroupsEntityIterator implements EntityIterator { 3734 private final Cursor mEntityCursor; 3735 private volatile boolean mIsClosed; 3736 3737 private static final String[] PROJECTION = new String[]{ 3738 Groups._ID, 3739 Groups.ACCOUNT_NAME, 3740 Groups.ACCOUNT_TYPE, 3741 Groups.SOURCE_ID, 3742 Groups.DIRTY, 3743 Groups.VERSION, 3744 Groups.RES_PACKAGE, 3745 Groups.TITLE, 3746 Groups.TITLE_RES, 3747 Groups.GROUP_VISIBLE, 3748 Groups.SYNC1, 3749 Groups.SYNC2, 3750 Groups.SYNC3, 3751 Groups.SYNC4, 3752 Groups.SYSTEM_ID, 3753 Groups.NOTES, 3754 Groups.DELETED}; 3755 3756 private static final int COLUMN_ID = 0; 3757 private static final int COLUMN_ACCOUNT_NAME = 1; 3758 private static final int COLUMN_ACCOUNT_TYPE = 2; 3759 private static final int COLUMN_SOURCE_ID = 3; 3760 private static final int COLUMN_DIRTY = 4; 3761 private static final int COLUMN_VERSION = 5; 3762 private static final int COLUMN_RES_PACKAGE = 6; 3763 private static final int COLUMN_TITLE = 7; 3764 private static final int COLUMN_TITLE_RES = 8; 3765 private static final int COLUMN_GROUP_VISIBLE = 9; 3766 private static final int COLUMN_SYNC1 = 10; 3767 private static final int COLUMN_SYNC2 = 11; 3768 private static final int COLUMN_SYNC3 = 12; 3769 private static final int COLUMN_SYNC4 = 13; 3770 private static final int COLUMN_SYSTEM_ID = 14; 3771 private static final int COLUMN_NOTES = 15; 3772 private static final int COLUMN_DELETED = 16; 3773 3774 public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri, 3775 String selection, String[] selectionArgs, String sortOrder) { 3776 mIsClosed = false; 3777 3778 final String updatedSortOrder = (sortOrder == null) 3779 ? Groups._ID 3780 : (Groups._ID + "," + sortOrder); 3781 3782 final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase(); 3783 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3784 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3785 qb.setProjectionMap(sGroupsProjectionMap); 3786 if (groupIdString != null) { 3787 qb.appendWhere(Groups._ID + "=" + groupIdString); 3788 } 3789 final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME); 3790 final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE); 3791 if (!TextUtils.isEmpty(accountName)) { 3792 qb.appendWhere(Groups.ACCOUNT_NAME + "=" 3793 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3794 + Groups.ACCOUNT_TYPE + "=" 3795 + DatabaseUtils.sqlEscapeString(accountType)); 3796 } 3797 mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs, 3798 null, null, updatedSortOrder); 3799 mEntityCursor.moveToFirst(); 3800 } 3801 3802 public void close() { 3803 if (mIsClosed) { 3804 throw new IllegalStateException("closing when already closed"); 3805 } 3806 mIsClosed = true; 3807 mEntityCursor.close(); 3808 } 3809 3810 public boolean hasNext() throws RemoteException { 3811 if (mIsClosed) { 3812 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 3813 } 3814 3815 return !mEntityCursor.isAfterLast(); 3816 } 3817 3818 public void reset() throws RemoteException { 3819 if (mIsClosed) { 3820 throw new IllegalStateException("calling reset() when the iterator is closed"); 3821 } 3822 mEntityCursor.moveToFirst(); 3823 } 3824 3825 public Entity next() throws RemoteException { 3826 if (mIsClosed) { 3827 throw new IllegalStateException("calling next() when the iterator is closed"); 3828 } 3829 if (!hasNext()) { 3830 throw new IllegalStateException("you may only call next() if hasNext() is true"); 3831 } 3832 3833 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 3834 3835 final long groupId = c.getLong(COLUMN_ID); 3836 3837 // we expect the cursor is already at the row we need to read from 3838 ContentValues groupValues = new ContentValues(); 3839 groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 3840 groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 3841 groupValues.put(Groups._ID, groupId); 3842 groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY)); 3843 groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION)); 3844 groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 3845 groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE)); 3846 groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE)); 3847 groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES)); 3848 groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE)); 3849 groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1)); 3850 groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2)); 3851 groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3)); 3852 groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4)); 3853 groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID)); 3854 groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED)); 3855 groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES)); 3856 Entity group = new Entity(groupValues); 3857 3858 mEntityCursor.moveToNext(); 3859 3860 return group; 3861 } 3862 } 3863 3864 @Override 3865 public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, 3866 String sortOrder) { 3867 waitForAccess(); 3868 3869 final int match = sUriMatcher.match(uri); 3870 switch (match) { 3871 case RAW_CONTACTS: 3872 case RAW_CONTACTS_ID: 3873 String contactsIdString = null; 3874 if (match == RAW_CONTACTS_ID) { 3875 contactsIdString = uri.getPathSegments().get(1); 3876 } 3877 3878 return new ContactsEntityIterator(this, contactsIdString, 3879 uri, selection, selectionArgs, sortOrder); 3880 case GROUPS: 3881 case GROUPS_ID: 3882 String idString = null; 3883 if (match == GROUPS_ID) { 3884 idString = uri.getPathSegments().get(1); 3885 } 3886 3887 return new GroupsEntityIterator(this, idString, 3888 uri, selection, selectionArgs, sortOrder); 3889 default: 3890 throw new UnsupportedOperationException("Unknown uri: " + uri); 3891 } 3892 } 3893 3894 @Override 3895 public String getType(Uri uri) { 3896 final int match = sUriMatcher.match(uri); 3897 switch (match) { 3898 case CONTACTS: 3899 case CONTACTS_LOOKUP: 3900 return Contacts.CONTENT_TYPE; 3901 case CONTACTS_ID: 3902 case CONTACTS_LOOKUP_ID: 3903 return Contacts.CONTENT_ITEM_TYPE; 3904 case RAW_CONTACTS: 3905 return RawContacts.CONTENT_TYPE; 3906 case RAW_CONTACTS_ID: 3907 return RawContacts.CONTENT_ITEM_TYPE; 3908 case DATA_ID: 3909 return mOpenHelper.getDataMimeType(ContentUris.parseId(uri)); 3910 case AGGREGATION_EXCEPTIONS: 3911 return AggregationExceptions.CONTENT_TYPE; 3912 case AGGREGATION_EXCEPTION_ID: 3913 return AggregationExceptions.CONTENT_ITEM_TYPE; 3914 case SETTINGS: 3915 return Settings.CONTENT_TYPE; 3916 case AGGREGATION_SUGGESTIONS: 3917 return Contacts.CONTENT_TYPE; 3918 case SEARCH_SUGGESTIONS: 3919 return SearchManager.SUGGEST_MIME_TYPE; 3920 case SEARCH_SHORTCUT: 3921 return SearchManager.SHORTCUT_MIME_TYPE; 3922 default: 3923 return mLegacyApiSupport.getType(uri); 3924 } 3925 } 3926 3927 private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) { 3928 if (displayName != null) { 3929 mRawContactDisplayNameUpdate.bindString(1, displayName); 3930 } else { 3931 mRawContactDisplayNameUpdate.bindNull(1); 3932 } 3933 mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource); 3934 mRawContactDisplayNameUpdate.bindLong(3, rawContactId); 3935 mRawContactDisplayNameUpdate.execute(); 3936 } 3937 3938 /** 3939 * Checks the {@link Data#MARK_AS_DIRTY} query parameter. 3940 * 3941 * Returns true if the parameter is missing or is either "true" or "1". 3942 */ 3943 private boolean shouldMarkRawContactAsDirty(Uri uri) { 3944 if (mImportMode) { 3945 return false; 3946 } 3947 3948 String param = uri.getQueryParameter(Data.MARK_AS_DIRTY); 3949 return param == null || (!param.equalsIgnoreCase("false") && !param.equals("0")); 3950 } 3951 3952 /** 3953 * Sets the {@link RawContacts#DIRTY} for the specified raw contact. 3954 */ 3955 private void setRawContactDirty(long rawContactId) { 3956 mRawContactDirtyUpdate.bindLong(1, rawContactId); 3957 mRawContactDirtyUpdate.execute(); 3958 } 3959 3960 /** 3961 * Checks the {@link Groups#MARK_AS_DIRTY} query parameter. 3962 * 3963 * Returns true if the parameter is missing or is either "true" or "1". 3964 */ 3965 private boolean shouldMarkGroupAsDirty(Uri uri) { 3966 if (mImportMode) { 3967 return false; 3968 } 3969 3970 return readBooleanQueryParameter(uri, Groups.MARK_AS_DIRTY, true); 3971 } 3972 3973 /* 3974 * Sets the given dataId record in the "data" table to primary, and resets all data records of 3975 * the same mimetype and under the same contact to not be primary. 3976 * 3977 * @param dataId the id of the data record to be set to primary. 3978 */ 3979 private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) { 3980 mSetPrimaryStatement.bindLong(1, dataId); 3981 mSetPrimaryStatement.bindLong(2, mimeTypeId); 3982 mSetPrimaryStatement.bindLong(3, rawContactId); 3983 mSetPrimaryStatement.execute(); 3984 } 3985 3986 /* 3987 * Sets the given dataId record in the "data" table to "super primary", and resets all data 3988 * records of the same mimetype and under the same aggregate to not be "super primary". 3989 * 3990 * @param dataId the id of the data record to be set to primary. 3991 */ 3992 private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) { 3993 mSetSuperPrimaryStatement.bindLong(1, dataId); 3994 mSetSuperPrimaryStatement.bindLong(2, mimeTypeId); 3995 mSetSuperPrimaryStatement.bindLong(3, rawContactId); 3996 mSetSuperPrimaryStatement.execute(); 3997 } 3998 3999 private void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 4000 sb.append("(SELECT DISTINCT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS 4001 + " JOIN name_lookup ON(" + RawContactsColumns.CONCRETE_ID + "=raw_contact_id)" 4002 + " WHERE normalized_name GLOB '"); 4003 sb.append(NameNormalizer.normalize(filterParam)); 4004 sb.append("*')"); 4005 } 4006 4007 public String getRawContactsByFilterAsNestedQuery(String filterParam) { 4008 StringBuilder sb = new StringBuilder(); 4009 appendRawContactsByFilterAsNestedQuery(sb, filterParam, null); 4010 return sb.toString(); 4011 } 4012 4013 public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam, 4014 String limit) { 4015 appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit); 4016 } 4017 4018 private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, 4019 String limit) { 4020 sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '"); 4021 sb.append(normalizedName); 4022 sb.append("*'"); 4023 if (limit != null) { 4024 sb.append(" LIMIT ").append(limit); 4025 } 4026 sb.append(")"); 4027 } 4028 4029 /** 4030 * Inserts an argument at the beginning of the selection arg list. 4031 */ 4032 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 4033 if (selectionArgs == null) { 4034 return new String[] {arg}; 4035 } else { 4036 int newLength = selectionArgs.length + 1; 4037 String[] newSelectionArgs = new String[newLength]; 4038 newSelectionArgs[0] = arg; 4039 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 4040 return newSelectionArgs; 4041 } 4042 } 4043 4044 protected Account getDefaultAccount() { 4045 AccountManager accountManager = AccountManager.get(getContext()); 4046 try { 4047 Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE, 4048 new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult(); 4049 if (accounts != null && accounts.length > 0) { 4050 return accounts[0]; 4051 } 4052 } catch (Throwable e) { 4053 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 4054 } 4055 return null; 4056 } 4057} 4058