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