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