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