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