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