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