ContactsProvider2.java revision 035b4cc204be2641079a0b04e9ee9791a8f8248b
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.ContactOptionsColumns; 24import com.android.providers.contacts.OpenHelper.DataColumns; 25import com.android.providers.contacts.OpenHelper.GroupsColumns; 26import com.android.providers.contacts.OpenHelper.MimetypesColumns; 27import com.android.providers.contacts.OpenHelper.PhoneLookupColumns; 28import com.android.providers.contacts.OpenHelper.Tables; 29 30import android.accounts.Account; 31import android.content.ContentProvider; 32import android.content.ContentProviderOperation; 33import android.content.ContentProviderResult; 34import android.content.ContentUris; 35import android.content.ContentValues; 36import android.content.Context; 37import android.content.Entity; 38import android.content.EntityIterator; 39import android.content.OperationApplicationException; 40import android.content.UriMatcher; 41import android.content.pm.PackageManager; 42import android.database.Cursor; 43import android.database.DatabaseUtils; 44import android.database.sqlite.SQLiteCursor; 45import android.database.sqlite.SQLiteDatabase; 46import android.database.sqlite.SQLiteQueryBuilder; 47import android.database.sqlite.SQLiteStatement; 48import android.net.Uri; 49import android.os.Binder; 50import android.os.RemoteException; 51import android.provider.BaseColumns; 52import android.provider.ContactsContract; 53import android.provider.Contacts.ContactMethods; 54import android.provider.ContactsContract.Aggregates; 55import android.provider.ContactsContract.AggregationExceptions; 56import android.provider.ContactsContract.CommonDataKinds; 57import android.provider.ContactsContract.Contacts; 58import android.provider.ContactsContract.Data; 59import android.provider.ContactsContract.Groups; 60import android.provider.ContactsContract.Presence; 61import android.provider.ContactsContract.RestrictionExceptions; 62import android.provider.ContactsContract.Aggregates.AggregationSuggestions; 63import android.provider.ContactsContract.CommonDataKinds.Email; 64import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 65import android.provider.ContactsContract.CommonDataKinds.Phone; 66import android.provider.ContactsContract.CommonDataKinds.Postal; 67import android.provider.ContactsContract.CommonDataKinds.StructuredName; 68import android.telephony.PhoneNumberUtils; 69import android.text.TextUtils; 70import android.util.Log; 71 72import java.util.ArrayList; 73import java.util.HashMap; 74 75/** 76 * Contacts content provider. The contract between this provider and applications 77 * is defined in {@link ContactsContract}. 78 */ 79public class ContactsProvider2 extends ContentProvider { 80 // TODO: clean up debug tag and rename this class 81 private static final String TAG = "ContactsProvider ~~~~"; 82 83 // TODO: define broadcastreceiver to catch app uninstalls that should clear exceptions 84 // TODO: carefully prevent all incoming nested queries; they can be gaping security holes 85 // TODO: check for restricted flag during insert(), update(), and delete() calls 86 87 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 88 89 private static final String STREQUENT_ORDER_BY = Aggregates.STARRED + " DESC, " 90 + Aggregates.TIMES_CONTACTED + " DESC, " 91 + Aggregates.DISPLAY_NAME + " ASC"; 92 private static final String STREQUENT_LIMIT = 93 "(SELECT COUNT(1) FROM " + Tables.AGGREGATES + " WHERE " 94 + Aggregates.STARRED + "=1) + 25"; 95 96 private static final int AGGREGATES = 1000; 97 private static final int AGGREGATES_ID = 1001; 98 private static final int AGGREGATES_DATA = 1002; 99 private static final int AGGREGATES_SUMMARY = 1003; 100 private static final int AGGREGATES_SUMMARY_ID = 1004; 101 private static final int AGGREGATES_SUMMARY_FILTER = 1005; 102 private static final int AGGREGATES_SUMMARY_STREQUENT = 1006; 103 private static final int AGGREGATES_SUMMARY_STREQUENT_FILTER = 1007; 104 105 private static final int CONTACTS = 2002; 106 private static final int CONTACTS_ID = 2003; 107 private static final int CONTACTS_DATA = 2004; 108 private static final int CONTACTS_FILTER_EMAIL = 2005; 109 110 private static final int DATA = 3000; 111 private static final int DATA_ID = 3001; 112 private static final int PHONES = 3002; 113 private static final int PHONES_FILTER = 3003; 114 private static final int POSTALS = 3004; 115 116 private static final int PHONE_LOOKUP = 4000; 117 118 private static final int AGGREGATION_EXCEPTIONS = 6000; 119 private static final int AGGREGATION_EXCEPTION_ID = 6001; 120 121 private static final int PRESENCE = 7000; 122 private static final int PRESENCE_ID = 7001; 123 124 private static final int AGGREGATION_SUGGESTIONS = 8000; 125 126 private static final int RESTRICTION_EXCEPTIONS = 9000; 127 128 private static final int GROUPS = 10000; 129 private static final int GROUPS_ID = 10001; 130 private static final int GROUPS_SUMMARY = 10003; 131 132 private interface Projections { 133 public static final String[] PROJ_CONTACTS = new String[] { 134 ContactsColumns.CONCRETE_ID, 135 }; 136 137 public static final String[] PROJ_DATA_CONTACTS = new String[] { 138 ContactsColumns.CONCRETE_ID, 139 DataColumns.CONCRETE_ID, 140 Contacts.AGGREGATE_ID, 141 ContactsColumns.PACKAGE_ID, 142 Contacts.IS_RESTRICTED, 143 Data.MIMETYPE, 144 }; 145 146 public static final int COL_CONTACT_ID = 0; 147 public static final int COL_DATA_ID = 1; 148 public static final int COL_AGGREGATE_ID = 2; 149 public static final int COL_PACKAGE_ID = 3; 150 public static final int COL_IS_RESTRICTED = 4; 151 public static final int COL_MIMETYPE = 5; 152 153 public static final String[] PROJ_DATA_AGGREGATES = new String[] { 154 ContactsColumns.CONCRETE_ID, 155 DataColumns.CONCRETE_ID, 156 AggregatesColumns.CONCRETE_ID, 157 MimetypesColumns.CONCRETE_ID, 158 Phone.NUMBER, 159 Email.DATA, 160 AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID, 161 AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID, 162 AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID, 163 AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID, 164 }; 165 166 public static final int COL_MIMETYPE_ID = 3; 167 public static final int COL_PHONE_NUMBER = 4; 168 public static final int COL_EMAIL_DATA = 5; 169 public static final int COL_OPTIMAL_PHONE_ID = 6; 170 public static final int COL_FALLBACK_PHONE_ID = 7; 171 public static final int COL_OPTIMAL_EMAIL_ID = 8; 172 public static final int COL_FALLBACK_EMAIL_ID = 9; 173 174 } 175 176 /** Default for the maximum number of returned aggregation suggestions. */ 177 private static final int DEFAULT_MAX_SUGGESTIONS = 5; 178 179 /** Contains just the contacts columns */ 180 private static final HashMap<String, String> sAggregatesProjectionMap; 181 /** Contains the aggregate columns along with primary phone */ 182 private static final HashMap<String, String> sAggregatesSummaryProjectionMap; 183 /** Contains the data, contacts, and aggregate columns, for joined tables. */ 184 private static final HashMap<String, String> sDataContactsAggregateProjectionMap; 185 /** Contains just the contacts columns */ 186 private static final HashMap<String, String> sContactsProjectionMap; 187 /** Contains just the data columns */ 188 private static final HashMap<String, String> sDataProjectionMap; 189 /** Contains the data and contacts columns, for joined tables */ 190 private static final HashMap<String, String> sDataContactsProjectionMap; 191 /** Contains the just the {@link Groups} columns */ 192 private static final HashMap<String, String> sGroupsProjectionMap; 193 /** Contains {@link Groups} columns along with summary details */ 194 private static final HashMap<String, String> sGroupsSummaryProjectionMap; 195 /** Contains the just the agg_exceptions columns */ 196 private static final HashMap<String, String> sAggregationExceptionsProjectionMap; 197 /** Contains the just the {@link RestrictionExceptions} columns */ 198 private static final HashMap<String, String> sRestrictionExceptionsProjectionMap; 199 200 /** Sql select statement that returns the contact id associated with a data record. */ 201 private static final String sNestedContactIdSelect; 202 /** Sql select statement that returns the mimetype id associated with a data record. */ 203 private static final String sNestedMimetypeSelect; 204 /** Sql select statement that returns the aggregate id associated with a contact record. */ 205 private static final String sNestedAggregateIdSelect; 206 /** Sql select statement that returns a list of contact ids associated with an aggregate record. */ 207 private static final String sNestedContactIdListSelect; 208 /** Sql where statement used to match all the data records that need to be updated when a new 209 * "primary" is selected.*/ 210 private static final String sSetPrimaryWhere; 211 /** Sql where statement used to match all the data records that need to be updated when a new 212 * "super primary" is selected.*/ 213 private static final String sSetSuperPrimaryWhere; 214 /** Precompiled sql statement for setting a data record to the primary. */ 215 private SQLiteStatement mSetPrimaryStatement; 216 /** Precomipled sql statement for setting a data record to the super primary. */ 217 private SQLiteStatement mSetSuperPrimaryStatement; 218 219 private static final String GTALK_PROTOCOL_STRING = ContactMethods 220 .encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK); 221 222 static { 223 // Contacts URI matching table 224 final UriMatcher matcher = sUriMatcher; 225 matcher.addURI(ContactsContract.AUTHORITY, "aggregates", AGGREGATES); 226 matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#", AGGREGATES_ID); 227 matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#/data", AGGREGATES_DATA); 228 matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary", AGGREGATES_SUMMARY); 229 matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/#", AGGREGATES_SUMMARY_ID); 230 matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/filter/*", 231 AGGREGATES_SUMMARY_FILTER); 232 matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/strequent/", 233 AGGREGATES_SUMMARY_STREQUENT); 234 matcher.addURI(ContactsContract.AUTHORITY, "aggregates_summary/strequent/filter/*", 235 AGGREGATES_SUMMARY_STREQUENT_FILTER); 236 matcher.addURI(ContactsContract.AUTHORITY, "aggregates/#/suggestions", 237 AGGREGATION_SUGGESTIONS); 238 matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS); 239 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 240 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA); 241 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter_email/*", 242 CONTACTS_FILTER_EMAIL); 243 244 matcher.addURI(ContactsContract.AUTHORITY, "data", DATA); 245 matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID); 246 matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES); 247 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER); 248 matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS); 249 250 matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS); 251 matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID); 252 matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY); 253 254 matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP); 255 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions", 256 AGGREGATION_EXCEPTIONS); 257 matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*", 258 AGGREGATION_EXCEPTION_ID); 259 260 matcher.addURI(ContactsContract.AUTHORITY, "presence", PRESENCE); 261 matcher.addURI(ContactsContract.AUTHORITY, "presence/#", PRESENCE_ID); 262 263 matcher.addURI(ContactsContract.AUTHORITY, "restriction_exceptions", RESTRICTION_EXCEPTIONS); 264 265 HashMap<String, String> columns; 266 267 // Aggregates projection map 268 columns = new HashMap<String, String>(); 269 columns.put(Aggregates._ID, "aggregates._id AS _id"); 270 columns.put(Aggregates.DISPLAY_NAME, Aggregates.DISPLAY_NAME); 271 columns.put(Aggregates.LAST_TIME_CONTACTED, Aggregates.LAST_TIME_CONTACTED); 272 columns.put(Aggregates.TIMES_CONTACTED, Aggregates.TIMES_CONTACTED); 273 columns.put(Aggregates.STARRED, Aggregates.STARRED); 274 columns.put(Aggregates.IN_VISIBLE_GROUP, Aggregates.IN_VISIBLE_GROUP); 275 columns.put(Aggregates.PRIMARY_PHONE_ID, Aggregates.PRIMARY_PHONE_ID); 276 columns.put(Aggregates.PRIMARY_EMAIL_ID, Aggregates.PRIMARY_EMAIL_ID); 277 columns.put(Aggregates.CUSTOM_RINGTONE, Aggregates.CUSTOM_RINGTONE); 278 columns.put(Aggregates.SEND_TO_VOICEMAIL, Aggregates.SEND_TO_VOICEMAIL); 279 columns.put(AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID, 280 AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID); 281 columns.put(AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID, 282 AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID); 283 sAggregatesProjectionMap = columns; 284 285 // Aggregates primaries projection map. The overall presence status is 286 // the most-present value, as indicated by the largest value. 287 columns = new HashMap<String, String>(); 288 columns.putAll(sAggregatesProjectionMap); 289 columns.put(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE); 290 columns.put(CommonDataKinds.Phone.LABEL, CommonDataKinds.Phone.LABEL); 291 columns.put(CommonDataKinds.Phone.NUMBER, CommonDataKinds.Phone.NUMBER); 292 columns.put(Presence.PRESENCE_STATUS, "MAX(" + Presence.PRESENCE_STATUS + ")"); 293 sAggregatesSummaryProjectionMap = columns; 294 295 // Contacts projection map 296 columns = new HashMap<String, String>(); 297 columns.put(Contacts._ID, "contacts._id AS _id"); 298 columns.put(Contacts.PACKAGE, Contacts.PACKAGE); 299 columns.put(Contacts.AGGREGATE_ID, Contacts.AGGREGATE_ID); 300 columns.put(Contacts.ACCOUNT_NAME, Contacts.ACCOUNT_NAME); 301 columns.put(Contacts.ACCOUNT_TYPE, Contacts.ACCOUNT_TYPE); 302 columns.put(Contacts.SOURCE_ID, Contacts.SOURCE_ID); 303 columns.put(Contacts.VERSION, Contacts.VERSION); 304 columns.put(Contacts.DIRTY, Contacts.DIRTY); 305 sContactsProjectionMap = columns; 306 307 // Data projection map 308 columns = new HashMap<String, String>(); 309 columns.put(Data._ID, "data._id AS _id"); 310 columns.put(Data.CONTACT_ID, Data.CONTACT_ID); 311 columns.put(Data.MIMETYPE, Data.MIMETYPE); 312 columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 313 columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 314 columns.put(Data.DATA_VERSION, Data.DATA_VERSION); 315 columns.put(Data.DATA1, "data.data1 as data1"); 316 columns.put(Data.DATA2, "data.data2 as data2"); 317 columns.put(Data.DATA3, "data.data3 as data3"); 318 columns.put(Data.DATA4, "data.data4 as data4"); 319 columns.put(Data.DATA5, "data.data5 as data5"); 320 columns.put(Data.DATA6, "data.data6 as data6"); 321 columns.put(Data.DATA7, "data.data7 as data7"); 322 columns.put(Data.DATA8, "data.data8 as data8"); 323 columns.put(Data.DATA9, "data.data9 as data9"); 324 columns.put(Data.DATA10, "data.data10 as data10"); 325 // Mappings used for backwards compatibility. 326 columns.put("number", Phone.NUMBER); 327 sDataProjectionMap = columns; 328 329 // Data and contacts projection map for joins. _id comes from the data table 330 columns = new HashMap<String, String>(); 331 columns.putAll(sContactsProjectionMap); 332 columns.putAll(sDataProjectionMap); // _id will be replaced with the one from data 333 columns.put(Data.CONTACT_ID, DataColumns.CONCRETE_CONTACT_ID); 334 sDataContactsProjectionMap = columns; 335 336 // Data and contacts projection map for joins. _id comes from the data table 337 columns = new HashMap<String, String>(); 338 columns.putAll(sAggregatesProjectionMap); 339 columns.putAll(sContactsProjectionMap); // 340 columns.putAll(sDataProjectionMap); // _id will be replaced with the one from data 341 columns.put(Data.CONTACT_ID, DataColumns.CONCRETE_CONTACT_ID); 342 sDataContactsAggregateProjectionMap = columns; 343 344 // Groups projection map 345 columns = new HashMap<String, String>(); 346 columns.put(Groups._ID, "groups._id AS _id"); 347 columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME); 348 columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE); 349 columns.put(Groups.PACKAGE, Groups.PACKAGE); 350 columns.put(Groups.PACKAGE_ID, GroupsColumns.CONCRETE_PACKAGE_ID); 351 columns.put(Groups.TITLE, Groups.TITLE); 352 columns.put(Groups.TITLE_RESOURCE, Groups.TITLE_RESOURCE); 353 columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE); 354 sGroupsProjectionMap = columns; 355 356 // Contacts and groups projection map 357 columns = new HashMap<String, String>(); 358 columns.putAll(sGroupsProjectionMap); 359 360 columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + AggregatesColumns.CONCRETE_ID 361 + ") FROM " + Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES + " WHERE " 362 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 363 + ") AS " + Groups.SUMMARY_COUNT); 364 365 columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT " 366 + AggregatesColumns.CONCRETE_ID + ") FROM " 367 + Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES + " WHERE " 368 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 369 + " AND " + Clauses.HAS_PRIMARY_PHONE + ") AS " + Groups.SUMMARY_WITH_PHONES); 370 371 sGroupsSummaryProjectionMap = columns; 372 373 // Aggregate exception projection map 374 columns = new HashMap<String, String>(); 375 columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id"); 376 columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE); 377 columns.put(AggregationExceptions.AGGREGATE_ID, 378 "contacts1." + Contacts.AGGREGATE_ID + " AS " + AggregationExceptions.AGGREGATE_ID); 379 columns.put(AggregationExceptions.CONTACT_ID, AggregationExceptionColumns.CONTACT_ID2); 380 sAggregationExceptionsProjectionMap = columns; 381 382 // Restriction exception projection map 383 columns = new HashMap<String, String>(); 384 columns.put(RestrictionExceptions.PACKAGE_PROVIDER, RestrictionExceptions.PACKAGE_PROVIDER); 385 columns.put(RestrictionExceptions.PACKAGE_CLIENT, RestrictionExceptions.PACKAGE_CLIENT); 386 columns.put(RestrictionExceptions.ALLOW_ACCESS, "1"); // Access granted if row returned 387 sRestrictionExceptionsProjectionMap = columns; 388 389 sNestedContactIdSelect = "SELECT " + Data.CONTACT_ID + " FROM " + Tables.DATA + " WHERE " 390 + Data._ID + "=?"; 391 sNestedMimetypeSelect = "SELECT " + DataColumns.MIMETYPE_ID + " FROM " + Tables.DATA 392 + " WHERE " + Data._ID + "=?"; 393 sNestedAggregateIdSelect = "SELECT " + Contacts.AGGREGATE_ID + " FROM " + Tables.CONTACTS 394 + " WHERE " + Contacts._ID + "=(" + sNestedContactIdSelect + ")"; 395 sNestedContactIdListSelect = "SELECT " + Contacts._ID + " FROM " + Tables.CONTACTS 396 + " WHERE " + Contacts.AGGREGATE_ID + "=(" + sNestedAggregateIdSelect + ")"; 397 sSetPrimaryWhere = Data.CONTACT_ID + "=(" + sNestedContactIdSelect + ") AND " 398 + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")"; 399 sSetSuperPrimaryWhere = Data.CONTACT_ID + " IN (" + sNestedContactIdListSelect + ") AND " 400 + DataColumns.MIMETYPE_ID + "=(" + sNestedMimetypeSelect + ")"; 401 } 402 403 private final ContactAggregationScheduler mAggregationScheduler; 404 private OpenHelper mOpenHelper; 405 406 private ContactAggregator mContactAggregator; 407 private NameSplitter mNameSplitter; 408 409 public ContactsProvider2() { 410 this(new ContactAggregationScheduler()); 411 } 412 413 /** 414 * Constructor for testing. 415 */ 416 /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) { 417 mAggregationScheduler = scheduler; 418 } 419 420 @Override 421 public boolean onCreate() { 422 final Context context = getContext(); 423 mOpenHelper = getOpenHelper(context); 424 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 425 426 mContactAggregator = new ContactAggregator(context, mOpenHelper, mAggregationScheduler); 427 428 mSetPrimaryStatement = db.compileStatement( 429 "UPDATE " + Tables.DATA + " SET " + Data.IS_PRIMARY 430 + "=(_id=?) WHERE " + sSetPrimaryWhere); 431 mSetSuperPrimaryStatement = db.compileStatement( 432 "UPDATE " + Tables.DATA + " SET " + Data.IS_SUPER_PRIMARY 433 + "=(_id=?) WHERE " + sSetSuperPrimaryWhere); 434 435 mNameSplitter = new NameSplitter( 436 context.getString(com.android.internal.R.string.common_name_prefixes), 437 context.getString(com.android.internal.R.string.common_last_name_prefixes), 438 context.getString(com.android.internal.R.string.common_name_suffixes), 439 context.getString(com.android.internal.R.string.common_name_conjunctions)); 440 441 return (db != null); 442 } 443 444 /* Visible for testing */ 445 protected OpenHelper getOpenHelper(final Context context) { 446 return OpenHelper.getInstance(context); 447 } 448 449 @Override 450 protected void finalize() throws Throwable { 451 if (mContactAggregator != null) { 452 mContactAggregator.quit(); 453 } 454 455 super.finalize(); 456 } 457 458 /** 459 * Wipes all data from the contacts database. 460 */ 461 /* package */ void wipeData() { 462 mOpenHelper.wipeData(); 463 } 464 465 /** 466 * Called when a change has been made. 467 * 468 * @param uri the uri that the change was made to 469 */ 470 private void onChange(Uri uri) { 471 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null); 472 } 473 474 @Override 475 public boolean isTemporary() { 476 return false; 477 } 478 479 @Override 480 public Uri insert(Uri uri, ContentValues values) { 481 final int match = sUriMatcher.match(uri); 482 long id = 0; 483 switch (match) { 484 case AGGREGATES: { 485 id = insertAggregate(values); 486 break; 487 } 488 489 case CONTACTS: { 490 final Account account = readAccountFromQueryParams(uri); 491 id = insertContact(values, account); 492 break; 493 } 494 495 case CONTACTS_DATA: { 496 values.put(Data.CONTACT_ID, uri.getPathSegments().get(1)); 497 id = insertData(values); 498 break; 499 } 500 501 case DATA: { 502 id = insertData(values); 503 break; 504 } 505 506 case GROUPS: { 507 final Account account = readAccountFromQueryParams(uri); 508 id = insertGroup(values, account); 509 break; 510 } 511 512 case PRESENCE: { 513 id = insertPresence(values); 514 break; 515 } 516 517 default: 518 throw new UnsupportedOperationException("Unknown uri: " + uri); 519 } 520 521 if (id < 0) { 522 return null; 523 } 524 525 final Uri result = ContentUris.withAppendedId(uri, id); 526 onChange(result); 527 return result; 528 } 529 530 /** 531 * If account is non-null then store it in the values. If the account is already 532 * specified in the values then it must be consistent with the account, if it is non-null. 533 * @param values the ContentValues to read from and update 534 * @param account the explicitly provided Account 535 * @return false if the accounts are inconsistent 536 */ 537 private boolean resolveAccount(ContentValues values, Account account) { 538 // If either is specified then both must be specified. 539 final String accountName = values.getAsString(Contacts.ACCOUNT_NAME); 540 final String accountType = values.getAsString(Contacts.ACCOUNT_TYPE); 541 if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) { 542 final Account valuesAccount = new Account(accountName, accountType); 543 if (account != null && !valuesAccount.equals(account)) { 544 return false; 545 } 546 account = valuesAccount; 547 } 548 if (account != null) { 549 values.put(Contacts.ACCOUNT_NAME, account.mName); 550 values.put(Contacts.ACCOUNT_TYPE, account.mType); 551 } 552 return true; 553 } 554 555 /** 556 * Inserts an item in the aggregates table 557 * 558 * @param values the values for the new row 559 * @return the row ID of the newly created row 560 */ 561 private long insertAggregate(ContentValues values) { 562 throw new UnsupportedOperationException("Aggregates are created automatically"); 563 } 564 565 /** 566 * Inserts an item in the contacts table 567 * 568 * @param values the values for the new row 569 * @param account the account this contact should be associated with. may be null. 570 * @return the row ID of the newly created row 571 */ 572 private long insertContact(ContentValues values, Account account) { 573 /* 574 * The contact record is inserted in the contacts table, but it needs to 575 * be processed by the aggregator before it will be returned by the 576 * "aggregates" queries. 577 */ 578 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 579 580 ContentValues overriddenValues = new ContentValues(values); 581 overriddenValues.putNull(Contacts.AGGREGATE_ID); 582 if (!resolveAccount(overriddenValues, account)) { 583 return -1; 584 } 585 586 // Replace package with internal mapping 587 final String packageName = overriddenValues.getAsString(Contacts.PACKAGE); 588 overriddenValues.put(ContactsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 589 overriddenValues.remove(Contacts.PACKAGE); 590 591 long rowId = db.insert(Tables.CONTACTS, Contacts.AGGREGATE_ID, overriddenValues); 592 593 mContactAggregator.schedule(); 594 595 return rowId; 596 } 597 598 /** 599 * Inserts an item in the data table 600 * 601 * @param values the values for the new row 602 * @return the row ID of the newly created row 603 */ 604 private long insertData(ContentValues values) { 605 boolean success = false; 606 607 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 608 long id = 0; 609 db.beginTransaction(); 610 try { 611 long contactId = values.getAsLong(Data.CONTACT_ID); 612 613 // Replace mimetype with internal mapping 614 final String mimeType = values.getAsString(Data.MIMETYPE); 615 values.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType)); 616 values.remove(Data.MIMETYPE); 617 618 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 619 parseStructuredName(values); 620 } 621 622 // Insert the data row itself 623 id = db.insert(Tables.DATA, Data.DATA1, values); 624 625 // If it's a phone number add the normalized version to the lookup table 626 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { 627 final ContentValues phoneValues = new ContentValues(); 628 final String number = values.getAsString(Phone.NUMBER); 629 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, 630 PhoneNumberUtils.getStrippedReversed(number)); 631 phoneValues.put(PhoneLookupColumns.DATA_ID, id); 632 phoneValues.put(PhoneLookupColumns.CONTACT_ID, contactId); 633 db.insert(Tables.PHONE_LOOKUP, null, phoneValues); 634 } 635 636 mContactAggregator.markContactForAggregation(contactId); 637 638 db.setTransactionSuccessful(); 639 success = true; 640 } finally { 641 db.endTransaction(); 642 } 643 644 if (success) { 645 mContactAggregator.schedule(); 646 } 647 648 return id; 649 } 650 651 /** 652 * Delete the given {@link Data} row, fixing up any {@link Aggregates} 653 * primaries that reference it. 654 */ 655 private int deleteData(long dataId) { 656 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 657 658 final long mimePhone = mOpenHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE); 659 final long mimeEmail = mOpenHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE); 660 661 // Check to see if the data about to be deleted was a super-primary on 662 // the parent aggregate, and set flags to fix-up once deleted. 663 long aggId = -1; 664 long mimeId = -1; 665 String dataRaw = null; 666 boolean fixOptimal = false; 667 boolean fixFallback = false; 668 669 Cursor cursor = null; 670 try { 671 cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES, 672 Projections.PROJ_DATA_AGGREGATES, DataColumns.CONCRETE_ID + "=" + dataId, null, 673 null, null, null); 674 if (cursor.moveToFirst()) { 675 aggId = cursor.getLong(Projections.COL_AGGREGATE_ID); 676 mimeId = cursor.getLong(Projections.COL_MIMETYPE_ID); 677 if (mimeId == mimePhone) { 678 dataRaw = cursor.getString(Projections.COL_PHONE_NUMBER); 679 fixOptimal = (cursor.getLong(Projections.COL_OPTIMAL_PHONE_ID) == dataId); 680 fixFallback = (cursor.getLong(Projections.COL_FALLBACK_PHONE_ID) == dataId); 681 } else if (mimeId == mimeEmail) { 682 dataRaw = cursor.getString(Projections.COL_EMAIL_DATA); 683 fixOptimal = (cursor.getLong(Projections.COL_OPTIMAL_EMAIL_ID) == dataId); 684 fixFallback = (cursor.getLong(Projections.COL_FALLBACK_EMAIL_ID) == dataId); 685 } 686 } 687 } finally { 688 if (cursor != null) { 689 cursor.close(); 690 cursor = null; 691 } 692 } 693 694 // Delete the requested data item. 695 int dataDeleted = db.delete(Tables.DATA, Data._ID + "=" + dataId, null); 696 697 // Fix-up any super-primary values that are now invalid. 698 if (fixOptimal || fixFallback) { 699 final ContentValues values = new ContentValues(); 700 final StringBuilder scoreClause = new StringBuilder(); 701 702 final String SCORE = "score"; 703 704 // Build scoring clause that will first pick data items under the 705 // same aggregate that have identical values, otherwise fall back to 706 // normal primary scoring from the member contacts. 707 scoreClause.append("(CASE WHEN "); 708 if (mimeId == mimePhone) { 709 scoreClause.append(Phone.NUMBER); 710 } else if (mimeId == mimeEmail) { 711 scoreClause.append(Email.DATA); 712 } 713 scoreClause.append("="); 714 DatabaseUtils.appendEscapedSQLString(scoreClause, dataRaw); 715 scoreClause.append(" THEN 2 ELSE " + Data.IS_PRIMARY + " END) AS " + SCORE); 716 717 final String[] PROJ_PRIMARY = new String[] { 718 DataColumns.CONCRETE_ID, 719 Contacts.IS_RESTRICTED, 720 ContactsColumns.PACKAGE_ID, 721 scoreClause.toString(), 722 }; 723 724 final int COL_DATA_ID = 0; 725 final int COL_IS_RESTRICTED = 1; 726 final int COL_PACKAGE_ID = 2; 727 final int COL_SCORE = 3; 728 729 cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_AGGREGATES, PROJ_PRIMARY, 730 AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND " + DataColumns.MIMETYPE_ID 731 + "=" + mimeId, null, null, null, SCORE); 732 733 if (fixOptimal) { 734 String colId = null; 735 String colPackageId = null; 736 if (mimeId == mimePhone) { 737 colId = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID; 738 colPackageId = AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID; 739 } else if (mimeId == mimeEmail) { 740 colId = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID; 741 colPackageId = AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID; 742 } 743 744 // Start by replacing with null, since fixOptimal told us that 745 // the previous aggregate values are bad. 746 values.putNull(colId); 747 values.putNull(colPackageId); 748 749 // When finding a new optimal primary, we only care about the 750 // highest scoring value, regardless of source. 751 if (cursor.moveToFirst()) { 752 final long newOptimal = cursor.getLong(COL_DATA_ID); 753 final long newOptimalPackage = cursor.getLong(COL_PACKAGE_ID); 754 755 if (newOptimal != 0) { 756 values.put(colId, newOptimal); 757 } 758 if (newOptimalPackage != 0) { 759 values.put(colPackageId, newOptimalPackage); 760 } 761 } 762 } 763 764 if (fixFallback) { 765 String colId = null; 766 if (mimeId == mimePhone) { 767 colId = AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID; 768 } else if (mimeId == mimeEmail) { 769 colId = AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID; 770 } 771 772 // Start by replacing with null, since fixFallback told us that 773 // the previous aggregate values are bad. 774 values.putNull(colId); 775 776 // The best fallback value is the highest scoring data item that 777 // hasn't been restricted. 778 cursor.moveToPosition(-1); 779 while (cursor.moveToNext()) { 780 final boolean isRestricted = (cursor.getInt(COL_IS_RESTRICTED) == 1); 781 if (!isRestricted) { 782 values.put(colId, cursor.getLong(COL_DATA_ID)); 783 break; 784 } 785 } 786 } 787 788 // Push through any aggregate updates we have 789 if (values.size() > 0) { 790 db.update(Tables.AGGREGATES, values, AggregatesColumns.CONCRETE_ID + "=" + aggId, 791 null); 792 } 793 } 794 795 return dataDeleted; 796 } 797 798 /** 799 * Parse the supplied display name, but only if the incoming values do not already contain 800 * structured name parts. 801 */ 802 private void parseStructuredName(ContentValues values) { 803 final String fullName = values.getAsString(StructuredName.DISPLAY_NAME); 804 if (TextUtils.isEmpty(fullName) 805 || !TextUtils.isEmpty(values.getAsString(StructuredName.PREFIX)) 806 || !TextUtils.isEmpty(values.getAsString(StructuredName.GIVEN_NAME)) 807 || !TextUtils.isEmpty(values.getAsString(StructuredName.MIDDLE_NAME)) 808 || !TextUtils.isEmpty(values.getAsString(StructuredName.FAMILY_NAME)) 809 || !TextUtils.isEmpty(values.getAsString(StructuredName.SUFFIX))) { 810 return; 811 } 812 813 NameSplitter.Name name = new NameSplitter.Name(); 814 mNameSplitter.split(name, fullName); 815 816 values.put(StructuredName.PREFIX, name.getPrefix()); 817 values.put(StructuredName.GIVEN_NAME, name.getGivenNames()); 818 values.put(StructuredName.MIDDLE_NAME, name.getMiddleName()); 819 values.put(StructuredName.FAMILY_NAME, name.getFamilyName()); 820 values.put(StructuredName.SUFFIX, name.getSuffix()); 821 } 822 823 /** 824 * Inserts an item in the groups table 825 */ 826 private long insertGroup(ContentValues values, Account account) { 827 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 828 829 ContentValues overriddenValues = new ContentValues(values); 830 if (!resolveAccount(overriddenValues, account)) { 831 return -1; 832 } 833 834 // Replace package with internal mapping 835 final String packageName = overriddenValues.getAsString(Groups.PACKAGE); 836 overriddenValues.put(Groups.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 837 overriddenValues.remove(Groups.PACKAGE); 838 839 return db.insert(Tables.GROUPS, Groups.TITLE, overriddenValues); 840 } 841 842 /** 843 * Inserts a presence update. 844 */ 845 private long insertPresence(ContentValues values) { 846 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 847 final String handle = values.getAsString(Presence.IM_HANDLE); 848 final String protocol = values.getAsString(Presence.IM_PROTOCOL); 849 if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) { 850 throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required"); 851 } 852 853 // TODO: generalize to allow other providers to match against email 854 boolean matchEmail = GTALK_PROTOCOL_STRING.equals(protocol); 855 856 String selection; 857 String[] selectionArgs; 858 if (matchEmail) { 859 selection = "(" + Clauses.WHERE_IM_MATCHES + ") OR (" + Clauses.WHERE_EMAIL_MATCHES + ")"; 860 selectionArgs = new String[] { protocol, handle, handle }; 861 } else { 862 selection = Clauses.WHERE_IM_MATCHES; 863 selectionArgs = new String[] { protocol, handle }; 864 } 865 866 long dataId = -1; 867 long aggId = -1; 868 Cursor cursor = null; 869 try { 870 cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES, 871 Projections.PROJ_DATA_CONTACTS, selection, selectionArgs, null, null, null); 872 if (cursor.moveToFirst()) { 873 dataId = cursor.getLong(Projections.COL_DATA_ID); 874 aggId = cursor.getLong(Projections.COL_AGGREGATE_ID); 875 } else { 876 // No contact found, return a null URI 877 return -1; 878 } 879 } finally { 880 if (cursor != null) { 881 cursor.close(); 882 } 883 } 884 885 values.put(Presence.DATA_ID, dataId); 886 values.put(Presence.AGGREGATE_ID, aggId); 887 888 // Insert the presence update 889 long presenceId = db.replace(Tables.PRESENCE, null, values); 890 return presenceId; 891 } 892 893 @Override 894 public int delete(Uri uri, String selection, String[] selectionArgs) { 895 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 896 897 final int match = sUriMatcher.match(uri); 898 switch (match) { 899 case AGGREGATES_ID: { 900 long aggregateId = ContentUris.parseId(uri); 901 902 // Remove references to the aggregate first 903 ContentValues values = new ContentValues(); 904 values.putNull(Contacts.AGGREGATE_ID); 905 db.update(Tables.CONTACTS, values, Contacts.AGGREGATE_ID + "=" + aggregateId, null); 906 907 return db.delete(Tables.AGGREGATES, BaseColumns._ID + "=" + aggregateId, null); 908 } 909 910 case CONTACTS_ID: { 911 long contactId = ContentUris.parseId(uri); 912 int contactsDeleted = db.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null); 913 int dataDeleted = db.delete(Tables.DATA, Data.CONTACT_ID + "=" + contactId, null); 914 return contactsDeleted + dataDeleted; 915 } 916 917 case DATA_ID: { 918 long dataId = ContentUris.parseId(uri); 919 return deleteData(dataId); 920 } 921 922 case GROUPS_ID: { 923 long groupId = ContentUris.parseId(uri); 924 final long groupMembershipMimetypeId = mOpenHelper 925 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 926 int groupsDeleted = db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 927 int dataDeleted = db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 928 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 929 + groupId, null); 930 mOpenHelper.updateAllVisible(); 931 return groupsDeleted + dataDeleted; 932 } 933 934 case PRESENCE: { 935 return db.delete(Tables.PRESENCE, null, null); 936 } 937 938 default: 939 throw new UnsupportedOperationException("Unknown uri: " + uri); 940 } 941 } 942 943 private static Account readAccountFromQueryParams(Uri uri) { 944 final String name = uri.getQueryParameter(Contacts.ACCOUNT_NAME); 945 final String type = uri.getQueryParameter(Contacts.ACCOUNT_TYPE); 946 if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) { 947 return null; 948 } 949 return new Account(name, type); 950 } 951 952 953 @Override 954 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 955 int count = 0; 956 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 957 958 final int match = sUriMatcher.match(uri); 959 switch(match) { 960 // TODO(emillar): We will want to disallow editing the aggregates table at some point. 961 case AGGREGATES: { 962 count = db.update(Tables.AGGREGATES, values, selection, selectionArgs); 963 break; 964 } 965 966 case AGGREGATES_ID: { 967 count = updateAggregateData(db, ContentUris.parseId(uri), values); 968 break; 969 } 970 971 case DATA_ID: { 972 boolean containsIsSuperPrimary = values.containsKey(Data.IS_SUPER_PRIMARY); 973 boolean containsIsPrimary = values.containsKey(Data.IS_PRIMARY); 974 final long id = ContentUris.parseId(uri); 975 976 // Remove primary or super primary values being set to 0. This is disallowed by the 977 // content provider. 978 if (containsIsSuperPrimary && values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { 979 containsIsSuperPrimary = false; 980 values.remove(Data.IS_SUPER_PRIMARY); 981 } 982 if (containsIsPrimary && values.getAsInteger(Data.IS_PRIMARY) == 0) { 983 containsIsPrimary = false; 984 values.remove(Data.IS_PRIMARY); 985 } 986 987 if (containsIsSuperPrimary) { 988 setIsSuperPrimary(id); 989 setIsPrimary(id); 990 991 // Now that we've taken care of setting these, remove them from "values". 992 values.remove(Data.IS_SUPER_PRIMARY); 993 if (containsIsPrimary) { 994 values.remove(Data.IS_PRIMARY); 995 } 996 } else if (containsIsPrimary) { 997 setIsPrimary(id); 998 999 // Now that we've taken care of setting this, remove it from "values". 1000 values.remove(Data.IS_PRIMARY); 1001 } 1002 1003 if (values.size() > 0) { 1004 String selectionWithId = (Data._ID + " = " + ContentUris.parseId(uri) + " ") 1005 + (selection == null ? "" : " AND " + selection); 1006 count = db.update(Tables.DATA, values, selectionWithId, selectionArgs); 1007 } 1008 break; 1009 } 1010 1011 case CONTACTS: { 1012 count = db.update(Tables.CONTACTS, values, selection, selectionArgs); 1013 break; 1014 } 1015 1016 case CONTACTS_ID: { 1017 String selectionWithId = (Contacts._ID + " = " + ContentUris.parseId(uri) + " ") 1018 + (selection == null ? "" : " AND " + selection); 1019 count = db.update(Tables.CONTACTS, values, selectionWithId, selectionArgs); 1020 Log.i(TAG, "Selection is: " + selectionWithId); 1021 break; 1022 } 1023 1024 case DATA: { 1025 count = db.update(Tables.DATA, values, selection, selectionArgs); 1026 break; 1027 } 1028 1029 case GROUPS: { 1030 count = db.update(Tables.GROUPS, values, selection, selectionArgs); 1031 mOpenHelper.updateAllVisible(); 1032 break; 1033 } 1034 1035 case GROUPS_ID: { 1036 long groupId = ContentUris.parseId(uri); 1037 String selectionWithId = (Groups._ID + "=" + groupId + " ") 1038 + (selection == null ? "" : " AND " + selection); 1039 count = db.update(Tables.GROUPS, values, selectionWithId, selectionArgs); 1040 1041 // If changing visibility, then update aggregates 1042 if (values.containsKey(Groups.GROUP_VISIBLE)) { 1043 mOpenHelper.updateAllVisible(); 1044 } 1045 1046 break; 1047 } 1048 1049 case AGGREGATION_EXCEPTIONS: { 1050 count = updateAggregationException(db, values); 1051 break; 1052 } 1053 1054 case RESTRICTION_EXCEPTIONS: { 1055 // Enforce required fields 1056 boolean hasFields = values.containsKey(RestrictionExceptions.PACKAGE_PROVIDER) 1057 && values.containsKey(RestrictionExceptions.PACKAGE_CLIENT) 1058 && values.containsKey(RestrictionExceptions.ALLOW_ACCESS); 1059 if (!hasFields) { 1060 throw new IllegalArgumentException("PACKAGE_PROVIDER, PACKAGE_CLIENT, and" 1061 + "ALLOW_ACCESS are all required fields"); 1062 } 1063 1064 final String packageProvider = values 1065 .getAsString(RestrictionExceptions.PACKAGE_PROVIDER); 1066 final boolean allowAccess = (values 1067 .getAsInteger(RestrictionExceptions.ALLOW_ACCESS) == 1); 1068 1069 final Context context = getContext(); 1070 final PackageManager pm = context.getPackageManager(); 1071 1072 // Enforce that caller has authority over the requested package 1073 // TODO: move back to Binder.getCallingUid() when we can stub-out test suite 1074 final int callingUid = OpenHelper 1075 .getUidForPackageName(pm, context.getPackageName()); 1076 final String[] ownedPackages = pm.getPackagesForUid(callingUid); 1077 if (!isContained(ownedPackages, packageProvider)) { 1078 throw new RuntimeException( 1079 "Requested PACKAGE_PROVIDER doesn't belong to calling UID."); 1080 } 1081 1082 // Add or remove exception using exception helper 1083 if (allowAccess) { 1084 mOpenHelper.addRestrictionException(context, values); 1085 } else { 1086 mOpenHelper.removeRestrictionException(context, values); 1087 } 1088 1089 break; 1090 } 1091 1092 default: 1093 throw new UnsupportedOperationException("Unknown uri: " + uri); 1094 } 1095 1096 if (count > 0) { 1097 getContext().getContentResolver().notifyChange(uri, null); 1098 } 1099 return count; 1100 } 1101 1102 private int updateAggregateData(SQLiteDatabase db, long aggregateId, ContentValues values) { 1103 1104 // First update all constituent contacts 1105 ContentValues optionValues = new ContentValues(3); 1106 if (values.containsKey(Aggregates.CUSTOM_RINGTONE)) { 1107 optionValues.put(ContactOptionsColumns.CUSTOM_RINGTONE, 1108 values.getAsString(Aggregates.CUSTOM_RINGTONE)); 1109 } 1110 if (values.containsKey(Aggregates.SEND_TO_VOICEMAIL)) { 1111 optionValues.put(ContactOptionsColumns.SEND_TO_VOICEMAIL, 1112 values.getAsBoolean(Aggregates.SEND_TO_VOICEMAIL)); 1113 } 1114 1115 // Nothing to update - just return 1116 if (optionValues.size() == 0) { 1117 return 0; 1118 } 1119 1120 Cursor c = db.query(Tables.CONTACTS, Projections.PROJ_CONTACTS, Contacts.AGGREGATE_ID + "=" 1121 + aggregateId, null, null, null, null); 1122 try { 1123 while (c.moveToNext()) { 1124 long contactId = c.getLong(Projections.COL_CONTACT_ID); 1125 1126 optionValues.put(ContactOptionsColumns._ID, contactId); 1127 db.replace(Tables.CONTACT_OPTIONS, null, optionValues); 1128 } 1129 } finally { 1130 c.close(); 1131 } 1132 1133 // Now update the aggregate itself. Ignore all supplied fields except rington and 1134 // send_to_voicemail 1135 optionValues.clear(); 1136 if (values.containsKey(Aggregates.CUSTOM_RINGTONE)) { 1137 optionValues.put(Aggregates.CUSTOM_RINGTONE, 1138 values.getAsString(Aggregates.CUSTOM_RINGTONE)); 1139 } 1140 if (values.containsKey(Aggregates.SEND_TO_VOICEMAIL)) { 1141 optionValues.put(Aggregates.SEND_TO_VOICEMAIL, 1142 values.getAsBoolean(Aggregates.SEND_TO_VOICEMAIL)); 1143 } 1144 1145 return db.update(Tables.AGGREGATES, optionValues, Aggregates._ID + "=" + aggregateId, null); 1146 } 1147 1148 private static class ContactPair { 1149 final long contactId1; 1150 final long contactId2; 1151 1152 /** 1153 * Constructor that ensures that this.contactId1 < this.contactId2 1154 */ 1155 public ContactPair(long contactId1, long contactId2) { 1156 if (contactId1 < contactId2) { 1157 this.contactId1 = contactId1; 1158 this.contactId2 = contactId2; 1159 } else { 1160 this.contactId2 = contactId1; 1161 this.contactId1 = contactId2; 1162 } 1163 } 1164 } 1165 1166 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 1167 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 1168 long aggregateId = values.getAsInteger(AggregationExceptions.AGGREGATE_ID); 1169 long contactId = values.getAsInteger(AggregationExceptions.CONTACT_ID); 1170 1171 // First, we build a list of contactID-contactID pairs for the given aggregate and contact. 1172 ArrayList<ContactPair> pairs = new ArrayList<ContactPair>(); 1173 Cursor c = db.query(Tables.CONTACTS, Projections.PROJ_CONTACTS, 1174 Contacts.AGGREGATE_ID + "=" + aggregateId, 1175 null, null, null, null); 1176 try { 1177 while (c.moveToNext()) { 1178 long aggregatedContactId = c.getLong(Projections.COL_CONTACT_ID); 1179 if (aggregatedContactId != contactId) { 1180 pairs.add(new ContactPair(aggregatedContactId, contactId)); 1181 } 1182 } 1183 } finally { 1184 c.close(); 1185 } 1186 1187 // Now we iterate through all contact pairs to see if we need to insert/delete/update 1188 // the corresponding exception 1189 ContentValues exceptionValues = new ContentValues(3); 1190 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 1191 for (ContactPair pair : pairs) { 1192 final String whereClause = 1193 AggregationExceptionColumns.CONTACT_ID1 + "=" + pair.contactId1 + " AND " 1194 + AggregationExceptionColumns.CONTACT_ID2 + "=" + pair.contactId2; 1195 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 1196 db.delete(Tables.AGGREGATION_EXCEPTIONS, whereClause, null); 1197 } else { 1198 exceptionValues.put(AggregationExceptionColumns.CONTACT_ID1, pair.contactId1); 1199 exceptionValues.put(AggregationExceptionColumns.CONTACT_ID2, pair.contactId2); 1200 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 1201 exceptionValues); 1202 } 1203 } 1204 1205 mContactAggregator.markContactForAggregation(contactId); 1206 mContactAggregator.aggregateContact(contactId); 1207 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC 1208 || exceptionType == AggregationExceptions.TYPE_KEEP_OUT) { 1209 mContactAggregator.updateAggregateData(aggregateId); 1210 } 1211 1212 // The return value is fake - we just confirm that we made a change, not count actual 1213 // rows changed. 1214 return 1; 1215 } 1216 1217 /** 1218 * Test if a {@link String} value appears in the given list. 1219 */ 1220 private boolean isContained(String[] array, String value) { 1221 if (array != null) { 1222 for (String test : array) { 1223 if (value.equals(test)) { 1224 return true; 1225 } 1226 } 1227 } 1228 return false; 1229 } 1230 1231 /** 1232 * Test if a {@link String} value appears in the given list, and add to the 1233 * array if the value doesn't already appear. 1234 */ 1235 private String[] assertContained(String[] array, String value) { 1236 if (array == null) { 1237 array = new String[] {value}; 1238 } else if (!isContained(array, value)) { 1239 String[] newArray = new String[array.length + 1]; 1240 System.arraycopy(array, 0, newArray, 0, array.length); 1241 newArray[array.length] = value; 1242 array = newArray; 1243 } 1244 return array; 1245 } 1246 1247 @Override 1248 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1249 String sortOrder) { 1250 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1251 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1252 String groupBy = null; 1253 String limit = null; 1254 String aggregateIdColName = Tables.AGGREGATES + "." + Aggregates._ID; 1255 1256 // TODO: Consider writing a test case for RestrictionExceptions when you 1257 // write a new query() block to make sure it protects restricted data. 1258 final int match = sUriMatcher.match(uri); 1259 switch (match) { 1260 case AGGREGATES: { 1261 qb.setTables(Tables.AGGREGATES); 1262 applyAggregateRestrictionExceptions(qb); 1263 applyAggregatePrimaryRestrictionExceptions(sAggregatesProjectionMap); 1264 qb.setProjectionMap(sAggregatesProjectionMap); 1265 break; 1266 } 1267 1268 case AGGREGATES_ID: { 1269 long aggId = ContentUris.parseId(uri); 1270 qb.setTables(Tables.AGGREGATES); 1271 qb.appendWhere(AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND "); 1272 applyAggregateRestrictionExceptions(qb); 1273 applyAggregatePrimaryRestrictionExceptions(sAggregatesProjectionMap); 1274 qb.setProjectionMap(sAggregatesProjectionMap); 1275 break; 1276 } 1277 1278 case AGGREGATES_SUMMARY: { 1279 // TODO: join into social status tables 1280 qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE); 1281 applyAggregateRestrictionExceptions(qb); 1282 applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap); 1283 projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID); 1284 qb.setProjectionMap(sAggregatesSummaryProjectionMap); 1285 groupBy = aggregateIdColName; 1286 break; 1287 } 1288 1289 case AGGREGATES_SUMMARY_ID: { 1290 // TODO: join into social status tables 1291 long aggId = ContentUris.parseId(uri); 1292 qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE); 1293 qb.appendWhere(AggregatesColumns.CONCRETE_ID + "=" + aggId + " AND "); 1294 applyAggregateRestrictionExceptions(qb); 1295 applyAggregatePrimaryRestrictionExceptions(sAggregatesSummaryProjectionMap); 1296 projection = assertContained(projection, Aggregates.PRIMARY_PHONE_ID); 1297 qb.setProjectionMap(sAggregatesSummaryProjectionMap); 1298 groupBy = aggregateIdColName; 1299 break; 1300 } 1301 1302 case AGGREGATES_SUMMARY_FILTER: { 1303 // TODO: filter query based on callingUid 1304 qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE); 1305 qb.setProjectionMap(sAggregatesSummaryProjectionMap); 1306 if (uri.getPathSegments().size() > 2) { 1307 qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment())); 1308 } 1309 groupBy = aggregateIdColName; 1310 break; 1311 } 1312 1313 case AGGREGATES_SUMMARY_STREQUENT_FILTER: 1314 case AGGREGATES_SUMMARY_STREQUENT: { 1315 // Build the first query for starred 1316 qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE); 1317 qb.setProjectionMap(sAggregatesSummaryProjectionMap); 1318 if (match == AGGREGATES_SUMMARY_STREQUENT_FILTER 1319 && uri.getPathSegments().size() > 3) { 1320 qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment())); 1321 } 1322 final String starredQuery = qb.buildQuery(projection, Aggregates.STARRED + "=1", 1323 null, aggregateIdColName, null, null, 1324 null /* limit */); 1325 1326 // Build the second query for frequent 1327 qb = new SQLiteQueryBuilder(); 1328 qb.setTables(Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE); 1329 qb.setProjectionMap(sAggregatesSummaryProjectionMap); 1330 if (match == AGGREGATES_SUMMARY_STREQUENT_FILTER 1331 && uri.getPathSegments().size() > 3) { 1332 qb.appendWhere(buildAggregateLookupWhereClause(uri.getLastPathSegment())); 1333 } 1334 final String frequentQuery = qb.buildQuery(projection, 1335 Aggregates.TIMES_CONTACTED + " > 0 AND (" + Aggregates.STARRED 1336 + " = 0 OR " + Aggregates.STARRED + " IS NULL)", 1337 null, aggregateIdColName, null, null, null); 1338 1339 // Put them together 1340 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 1341 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 1342 Cursor c = db.rawQueryWithFactory(null, query, null, 1343 Tables.AGGREGATES_JOIN_PRESENCE_PRIMARY_PHONE); 1344 1345 if ((c != null) && !isTemporary()) { 1346 c.setNotificationUri(getContext().getContentResolver(), 1347 ContactsContract.AUTHORITY_URI); 1348 } 1349 return c; 1350 } 1351 1352 case AGGREGATES_DATA: { 1353 long aggId = Long.parseLong(uri.getPathSegments().get(1)); 1354 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES); 1355 qb.setProjectionMap(sDataContactsAggregateProjectionMap); 1356 qb.appendWhere(Contacts.AGGREGATE_ID + "=" + aggId + " AND "); 1357 applyDataRestrictionExceptions(qb); 1358 break; 1359 } 1360 1361 case PHONES_FILTER: { 1362 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES); 1363 qb.setProjectionMap(sDataContactsAggregateProjectionMap); 1364 qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 1365 if (uri.getPathSegments().size() > 2) { 1366 qb.appendWhere(" AND " + buildAggregateLookupWhereClause( 1367 uri.getLastPathSegment())); 1368 } 1369 break; 1370 } 1371 1372 case PHONES: { 1373 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES); 1374 qb.setProjectionMap(sDataContactsAggregateProjectionMap); 1375 qb.appendWhere(Data.MIMETYPE + " = \"" + Phone.CONTENT_ITEM_TYPE + "\""); 1376 break; 1377 } 1378 1379 case POSTALS: { 1380 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES); 1381 qb.setProjectionMap(sDataContactsAggregateProjectionMap); 1382 qb.appendWhere(Data.MIMETYPE + " = \"" + Postal.CONTENT_ITEM_TYPE + "\""); 1383 break; 1384 } 1385 1386 case CONTACTS: { 1387 qb.setTables(Tables.CONTACTS_JOIN_PACKAGES); 1388 qb.setProjectionMap(sContactsProjectionMap); 1389 applyContactsRestrictionExceptions(qb); 1390 break; 1391 } 1392 1393 case CONTACTS_ID: { 1394 long contactId = ContentUris.parseId(uri); 1395 qb.setTables(Tables.CONTACTS_JOIN_PACKAGES); 1396 qb.setProjectionMap(sContactsProjectionMap); 1397 qb.appendWhere(ContactsColumns.CONCRETE_ID + "=" + contactId + " AND "); 1398 applyContactsRestrictionExceptions(qb); 1399 break; 1400 } 1401 1402 case CONTACTS_DATA: { 1403 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 1404 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES); 1405 qb.setProjectionMap(sDataContactsProjectionMap); 1406 qb.appendWhere(Data.CONTACT_ID + "=" + contactId + " AND "); 1407 applyDataRestrictionExceptions(qb); 1408 break; 1409 } 1410 1411 case CONTACTS_FILTER_EMAIL: { 1412 // TODO: filter query based on callingUid 1413 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES_AGGREGATES); 1414 qb.setProjectionMap(sDataContactsProjectionMap); 1415 qb.appendWhere(Data.MIMETYPE + "='" + CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'"); 1416 qb.appendWhere(" AND " + CommonDataKinds.Email.DATA + "="); 1417 qb.appendWhereEscapeString(uri.getPathSegments().get(2)); 1418 break; 1419 } 1420 1421 case DATA: { 1422 final String accountName = uri.getQueryParameter(Contacts.ACCOUNT_NAME); 1423 final String accountType = uri.getQueryParameter(Contacts.ACCOUNT_TYPE); 1424 if (!TextUtils.isEmpty(accountName)) { 1425 qb.appendWhere(Contacts.ACCOUNT_NAME + "=" 1426 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 1427 + Contacts.ACCOUNT_TYPE + "=" 1428 + DatabaseUtils.sqlEscapeString(accountType) + " AND "); 1429 } 1430 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES); 1431 qb.setProjectionMap(sDataProjectionMap); 1432 applyDataRestrictionExceptions(qb); 1433 break; 1434 } 1435 1436 case DATA_ID: { 1437 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES); 1438 qb.setProjectionMap(sDataProjectionMap); 1439 qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri) + " AND "); 1440 applyDataRestrictionExceptions(qb); 1441 break; 1442 } 1443 1444 case PHONE_LOOKUP: { 1445 // TODO: filter query based on callingUid 1446 if (TextUtils.isEmpty(sortOrder)) { 1447 // Default the sort order to something reasonable so we get consistent 1448 // results when callers don't request an ordering 1449 sortOrder = Data.CONTACT_ID; 1450 } 1451 1452 final String number = uri.getLastPathSegment(); 1453 OpenHelper.buildPhoneLookupQuery(qb, number); 1454 qb.setProjectionMap(sDataContactsProjectionMap); 1455 break; 1456 } 1457 1458 case GROUPS: { 1459 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 1460 qb.setProjectionMap(sGroupsProjectionMap); 1461 break; 1462 } 1463 1464 case GROUPS_ID: { 1465 long groupId = ContentUris.parseId(uri); 1466 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 1467 qb.setProjectionMap(sGroupsProjectionMap); 1468 qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId); 1469 break; 1470 } 1471 1472 case GROUPS_SUMMARY: { 1473 qb.setTables(Tables.GROUPS_JOIN_PACKAGES_DATA_CONTACTS_AGGREGATES); 1474 qb.setProjectionMap(sGroupsSummaryProjectionMap); 1475 groupBy = GroupsColumns.CONCRETE_ID; 1476 break; 1477 } 1478 1479 case AGGREGATION_EXCEPTIONS: { 1480 qb.setTables(Tables.AGGREGATION_EXCEPTIONS_JOIN_CONTACTS); 1481 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 1482 break; 1483 } 1484 1485 case AGGREGATION_SUGGESTIONS: { 1486 long aggregateId = Long.parseLong(uri.getPathSegments().get(1)); 1487 final String maxSuggestionsParam = 1488 uri.getQueryParameter(AggregationSuggestions.MAX_SUGGESTIONS); 1489 1490 final int maxSuggestions; 1491 if (maxSuggestionsParam != null) { 1492 maxSuggestions = Integer.parseInt(maxSuggestionsParam); 1493 } else { 1494 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 1495 } 1496 1497 return mContactAggregator.queryAggregationSuggestions(aggregateId, projection, 1498 sAggregatesProjectionMap, maxSuggestions); 1499 } 1500 1501 case RESTRICTION_EXCEPTIONS: { 1502 qb.setTables(Tables.RESTRICTION_EXCEPTIONS); 1503 qb.setProjectionMap(sRestrictionExceptionsProjectionMap); 1504 break; 1505 } 1506 1507 default: 1508 throw new UnsupportedOperationException("Unknown uri: " + uri); 1509 } 1510 1511 // Perform the query and set the notification uri 1512 final Cursor c = qb.query(db, projection, selection, selectionArgs, 1513 groupBy, null, sortOrder, limit); 1514 if (c != null) { 1515 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 1516 } 1517 return c; 1518 } 1519 1520 /** 1521 * Restrict selection of {@link Aggregates} to only public ones, or those 1522 * the caller has been granted a {@link RestrictionExceptions} to. 1523 */ 1524 private void applyAggregateRestrictionExceptions(SQLiteQueryBuilder qb) { 1525 final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(), 1526 getContext().getPackageName()); 1527 1528 qb.appendWhere("(" + AggregatesColumns.SINGLE_RESTRICTED_PACKAGE_ID + " IS NULL"); 1529 final String exceptionClause = mOpenHelper.getRestrictionExceptionClause(clientUid, 1530 AggregatesColumns.SINGLE_RESTRICTED_PACKAGE_ID); 1531 if (exceptionClause != null) { 1532 qb.appendWhere(" OR (" + exceptionClause + ")"); 1533 } 1534 qb.appendWhere(")"); 1535 } 1536 1537 /** 1538 * Find any exceptions that have been granted to the calling process, and 1539 * add projections to correctly select {@link Aggregates#PRIMARY_PHONE_ID} 1540 * and {@link Aggregates#PRIMARY_EMAIL_ID}. 1541 */ 1542 private void applyAggregatePrimaryRestrictionExceptions(HashMap<String, String> projection) { 1543 // TODO: move back to Binder.getCallingUid() when we can stub-out test suite 1544 final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(), 1545 getContext().getPackageName()); 1546 1547 final String projectionPhone = "(CASE WHEN " 1548 + mOpenHelper.getRestrictionExceptionClause(clientUid, 1549 AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID) + " THEN " 1550 + AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID + " ELSE " 1551 + AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID + " END) AS " 1552 + Aggregates.PRIMARY_PHONE_ID; 1553 projection.remove(Aggregates.PRIMARY_PHONE_ID); 1554 projection.put(Aggregates.PRIMARY_PHONE_ID, projectionPhone); 1555 1556 final String projectionEmail = "(CASE WHEN " 1557 + mOpenHelper.getRestrictionExceptionClause(clientUid, 1558 AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID) + " THEN " 1559 + AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID + " ELSE " 1560 + AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID + " END) AS " 1561 + Aggregates.PRIMARY_EMAIL_ID; 1562 projection.remove(Aggregates.PRIMARY_EMAIL_ID); 1563 projection.put(Aggregates.PRIMARY_EMAIL_ID, projectionEmail); 1564 } 1565 1566 /** 1567 * Find any exceptions that have been granted to the 1568 * {@link Binder#getCallingUid()}, and add a limiting clause to the given 1569 * {@link SQLiteQueryBuilder} to hide restricted data. 1570 */ 1571 private void applyContactsRestrictionExceptions(SQLiteQueryBuilder qb) { 1572 // TODO: move back to Binder.getCallingUid() when we can stub-out test suite 1573 final int clientUid = OpenHelper.getUidForPackageName(getContext().getPackageManager(), 1574 getContext().getPackageName()); 1575 1576 qb.appendWhere("(" + Contacts.IS_RESTRICTED + "=0"); 1577 final String exceptionClause = mOpenHelper.getRestrictionExceptionClause(clientUid, 1578 ContactsColumns.PACKAGE_ID); 1579 if (exceptionClause != null) { 1580 qb.appendWhere(" OR (" + exceptionClause + ")"); 1581 } 1582 qb.appendWhere(")"); 1583 } 1584 1585 /** 1586 * Find any exceptions that have been granted to the 1587 * {@link Binder#getCallingUid()}, and add a limiting clause to the given 1588 * {@link SQLiteQueryBuilder} to hide restricted data. 1589 */ 1590 private void applyDataRestrictionExceptions(SQLiteQueryBuilder qb) { 1591 applyContactsRestrictionExceptions(qb); 1592 } 1593 1594 /** 1595 * An implementation of EntityIterator that joins the contacts and data tables 1596 * and consumes all the data rows for a contact in order to build the Entity for a contact. 1597 */ 1598 private static class ContactsEntityIterator implements EntityIterator { 1599 private final Cursor mEntityCursor; 1600 private volatile boolean mIsClosed; 1601 1602 private static final String[] DATA_KEYS = new String[]{ 1603 "data1", 1604 "data2", 1605 "data3", 1606 "data4", 1607 "data5", 1608 "data6", 1609 "data7", 1610 "data8", 1611 "data9", 1612 "data10"}; 1613 1614 private static final String[] PROJECTION = new String[]{ 1615 Contacts.ACCOUNT_NAME, 1616 Contacts.ACCOUNT_TYPE, 1617 Contacts.SOURCE_ID, 1618 Contacts.VERSION, 1619 Contacts.DIRTY, 1620 Contacts.Data._ID, 1621 Contacts.Data.MIMETYPE, 1622 Contacts.Data.DATA1, 1623 Contacts.Data.DATA2, 1624 Contacts.Data.DATA3, 1625 Contacts.Data.DATA4, 1626 Contacts.Data.DATA5, 1627 Contacts.Data.DATA6, 1628 Contacts.Data.DATA7, 1629 Contacts.Data.DATA8, 1630 Contacts.Data.DATA9, 1631 Contacts.Data.DATA10, 1632 Contacts.Data.CONTACT_ID, 1633 Contacts.Data.IS_PRIMARY, 1634 Contacts.Data.DATA_VERSION}; 1635 1636 private static final int COLUMN_ACCOUNT_NAME = 0; 1637 private static final int COLUMN_ACCOUNT_TYPE = 1; 1638 private static final int COLUMN_SOURCE_ID = 2; 1639 private static final int COLUMN_VERSION = 3; 1640 private static final int COLUMN_DIRTY = 4; 1641 private static final int COLUMN_DATA_ID = 5; 1642 private static final int COLUMN_MIMETYPE = 6; 1643 private static final int COLUMN_DATA1 = 7; 1644 private static final int COLUMN_CONTACT_ID = 17; 1645 private static final int COLUMN_IS_PRIMARY = 18; 1646 private static final int COLUMN_DATA_VERSION = 19; 1647 1648 public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri, 1649 String selection, String[] selectionArgs, String sortOrder) { 1650 mIsClosed = false; 1651 1652 final String updatedSortOrder = (sortOrder == null) 1653 ? Contacts.Data.CONTACT_ID 1654 : (Contacts.Data.CONTACT_ID + "," + sortOrder); 1655 1656 final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase(); 1657 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1658 qb.setTables(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES); 1659 qb.setProjectionMap(sDataContactsProjectionMap); 1660 if (contactsIdString != null) { 1661 qb.appendWhere(Data.CONTACT_ID + "=" + contactsIdString); 1662 } 1663 final String accountName = uri.getQueryParameter(Contacts.ACCOUNT_NAME); 1664 final String accountType = uri.getQueryParameter(Contacts.ACCOUNT_TYPE); 1665 if (!TextUtils.isEmpty(accountName)) { 1666 qb.appendWhere(Contacts.ACCOUNT_NAME + "=" 1667 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 1668 + Contacts.ACCOUNT_TYPE + "=" 1669 + DatabaseUtils.sqlEscapeString(accountType)); 1670 } 1671 mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs, 1672 null, null, updatedSortOrder); 1673 mEntityCursor.moveToFirst(); 1674 } 1675 1676 public void close() { 1677 if (mIsClosed) { 1678 throw new IllegalStateException("closing when already closed"); 1679 } 1680 mIsClosed = true; 1681 mEntityCursor.close(); 1682 } 1683 1684 public boolean hasNext() throws RemoteException { 1685 if (mIsClosed) { 1686 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 1687 } 1688 1689 return !mEntityCursor.isAfterLast(); 1690 } 1691 1692 public Entity next() throws RemoteException { 1693 if (mIsClosed) { 1694 throw new IllegalStateException("calling next() when the iterator is closed"); 1695 } 1696 if (!hasNext()) { 1697 throw new IllegalStateException("you may only call next() if hasNext() is true"); 1698 } 1699 1700 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 1701 1702 final long contactId = c.getLong(COLUMN_CONTACT_ID); 1703 1704 // we expect the cursor is already at the row we need to read from 1705 ContentValues contactValues = new ContentValues(); 1706 contactValues.put(Contacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 1707 contactValues.put(Contacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 1708 contactValues.put(Contacts._ID, contactId); 1709 contactValues.put(Contacts.DIRTY, c.getLong(COLUMN_DIRTY)); 1710 contactValues.put(Contacts.VERSION, c.getLong(COLUMN_VERSION)); 1711 contactValues.put(Contacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 1712 Entity contact = new Entity(contactValues); 1713 1714 // read data rows until the contact id changes 1715 do { 1716 if (contactId != c.getLong(COLUMN_CONTACT_ID)) { 1717 break; 1718 } 1719 // add the data to to the contact 1720 ContentValues dataValues = new ContentValues(); 1721 dataValues.put(Contacts.Data._ID, c.getString(COLUMN_DATA_ID)); 1722 dataValues.put(Contacts.Data.MIMETYPE, c.getString(COLUMN_MIMETYPE)); 1723 dataValues.put(Contacts.Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY)); 1724 dataValues.put(Contacts.Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 1725 for (int i = 0; i < 10; i++) { 1726 final int columnIndex = i + COLUMN_DATA1; 1727 String key = DATA_KEYS[i]; 1728 if (c.isNull(columnIndex)) { 1729 // don't put anything 1730 } else if (c.isLong(columnIndex)) { 1731 dataValues.put(key, c.getLong(columnIndex)); 1732 } else if (c.isFloat(columnIndex)) { 1733 dataValues.put(key, c.getFloat(columnIndex)); 1734 } else if (c.isString(columnIndex)) { 1735 dataValues.put(key, c.getString(columnIndex)); 1736 } else if (c.isBlob(columnIndex)) { 1737 dataValues.put(key, c.getBlob(columnIndex)); 1738 } 1739 } 1740 contact.addSubValue(Data.CONTENT_URI, dataValues); 1741 } while (mEntityCursor.moveToNext()); 1742 1743 return contact; 1744 } 1745 } 1746 1747 @Override 1748 public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, 1749 String sortOrder) { 1750 final int match = sUriMatcher.match(uri); 1751 switch (match) { 1752 case CONTACTS: 1753 case CONTACTS_ID: 1754 String contactsIdString = null; 1755 if (match == CONTACTS_ID) { 1756 contactsIdString = uri.getPathSegments().get(1); 1757 } 1758 1759 return new ContactsEntityIterator(this, contactsIdString, 1760 uri, selection, selectionArgs, sortOrder); 1761 default: 1762 throw new UnsupportedOperationException("Unknown uri: " + uri); 1763 } 1764 } 1765 1766 @Override 1767 public String getType(Uri uri) { 1768 final int match = sUriMatcher.match(uri); 1769 switch (match) { 1770 case AGGREGATES: return Aggregates.CONTENT_TYPE; 1771 case AGGREGATES_ID: return Aggregates.CONTENT_ITEM_TYPE; 1772 case CONTACTS: return Contacts.CONTENT_TYPE; 1773 case CONTACTS_ID: return Contacts.CONTENT_ITEM_TYPE; 1774 case DATA_ID: 1775 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1776 long dataId = ContentUris.parseId(uri); 1777 return mOpenHelper.getDataMimeType(dataId); 1778 case AGGREGATION_EXCEPTIONS: return AggregationExceptions.CONTENT_TYPE; 1779 case AGGREGATION_EXCEPTION_ID: return AggregationExceptions.CONTENT_ITEM_TYPE; 1780 case AGGREGATION_SUGGESTIONS: return Aggregates.CONTENT_TYPE; 1781 } 1782 throw new UnsupportedOperationException("Unknown uri: " + uri); 1783 } 1784 1785 @Override 1786 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1787 throws OperationApplicationException { 1788 1789 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1790 db.beginTransaction(); 1791 try { 1792 ContentProviderResult[] results = super.applyBatch(operations); 1793 db.setTransactionSuccessful(); 1794 return results; 1795 } finally { 1796 db.endTransaction(); 1797 } 1798 } 1799 1800 /* 1801 * Sets the given dataId record in the "data" table to primary, and resets all data records of 1802 * the same mimetype and under the same contact to not be primary. 1803 * 1804 * @param dataId the id of the data record to be set to primary. 1805 */ 1806 private void setIsPrimary(long dataId) { 1807 mSetPrimaryStatement.bindLong(1, dataId); 1808 mSetPrimaryStatement.bindLong(2, dataId); 1809 mSetPrimaryStatement.bindLong(3, dataId); 1810 mSetPrimaryStatement.execute(); 1811 } 1812 1813 /* 1814 * Sets the given dataId record in the "data" table to "super primary", and resets all data 1815 * records of the same mimetype and under the same aggregate to not be "super primary". 1816 * 1817 * @param dataId the id of the data record to be set to primary. 1818 */ 1819 private void setIsSuperPrimary(long dataId) { 1820 mSetSuperPrimaryStatement.bindLong(1, dataId); 1821 mSetSuperPrimaryStatement.bindLong(2, dataId); 1822 mSetSuperPrimaryStatement.bindLong(3, dataId); 1823 mSetSuperPrimaryStatement.execute(); 1824 1825 // Find the parent aggregate and package for this new primary 1826 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1827 1828 long aggId = -1; 1829 long packageId = -1; 1830 boolean isRestricted = false; 1831 String mimeType = null; 1832 1833 Cursor cursor = null; 1834 try { 1835 cursor = db.query(Tables.DATA_JOIN_MIMETYPES_CONTACTS_PACKAGES, 1836 Projections.PROJ_DATA_CONTACTS, DataColumns.CONCRETE_ID + "=" + dataId, null, 1837 null, null, null); 1838 if (cursor.moveToFirst()) { 1839 aggId = cursor.getLong(Projections.COL_AGGREGATE_ID); 1840 packageId = cursor.getLong(Projections.COL_PACKAGE_ID); 1841 isRestricted = (cursor.getInt(Projections.COL_IS_RESTRICTED) == 1); 1842 mimeType = cursor.getString(Projections.COL_MIMETYPE); 1843 } 1844 } finally { 1845 if (cursor != null) { 1846 cursor.close(); 1847 } 1848 } 1849 1850 // Bypass aggregate update if no parent found, or if we don't keep track 1851 // of super-primary for this mimetype. 1852 if (aggId == -1) { 1853 return; 1854 } 1855 1856 boolean isPhone = CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType); 1857 boolean isEmail = CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType); 1858 1859 // Record this value as the new primary for the parent aggregate 1860 final ContentValues values = new ContentValues(); 1861 if (isPhone) { 1862 values.put(AggregatesColumns.OPTIMAL_PRIMARY_PHONE_ID, dataId); 1863 values.put(AggregatesColumns.OPTIMAL_PRIMARY_PHONE_PACKAGE_ID, packageId); 1864 } else if (isEmail) { 1865 values.put(AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_ID, dataId); 1866 values.put(AggregatesColumns.OPTIMAL_PRIMARY_EMAIL_PACKAGE_ID, packageId); 1867 } 1868 1869 // If this data is unrestricted, then also set as fallback 1870 if (!isRestricted && isPhone) { 1871 values.put(AggregatesColumns.FALLBACK_PRIMARY_PHONE_ID, dataId); 1872 } else if (!isRestricted && isEmail) { 1873 values.put(AggregatesColumns.FALLBACK_PRIMARY_EMAIL_ID, dataId); 1874 } 1875 1876 // Push update into aggregates table, if needed 1877 if (values.size() > 0) { 1878 db.update(Tables.AGGREGATES, values, Aggregates._ID + "=" + aggId, null); 1879 } 1880 1881 } 1882 1883 private String buildAggregateLookupWhereClause(String filterParam) { 1884 StringBuilder filter = new StringBuilder(); 1885 filter.append(Tables.AGGREGATES); 1886 filter.append("."); 1887 filter.append(Aggregates._ID); 1888 filter.append(" IN (SELECT "); 1889 filter.append(Contacts.AGGREGATE_ID); 1890 filter.append(" FROM "); 1891 filter.append(Tables.CONTACTS); 1892 filter.append(" WHERE "); 1893 filter.append(Contacts._ID); 1894 filter.append(" IN (SELECT contact_id FROM name_lookup WHERE normalized_name GLOB '"); 1895 // NOTE: Query parameters won't work here since the SQL compiler 1896 // needs to parse the actual string to know that it can use the 1897 // index to do a prefix scan. 1898 filter.append(NameNormalizer.normalize(filterParam) + "*"); 1899 filter.append("'))"); 1900 return filter.toString(); 1901 } 1902 1903} 1904