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