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