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