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