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