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