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