ContactsProvider2.java revision 5e99505757457d11d9388f6d04960e97fc776a59
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.ContactLookupKey.LookupKeySegment; 21import com.android.providers.contacts.OpenHelper.AggregatedPresenceColumns; 22import com.android.providers.contacts.OpenHelper.AggregationExceptionColumns; 23import com.android.providers.contacts.OpenHelper.Clauses; 24import com.android.providers.contacts.OpenHelper.ContactsColumns; 25import com.android.providers.contacts.OpenHelper.DataColumns; 26import com.android.providers.contacts.OpenHelper.DisplayNameSources; 27import com.android.providers.contacts.OpenHelper.GroupsColumns; 28import com.android.providers.contacts.OpenHelper.MimetypesColumns; 29import com.android.providers.contacts.OpenHelper.NameLookupColumns; 30import com.android.providers.contacts.OpenHelper.NameLookupType; 31import com.android.providers.contacts.OpenHelper.PackagesColumns; 32import com.android.providers.contacts.OpenHelper.PhoneColumns; 33import com.android.providers.contacts.OpenHelper.PhoneLookupColumns; 34import com.android.providers.contacts.OpenHelper.PresenceColumns; 35import com.android.providers.contacts.OpenHelper.RawContactsColumns; 36import com.android.providers.contacts.OpenHelper.Tables; 37import com.google.android.collect.Lists; 38import com.google.android.collect.Sets; 39 40import android.accounts.Account; 41import android.accounts.AccountManager; 42import android.accounts.OnAccountsUpdatedListener; 43import android.app.SearchManager; 44import android.content.ContentProviderOperation; 45import android.content.ContentProviderResult; 46import android.content.ContentUris; 47import android.content.ContentValues; 48import android.content.Context; 49import android.content.Entity; 50import android.content.EntityIterator; 51import android.content.OperationApplicationException; 52import android.content.SharedPreferences; 53import android.content.UriMatcher; 54import android.content.SharedPreferences.Editor; 55import android.content.res.AssetFileDescriptor; 56import android.database.Cursor; 57import android.database.DatabaseUtils; 58import android.database.sqlite.SQLiteContentHelper; 59import android.database.sqlite.SQLiteCursor; 60import android.database.sqlite.SQLiteDatabase; 61import android.database.sqlite.SQLiteQueryBuilder; 62import android.database.sqlite.SQLiteStatement; 63import android.net.Uri; 64import android.os.RemoteException; 65import android.preference.PreferenceManager; 66import android.provider.BaseColumns; 67import android.provider.ContactsContract; 68import android.provider.LiveFolders; 69import android.provider.ContactsContract.AggregationExceptions; 70import android.provider.ContactsContract.CommonDataKinds; 71import android.provider.ContactsContract.Contacts; 72import android.provider.ContactsContract.Data; 73import android.provider.ContactsContract.Groups; 74import android.provider.ContactsContract.PhoneLookup; 75import android.provider.ContactsContract.Presence; 76import android.provider.ContactsContract.RawContacts; 77import android.provider.ContactsContract.Settings; 78import android.provider.ContactsContract.CommonDataKinds.BaseTypes; 79import android.provider.ContactsContract.CommonDataKinds.Email; 80import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 81import android.provider.ContactsContract.CommonDataKinds.Im; 82import android.provider.ContactsContract.CommonDataKinds.Nickname; 83import android.provider.ContactsContract.CommonDataKinds.Organization; 84import android.provider.ContactsContract.CommonDataKinds.Phone; 85import android.provider.ContactsContract.CommonDataKinds.Photo; 86import android.provider.ContactsContract.CommonDataKinds.StructuredName; 87import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 88import android.telephony.PhoneNumberUtils; 89import android.text.TextUtils; 90import android.util.Log; 91 92import java.io.FileNotFoundException; 93import java.util.ArrayList; 94import java.util.Collections; 95import java.util.HashMap; 96import java.util.List; 97import java.util.Locale; 98import java.util.Set; 99import java.util.concurrent.CountDownLatch; 100 101/** 102 * Contacts content provider. The contract between this provider and applications 103 * is defined in {@link ContactsContract}. 104 */ 105public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdatedListener { 106 107 // TODO: clean up debug tag and rename this class 108 private static final String TAG = "ContactsProvider ~~~~"; 109 110 // TODO: carefully prevent all incoming nested queries; they can be gaping security holes 111 // TODO: check for restricted flag during insert(), update(), and delete() calls 112 113 /** Default for the maximum number of returned aggregation suggestions. */ 114 private static final int DEFAULT_MAX_SUGGESTIONS = 5; 115 116 /** 117 * Shared preference key for the legacy contact import version. The need for a version 118 * as opposed to a boolean flag is that if we discover bugs in the contact import process, 119 * we can trigger re-import by incrementing the import version. 120 */ 121 private static final String PREF_CONTACTS_IMPORTED = "contacts_imported_v1"; 122 private static final int PREF_CONTACTS_IMPORT_VERSION = 1; 123 124 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 125 126 private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, " 127 + Contacts.TIMES_CONTACTED + " DESC, " 128 + Contacts.DISPLAY_NAME + " ASC"; 129 private static final String STREQUENT_LIMIT = 130 "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE " 131 + Contacts.STARRED + "=1) + 25"; 132 133 private static final int CONTACTS = 1000; 134 private static final int CONTACTS_ID = 1001; 135 private static final int CONTACTS_LOOKUP = 1002; 136 private static final int CONTACTS_LOOKUP_ID = 1003; 137 private static final int CONTACTS_DATA = 1004; 138 private static final int CONTACTS_FILTER = 1005; 139 private static final int CONTACTS_STREQUENT = 1006; 140 private static final int CONTACTS_STREQUENT_FILTER = 1007; 141 private static final int CONTACTS_GROUP = 1008; 142 private static final int CONTACTS_PHOTO = 1009; 143 144 private static final int RAW_CONTACTS = 2002; 145 private static final int RAW_CONTACTS_ID = 2003; 146 private static final int RAW_CONTACTS_DATA = 2004; 147 148 private static final int DATA = 3000; 149 private static final int DATA_ID = 3001; 150 private static final int PHONES = 3002; 151 private static final int PHONES_FILTER = 3003; 152 private static final int EMAILS = 3004; 153 private static final int EMAILS_LOOKUP = 3005; 154 private static final int EMAILS_FILTER = 3006; 155 private static final int POSTALS = 3007; 156 157 private static final int PHONE_LOOKUP = 4000; 158 159 private static final int AGGREGATION_EXCEPTIONS = 6000; 160 private static final int AGGREGATION_EXCEPTION_ID = 6001; 161 162 private static final int PRESENCE = 7000; 163 private static final int PRESENCE_ID = 7001; 164 165 private static final int AGGREGATION_SUGGESTIONS = 8000; 166 167 private static final int SETTINGS = 9000; 168 169 private static final int GROUPS = 10000; 170 private static final int GROUPS_ID = 10001; 171 private static final int GROUPS_SUMMARY = 10003; 172 173 private static final int SYNCSTATE = 11000; 174 175 private static final int SEARCH_SUGGESTIONS = 12001; 176 private static final int SEARCH_SHORTCUT = 12002; 177 178 private static final int DATA_WITH_PRESENCE = 13000; 179 180 private static final int LIVE_FOLDERS_CONTACTS = 14000; 181 private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001; 182 private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002; 183 private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003; 184 185 private interface ContactsQuery { 186 public static final String TABLE = Tables.RAW_CONTACTS; 187 188 public static final String[] PROJECTION = new String[] { 189 RawContactsColumns.CONCRETE_ID, 190 RawContacts.ACCOUNT_NAME, 191 RawContacts.ACCOUNT_TYPE, 192 }; 193 194 public static final int RAW_CONTACT_ID = 0; 195 public static final int ACCOUNT_NAME = 1; 196 public static final int ACCOUNT_TYPE = 2; 197 } 198 199 private interface DataContactsQuery { 200 public static final String TABLE = Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS; 201 202 public static final String[] PROJECTION = new String[] { 203 RawContactsColumns.CONCRETE_ID, 204 DataColumns.CONCRETE_ID, 205 ContactsColumns.CONCRETE_ID, 206 MimetypesColumns.CONCRETE_ID, 207 }; 208 209 public static final int RAW_CONTACT_ID = 0; 210 public static final int DATA_ID = 1; 211 public static final int CONTACT_ID = 2; 212 public static final int MIMETYPE_ID = 3; 213 } 214 215 private interface DisplayNameQuery { 216 public static final String TABLE = Tables.DATA_JOIN_MIMETYPES; 217 218 public static final String[] COLUMNS = new String[] { 219 MimetypesColumns.MIMETYPE, 220 Data.IS_PRIMARY, 221 Data.DATA2, 222 StructuredName.DISPLAY_NAME, 223 }; 224 225 public static final int MIMETYPE = 0; 226 public static final int IS_PRIMARY = 1; 227 public static final int DATA2 = 2; 228 public static final int DISPLAY_NAME = 3; 229 } 230 231 private interface DataDeleteQuery { 232 public static final String TABLE = Tables.DATA_JOIN_MIMETYPES; 233 234 public static final String[] CONCRETE_COLUMNS = new String[] { 235 DataColumns.CONCRETE_ID, 236 MimetypesColumns.MIMETYPE, 237 Data.RAW_CONTACT_ID, 238 Data.IS_PRIMARY, 239 Data.DATA2, 240 }; 241 242 public static final String[] COLUMNS = new String[] { 243 Data._ID, 244 MimetypesColumns.MIMETYPE, 245 Data.RAW_CONTACT_ID, 246 Data.IS_PRIMARY, 247 Data.DATA2, 248 }; 249 250 public static final int _ID = 0; 251 public static final int MIMETYPE = 1; 252 public static final int RAW_CONTACT_ID = 2; 253 public static final int IS_PRIMARY = 3; 254 public static final int DATA2 = 4; 255 } 256 257 private interface DataUpdateQuery { 258 String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE }; 259 260 int _ID = 0; 261 int RAW_CONTACT_ID = 1; 262 int MIMETYPE = 2; 263 } 264 265 private static final HashMap<String, Integer> sDisplayNameSources; 266 static { 267 sDisplayNameSources = new HashMap<String, Integer>(); 268 sDisplayNameSources.put(StructuredName.CONTENT_ITEM_TYPE, 269 DisplayNameSources.STRUCTURED_NAME); 270 sDisplayNameSources.put(Organization.CONTENT_ITEM_TYPE, 271 DisplayNameSources.ORGANIZATION); 272 sDisplayNameSources.put(Phone.CONTENT_ITEM_TYPE, 273 DisplayNameSources.PHONE); 274 sDisplayNameSources.put(Email.CONTENT_ITEM_TYPE, 275 DisplayNameSources.EMAIL); 276 } 277 278 public static final String DEFAULT_ACCOUNT_TYPE = "com.google.GAIA"; 279 public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google"; 280 281 /** Contains just BaseColumns._COUNT */ 282 private static final HashMap<String, String> sCountProjectionMap; 283 /** Contains just the contacts columns */ 284 private static final HashMap<String, String> sContactsProjectionMap; 285 /** Contains contacts and presence columns */ 286 private static final HashMap<String, String> sContactsWithPresenceProjectionMap; 287 /** Contains just the raw contacts columns */ 288 private static final HashMap<String, String> sRawContactsProjectionMap; 289 /** Contains columns from the data view */ 290 private static final HashMap<String, String> sDataProjectionMap; 291 /** Contains columns from the data view */ 292 private static final HashMap<String, String> sDistinctDataProjectionMap; 293 /** Contains the data and contacts columns, for joined tables */ 294 private static final HashMap<String, String> sPhoneLookupProjectionMap; 295 /** Contains the just the {@link Groups} columns */ 296 private static final HashMap<String, String> sGroupsProjectionMap; 297 /** Contains {@link Groups} columns along with summary details */ 298 private static final HashMap<String, String> sGroupsSummaryProjectionMap; 299 /** Contains the agg_exceptions columns */ 300 private static final HashMap<String, String> sAggregationExceptionsProjectionMap; 301 /** Contains the agg_exceptions columns */ 302 private static final HashMap<String, String> sSettingsProjectionMap; 303 /** Contains Presence columns */ 304 private static final HashMap<String, String> sPresenceProjectionMap; 305 /** Contains Presence columns */ 306 private static final HashMap<String, String> sDataWithPresenceProjectionMap; 307 /** Contains Live Folders columns */ 308 private static final HashMap<String, String> sLiveFoldersProjectionMap; 309 310 /** Sql where statement for filtering on groups. */ 311 private static final String sContactsInGroupSelect; 312 313 /** Precompiled sql statement for setting a data record to the primary. */ 314 private SQLiteStatement mSetPrimaryStatement; 315 /** Precompiled sql statement for setting a data record to the super primary. */ 316 private SQLiteStatement mSetSuperPrimaryStatement; 317 /** Precompiled sql statement for incrementing times contacted for an contact */ 318 private SQLiteStatement mLastTimeContactedUpdate; 319 /** Precompiled sql statement for updating a contact display name */ 320 private SQLiteStatement mRawContactDisplayNameUpdate; 321 /** Precompiled sql statement for marking a raw contact as dirty */ 322 private SQLiteStatement mRawContactDirtyUpdate; 323 /** Precompiled sql statement for setting an aggregated presence */ 324 private SQLiteStatement mAggregatedPresenceReplace; 325 /** Precompiled sql statement for updating an aggregated presence status */ 326 private SQLiteStatement mAggregatedPresenceStatusUpdate; 327 328 static { 329 // Contacts URI matching table 330 final UriMatcher matcher = sUriMatcher; 331 matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS); 332 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 333 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA); 334 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions", 335 AGGREGATION_SUGGESTIONS); 336 matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO); 337 matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER); 338 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP); 339 matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID); 340 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT); 341 matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*", 342 CONTACTS_STREQUENT_FILTER); 343 matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP); 344 345 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS); 346 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID); 347 matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA); 348 349 matcher.addURI(ContactsContract.AUTHORITY, "data", DATA); 350 matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID); 351 matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES); 352 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER); 353 matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER); 354 matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS); 355 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP); 356 matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER); 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 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts", 385 LIVE_FOLDERS_CONTACTS); 386 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*", 387 LIVE_FOLDERS_CONTACTS_GROUP_NAME); 388 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones", 389 LIVE_FOLDERS_CONTACTS_WITH_PHONES); 390 matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites", 391 LIVE_FOLDERS_CONTACTS_FAVORITES); 392 393 // Private API 394 matcher.addURI(ContactsContract.AUTHORITY, "data_with_presence", DATA_WITH_PRESENCE); 395 } 396 397 static { 398 sCountProjectionMap = new HashMap<String, String>(); 399 sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)"); 400 401 sContactsProjectionMap = new HashMap<String, String>(); 402 sContactsProjectionMap.put(Contacts._ID, Contacts._ID); 403 sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 404 sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 405 sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 406 sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 407 sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP); 408 sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 409 sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 410 sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER); 411 sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 412 sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY); 413 414 sContactsWithPresenceProjectionMap = new HashMap<String, String>(); 415 sContactsWithPresenceProjectionMap.putAll(sContactsProjectionMap); 416 sContactsWithPresenceProjectionMap.put(Contacts.PRESENCE_STATUS, 417 Presence.PRESENCE_STATUS + " AS " + Contacts.PRESENCE_STATUS); 418 sContactsWithPresenceProjectionMap.put(Contacts.PRESENCE_CUSTOM_STATUS, 419 Presence.PRESENCE_CUSTOM_STATUS + " AS " + Contacts.PRESENCE_CUSTOM_STATUS); 420 421 sRawContactsProjectionMap = new HashMap<String, String>(); 422 sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID); 423 sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 424 sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 425 sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 426 sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 427 sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); 428 sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); 429 sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED); 430 sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED); 431 sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED, 432 RawContacts.LAST_TIME_CONTACTED); 433 sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE); 434 sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL); 435 sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED); 436 sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE); 437 sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1); 438 sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2); 439 sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3); 440 sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4); 441 442 sDataProjectionMap = new HashMap<String, String>(); 443 sDataProjectionMap.put(Data._ID, Data._ID); 444 sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID); 445 sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); 446 sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 447 sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 448 sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 449 sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); 450 sDataProjectionMap.put(Data.DATA1, Data.DATA1); 451 sDataProjectionMap.put(Data.DATA2, Data.DATA2); 452 sDataProjectionMap.put(Data.DATA3, Data.DATA3); 453 sDataProjectionMap.put(Data.DATA4, Data.DATA4); 454 sDataProjectionMap.put(Data.DATA5, Data.DATA5); 455 sDataProjectionMap.put(Data.DATA6, Data.DATA6); 456 sDataProjectionMap.put(Data.DATA7, Data.DATA7); 457 sDataProjectionMap.put(Data.DATA8, Data.DATA8); 458 sDataProjectionMap.put(Data.DATA9, Data.DATA9); 459 sDataProjectionMap.put(Data.DATA10, Data.DATA10); 460 sDataProjectionMap.put(Data.DATA11, Data.DATA11); 461 sDataProjectionMap.put(Data.DATA12, Data.DATA12); 462 sDataProjectionMap.put(Data.DATA13, Data.DATA13); 463 sDataProjectionMap.put(Data.DATA14, Data.DATA14); 464 sDataProjectionMap.put(Data.DATA15, Data.DATA15); 465 sDataProjectionMap.put(Data.SYNC1, Data.SYNC1); 466 sDataProjectionMap.put(Data.SYNC2, Data.SYNC2); 467 sDataProjectionMap.put(Data.SYNC3, Data.SYNC3); 468 sDataProjectionMap.put(Data.SYNC4, Data.SYNC4); 469 sDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 470 sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME); 471 sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE); 472 sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID); 473 sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION); 474 sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY); 475 sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 476 sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 477 sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 478 sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 479 sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 480 sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 481 sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 482 sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID); 483 484 // Projection map for data grouped by contact (not raw contact) and some data field(s) 485 sDistinctDataProjectionMap = new HashMap<String, String>(); 486 sDistinctDataProjectionMap.put(Data._ID, 487 "MIN(" + Data._ID + ") AS " + Data._ID); 488 sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION); 489 sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY); 490 sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY); 491 sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE); 492 sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE); 493 sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1); 494 sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2); 495 sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3); 496 sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4); 497 sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5); 498 sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6); 499 sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7); 500 sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8); 501 sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9); 502 sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10); 503 sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11); 504 sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12); 505 sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13); 506 sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14); 507 sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15); 508 sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1); 509 sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2); 510 sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3); 511 sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4); 512 sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID); 513 sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME); 514 sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE); 515 sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL); 516 sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED); 517 sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED); 518 sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED); 519 sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID); 520 sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, 521 GroupMembership.GROUP_SOURCE_ID); 522 523 sPhoneLookupProjectionMap = new HashMap<String, String>(); 524 sPhoneLookupProjectionMap.put(PhoneLookup._ID, 525 ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID); 526 sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME, 527 ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME); 528 sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED, 529 ContactsColumns.CONCRETE_LAST_TIME_CONTACTED 530 + " AS " + PhoneLookup.LAST_TIME_CONTACTED); 531 sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED, 532 ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED); 533 sPhoneLookupProjectionMap.put(PhoneLookup.STARRED, 534 ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED); 535 sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP, 536 Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP); 537 sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID, 538 Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID); 539 sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE, 540 ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE); 541 sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER, 542 Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER); 543 sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL, 544 ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL 545 + " AS " + PhoneLookup.SEND_TO_VOICEMAIL); 546 sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER, 547 Phone.NUMBER + " AS " + PhoneLookup.NUMBER); 548 sPhoneLookupProjectionMap.put(PhoneLookup.TYPE, 549 Phone.TYPE + " AS " + PhoneLookup.TYPE); 550 sPhoneLookupProjectionMap.put(PhoneLookup.LABEL, 551 Phone.LABEL + " AS " + PhoneLookup.LABEL); 552 553 HashMap<String, String> columns; 554 555 // Groups projection map 556 columns = new HashMap<String, String>(); 557 columns.put(Groups._ID, "groups._id AS _id"); 558 columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME); 559 columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE); 560 columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID); 561 columns.put(Groups.DIRTY, Groups.DIRTY); 562 columns.put(Groups.VERSION, Groups.VERSION); 563 columns.put(Groups.RES_PACKAGE, PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE); 564 columns.put(Groups.TITLE, Groups.TITLE); 565 columns.put(Groups.TITLE_RES, Groups.TITLE_RES); 566 columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE); 567 columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID); 568 columns.put(Groups.DELETED, Groups.DELETED); 569 columns.put(Groups.NOTES, Groups.NOTES); 570 columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC); 571 columns.put(Groups.SYNC1, Tables.GROUPS + "." + Groups.SYNC1 + " AS " + Groups.SYNC1); 572 columns.put(Groups.SYNC2, Tables.GROUPS + "." + Groups.SYNC2 + " AS " + Groups.SYNC2); 573 columns.put(Groups.SYNC3, Tables.GROUPS + "." + Groups.SYNC3 + " AS " + Groups.SYNC3); 574 columns.put(Groups.SYNC4, Tables.GROUPS + "." + Groups.SYNC4 + " AS " + Groups.SYNC4); 575 sGroupsProjectionMap = columns; 576 577 // RawContacts and groups projection map 578 columns = new HashMap<String, String>(); 579 columns.putAll(sGroupsProjectionMap); 580 columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID 581 + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " 582 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 583 + ") AS " + Groups.SUMMARY_COUNT); 584 columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT " 585 + ContactsColumns.CONCRETE_ID + ") FROM " 586 + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE " 587 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP 588 + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES); 589 sGroupsSummaryProjectionMap = columns; 590 591 // Aggregate exception projection map 592 columns = new HashMap<String, String>(); 593 columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id"); 594 columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE); 595 columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1); 596 columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2); 597 sAggregationExceptionsProjectionMap = columns; 598 599 // Settings projection map 600 columns = new HashMap<String, String>(); 601 columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME); 602 columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE); 603 columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE); 604 columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC); 605 columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM " 606 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY " 607 + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS 608 + ")) AS " + Settings.UNGROUPED_COUNT); 609 columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM " 610 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE " 611 + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID 612 + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS " 613 + Settings.UNGROUPED_WITH_PHONES); 614 sSettingsProjectionMap = columns; 615 616 columns = new HashMap<String, String>(); 617 columns.put(Presence._ID, Presence._ID); 618 columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID); 619 columns.put(Presence.DATA_ID, Presence.DATA_ID); 620 columns.put(Presence.IM_ACCOUNT, Presence.IM_ACCOUNT); 621 columns.put(Presence.IM_HANDLE, Presence.IM_HANDLE); 622 columns.put(Presence.PROTOCOL, Presence.PROTOCOL); 623 columns.put(Presence.CUSTOM_PROTOCOL, Presence.CUSTOM_PROTOCOL); 624 columns.put(Presence.PRESENCE_STATUS, Presence.PRESENCE_STATUS); 625 columns.put(Presence.PRESENCE_CUSTOM_STATUS, Presence.PRESENCE_CUSTOM_STATUS); 626 sPresenceProjectionMap = columns; 627 628 sDataWithPresenceProjectionMap = new HashMap<String, String>(); 629 sDataWithPresenceProjectionMap.putAll(sDataProjectionMap); 630 sDataWithPresenceProjectionMap.put(Presence.PRESENCE_STATUS, 631 Presence.PRESENCE_STATUS); 632 sDataWithPresenceProjectionMap.put(Presence.PRESENCE_CUSTOM_STATUS, 633 Presence.PRESENCE_CUSTOM_STATUS); 634 635 // Live folder projection 636 sLiveFoldersProjectionMap = new HashMap<String, String>(); 637 sLiveFoldersProjectionMap.put(LiveFolders._ID, 638 Contacts._ID + " AS " + LiveFolders._ID); 639 sLiveFoldersProjectionMap.put(LiveFolders.NAME, 640 Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME); 641 642 // TODO: Put contact photo back when we have a way to display a default icon 643 // for contacts without a photo 644 // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP, 645 // Photos.DATA + " AS " + LiveFolders.ICON_BITMAP); 646 647 sContactsInGroupSelect = Contacts._ID + " IN " 648 + "(SELECT " + RawContacts.CONTACT_ID 649 + " FROM " + Tables.RAW_CONTACTS 650 + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN " 651 + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID 652 + " FROM " + Tables.DATA_JOIN_MIMETYPES 653 + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 654 + "' AND " + GroupMembership.GROUP_ROW_ID + "=" 655 + "(SELECT " + Tables.GROUPS + "." + Groups._ID 656 + " FROM " + Tables.GROUPS 657 + " WHERE " + Groups.TITLE + "=?)))"; 658 } 659 660 /** 661 * Handles inserts and update for a specific Data type. 662 */ 663 private abstract class DataRowHandler { 664 665 protected final String mMimetype; 666 protected long mMimetypeId; 667 668 public DataRowHandler(String mimetype) { 669 mMimetype = mimetype; 670 } 671 672 protected long getMimeTypeId() { 673 if (mMimetypeId == 0) { 674 mMimetypeId = mOpenHelper.getMimeTypeId(mMimetype); 675 } 676 return mMimetypeId; 677 } 678 679 /** 680 * Inserts a row into the {@link Data} table. 681 */ 682 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 683 final long dataId = db.insert(Tables.DATA, null, values); 684 685 Integer primary = values.getAsInteger(Data.IS_PRIMARY); 686 if (primary != null && primary != 0) { 687 setIsPrimary(rawContactId, dataId, getMimeTypeId()); 688 } 689 690 return dataId; 691 } 692 693 /** 694 * Validates data and updates a {@link Data} row using the cursor, which contains 695 * the current data. 696 */ 697 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 698 boolean markRawContactAsDirty) { 699 long dataId = c.getLong(DataUpdateQuery._ID); 700 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 701 702 if (values.containsKey(Data.IS_SUPER_PRIMARY)) { 703 long mimeTypeId = getMimeTypeId(); 704 setIsSuperPrimary(rawContactId, dataId, mimeTypeId); 705 setIsPrimary(rawContactId, dataId, mimeTypeId); 706 707 // Now that we've taken care of setting these, remove them from "values". 708 values.remove(Data.IS_SUPER_PRIMARY); 709 values.remove(Data.IS_PRIMARY); 710 } else if (values.containsKey(Data.IS_PRIMARY)) { 711 setIsPrimary(rawContactId, dataId, getMimeTypeId()); 712 713 // Now that we've taken care of setting this, remove it from "values". 714 values.remove(Data.IS_PRIMARY); 715 } 716 717 if (values.size() > 0) { 718 mDb.update(Tables.DATA, values, Data._ID + " = " + dataId, null); 719 } 720 721 if (markRawContactAsDirty) { 722 setRawContactDirty(rawContactId); 723 } 724 } 725 726 public int delete(SQLiteDatabase db, Cursor c) { 727 long dataId = c.getLong(DataDeleteQuery._ID); 728 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 729 boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0; 730 int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null); 731 if (count != 0 && primary) { 732 fixPrimary(db, rawContactId); 733 } 734 return count; 735 } 736 737 private void fixPrimary(SQLiteDatabase db, long rawContactId) { 738 long newPrimaryId = findNewPrimaryDataId(db, rawContactId); 739 if (newPrimaryId != -1) { 740 setIsPrimary(rawContactId, newPrimaryId, getMimeTypeId()); 741 } 742 } 743 744 protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) { 745 long primaryId = -1; 746 int primaryType = -1; 747 Cursor c = queryData(db, rawContactId); 748 try { 749 while (c.moveToNext()) { 750 long dataId = c.getLong(DataDeleteQuery._ID); 751 int type = c.getInt(DataDeleteQuery.DATA2); 752 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) { 753 primaryId = dataId; 754 primaryType = type; 755 } 756 } 757 } finally { 758 c.close(); 759 } 760 return primaryId; 761 } 762 763 /** 764 * Returns the rank of a specific record type to be used in determining the primary 765 * row. Lower number represents higher priority. 766 */ 767 protected int getTypeRank(int type) { 768 return 0; 769 } 770 771 protected Cursor queryData(SQLiteDatabase db, long rawContactId) { 772 return db.query(DataDeleteQuery.TABLE, DataDeleteQuery.CONCRETE_COLUMNS, 773 Data.RAW_CONTACT_ID + "=" + rawContactId + 774 " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'", 775 null, null, null, null); 776 } 777 778 protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) { 779 String bestDisplayName = null; 780 int bestDisplayNameSource = DisplayNameSources.UNDEFINED; 781 782 Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS, 783 Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null); 784 try { 785 while (c.moveToNext()) { 786 String mimeType = c.getString(DisplayNameQuery.MIMETYPE); 787 boolean primary; 788 String name; 789 790 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 791 name = c.getString(DisplayNameQuery.DISPLAY_NAME); 792 primary = true; 793 } else { 794 name = c.getString(DisplayNameQuery.DATA2); 795 primary = (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0); 796 } 797 798 if (primary && name != null) { 799 Integer source = sDisplayNameSources.get(mimeType); 800 if (source != null && source > bestDisplayNameSource) { 801 bestDisplayNameSource = source; 802 bestDisplayName = name; 803 } 804 } 805 } 806 807 } finally { 808 c.close(); 809 } 810 811 setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource); 812 if (!isNewRawContact(rawContactId)) { 813 mContactAggregator.updateDisplayName(db, rawContactId); 814 } 815 } 816 817 public boolean isAggregationRequired() { 818 return true; 819 } 820 821 /** 822 * Return set of values, using current values at given {@link Data#_ID} 823 * as baseline, but augmented with any updates. 824 */ 825 public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId, 826 ContentValues update) { 827 final ContentValues values = new ContentValues(); 828 final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=" + dataId, 829 null, null, null, null); 830 try { 831 if (cursor.moveToFirst()) { 832 for (int i = 0; i < cursor.getColumnCount(); i++) { 833 final String key = cursor.getColumnName(i); 834 values.put(key, cursor.getString(i)); 835 } 836 } 837 } finally { 838 cursor.close(); 839 } 840 values.putAll(update); 841 return values; 842 } 843 } 844 845 public class CustomDataRowHandler extends DataRowHandler { 846 847 public CustomDataRowHandler(String mimetype) { 848 super(mimetype); 849 } 850 } 851 852 public class StructuredNameRowHandler extends DataRowHandler { 853 private final NameSplitter mSplitter; 854 855 public StructuredNameRowHandler(NameSplitter splitter) { 856 super(StructuredName.CONTENT_ITEM_TYPE); 857 mSplitter = splitter; 858 } 859 860 @Override 861 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 862 fixStructuredNameComponents(values, values); 863 864 long dataId = super.insert(db, rawContactId, values); 865 866 String givenName = values.getAsString(StructuredName.GIVEN_NAME); 867 String familyName = values.getAsString(StructuredName.FAMILY_NAME); 868 mOpenHelper.insertNameLookupForStructuredName(rawContactId, dataId, givenName, 869 familyName); 870 fixRawContactDisplayName(db, rawContactId); 871 return dataId; 872 } 873 874 @Override 875 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 876 boolean markRawContactAsDirty) { 877 final long dataId = c.getLong(DataUpdateQuery._ID); 878 final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 879 880 final ContentValues augmented = getAugmentedValues(db, dataId, values); 881 fixStructuredNameComponents(augmented, values); 882 883 super.update(db, values, c, markRawContactAsDirty); 884 885 boolean hasGivenName = values.containsKey(StructuredName.GIVEN_NAME); 886 boolean hasFamilyName = values.containsKey(StructuredName.FAMILY_NAME); 887 if (hasGivenName || hasFamilyName) { 888 String givenName; 889 String familyName;// = values.getAsString(StructuredName.FAMILY_NAME); 890 if (hasGivenName) { 891 givenName = values.getAsString(StructuredName.GIVEN_NAME); 892 } else { 893 894 // TODO compiled statement 895 givenName = DatabaseUtils.stringForQuery(db, 896 "SELECT " + StructuredName.GIVEN_NAME + 897 " FROM " + Tables.DATA + 898 " WHERE " + Data._ID + "=" + dataId, null); 899 } 900 if (hasFamilyName) { 901 familyName = values.getAsString(StructuredName.FAMILY_NAME); 902 } else { 903 904 // TODO compiled statement 905 familyName = DatabaseUtils.stringForQuery(db, 906 "SELECT " + StructuredName.FAMILY_NAME + 907 " FROM " + Tables.DATA + 908 " WHERE " + Data._ID + "=" + dataId, null); 909 } 910 911 mOpenHelper.deleteNameLookup(dataId); 912 mOpenHelper.insertNameLookupForStructuredName(rawContactId, dataId, givenName, 913 familyName); 914 } 915 fixRawContactDisplayName(db, rawContactId); 916 } 917 918 @Override 919 public int delete(SQLiteDatabase db, Cursor c) { 920 long dataId = c.getLong(DataDeleteQuery._ID); 921 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 922 923 int count = super.delete(db, c); 924 925 mOpenHelper.deleteNameLookup(dataId); 926 fixRawContactDisplayName(db, rawContactId); 927 return count; 928 } 929 930 /** 931 * Specific list of structured fields. 932 */ 933 private final String[] STRUCTURED_FIELDS = new String[] { 934 StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME, 935 StructuredName.FAMILY_NAME, StructuredName.SUFFIX 936 }; 937 938 /** 939 * Parses the supplied display name, but only if the incoming values do 940 * not already contain structured name parts. Also, if the display name 941 * is not provided, generate one by concatenating first name and last 942 * name. 943 */ 944 private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) { 945 final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME); 946 947 final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); 948 final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); 949 950 final NameSplitter.Name name = new NameSplitter.Name(); 951 952 if (touchedUnstruct && !touchedStruct) { 953 mSplitter.split(name, unstruct); 954 name.toValues(update); 955 } else if (!touchedUnstruct && touchedStruct) { 956 name.fromValues(augmented); 957 final String joined = mSplitter.join(name); 958 update.put(StructuredName.DISPLAY_NAME, joined); 959 } 960 } 961 } 962 963 public class StructuredPostalRowHandler extends DataRowHandler { 964 private PostalSplitter mSplitter; 965 966 public StructuredPostalRowHandler(PostalSplitter splitter) { 967 super(StructuredPostal.CONTENT_ITEM_TYPE); 968 mSplitter = splitter; 969 } 970 971 @Override 972 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 973 fixStructuredPostalComponents(values, values); 974 return super.insert(db, rawContactId, values); 975 } 976 977 @Override 978 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 979 boolean markRawContactAsDirty) { 980 final long dataId = c.getLong(DataUpdateQuery._ID); 981 final ContentValues augmented = getAugmentedValues(db, dataId, values); 982 fixStructuredPostalComponents(augmented, values); 983 super.update(db, values, c, markRawContactAsDirty); 984 } 985 986 /** 987 * Specific list of structured fields. 988 */ 989 private final String[] STRUCTURED_FIELDS = new String[] { 990 StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD, 991 StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE, 992 StructuredPostal.COUNTRY, 993 }; 994 995 /** 996 * Prepares the given {@link StructuredPostal} row, building 997 * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured 998 * values when missing. When structured components are missing, the 999 * unstructured value is assigned to {@link StructuredPostal#STREET}. 1000 */ 1001 private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) { 1002 final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS); 1003 1004 final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct); 1005 final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS); 1006 1007 final PostalSplitter.Postal postal = new PostalSplitter.Postal(); 1008 1009 if (touchedUnstruct && !touchedStruct) { 1010 mSplitter.split(postal, unstruct); 1011 postal.toValues(update); 1012 } else if (!touchedUnstruct && touchedStruct) { 1013 postal.fromValues(augmented); 1014 final String joined = mSplitter.join(postal); 1015 update.put(StructuredPostal.FORMATTED_ADDRESS, joined); 1016 } 1017 } 1018 } 1019 1020 public class CommonDataRowHandler extends DataRowHandler { 1021 1022 private final String mTypeColumn; 1023 private final String mLabelColumn; 1024 1025 public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) { 1026 super(mimetype); 1027 mTypeColumn = typeColumn; 1028 mLabelColumn = labelColumn; 1029 } 1030 1031 @Override 1032 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1033 enforceTypeAndLabel(values, values); 1034 return super.insert(db, rawContactId, values); 1035 } 1036 1037 @Override 1038 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1039 boolean markRawContactAsDirty) { 1040 final long dataId = c.getLong(DataUpdateQuery._ID); 1041 final ContentValues augmented = getAugmentedValues(db, dataId, values); 1042 enforceTypeAndLabel(augmented, values); 1043 super.update(db, values, c, markRawContactAsDirty); 1044 } 1045 1046 /** 1047 * If the given {@link ContentValues} defines {@link #mTypeColumn}, 1048 * enforce that {@link #mLabelColumn} only appears when type is 1049 * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise. 1050 */ 1051 private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) { 1052 final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn)); 1053 final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn)); 1054 1055 if (hasLabel && !hasType) { 1056 // When label exists, assert that some type is defined 1057 throw new IllegalArgumentException(mTypeColumn + " must be specified when " 1058 + mLabelColumn + " is defined."); 1059 } 1060 } 1061 } 1062 1063 public class OrganizationDataRowHandler extends CommonDataRowHandler { 1064 1065 public OrganizationDataRowHandler() { 1066 super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL); 1067 } 1068 1069 @Override 1070 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1071 long id = super.insert(db, rawContactId, values); 1072 fixRawContactDisplayName(db, rawContactId); 1073 return id; 1074 } 1075 1076 @Override 1077 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1078 boolean markRawContactAsDirty) { 1079 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1080 1081 super.update(db, values, c, markRawContactAsDirty); 1082 1083 fixRawContactDisplayName(db, rawContactId); 1084 } 1085 1086 @Override 1087 public int delete(SQLiteDatabase db, Cursor c) { 1088 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1089 1090 int count = super.delete(db, c); 1091 fixRawContactDisplayName(db, rawContactId); 1092 return count; 1093 } 1094 1095 @Override 1096 protected int getTypeRank(int type) { 1097 switch (type) { 1098 case Organization.TYPE_WORK: return 0; 1099 case Organization.TYPE_CUSTOM: return 1; 1100 case Organization.TYPE_OTHER: return 2; 1101 default: return 1000; 1102 } 1103 } 1104 1105 @Override 1106 public boolean isAggregationRequired() { 1107 return false; 1108 } 1109 } 1110 1111 public class EmailDataRowHandler extends CommonDataRowHandler { 1112 1113 public EmailDataRowHandler() { 1114 super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL); 1115 } 1116 1117 @Override 1118 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1119 String address = values.getAsString(Email.DATA); 1120 1121 long dataId = super.insert(db, rawContactId, values); 1122 1123 fixRawContactDisplayName(db, rawContactId); 1124 mOpenHelper.insertNameLookupForEmail(rawContactId, dataId, address); 1125 return dataId; 1126 } 1127 1128 @Override 1129 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1130 boolean markRawContactAsDirty) { 1131 long dataId = c.getLong(DataUpdateQuery._ID); 1132 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1133 String address = values.getAsString(Email.DATA); 1134 1135 super.update(db, values, c, markRawContactAsDirty); 1136 1137 mOpenHelper.deleteNameLookup(dataId); 1138 mOpenHelper.insertNameLookupForEmail(rawContactId, dataId, address); 1139 fixRawContactDisplayName(db, rawContactId); 1140 } 1141 1142 @Override 1143 public int delete(SQLiteDatabase db, Cursor c) { 1144 long dataId = c.getLong(DataDeleteQuery._ID); 1145 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1146 1147 int count = super.delete(db, c); 1148 1149 mOpenHelper.deleteNameLookup(dataId); 1150 fixRawContactDisplayName(db, rawContactId); 1151 return count; 1152 } 1153 1154 @Override 1155 protected int getTypeRank(int type) { 1156 switch (type) { 1157 case Email.TYPE_HOME: return 0; 1158 case Email.TYPE_WORK: return 1; 1159 case Email.TYPE_CUSTOM: return 2; 1160 case Email.TYPE_OTHER: return 3; 1161 default: return 1000; 1162 } 1163 } 1164 } 1165 1166 public class NicknameDataRowHandler extends CommonDataRowHandler { 1167 1168 public NicknameDataRowHandler() { 1169 super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL); 1170 } 1171 1172 @Override 1173 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1174 String nickname = values.getAsString(Nickname.NAME); 1175 1176 long dataId = super.insert(db, rawContactId, values); 1177 1178 fixRawContactDisplayName(db, rawContactId); 1179 mOpenHelper.insertNameLookupForNickname(rawContactId, dataId, nickname); 1180 return dataId; 1181 } 1182 1183 @Override 1184 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1185 boolean markRawContactAsDirty) { 1186 long dataId = c.getLong(DataUpdateQuery._ID); 1187 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1188 String nickname = values.getAsString(Nickname.NAME); 1189 1190 super.update(db, values, c, markRawContactAsDirty); 1191 1192 mOpenHelper.deleteNameLookup(dataId); 1193 mOpenHelper.insertNameLookupForNickname(rawContactId, dataId, nickname); 1194 fixRawContactDisplayName(db, rawContactId); 1195 } 1196 1197 @Override 1198 public int delete(SQLiteDatabase db, Cursor c) { 1199 long dataId = c.getLong(DataDeleteQuery._ID); 1200 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1201 1202 int count = super.delete(db, c); 1203 1204 mOpenHelper.deleteNameLookup(dataId); 1205 fixRawContactDisplayName(db, rawContactId); 1206 return count; 1207 } 1208 } 1209 1210 public class PhoneDataRowHandler extends CommonDataRowHandler { 1211 1212 public PhoneDataRowHandler() { 1213 super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL); 1214 } 1215 1216 @Override 1217 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1218 long dataId; 1219 if (values.containsKey(Phone.NUMBER)) { 1220 String number = values.getAsString(Phone.NUMBER); 1221 String normalizedNumber = computeNormalizedNumber(number, values); 1222 1223 dataId = super.insert(db, rawContactId, values); 1224 1225 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); 1226 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1227 fixRawContactDisplayName(db, rawContactId); 1228 } else { 1229 dataId = super.insert(db, rawContactId, values); 1230 } 1231 return dataId; 1232 } 1233 1234 @Override 1235 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1236 boolean markRawContactAsDirty) { 1237 long dataId = c.getLong(DataUpdateQuery._ID); 1238 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1239 if (values.containsKey(Phone.NUMBER)) { 1240 String number = values.getAsString(Phone.NUMBER); 1241 String normalizedNumber = computeNormalizedNumber(number, values); 1242 1243 super.update(db, values, c, markRawContactAsDirty); 1244 1245 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber); 1246 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1247 fixRawContactDisplayName(db, rawContactId); 1248 } else { 1249 super.update(db, values, c, markRawContactAsDirty); 1250 } 1251 } 1252 1253 @Override 1254 public int delete(SQLiteDatabase db, Cursor c) { 1255 long dataId = c.getLong(DataDeleteQuery._ID); 1256 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1257 1258 int count = super.delete(db, c); 1259 1260 updatePhoneLookup(db, rawContactId, dataId, null, null); 1261 mContactAggregator.updateHasPhoneNumber(db, rawContactId); 1262 fixRawContactDisplayName(db, rawContactId); 1263 return count; 1264 } 1265 1266 private String computeNormalizedNumber(String number, ContentValues values) { 1267 String normalizedNumber = null; 1268 if (number != null) { 1269 normalizedNumber = PhoneNumberUtils.getStrippedReversed(number); 1270 } 1271 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber); 1272 return normalizedNumber; 1273 } 1274 1275 private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId, 1276 String number, String normalizedNumber) { 1277 if (number != null) { 1278 ContentValues phoneValues = new ContentValues(); 1279 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId); 1280 phoneValues.put(PhoneLookupColumns.DATA_ID, dataId); 1281 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber); 1282 db.replace(Tables.PHONE_LOOKUP, null, phoneValues); 1283 } else { 1284 db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=" + dataId, null); 1285 } 1286 } 1287 1288 @Override 1289 protected int getTypeRank(int type) { 1290 switch (type) { 1291 case Phone.TYPE_MOBILE: return 0; 1292 case Phone.TYPE_WORK: return 1; 1293 case Phone.TYPE_HOME: return 2; 1294 case Phone.TYPE_PAGER: return 3; 1295 case Phone.TYPE_CUSTOM: return 4; 1296 case Phone.TYPE_OTHER: return 5; 1297 case Phone.TYPE_FAX_WORK: return 6; 1298 case Phone.TYPE_FAX_HOME: return 7; 1299 default: return 1000; 1300 } 1301 } 1302 } 1303 1304 public class GroupMembershipRowHandler extends DataRowHandler { 1305 1306 public GroupMembershipRowHandler() { 1307 super(GroupMembership.CONTENT_ITEM_TYPE); 1308 } 1309 1310 @Override 1311 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1312 resolveGroupSourceIdInValues(rawContactId, db, values, true); 1313 return super.insert(db, rawContactId, values); 1314 } 1315 1316 @Override 1317 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1318 boolean markRawContactAsDirty) { 1319 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1320 resolveGroupSourceIdInValues(rawContactId, db, values, false); 1321 super.update(db, values, c, markRawContactAsDirty); 1322 } 1323 1324 private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db, 1325 ContentValues values, boolean isInsert) { 1326 boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID); 1327 boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID); 1328 if (containsGroupSourceId && containsGroupId) { 1329 throw new IllegalArgumentException( 1330 "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID " 1331 + "and GroupMembership.GROUP_ROW_ID"); 1332 } 1333 1334 if (!containsGroupSourceId && !containsGroupId) { 1335 if (isInsert) { 1336 throw new IllegalArgumentException( 1337 "you must set exactly one of GroupMembership.GROUP_SOURCE_ID " 1338 + "and GroupMembership.GROUP_ROW_ID"); 1339 } else { 1340 return; 1341 } 1342 } 1343 1344 if (containsGroupSourceId) { 1345 final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID); 1346 final long groupId = getOrMakeGroup(db, rawContactId, sourceId); 1347 values.remove(GroupMembership.GROUP_SOURCE_ID); 1348 values.put(GroupMembership.GROUP_ROW_ID, groupId); 1349 } 1350 } 1351 1352 @Override 1353 public boolean isAggregationRequired() { 1354 return false; 1355 } 1356 } 1357 1358 public class PhotoDataRowHandler extends DataRowHandler { 1359 1360 public PhotoDataRowHandler() { 1361 super(Photo.CONTENT_ITEM_TYPE); 1362 } 1363 1364 @Override 1365 public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) { 1366 long dataId = super.insert(db, rawContactId, values); 1367 if (!isNewRawContact(rawContactId)) { 1368 mContactAggregator.updatePhotoId(db, rawContactId); 1369 } 1370 return dataId; 1371 } 1372 1373 @Override 1374 public void update(SQLiteDatabase db, ContentValues values, Cursor c, 1375 boolean markRawContactAsDirty) { 1376 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 1377 super.update(db, values, c, markRawContactAsDirty); 1378 mContactAggregator.updatePhotoId(db, rawContactId); 1379 } 1380 1381 @Override 1382 public int delete(SQLiteDatabase db, Cursor c) { 1383 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1384 int count = super.delete(db, c); 1385 mContactAggregator.updatePhotoId(db, rawContactId); 1386 return count; 1387 } 1388 1389 @Override 1390 public boolean isAggregationRequired() { 1391 return false; 1392 } 1393 } 1394 1395 1396 private HashMap<String, DataRowHandler> mDataRowHandlers; 1397 private final ContactAggregationScheduler mAggregationScheduler; 1398 private OpenHelper mOpenHelper; 1399 1400 private NameSplitter mNameSplitter; 1401 private PostalSplitter mPostalSplitter; 1402 1403 private ContactAggregator mContactAggregator; 1404 private LegacyApiSupport mLegacyApiSupport; 1405 private GlobalSearchSupport mGlobalSearchSupport; 1406 1407 private ContentValues mValues = new ContentValues(); 1408 1409 private volatile CountDownLatch mAccessLatch; 1410 private boolean mImportMode; 1411 1412 private boolean mScheduleAggregation; 1413 private ArrayList<Long> mInsertedRawContacts = new ArrayList<Long>(); 1414 1415 public ContactsProvider2() { 1416 this(new ContactAggregationScheduler()); 1417 } 1418 1419 /** 1420 * Constructor for testing. 1421 */ 1422 /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) { 1423 mAggregationScheduler = scheduler; 1424 } 1425 1426 @Override 1427 public boolean onCreate() { 1428 super.onCreate(); 1429 1430 final Context context = getContext(); 1431 mOpenHelper = (OpenHelper)getOpenHelper(); 1432 mGlobalSearchSupport = new GlobalSearchSupport(this); 1433 mLegacyApiSupport = new LegacyApiSupport(context, mOpenHelper, this, mGlobalSearchSupport); 1434 mContactAggregator = new ContactAggregator(this, mOpenHelper, mAggregationScheduler); 1435 1436 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1437 1438 mSetPrimaryStatement = db.compileStatement( 1439 "UPDATE " + Tables.DATA + 1440 " SET " + Data.IS_PRIMARY + "=(_id=?)" + 1441 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1442 " AND " + Data.RAW_CONTACT_ID + "=?"); 1443 1444 mSetSuperPrimaryStatement = db.compileStatement( 1445 "UPDATE " + Tables.DATA + 1446 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" + 1447 " WHERE " + DataColumns.MIMETYPE_ID + "=?" + 1448 " AND " + Data.RAW_CONTACT_ID + " IN (" + 1449 "SELECT " + RawContacts._ID + 1450 " FROM " + Tables.RAW_CONTACTS + 1451 " WHERE " + RawContacts.CONTACT_ID + " =(" + 1452 "SELECT " + RawContacts.CONTACT_ID + 1453 " FROM " + Tables.RAW_CONTACTS + 1454 " WHERE " + RawContacts._ID + "=?))"); 1455 1456 mLastTimeContactedUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET " 1457 + RawContacts.TIMES_CONTACTED + "=" + RawContacts.TIMES_CONTACTED + "+1," 1458 + RawContacts.LAST_TIME_CONTACTED + "=? WHERE " + RawContacts.CONTACT_ID + "=?"); 1459 1460 mRawContactDisplayNameUpdate = db.compileStatement( 1461 "UPDATE " + Tables.RAW_CONTACTS + 1462 " SET " + RawContactsColumns.DISPLAY_NAME + "=?," 1463 + RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" + 1464 " WHERE " + RawContacts._ID + "=?"); 1465 1466 mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET " 1467 + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?"); 1468 1469 mAggregatedPresenceReplace = db.compileStatement( 1470 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "(" 1471 + AggregatedPresenceColumns.CONTACT_ID + ", " 1472 + Presence.PRESENCE_STATUS 1473 + ") VALUES (?, (SELECT MAX(" + Presence.PRESENCE_STATUS + ")" 1474 + " FROM " + Tables.PRESENCE + "," + Tables.RAW_CONTACTS 1475 + " WHERE " + PresenceColumns.RAW_CONTACT_ID + "=" 1476 + RawContactsColumns.CONCRETE_ID 1477 + " AND " + RawContacts.CONTACT_ID + "=?))"); 1478 1479 mAggregatedPresenceStatusUpdate = db.compileStatement( 1480 "UPDATE " + Tables.AGGREGATED_PRESENCE 1481 + " SET " + Presence.PRESENCE_CUSTOM_STATUS + "=? " 1482 + " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?"); 1483 1484 final Locale locale = Locale.getDefault(); 1485 mNameSplitter = new NameSplitter( 1486 context.getString(com.android.internal.R.string.common_name_prefixes), 1487 context.getString(com.android.internal.R.string.common_last_name_prefixes), 1488 context.getString(com.android.internal.R.string.common_name_suffixes), 1489 context.getString(com.android.internal.R.string.common_name_conjunctions), 1490 locale); 1491 mPostalSplitter = new PostalSplitter(locale); 1492 1493 mDataRowHandlers = new HashMap<String, DataRowHandler>(); 1494 1495 mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler()); 1496 mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE, 1497 new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL)); 1498 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler( 1499 StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL)); 1500 mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler()); 1501 mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler()); 1502 mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler()); 1503 mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE, 1504 new StructuredNameRowHandler(mNameSplitter)); 1505 mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE, 1506 new StructuredPostalRowHandler(mPostalSplitter)); 1507 mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler()); 1508 mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler()); 1509 1510 if (isLegacyContactImportNeeded()) { 1511 importLegacyContactsAsync(); 1512 } 1513 1514 AccountManager.get(context).addOnAccountsUpdatedListener(this, null, false); 1515 onAccountsUpdated(AccountManager.get(context).getAccounts()); 1516 1517 return (db != null); 1518 } 1519 1520 /* Visible for testing */ 1521 @Override 1522 protected OpenHelper getOpenHelper(final Context context) { 1523 return OpenHelper.getInstance(context); 1524 } 1525 1526 /* package */ ContactAggregationScheduler getContactAggregationScheduler() { 1527 return mAggregationScheduler; 1528 } 1529 1530 /* package */ NameSplitter getNameSplitter() { 1531 return mNameSplitter; 1532 } 1533 1534 protected boolean isLegacyContactImportNeeded() { 1535 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1536 return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION; 1537 } 1538 1539 protected LegacyContactImporter getLegacyContactImporter() { 1540 return new LegacyContactImporter(getContext(), this); 1541 } 1542 1543 /** 1544 * Imports legacy contacts in a separate thread. As long as the import process is running 1545 * all other access to the contacts is blocked. 1546 */ 1547 private void importLegacyContactsAsync() { 1548 mAccessLatch = new CountDownLatch(1); 1549 1550 Thread importThread = new Thread("LegacyContactImport") { 1551 @Override 1552 public void run() { 1553 if (importLegacyContacts()) { 1554 1555 /* 1556 * When the import process is done, we can unlock the provider and 1557 * start aggregating the imported contacts asynchronously. 1558 */ 1559 mAccessLatch.countDown(); 1560 mAccessLatch = null; 1561 scheduleContactAggregation(); 1562 } 1563 } 1564 }; 1565 1566 importThread.start(); 1567 } 1568 1569 private boolean importLegacyContacts() { 1570 LegacyContactImporter importer = getLegacyContactImporter(); 1571 if (importLegacyContacts(importer)) { 1572 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1573 Editor editor = prefs.edit(); 1574 editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION); 1575 editor.commit(); 1576 return true; 1577 } else { 1578 return false; 1579 } 1580 } 1581 1582 /* Visible for testing */ 1583 /* package */ boolean importLegacyContacts(LegacyContactImporter importer) { 1584 mContactAggregator.setEnabled(false); 1585 mImportMode = true; 1586 try { 1587 importer.importContacts(); 1588 mContactAggregator.setEnabled(true); 1589 return true; 1590 } catch (Throwable e) { 1591 Log.e(TAG, "Legacy contact import failed", e); 1592 return false; 1593 } finally { 1594 mImportMode = false; 1595 } 1596 } 1597 1598 @Override 1599 protected void finalize() throws Throwable { 1600 if (mContactAggregator != null) { 1601 mContactAggregator.quit(); 1602 } 1603 1604 super.finalize(); 1605 } 1606 1607 /** 1608 * Wipes all data from the contacts database. 1609 */ 1610 /* package */ void wipeData() { 1611 mOpenHelper.wipeData(); 1612 } 1613 1614 /** 1615 * While importing and aggregating contacts, this content provider will 1616 * block all attempts to change contacts data. In particular, it will hold 1617 * up all contact syncs. As soon as the import process is complete, all 1618 * processes waiting to write to the provider are unblocked and can proceed 1619 * to compete for the database transaction monitor. 1620 */ 1621 private void waitForAccess() { 1622 CountDownLatch latch = mAccessLatch; 1623 if (latch != null) { 1624 while (true) { 1625 try { 1626 latch.await(); 1627 mAccessLatch = null; 1628 return; 1629 } catch (InterruptedException e) { 1630 } 1631 } 1632 } 1633 } 1634 1635 @Override 1636 public Uri insert(Uri uri, ContentValues values) { 1637 waitForAccess(); 1638 return super.insert(uri, values); 1639 } 1640 1641 @Override 1642 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1643 waitForAccess(); 1644 return super.update(uri, values, selection, selectionArgs); 1645 } 1646 1647 @Override 1648 public int delete(Uri uri, String selection, String[] selectionArgs) { 1649 waitForAccess(); 1650 return super.delete(uri, selection, selectionArgs); 1651 } 1652 1653 @Override 1654 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 1655 throws OperationApplicationException { 1656 waitForAccess(); 1657 return super.applyBatch(operations); 1658 } 1659 1660 @Override 1661 protected void onBeginTransaction() { 1662 super.onBeginTransaction(); 1663 mInsertedRawContacts.clear(); 1664 } 1665 1666 @Override 1667 protected void beforeTransactionCommit() { 1668 super.beforeTransactionCommit(); 1669 int count = mInsertedRawContacts.size(); 1670 for (int i = 0; i < count; i++) { 1671 mContactAggregator.insertContact(mDb, mInsertedRawContacts.get(i)); 1672 } 1673 } 1674 1675 @Override 1676 protected void onEndTransaction() { 1677 if (mScheduleAggregation) { 1678 mScheduleAggregation = false; 1679 scheduleContactAggregation(); 1680 } 1681 super.onEndTransaction(); 1682 } 1683 1684 @Override 1685 protected void notifyChange() { 1686 getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null); 1687 } 1688 1689 protected void scheduleContactAggregation() { 1690 mContactAggregator.schedule(); 1691 } 1692 1693 private boolean isNewRawContact(long rawContactId) { 1694 return mInsertedRawContacts.contains(rawContactId); 1695 } 1696 1697 private DataRowHandler getDataRowHandler(final String mimeType) { 1698 DataRowHandler handler = mDataRowHandlers.get(mimeType); 1699 if (handler == null) { 1700 handler = new CustomDataRowHandler(mimeType); 1701 mDataRowHandlers.put(mimeType, handler); 1702 } 1703 return handler; 1704 } 1705 1706 @Override 1707 protected Uri insertInTransaction(Uri uri, ContentValues values) { 1708 final int match = sUriMatcher.match(uri); 1709 long id = 0; 1710 1711 switch (match) { 1712 case SYNCSTATE: 1713 id = mOpenHelper.getSyncState().insert(mDb, values); 1714 break; 1715 1716 case CONTACTS: { 1717 insertContact(values); 1718 break; 1719 } 1720 1721 case RAW_CONTACTS: { 1722 final Account account = readAccountFromQueryParams(uri); 1723 id = insertRawContact(values, account); 1724 break; 1725 } 1726 1727 case RAW_CONTACTS_DATA: { 1728 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1)); 1729 id = insertData(values, shouldMarkRawContactAsDirty(uri)); 1730 break; 1731 } 1732 1733 case DATA: { 1734 id = insertData(values, shouldMarkRawContactAsDirty(uri)); 1735 break; 1736 } 1737 1738 case GROUPS: { 1739 final Account account = readAccountFromQueryParams(uri); 1740 id = insertGroup(values, account, shouldMarkGroupAsDirty(uri)); 1741 break; 1742 } 1743 1744 case SETTINGS: { 1745 id = insertSettings(values); 1746 break; 1747 } 1748 1749 case PRESENCE: { 1750 id = insertPresence(values); 1751 break; 1752 } 1753 1754 default: 1755 return mLegacyApiSupport.insert(uri, values); 1756 } 1757 1758 if (id < 0) { 1759 return null; 1760 } 1761 1762 return ContentUris.withAppendedId(uri, id); 1763 } 1764 1765 /** 1766 * If account is non-null then store it in the values. If the account is already 1767 * specified in the values then it must be consistent with the account, if it is non-null. 1768 * @param values the ContentValues to read from and update 1769 * @param account the explicitly provided Account 1770 * @return false if the accounts are inconsistent 1771 */ 1772 private boolean resolveAccount(ContentValues values, Account account) { 1773 // If either is specified then both must be specified. 1774 final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME); 1775 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 1776 if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) { 1777 final Account valuesAccount = new Account(accountName, accountType); 1778 if (account != null && !valuesAccount.equals(account)) { 1779 return false; 1780 } 1781 account = valuesAccount; 1782 } 1783 if (account != null) { 1784 values.put(RawContacts.ACCOUNT_NAME, account.name); 1785 values.put(RawContacts.ACCOUNT_TYPE, account.type); 1786 } 1787 return true; 1788 } 1789 1790 /** 1791 * Inserts an item in the contacts table 1792 * 1793 * @param values the values for the new row 1794 * @return the row ID of the newly created row 1795 */ 1796 private long insertContact(ContentValues values) { 1797 throw new UnsupportedOperationException("Aggregate contacts are created automatically"); 1798 } 1799 1800 /** 1801 * Inserts an item in the contacts table 1802 * 1803 * @param values the values for the new row 1804 * @param account the account this contact should be associated with. may be null. 1805 * @return the row ID of the newly created row 1806 */ 1807 private long insertRawContact(ContentValues values, Account account) { 1808 ContentValues overriddenValues = new ContentValues(values); 1809 overriddenValues.putNull(RawContacts.CONTACT_ID); 1810 if (!resolveAccount(overriddenValues, account)) { 1811 return -1; 1812 } 1813 1814 if (values.containsKey(RawContacts.DELETED) 1815 && values.getAsInteger(RawContacts.DELETED) != 0) { 1816 overriddenValues.put(RawContacts.AGGREGATION_MODE, 1817 RawContacts.AGGREGATION_MODE_DISABLED); 1818 } 1819 1820 long rawContactId = 1821 mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues); 1822 mContactAggregator.markNewForAggregation(rawContactId); 1823 1824 // Trigger creation of a Contact based on this RawContact at the end of transaction 1825 mInsertedRawContacts.add(rawContactId); 1826 return rawContactId; 1827 } 1828 1829 /** 1830 * Inserts an item in the data table 1831 * 1832 * @param values the values for the new row 1833 * @return the row ID of the newly created row 1834 */ 1835 private long insertData(ContentValues values, boolean markRawContactAsDirty) { 1836 long id = 0; 1837 mValues.clear(); 1838 mValues.putAll(values); 1839 1840 long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID); 1841 1842 // Replace package with internal mapping 1843 final String packageName = mValues.getAsString(Data.RES_PACKAGE); 1844 if (packageName != null) { 1845 mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 1846 } 1847 mValues.remove(Data.RES_PACKAGE); 1848 1849 // Replace mimetype with internal mapping 1850 final String mimeType = mValues.getAsString(Data.MIMETYPE); 1851 if (TextUtils.isEmpty(mimeType)) { 1852 throw new IllegalArgumentException(Data.MIMETYPE + " is required"); 1853 } 1854 1855 mValues.put(DataColumns.MIMETYPE_ID, mOpenHelper.getMimeTypeId(mimeType)); 1856 mValues.remove(Data.MIMETYPE); 1857 1858 DataRowHandler rowHandler = getDataRowHandler(mimeType); 1859 id = rowHandler.insert(mDb, rawContactId, mValues); 1860 if (markRawContactAsDirty) { 1861 setRawContactDirty(rawContactId); 1862 } 1863 1864 if (rowHandler.isAggregationRequired()) { 1865 triggerAggregation(rawContactId); 1866 } 1867 return id; 1868 } 1869 1870 private void triggerAggregation(long rawContactId) { 1871 if (!mContactAggregator.isEnabled()) { 1872 return; 1873 } 1874 1875 int aggregationMode = mOpenHelper.getAggregationMode(rawContactId); 1876 switch (aggregationMode) { 1877 case RawContacts.AGGREGATION_MODE_DISABLED: 1878 break; 1879 1880 case RawContacts.AGGREGATION_MODE_DEFAULT: { 1881 mContactAggregator.markForAggregation(rawContactId); 1882 mScheduleAggregation = true; 1883 break; 1884 } 1885 1886 case RawContacts.AGGREGATION_MODE_SUSPENDED: { 1887 long contactId = mOpenHelper.getContactId(rawContactId); 1888 1889 if (contactId != 0) { 1890 mContactAggregator.updateAggregateData(contactId); 1891 } 1892 break; 1893 } 1894 1895 case RawContacts.AGGREGATION_MODE_IMMEDITATE: { 1896 long contactId = mOpenHelper.getContactId(rawContactId); 1897 mContactAggregator.aggregateContact(mDb, rawContactId, contactId); 1898 break; 1899 } 1900 } 1901 } 1902 1903 /** 1904 * Returns the group id of the group with sourceId and the same account as rawContactId. 1905 * If the group doesn't already exist then it is first created, 1906 * @param db SQLiteDatabase to use for this operation 1907 * @param rawContactId the contact this group is associated with 1908 * @param sourceId the sourceIf of the group to query or create 1909 * @return the group id of the existing or created group 1910 * @throws IllegalArgumentException if the contact is not associated with an account 1911 * @throws IllegalStateException if a group needs to be created but the creation failed 1912 */ 1913 private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) { 1914 Account account = null; 1915 Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "=" 1916 + rawContactId, null, null, null, null); 1917 try { 1918 if (c.moveToNext()) { 1919 final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME); 1920 final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE); 1921 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 1922 account = new Account(accountName, accountType); 1923 } 1924 } 1925 } finally { 1926 c.close(); 1927 } 1928 if (account == null) { 1929 throw new IllegalArgumentException("if the groupmembership only " 1930 + "has a sourceid the the contact must be associate with " 1931 + "an account"); 1932 } 1933 1934 // look up the group that contains this sourceId and has the same account name and type 1935 // as the contact refered to by rawContactId 1936 c = db.query(Tables.GROUPS, new String[]{RawContacts._ID}, 1937 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID, 1938 new String[]{sourceId, account.name, account.type}, null, null, null); 1939 try { 1940 if (c.moveToNext()) { 1941 return c.getLong(0); 1942 } else { 1943 ContentValues groupValues = new ContentValues(); 1944 groupValues.put(Groups.ACCOUNT_NAME, account.name); 1945 groupValues.put(Groups.ACCOUNT_TYPE, account.type); 1946 groupValues.put(Groups.SOURCE_ID, sourceId); 1947 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues); 1948 if (groupId < 0) { 1949 throw new IllegalStateException("unable to create a new group with " 1950 + "this sourceid: " + groupValues); 1951 } 1952 return groupId; 1953 } 1954 } finally { 1955 c.close(); 1956 } 1957 } 1958 1959 /** 1960 * Delete data row by row so that fixing of primaries etc work correctly. 1961 */ 1962 private int deleteData(String selection, String[] selectionArgs, 1963 boolean markRawContactAsDirty) { 1964 int count = 0; 1965 1966 // Note that the query will return data according to the access restrictions, 1967 // so we don't need to worry about deleting data we don't have permission to read. 1968 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null); 1969 try { 1970 while(c.moveToNext()) { 1971 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 1972 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 1973 DataRowHandler rowHandler = getDataRowHandler(mimeType); 1974 count += rowHandler.delete(mDb, c); 1975 if (markRawContactAsDirty) { 1976 setRawContactDirty(rawContactId); 1977 if (rowHandler.isAggregationRequired()) { 1978 triggerAggregation(rawContactId); 1979 } 1980 } 1981 } 1982 } finally { 1983 c.close(); 1984 } 1985 1986 return count; 1987 } 1988 1989 /** 1990 * Delete a data row provided that it is one of the allowed mime types. 1991 */ 1992 public int deleteData(long dataId, String[] allowedMimeTypes) { 1993 1994 // Note that the query will return data according to the access restrictions, 1995 // so we don't need to worry about deleting data we don't have permission to read. 1996 Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=" + dataId, null, 1997 null); 1998 1999 try { 2000 if (!c.moveToFirst()) { 2001 return 0; 2002 } 2003 2004 String mimeType = c.getString(DataDeleteQuery.MIMETYPE); 2005 boolean valid = false; 2006 for (int i = 0; i < allowedMimeTypes.length; i++) { 2007 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) { 2008 valid = true; 2009 break; 2010 } 2011 } 2012 2013 if (!valid) { 2014 throw new IllegalArgumentException("Data type mismatch: expected " 2015 + Lists.newArrayList(allowedMimeTypes)); 2016 } 2017 2018 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2019 int count = rowHandler.delete(mDb, c); 2020 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 2021 if (rowHandler.isAggregationRequired()) { 2022 triggerAggregation(rawContactId); 2023 } 2024 return count; 2025 } finally { 2026 c.close(); 2027 } 2028 } 2029 2030 /** 2031 * Inserts an item in the groups table 2032 */ 2033 private long insertGroup(ContentValues values, Account account, boolean markAsDirty) { 2034 ContentValues overriddenValues = new ContentValues(values); 2035 if (!resolveAccount(overriddenValues, account)) { 2036 return -1; 2037 } 2038 2039 // Replace package with internal mapping 2040 final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE); 2041 if (packageName != null) { 2042 overriddenValues.put(GroupsColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 2043 } 2044 overriddenValues.remove(Groups.RES_PACKAGE); 2045 2046 if (markAsDirty) { 2047 overriddenValues.put(Groups.DIRTY, 1); 2048 } 2049 2050 long result = mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues); 2051 2052 if (overriddenValues.containsKey(Groups.GROUP_VISIBLE)) { 2053 mOpenHelper.updateAllVisible(); 2054 } 2055 2056 return result; 2057 } 2058 2059 private long insertSettings(ContentValues values) { 2060 final long id = mDb.insert(Tables.SETTINGS, null, values); 2061 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2062 mOpenHelper.updateAllVisible(); 2063 } 2064 return id; 2065 } 2066 2067 /** 2068 * Inserts a presence update. 2069 */ 2070 public long insertPresence(ContentValues values) { 2071 final String handle = values.getAsString(Presence.IM_HANDLE); 2072 if (TextUtils.isEmpty(handle) || !values.containsKey(Presence.PROTOCOL)) { 2073 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required"); 2074 } 2075 2076 final long protocol = values.getAsLong(Presence.PROTOCOL); 2077 String customProtocol = null; 2078 2079 if (protocol == Im.PROTOCOL_CUSTOM) { 2080 customProtocol = values.getAsString(Presence.CUSTOM_PROTOCOL); 2081 if (TextUtils.isEmpty(customProtocol)) { 2082 throw new IllegalArgumentException( 2083 "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM"); 2084 } 2085 } 2086 2087 // TODO: generalize to allow other providers to match against email 2088 boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol; 2089 2090 StringBuilder selection = new StringBuilder(); 2091 String[] selectionArgs; 2092 if (matchEmail) { 2093 selection.append( 2094 "((" + MimetypesColumns.MIMETYPE + "='" + Im.CONTENT_ITEM_TYPE + "'" 2095 + " AND " + Im.PROTOCOL + "=?" 2096 + " AND " + Im.DATA + "=?"); 2097 if (customProtocol != null) { 2098 selection.append(" AND " + Im.CUSTOM_PROTOCOL + "="); 2099 DatabaseUtils.appendEscapedSQLString(selection, customProtocol); 2100 } 2101 selection.append(") OR (" 2102 + MimetypesColumns.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "'" 2103 + " AND " + Email.DATA + "=?" 2104 + "))"); 2105 selectionArgs = new String[] { String.valueOf(protocol), handle, handle }; 2106 } else { 2107 selection.append( 2108 MimetypesColumns.MIMETYPE + "='" + Im.CONTENT_ITEM_TYPE + "'" 2109 + " AND " + Im.PROTOCOL + "=?" 2110 + " AND " + Im.DATA + "=?"); 2111 if (customProtocol != null) { 2112 selection.append(" AND " + Im.CUSTOM_PROTOCOL + "="); 2113 DatabaseUtils.appendEscapedSQLString(selection, customProtocol); 2114 } 2115 2116 selectionArgs = new String[] { String.valueOf(protocol), handle }; 2117 } 2118 2119 if (values.containsKey(Presence.DATA_ID)) { 2120 selection.append(" AND " + DataColumns.CONCRETE_ID + "=") 2121 .append(values.getAsLong(Presence.DATA_ID)); 2122 } 2123 2124 selection.append(" AND ").append(getContactsRestrictions()); 2125 2126 long dataId = -1; 2127 long rawContactId = -1; 2128 long contactId = -1; 2129 2130 Cursor cursor = null; 2131 try { 2132 cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION, 2133 selection.toString(), selectionArgs, null, null, null); 2134 if (cursor.moveToFirst()) { 2135 dataId = cursor.getLong(DataContactsQuery.DATA_ID); 2136 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID); 2137 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID); 2138 } else { 2139 // No contact found, return a null URI 2140 return -1; 2141 } 2142 } finally { 2143 if (cursor != null) { 2144 cursor.close(); 2145 } 2146 } 2147 2148 values.put(Presence.DATA_ID, dataId); 2149 values.put(PresenceColumns.RAW_CONTACT_ID, rawContactId); 2150 2151 // Insert the presence update 2152 long presenceId = mDb.replace(Tables.PRESENCE, null, values); 2153 2154 if (contactId != -1) { 2155 if (values.containsKey(Presence.PRESENCE_STATUS)) { 2156 mAggregatedPresenceReplace.bindLong(1, contactId); 2157 mAggregatedPresenceReplace.bindLong(2, contactId); 2158 mAggregatedPresenceReplace.execute(); 2159 } 2160 String status = values.getAsString(Presence.PRESENCE_CUSTOM_STATUS); 2161 if (status != null) { 2162 mAggregatedPresenceStatusUpdate.bindString(1, status); 2163 mAggregatedPresenceStatusUpdate.bindLong(2, contactId); 2164 mAggregatedPresenceStatusUpdate.execute(); 2165 } 2166 } 2167 return presenceId; 2168 } 2169 2170 @Override 2171 protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 2172 final int match = sUriMatcher.match(uri); 2173 switch (match) { 2174 case SYNCSTATE: 2175 return mOpenHelper.getSyncState().delete(mDb, selection, selectionArgs); 2176 2177 case CONTACTS_ID: { 2178 long contactId = ContentUris.parseId(uri); 2179 2180 // Remove references to the contact first 2181 ContentValues values = new ContentValues(); 2182 values.putNull(RawContacts.CONTACT_ID); 2183 mDb.update(Tables.RAW_CONTACTS, values, 2184 RawContacts.CONTACT_ID + "=" + contactId, null); 2185 2186 return mDb.delete(Tables.CONTACTS, BaseColumns._ID + "=" + contactId, null); 2187 } 2188 2189 case RAW_CONTACTS: { 2190 final boolean permanently = 2191 readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false); 2192 int numDeletes = 0; 2193 Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2194 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2195 try { 2196 while (c.moveToNext()) { 2197 final long rawContactId = c.getLong(0); 2198 numDeletes += deleteRawContact(rawContactId, permanently); 2199 } 2200 } finally { 2201 c.close(); 2202 } 2203 return numDeletes; 2204 } 2205 2206 case RAW_CONTACTS_ID: { 2207 final boolean permanently = 2208 readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false); 2209 final long rawContactId = ContentUris.parseId(uri); 2210 return deleteRawContact(rawContactId, permanently); 2211 } 2212 2213 case DATA: { 2214 return deleteData(appendAccountToSelection(uri, selection), selectionArgs, 2215 shouldMarkRawContactAsDirty(uri)); 2216 } 2217 2218 case DATA_ID: { 2219 long dataId = ContentUris.parseId(uri); 2220 return deleteData(Data._ID + "=" + dataId, null, shouldMarkRawContactAsDirty(uri)); 2221 } 2222 2223 case GROUPS_ID: { 2224 boolean markAsDirty = shouldMarkGroupAsDirty(uri); 2225 final boolean deletePermanently = 2226 readBooleanQueryParameter(uri, Groups.DELETE_PERMANENTLY, false); 2227 return deleteGroup(ContentUris.parseId(uri), markAsDirty, deletePermanently); 2228 } 2229 2230 case GROUPS: { 2231 boolean markAsDirty = shouldMarkGroupAsDirty(uri); 2232 final boolean permanently = 2233 readBooleanQueryParameter(uri, RawContacts.DELETE_PERMANENTLY, false); 2234 int numDeletes = 0; 2235 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID}, 2236 appendAccountToSelection(uri, selection), selectionArgs, null, null, null); 2237 try { 2238 while (c.moveToNext()) { 2239 numDeletes += deleteGroup(c.getLong(0), markAsDirty, permanently); 2240 } 2241 } finally { 2242 c.close(); 2243 } 2244 return numDeletes; 2245 } 2246 2247 case SETTINGS: { 2248 return deleteSettings(selection, selectionArgs); 2249 } 2250 2251 case PRESENCE: { 2252 return mDb.delete(Tables.PRESENCE, selection, selectionArgs); 2253 } 2254 2255 default: 2256 return mLegacyApiSupport.delete(uri, selection, selectionArgs); 2257 } 2258 } 2259 2260 private boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) { 2261 final String flag = uri.getQueryParameter(name); 2262 return flag == null 2263 ? defaultValue 2264 : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase())); 2265 } 2266 2267 private int deleteGroup(long groupId, boolean markAsDirty, boolean permanently) { 2268 final long groupMembershipMimetypeId = mOpenHelper 2269 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 2270 mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "=" 2271 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "=" 2272 + groupId, null); 2273 2274 try { 2275 if (permanently) { 2276 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null); 2277 } else { 2278 mValues.clear(); 2279 mValues.put(Groups.DELETED, 1); 2280 if (markAsDirty) { 2281 mValues.put(Groups.DIRTY, 1); 2282 } 2283 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null); 2284 } 2285 } finally { 2286 mOpenHelper.updateAllVisible(); 2287 } 2288 } 2289 2290 private int deleteSettings(String selection, String[] selectionArgs) { 2291 final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs); 2292 if (count > 0) { 2293 mOpenHelper.updateAllVisible(); 2294 } 2295 return count; 2296 } 2297 2298 public int deleteRawContact(long rawContactId, boolean permanently) { 2299 if (permanently) { 2300 mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null); 2301 return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null); 2302 } else { 2303 mOpenHelper.removeContactIfSingleton(rawContactId); 2304 2305 mValues.clear(); 2306 mValues.put(RawContacts.DELETED, 1); 2307 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED); 2308 mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1); 2309 mValues.putNull(RawContacts.CONTACT_ID); 2310 mValues.put(RawContacts.DIRTY, 1); 2311 return updateRawContact(rawContactId, mValues, null, null); 2312 } 2313 } 2314 2315 private static Account readAccountFromQueryParams(Uri uri) { 2316 final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 2317 final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 2318 if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) { 2319 return null; 2320 } 2321 return new Account(name, type); 2322 } 2323 2324 @Override 2325 protected int updateInTransaction(Uri uri, ContentValues values, String selection, 2326 String[] selectionArgs) { 2327 int count = 0; 2328 2329 final int match = sUriMatcher.match(uri); 2330 switch(match) { 2331 case SYNCSTATE: 2332 return mOpenHelper.getSyncState().update(mDb, values, selection, selectionArgs); 2333 2334 // TODO(emillar): We will want to disallow editing the contacts table at some point. 2335 case CONTACTS: { 2336 count = mDb.update(Tables.CONTACTS, values, 2337 appendAccountToSelection(uri, selection), selectionArgs); 2338 break; 2339 } 2340 2341 case CONTACTS_ID: { 2342 count = updateContactData(ContentUris.parseId(uri), values); 2343 break; 2344 } 2345 2346 case DATA: { 2347 count = updateData(uri, values, appendAccountToSelection(uri, selection), 2348 selectionArgs, shouldMarkRawContactAsDirty(uri)); 2349 break; 2350 } 2351 2352 case DATA_ID: { 2353 count = updateData(uri, values, selection, selectionArgs, 2354 shouldMarkRawContactAsDirty(uri)); 2355 break; 2356 } 2357 2358 case RAW_CONTACTS: { 2359 // TODO: security checks 2360 count = mDb.update(Tables.RAW_CONTACTS, values, 2361 appendAccountToSelection(uri, selection), selectionArgs); 2362 2363 if (values.containsKey(RawContacts.STARRED)) { 2364 mContactAggregator.updateStarred(mDb, selection, selectionArgs); 2365 } 2366 break; 2367 } 2368 2369 case RAW_CONTACTS_ID: { 2370 long rawContactId = ContentUris.parseId(uri); 2371 count = updateRawContact(rawContactId, values, selection, selectionArgs); 2372 break; 2373 } 2374 2375 case GROUPS: { 2376 count = updateGroups(values, appendAccountToSelection(uri, selection), 2377 selectionArgs, shouldMarkGroupAsDirty(uri)); 2378 break; 2379 } 2380 2381 case GROUPS_ID: { 2382 long groupId = ContentUris.parseId(uri); 2383 String selectionWithId = (Groups._ID + "=" + groupId + " ") 2384 + (selection == null ? "" : " AND " + selection); 2385 count = updateGroups(values, selectionWithId, selectionArgs, 2386 shouldMarkGroupAsDirty(uri)); 2387 break; 2388 } 2389 2390 case AGGREGATION_EXCEPTIONS: { 2391 count = updateAggregationException(mDb, values); 2392 break; 2393 } 2394 2395 case SETTINGS: { 2396 count = updateSettings(values, selection, selectionArgs); 2397 break; 2398 } 2399 2400 default: 2401 return mLegacyApiSupport.update(uri, values, selection, selectionArgs); 2402 } 2403 2404 return count; 2405 } 2406 2407 private int updateGroups(ContentValues values, String selectionWithId, 2408 String[] selectionArgs, boolean markAsDirty) { 2409 2410 ContentValues updatedValues; 2411 if (markAsDirty) { 2412 updatedValues = mValues; 2413 updatedValues.clear(); 2414 updatedValues.putAll(values); 2415 updatedValues.put(Groups.DIRTY, 1); 2416 } else { 2417 updatedValues = values; 2418 } 2419 2420 int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs); 2421 2422 // If changing visibility, then update contacts 2423 if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) { 2424 mOpenHelper.updateAllVisible(); 2425 } 2426 return count; 2427 } 2428 2429 private int updateSettings(ContentValues values, String selection, String[] selectionArgs) { 2430 final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs); 2431 if (values.containsKey(Settings.UNGROUPED_VISIBLE)) { 2432 mOpenHelper.updateAllVisible(); 2433 } 2434 return count; 2435 } 2436 2437 private int updateRawContact(long rawContactId, ContentValues values, String selection, 2438 String[] selectionArgs) { 2439 2440 // TODO: security checks 2441 String selectionWithId = (RawContacts._ID + " = " + rawContactId + " ") 2442 + (selection == null ? "" : " AND " + selection); 2443 int count = mDb.update(Tables.RAW_CONTACTS, values, selectionWithId, selectionArgs); 2444 if (count != 0) { 2445 if (values.containsKey(RawContacts.ACCOUNT_TYPE) 2446 || values.containsKey(RawContacts.ACCOUNT_NAME) 2447 || values.containsKey(RawContacts.SOURCE_ID)) { 2448 triggerAggregation(rawContactId); 2449 } 2450 2451 if (values.containsKey(RawContacts.STARRED)) { 2452 mContactAggregator.updateStarred(mDb, selectionWithId, selectionArgs); 2453 } 2454 if (values.containsKey(RawContacts.SOURCE_ID)) { 2455 mContactAggregator.updateLookupKey(mDb, rawContactId); 2456 } 2457 } 2458 return count; 2459 } 2460 2461 private int updateData(Uri uri, ContentValues values, String selection, 2462 String[] selectionArgs, boolean markRawContactAsDirty) { 2463 mValues.clear(); 2464 mValues.putAll(values); 2465 mValues.remove(Data._ID); 2466 mValues.remove(Data.RAW_CONTACT_ID); 2467 mValues.remove(Data.MIMETYPE); 2468 2469 String packageName = values.getAsString(Data.RES_PACKAGE); 2470 if (packageName != null) { 2471 mValues.remove(Data.RES_PACKAGE); 2472 mValues.put(DataColumns.PACKAGE_ID, mOpenHelper.getPackageId(packageName)); 2473 } 2474 2475 boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY); 2476 boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY); 2477 2478 // Remove primary or super primary values being set to 0. This is disallowed by the 2479 // content provider. 2480 if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) { 2481 containsIsSuperPrimary = false; 2482 mValues.remove(Data.IS_SUPER_PRIMARY); 2483 } 2484 if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) { 2485 containsIsPrimary = false; 2486 mValues.remove(Data.IS_PRIMARY); 2487 } 2488 2489 int count = 0; 2490 2491 // Note that the query will return data according to the access restrictions, 2492 // so we don't need to worry about updating data we don't have permission to read. 2493 Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null); 2494 try { 2495 while(c.moveToNext()) { 2496 count += updateData(mValues, c, markRawContactAsDirty); 2497 } 2498 } finally { 2499 c.close(); 2500 } 2501 2502 return count; 2503 } 2504 2505 private int updateData(ContentValues values, Cursor c, boolean markRawContactAsDirty) { 2506 if (values.size() == 0) { 2507 return 0; 2508 } 2509 2510 final String mimeType = c.getString(DataUpdateQuery.MIMETYPE); 2511 DataRowHandler rowHandler = getDataRowHandler(mimeType); 2512 rowHandler.update(mDb, values, c, markRawContactAsDirty); 2513 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); 2514 if (rowHandler.isAggregationRequired()) { 2515 triggerAggregation(rawContactId); 2516 } 2517 2518 return 1; 2519 } 2520 2521 private int updateContactData(long contactId, ContentValues values) { 2522 2523 // First update all constituent contacts 2524 ContentValues optionValues = new ContentValues(5); 2525 OpenHelper.copyStringValue(optionValues, RawContacts.CUSTOM_RINGTONE, 2526 values, Contacts.CUSTOM_RINGTONE); 2527 OpenHelper.copyLongValue(optionValues, RawContacts.SEND_TO_VOICEMAIL, 2528 values, Contacts.SEND_TO_VOICEMAIL); 2529 OpenHelper.copyLongValue(optionValues, RawContacts.LAST_TIME_CONTACTED, 2530 values, Contacts.LAST_TIME_CONTACTED); 2531 OpenHelper.copyLongValue(optionValues, RawContacts.TIMES_CONTACTED, 2532 values, Contacts.TIMES_CONTACTED); 2533 OpenHelper.copyLongValue(optionValues, RawContacts.STARRED, 2534 values, Contacts.STARRED); 2535 2536 // Nothing to update - just return 2537 if (optionValues.size() == 0) { 2538 return 0; 2539 } 2540 2541 if (optionValues.containsKey(RawContacts.STARRED)) { 2542 // Mark dirty when changing starred to trigger sync 2543 optionValues.put(RawContacts.DIRTY, 1); 2544 } 2545 2546 mDb.update(Tables.RAW_CONTACTS, optionValues, 2547 RawContacts.CONTACT_ID + "=" + contactId, null); 2548 return mDb.update(Tables.CONTACTS, values, Contacts._ID + "=" + contactId, null); 2549 } 2550 2551 public void updateContactTime(long contactId, long lastTimeContacted) { 2552 mLastTimeContactedUpdate.bindLong(1, lastTimeContacted); 2553 mLastTimeContactedUpdate.bindLong(2, contactId); 2554 mLastTimeContactedUpdate.execute(); 2555 } 2556 2557 private int updateAggregationException(SQLiteDatabase db, ContentValues values) { 2558 int exceptionType = values.getAsInteger(AggregationExceptions.TYPE); 2559 long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1); 2560 long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2); 2561 2562 long rawContactId1, rawContactId2; 2563 if (rcId1 < rcId2) { 2564 rawContactId1 = rcId1; 2565 rawContactId2 = rcId2; 2566 } else { 2567 rawContactId2 = rcId1; 2568 rawContactId1 = rcId2; 2569 } 2570 2571 ContentValues exceptionValues = new ContentValues(3); 2572 exceptionValues.put(AggregationExceptions.TYPE, exceptionType); 2573 if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) { 2574 db.delete(Tables.AGGREGATION_EXCEPTIONS, 2575 AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND " 2576 + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null); 2577 } else { 2578 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 2579 exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 2580 db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, 2581 exceptionValues); 2582 } 2583 2584 long contactId1 = mOpenHelper.getContactId(rawContactId1); 2585 mContactAggregator.aggregateContact(db, rawContactId1, contactId1); 2586 2587 long contactId2 = mOpenHelper.getContactId(rawContactId2); 2588 mContactAggregator.aggregateContact(db, rawContactId2, contactId2); 2589 2590 // The return value is fake - we just confirm that we made a change, not count actual 2591 // rows changed. 2592 return 1; 2593 } 2594 2595 public void onAccountsUpdated(Account[] accounts) { 2596 mDb = mOpenHelper.getWritableDatabase(); 2597 if (mDb == null) return; 2598 2599 Set<Account> validAccounts = Sets.newHashSet(); 2600 for (Account account : accounts) { 2601 validAccounts.add(new Account(account.name, account.type)); 2602 } 2603 ArrayList<Account> accountsToDelete = new ArrayList<Account>(); 2604 2605 mDb.beginTransaction(); 2606 try { 2607 // Find all the accounts the contacts DB knows about, mark the ones that aren't in the 2608 // valid set for deletion. 2609 Cursor c = mDb.rawQuery("SELECT DISTINCT account_name, account_type from " 2610 + Tables.RAW_CONTACTS, null); 2611 while (c.moveToNext()) { 2612 if (c.getString(0) != null && c.getString(1) != null) { 2613 Account currAccount = new Account(c.getString(0), c.getString(1)); 2614 if (!validAccounts.contains(currAccount)) { 2615 accountsToDelete.add(currAccount); 2616 } 2617 } 2618 } 2619 c.close(); 2620 2621 for (Account account : accountsToDelete) { 2622 String[] params = new String[]{account.name, account.type}; 2623 mDb.execSQL("DELETE FROM " + Tables.GROUPS 2624 + " WHERE account_name = ? AND account_type = ?", params); 2625 mDb.execSQL("DELETE FROM " + Tables.PRESENCE 2626 + " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (SELECT " 2627 + RawContacts._ID + " FROM " + Tables.RAW_CONTACTS 2628 + " WHERE account_name = ? AND account_type = ?)", params); 2629 mDb.execSQL("DELETE FROM " + Tables.RAW_CONTACTS 2630 + " WHERE account_name = ? AND account_type = ?", params); 2631 } 2632 mDb.setTransactionSuccessful(); 2633 } finally { 2634 mDb.endTransaction(); 2635 } 2636 } 2637 2638 /** 2639 * Test all against {@link TextUtils#isEmpty(CharSequence)}. 2640 */ 2641 private static boolean areAllEmpty(ContentValues values, String[] keys) { 2642 for (String key : keys) { 2643 if (!TextUtils.isEmpty(values.getAsString(key))) { 2644 return false; 2645 } 2646 } 2647 return true; 2648 } 2649 2650 @Override 2651 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 2652 String sortOrder) { 2653 2654 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 2655 2656 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2657 String groupBy = null; 2658 String limit = getLimit(uri); 2659 2660 // TODO: Consider writing a test case for RestrictionExceptions when you 2661 // write a new query() block to make sure it protects restricted data. 2662 final int match = sUriMatcher.match(uri); 2663 switch (match) { 2664 case SYNCSTATE: 2665 return mOpenHelper.getSyncState().query(db, projection, selection, selectionArgs, 2666 sortOrder); 2667 2668 case CONTACTS: { 2669 setTablesAndProjectionMapForContacts(qb, projection); 2670 break; 2671 } 2672 2673 case CONTACTS_ID: { 2674 long contactId = ContentUris.parseId(uri); 2675 setTablesAndProjectionMapForContacts(qb, projection); 2676 qb.appendWhere(Contacts._ID + "=" + contactId); 2677 break; 2678 } 2679 2680 case CONTACTS_LOOKUP: 2681 case CONTACTS_LOOKUP_ID: { 2682 List<String> pathSegments = uri.getPathSegments(); 2683 int segmentCount = pathSegments.size(); 2684 if (segmentCount < 3) { 2685 throw new IllegalArgumentException("URI " + uri + " is missing a lookup key"); 2686 } 2687 String lookupKey = pathSegments.get(2); 2688 if (segmentCount == 4) { 2689 long contactId = Long.parseLong(pathSegments.get(3)); 2690 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder(); 2691 setTablesAndProjectionMapForContacts(lookupQb, projection); 2692 lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " + 2693 Contacts.LOOKUP_KEY + "="); 2694 lookupQb.appendWhereEscapeString(lookupKey); 2695 Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder, 2696 groupBy, limit); 2697 if (c.getCount() != 0) { 2698 return c; 2699 } 2700 2701 c.close(); 2702 } 2703 2704 setTablesAndProjectionMapForContacts(qb, projection); 2705 qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey)); 2706 break; 2707 } 2708 2709 case CONTACTS_FILTER: { 2710 setTablesAndProjectionMapForContacts(qb, projection); 2711 if (uri.getPathSegments().size() > 2) { 2712 String filterParam = uri.getLastPathSegment(); 2713 StringBuilder sb = new StringBuilder(); 2714 sb.append(Contacts._ID + " IN "); 2715 appendContactFilterAsNestedQuery(sb, filterParam); 2716 qb.appendWhere(sb.toString()); 2717 } 2718 break; 2719 } 2720 2721 case CONTACTS_STREQUENT_FILTER: 2722 case CONTACTS_STREQUENT: { 2723 String filterSql = null; 2724 if (match == CONTACTS_STREQUENT_FILTER 2725 && uri.getPathSegments().size() > 3) { 2726 String filterParam = uri.getLastPathSegment(); 2727 StringBuilder sb = new StringBuilder(); 2728 sb.append(Contacts._ID + " IN "); 2729 appendContactFilterAsNestedQuery(sb, filterParam); 2730 filterSql = sb.toString(); 2731 } 2732 2733 setTablesAndProjectionMapForContacts(qb, projection); 2734 2735 // Build the first query for starred 2736 if (filterSql != null) { 2737 qb.appendWhere(filterSql); 2738 } 2739 final String starredQuery = qb.buildQuery(projection, Contacts.STARRED + "=1", 2740 null, Contacts._ID, null, null, null); 2741 2742 // Build the second query for frequent 2743 qb = new SQLiteQueryBuilder(); 2744 setTablesAndProjectionMapForContacts(qb, projection); 2745 if (filterSql != null) { 2746 qb.appendWhere(filterSql); 2747 } 2748 final String frequentQuery = qb.buildQuery(projection, 2749 Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED 2750 + " = 0 OR " + Contacts.STARRED + " IS NULL)", 2751 null, Contacts._ID, null, null, null); 2752 2753 // Put them together 2754 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 2755 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 2756 Cursor c = db.rawQuery(query, null); 2757 if (c != null) { 2758 c.setNotificationUri(getContext().getContentResolver(), 2759 ContactsContract.AUTHORITY_URI); 2760 } 2761 return c; 2762 } 2763 2764 case CONTACTS_GROUP: { 2765 setTablesAndProjectionMapForContacts(qb, projection); 2766 if (uri.getPathSegments().size() > 2) { 2767 qb.appendWhere(sContactsInGroupSelect); 2768 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 2769 } 2770 break; 2771 } 2772 2773 case CONTACTS_DATA: { 2774 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 2775 2776 qb.setTables(mOpenHelper.getDataView()); 2777 qb.setProjectionMap(sDataProjectionMap); 2778 appendAccountFromParameter(qb, uri); 2779 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId); 2780 break; 2781 } 2782 2783 case CONTACTS_PHOTO: { 2784 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 2785 2786 qb.setTables(mOpenHelper.getDataView()); 2787 qb.setProjectionMap(sDataProjectionMap); 2788 appendAccountFromParameter(qb, uri); 2789 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId); 2790 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID); 2791 break; 2792 } 2793 2794 case PHONES: { 2795 qb.setTables(mOpenHelper.getDataView()); 2796 qb.setProjectionMap(sDataProjectionMap); 2797 qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 2798 break; 2799 } 2800 2801 case PHONES_FILTER: { 2802 qb.setTables(mOpenHelper.getDataView()); 2803 qb.setProjectionMap(sDistinctDataProjectionMap); 2804 qb.appendWhere(Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"); 2805 if (uri.getPathSegments().size() > 2) { 2806 String filterParam = uri.getLastPathSegment(); 2807 StringBuilder sb = new StringBuilder(); 2808 sb.append("("); 2809 2810 boolean orNeeded = false; 2811 String normalizedName = NameNormalizer.normalize(filterParam); 2812 if (normalizedName.length() > 0) { 2813 sb.append(Data.RAW_CONTACT_ID + " IN "); 2814 appendRawContactsByNormalizedNameFilter(sb, normalizedName, null); 2815 orNeeded = true; 2816 } 2817 2818 if (isPhoneNumber(filterParam)) { 2819 if (orNeeded) { 2820 sb.append(" OR "); 2821 } 2822 String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam); 2823 String reversed = PhoneNumberUtils.getStrippedReversed(number); 2824 sb.append(Data._ID + 2825 " IN (SELECT " + PhoneLookupColumns.DATA_ID 2826 + " FROM " + Tables.PHONE_LOOKUP 2827 + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%"); 2828 sb.append(reversed); 2829 sb.append("')"); 2830 } 2831 sb.append(")"); 2832 qb.appendWhere(" AND " + sb); 2833 } 2834 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID; 2835 break; 2836 } 2837 2838 case EMAILS: { 2839 qb.setTables(mOpenHelper.getDataView()); 2840 qb.setProjectionMap(sDataProjectionMap); 2841 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 2842 break; 2843 } 2844 2845 case EMAILS_LOOKUP: { 2846 qb.setTables(mOpenHelper.getDataView()); 2847 qb.setProjectionMap(sDataProjectionMap); 2848 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 2849 if (uri.getPathSegments().size() > 2) { 2850 qb.appendWhere(" AND " + Email.DATA + "="); 2851 qb.appendWhereEscapeString(uri.getLastPathSegment()); 2852 } 2853 break; 2854 } 2855 2856 case EMAILS_FILTER: { 2857 qb.setTables(mOpenHelper.getDataView()); 2858 qb.setProjectionMap(sDistinctDataProjectionMap); 2859 qb.appendWhere(Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"); 2860 if (uri.getPathSegments().size() > 2) { 2861 String filterParam = uri.getLastPathSegment(); 2862 StringBuilder sb = new StringBuilder(); 2863 sb.append("("); 2864 2865 String normalizedName = NameNormalizer.normalize(filterParam); 2866 if (normalizedName.length() > 0) { 2867 sb.append(Data.RAW_CONTACT_ID + " IN "); 2868 appendRawContactsByNormalizedNameFilter(sb, normalizedName, null); 2869 sb.append(" OR "); 2870 } 2871 2872 sb.append(Email.DATA + " LIKE "); 2873 sb.append(DatabaseUtils.sqlEscapeString(filterParam)); 2874 sb.append(")"); 2875 qb.appendWhere(" AND " + sb); 2876 } 2877 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID; 2878 break; 2879 } 2880 2881 case POSTALS: { 2882 qb.setTables(mOpenHelper.getDataView()); 2883 qb.setProjectionMap(sDataProjectionMap); 2884 qb.appendWhere(Data.MIMETYPE + " = '" + StructuredPostal.CONTENT_ITEM_TYPE + "'"); 2885 break; 2886 } 2887 2888 case RAW_CONTACTS: { 2889 qb.setTables(mOpenHelper.getRawContactView()); 2890 qb.setProjectionMap(sRawContactsProjectionMap); 2891 break; 2892 } 2893 2894 case RAW_CONTACTS_ID: { 2895 long rawContactId = ContentUris.parseId(uri); 2896 qb.setTables(mOpenHelper.getRawContactView()); 2897 qb.setProjectionMap(sRawContactsProjectionMap); 2898 qb.appendWhere(RawContacts._ID + "=" + rawContactId); 2899 break; 2900 } 2901 2902 case RAW_CONTACTS_DATA: { 2903 long rawContactId = Long.parseLong(uri.getPathSegments().get(1)); 2904 qb.setTables(mOpenHelper.getDataView()); 2905 qb.setProjectionMap(sDataProjectionMap); 2906 qb.appendWhere(Data.RAW_CONTACT_ID + "=" + rawContactId); 2907 break; 2908 } 2909 2910 case DATA: { 2911 qb.setTables(mOpenHelper.getDataView()); 2912 qb.setProjectionMap(sDataProjectionMap); 2913 appendAccountFromParameter(qb, uri); 2914 break; 2915 } 2916 2917 case DATA_ID: { 2918 qb.setTables(mOpenHelper.getDataView()); 2919 qb.setProjectionMap(sDataProjectionMap); 2920 qb.appendWhere(Data._ID + "=" + ContentUris.parseId(uri)); 2921 break; 2922 } 2923 2924 case DATA_WITH_PRESENCE: { 2925 qb.setTables(mOpenHelper.getDataView() + " data" 2926 + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE 2927 + " ON (" + AggregatedPresenceColumns.CONTACT_ID + "=" 2928 + RawContacts.CONTACT_ID + ")"); 2929 qb.setProjectionMap(sDataWithPresenceProjectionMap); 2930 appendAccountFromParameter(qb, uri); 2931 break; 2932 } 2933 2934 case PHONE_LOOKUP: { 2935 2936 if (TextUtils.isEmpty(sortOrder)) { 2937 // Default the sort order to something reasonable so we get consistent 2938 // results when callers don't request an ordering 2939 sortOrder = RawContactsColumns.CONCRETE_ID; 2940 } 2941 2942 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : ""; 2943 mOpenHelper.buildPhoneLookupAndContactQuery(qb, number); 2944 qb.setProjectionMap(sPhoneLookupProjectionMap); 2945 2946 // Phone lookup cannot be combined with a selection 2947 selection = null; 2948 selectionArgs = null; 2949 break; 2950 } 2951 2952 case GROUPS: { 2953 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 2954 qb.setProjectionMap(sGroupsProjectionMap); 2955 break; 2956 } 2957 2958 case GROUPS_ID: { 2959 long groupId = ContentUris.parseId(uri); 2960 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 2961 qb.setProjectionMap(sGroupsProjectionMap); 2962 qb.appendWhere(GroupsColumns.CONCRETE_ID + "=" + groupId); 2963 break; 2964 } 2965 2966 case GROUPS_SUMMARY: { 2967 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 2968 qb.setProjectionMap(sGroupsSummaryProjectionMap); 2969 groupBy = GroupsColumns.CONCRETE_ID; 2970 break; 2971 } 2972 2973 case AGGREGATION_EXCEPTIONS: { 2974 qb.setTables(Tables.AGGREGATION_EXCEPTIONS); 2975 qb.setProjectionMap(sAggregationExceptionsProjectionMap); 2976 break; 2977 } 2978 2979 case AGGREGATION_SUGGESTIONS: { 2980 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 2981 final int maxSuggestions; 2982 if (limit != null) { 2983 maxSuggestions = Integer.parseInt(limit); 2984 } else { 2985 maxSuggestions = DEFAULT_MAX_SUGGESTIONS; 2986 } 2987 2988 return mContactAggregator.queryAggregationSuggestions(contactId, projection, 2989 sContactsProjectionMap, maxSuggestions); 2990 } 2991 2992 case SETTINGS: { 2993 qb.setTables(Tables.SETTINGS); 2994 qb.setProjectionMap(sSettingsProjectionMap); 2995 2996 // When requesting specific columns, this query requires 2997 // late-binding of the GroupMembership MIME-type. 2998 final String groupMembershipMimetypeId = Long.toString(mOpenHelper 2999 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)); 3000 if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) { 3001 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3002 } 3003 if (mOpenHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) { 3004 selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId); 3005 } 3006 3007 break; 3008 } 3009 3010 case PRESENCE: { 3011 qb.setTables(Tables.PRESENCE); 3012 qb.setProjectionMap(sPresenceProjectionMap); 3013 break; 3014 } 3015 3016 case PRESENCE_ID: { 3017 qb.setTables(Tables.PRESENCE); 3018 qb.setProjectionMap(sPresenceProjectionMap); 3019 qb.appendWhere(Presence._ID + "=" + ContentUris.parseId(uri)); 3020 break; 3021 } 3022 3023 case SEARCH_SUGGESTIONS: { 3024 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit); 3025 } 3026 3027 case SEARCH_SHORTCUT: { 3028 long contactId = ContentUris.parseId(uri); 3029 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection); 3030 } 3031 3032 case LIVE_FOLDERS_CONTACTS: 3033 qb.setTables(mOpenHelper.getContactView()); 3034 qb.setProjectionMap(sLiveFoldersProjectionMap); 3035 break; 3036 3037 case LIVE_FOLDERS_CONTACTS_WITH_PHONES: 3038 qb.setTables(mOpenHelper.getContactView()); 3039 qb.setProjectionMap(sLiveFoldersProjectionMap); 3040 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1"); 3041 break; 3042 3043 case LIVE_FOLDERS_CONTACTS_FAVORITES: 3044 qb.setTables(mOpenHelper.getContactView()); 3045 qb.setProjectionMap(sLiveFoldersProjectionMap); 3046 qb.appendWhere(Contacts.STARRED + "=1"); 3047 break; 3048 3049 case LIVE_FOLDERS_CONTACTS_GROUP_NAME: 3050 qb.setTables(mOpenHelper.getContactView()); 3051 qb.setProjectionMap(sLiveFoldersProjectionMap); 3052 qb.appendWhere(sContactsInGroupSelect); 3053 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment()); 3054 break; 3055 3056 default: 3057 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs, 3058 sortOrder, limit); 3059 } 3060 3061 return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit); 3062 } 3063 3064 private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection, 3065 String selection, String[] selectionArgs, String sortOrder, String groupBy, 3066 String limit) { 3067 if (projection != null && projection.length == 1 3068 && BaseColumns._COUNT.equals(projection[0])) { 3069 qb.setProjectionMap(sCountProjectionMap); 3070 } 3071 final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null, 3072 sortOrder, limit); 3073 if (c != null) { 3074 c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI); 3075 } 3076 return c; 3077 } 3078 3079 private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) { 3080 ContactLookupKey key = new ContactLookupKey(); 3081 ArrayList<LookupKeySegment> segments = key.parse(lookupKey); 3082 3083 long contactId = lookupContactIdBySourceIds(db, segments); 3084 if (contactId == -1) { 3085 contactId = lookupContactIdByDisplayNames(db, segments); 3086 } 3087 3088 return contactId; 3089 } 3090 3091 private interface LookupBySourceIdQuery { 3092 String TABLE = Tables.RAW_CONTACTS; 3093 3094 String COLUMNS[] = { 3095 RawContacts.CONTACT_ID, 3096 RawContacts.ACCOUNT_TYPE, 3097 RawContacts.ACCOUNT_NAME, 3098 RawContacts.SOURCE_ID 3099 }; 3100 3101 int CONTACT_ID = 0; 3102 int ACCOUNT_TYPE = 1; 3103 int ACCOUNT_NAME = 2; 3104 int SOURCE_ID = 3; 3105 } 3106 3107 private long lookupContactIdBySourceIds(SQLiteDatabase db, 3108 ArrayList<LookupKeySegment> segments) { 3109 int sourceIdCount = 0; 3110 for (int i = 0; i < segments.size(); i++) { 3111 LookupKeySegment segment = segments.get(i); 3112 if (segment.sourceIdLookup) { 3113 sourceIdCount++; 3114 } 3115 } 3116 3117 if (sourceIdCount == 0) { 3118 return -1; 3119 } 3120 3121 // First try sync ids 3122 StringBuilder sb = new StringBuilder(); 3123 sb.append(RawContacts.SOURCE_ID + " IN ("); 3124 for (int i = 0; i < segments.size(); i++) { 3125 LookupKeySegment segment = segments.get(i); 3126 if (segment.sourceIdLookup) { 3127 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 3128 sb.append(","); 3129 } 3130 } 3131 sb.setLength(sb.length() - 1); // Last comma 3132 sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL"); 3133 3134 Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS, 3135 sb.toString(), null, null, null, null); 3136 try { 3137 while (c.moveToNext()) { 3138 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE); 3139 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME); 3140 int accountHashCode = 3141 ContactLookupKey.getAccountHashCode(accountType, accountName); 3142 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID); 3143 for (int i = 0; i < segments.size(); i++) { 3144 LookupKeySegment segment = segments.get(i); 3145 if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode 3146 && segment.key.equals(sourceId)) { 3147 segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID); 3148 break; 3149 } 3150 } 3151 } 3152 } finally { 3153 c.close(); 3154 } 3155 3156 return getMostReferencedContactId(segments); 3157 } 3158 3159 private interface LookupByDisplayNameQuery { 3160 String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS; 3161 3162 String COLUMNS[] = { 3163 RawContacts.CONTACT_ID, 3164 RawContacts.ACCOUNT_TYPE, 3165 RawContacts.ACCOUNT_NAME, 3166 NameLookupColumns.NORMALIZED_NAME 3167 }; 3168 3169 int CONTACT_ID = 0; 3170 int ACCOUNT_TYPE = 1; 3171 int ACCOUNT_NAME = 2; 3172 int NORMALIZED_NAME = 3; 3173 } 3174 3175 private long lookupContactIdByDisplayNames(SQLiteDatabase db, 3176 ArrayList<LookupKeySegment> segments) { 3177 int displayNameCount = 0; 3178 for (int i = 0; i < segments.size(); i++) { 3179 LookupKeySegment segment = segments.get(i); 3180 if (!segment.sourceIdLookup) { 3181 displayNameCount++; 3182 } 3183 } 3184 3185 if (displayNameCount == 0) { 3186 return -1; 3187 } 3188 3189 // First try sync ids 3190 StringBuilder sb = new StringBuilder(); 3191 sb.append(NameLookupColumns.NORMALIZED_NAME + " IN ("); 3192 for (int i = 0; i < segments.size(); i++) { 3193 LookupKeySegment segment = segments.get(i); 3194 if (!segment.sourceIdLookup) { 3195 DatabaseUtils.appendEscapedSQLString(sb, segment.key); 3196 sb.append(","); 3197 } 3198 } 3199 sb.setLength(sb.length() - 1); // Last comma 3200 sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY 3201 + " AND " + RawContacts.CONTACT_ID + " NOT NULL"); 3202 3203 Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS, 3204 sb.toString(), null, null, null, null); 3205 try { 3206 while (c.moveToNext()) { 3207 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE); 3208 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME); 3209 int accountHashCode = 3210 ContactLookupKey.getAccountHashCode(accountType, accountName); 3211 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME); 3212 for (int i = 0; i < segments.size(); i++) { 3213 LookupKeySegment segment = segments.get(i); 3214 if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode 3215 && segment.key.equals(name)) { 3216 segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID); 3217 break; 3218 } 3219 } 3220 } 3221 } finally { 3222 c.close(); 3223 } 3224 3225 return getMostReferencedContactId(segments); 3226 } 3227 3228 /** 3229 * Returns the contact ID that is mentioned the highest number of times. 3230 */ 3231 private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) { 3232 Collections.sort(segments); 3233 3234 long bestContactId = -1; 3235 int bestRefCount = 0; 3236 3237 long contactId = -1; 3238 int count = 0; 3239 3240 int segmentCount = segments.size(); 3241 for (int i = 0; i < segmentCount; i++) { 3242 LookupKeySegment segment = segments.get(i); 3243 if (segment.contactId != -1) { 3244 if (segment.contactId == contactId) { 3245 count++; 3246 } else { 3247 if (count > bestRefCount) { 3248 bestContactId = contactId; 3249 bestRefCount = count; 3250 } 3251 contactId = segment.contactId; 3252 count = 1; 3253 } 3254 } 3255 } 3256 if (count > bestRefCount) { 3257 return contactId; 3258 } else { 3259 return bestContactId; 3260 } 3261 } 3262 3263 private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, String[] projection) { 3264 String contactView = mOpenHelper.getContactView(); 3265 boolean needsPresence = mOpenHelper.isInProjection(projection, Contacts.PRESENCE_STATUS, 3266 Contacts.PRESENCE_CUSTOM_STATUS); 3267 if (!needsPresence) { 3268 qb.setTables(contactView); 3269 qb.setProjectionMap(sContactsProjectionMap); 3270 } else { 3271 qb.setTables(contactView + " LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE + " ON (" 3272 + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ") "); 3273 qb.setProjectionMap(sContactsWithPresenceProjectionMap); 3274 3275 } 3276 } 3277 3278 private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) { 3279 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3280 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3281 if (!TextUtils.isEmpty(accountName)) { 3282 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 3283 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3284 + RawContacts.ACCOUNT_TYPE + "=" 3285 + DatabaseUtils.sqlEscapeString(accountType)); 3286 } else { 3287 qb.appendWhere("1"); 3288 } 3289 } 3290 3291 private String appendAccountToSelection(Uri uri, String selection) { 3292 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3293 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3294 if (!TextUtils.isEmpty(accountName)) { 3295 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 3296 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3297 + RawContacts.ACCOUNT_TYPE + "=" 3298 + DatabaseUtils.sqlEscapeString(accountType)); 3299 if (!TextUtils.isEmpty(selection)) { 3300 selectionSb.append(" AND ("); 3301 selectionSb.append(selection); 3302 selectionSb.append(')'); 3303 } 3304 return selectionSb.toString(); 3305 } else { 3306 return selection; 3307 } 3308 } 3309 3310 /** 3311 * Gets the value of the "limit" URI query parameter. 3312 * 3313 * @return A string containing a non-negative integer, or <code>null</code> if 3314 * the parameter is not set, or is set to an invalid value. 3315 */ 3316 private String getLimit(Uri url) { 3317 String limitParam = url.getQueryParameter("limit"); 3318 if (limitParam == null) { 3319 return null; 3320 } 3321 // make sure that the limit is a non-negative integer 3322 try { 3323 int l = Integer.parseInt(limitParam); 3324 if (l < 0) { 3325 Log.w(TAG, "Invalid limit parameter: " + limitParam); 3326 return null; 3327 } 3328 return String.valueOf(l); 3329 } catch (NumberFormatException ex) { 3330 Log.w(TAG, "Invalid limit parameter: " + limitParam); 3331 return null; 3332 } 3333 } 3334 3335 /** 3336 * Returns true if all the characters are meaningful as digits 3337 * in a phone number -- letters, digits, and a few punctuation marks. 3338 */ 3339 private boolean isPhoneNumber(CharSequence cons) { 3340 int len = cons.length(); 3341 3342 for (int i = 0; i < len; i++) { 3343 char c = cons.charAt(i); 3344 3345 if ((c >= '0') && (c <= '9')) { 3346 continue; 3347 } 3348 if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+') 3349 || (c == '#') || (c == '*')) { 3350 continue; 3351 } 3352 if ((c >= 'A') && (c <= 'Z')) { 3353 continue; 3354 } 3355 if ((c >= 'a') && (c <= 'z')) { 3356 continue; 3357 } 3358 3359 return false; 3360 } 3361 3362 return true; 3363 } 3364 3365 String getContactsRestrictions() { 3366 if (mOpenHelper.hasRestrictedAccess()) { 3367 return "1"; 3368 } else { 3369 return RawContacts.IS_RESTRICTED + "=0"; 3370 } 3371 } 3372 3373 public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) { 3374 if (mOpenHelper.hasRestrictedAccess()) { 3375 return "1"; 3376 } else { 3377 return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS 3378 + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0"; 3379 } 3380 } 3381 3382 @Override 3383 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 3384 int match = sUriMatcher.match(uri); 3385 switch (match) { 3386 case CONTACTS_PHOTO: 3387 if (!"r".equals(mode)) { 3388 throw new FileNotFoundException("Mode " + mode + " not supported."); 3389 } 3390 3391 long contactId = Long.parseLong(uri.getPathSegments().get(1)); 3392 3393 String sql = 3394 "SELECT " + Photo.PHOTO + " FROM " + mOpenHelper.getDataView() + 3395 " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID 3396 + " AND " + RawContacts.CONTACT_ID + "=" + contactId; 3397 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 3398 return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null); 3399 3400 default: 3401 throw new FileNotFoundException("No file at: " + uri); 3402 } 3403 } 3404 3405 3406 3407 /** 3408 * An implementation of EntityIterator that joins the contacts and data tables 3409 * and consumes all the data rows for a contact in order to build the Entity for a contact. 3410 */ 3411 private static class ContactsEntityIterator implements EntityIterator { 3412 private final Cursor mEntityCursor; 3413 private volatile boolean mIsClosed; 3414 3415 private static final String[] DATA_KEYS = new String[]{ 3416 Data.DATA1, 3417 Data.DATA2, 3418 Data.DATA3, 3419 Data.DATA4, 3420 Data.DATA5, 3421 Data.DATA6, 3422 Data.DATA7, 3423 Data.DATA8, 3424 Data.DATA9, 3425 Data.DATA10, 3426 Data.DATA11, 3427 Data.DATA12, 3428 Data.DATA13, 3429 Data.DATA14, 3430 Data.DATA15, 3431 Data.SYNC1, 3432 Data.SYNC2, 3433 Data.SYNC3, 3434 Data.SYNC4}; 3435 3436 private static final String[] PROJECTION = new String[]{ 3437 RawContacts.ACCOUNT_NAME, 3438 RawContacts.ACCOUNT_TYPE, 3439 RawContacts.SOURCE_ID, 3440 RawContacts.VERSION, 3441 RawContacts.DIRTY, 3442 Data._ID, 3443 Data.RES_PACKAGE, 3444 Data.MIMETYPE, 3445 Data.DATA1, 3446 Data.DATA2, 3447 Data.DATA3, 3448 Data.DATA4, 3449 Data.DATA5, 3450 Data.DATA6, 3451 Data.DATA7, 3452 Data.DATA8, 3453 Data.DATA9, 3454 Data.DATA10, 3455 Data.DATA11, 3456 Data.DATA12, 3457 Data.DATA13, 3458 Data.DATA14, 3459 Data.DATA15, 3460 Data.SYNC1, 3461 Data.SYNC2, 3462 Data.SYNC3, 3463 Data.SYNC4, 3464 Data.RAW_CONTACT_ID, 3465 Data.IS_PRIMARY, 3466 Data.IS_SUPER_PRIMARY, 3467 Data.DATA_VERSION, 3468 GroupMembership.GROUP_SOURCE_ID, 3469 RawContacts.SYNC1, 3470 RawContacts.SYNC2, 3471 RawContacts.SYNC3, 3472 RawContacts.SYNC4, 3473 RawContacts.DELETED, 3474 RawContacts.CONTACT_ID, 3475 RawContacts.STARRED}; 3476 3477 private static final int COLUMN_ACCOUNT_NAME = 0; 3478 private static final int COLUMN_ACCOUNT_TYPE = 1; 3479 private static final int COLUMN_SOURCE_ID = 2; 3480 private static final int COLUMN_VERSION = 3; 3481 private static final int COLUMN_DIRTY = 4; 3482 private static final int COLUMN_DATA_ID = 5; 3483 private static final int COLUMN_RES_PACKAGE = 6; 3484 private static final int COLUMN_MIMETYPE = 7; 3485 private static final int COLUMN_DATA1 = 8; 3486 private static final int COLUMN_RAW_CONTACT_ID = 27; 3487 private static final int COLUMN_IS_PRIMARY = 28; 3488 private static final int COLUMN_IS_SUPER_PRIMARY = 29; 3489 private static final int COLUMN_DATA_VERSION = 30; 3490 private static final int COLUMN_GROUP_SOURCE_ID = 31; 3491 private static final int COLUMN_SYNC1 = 32; 3492 private static final int COLUMN_SYNC2 = 33; 3493 private static final int COLUMN_SYNC3 = 34; 3494 private static final int COLUMN_SYNC4 = 35; 3495 private static final int COLUMN_DELETED = 36; 3496 private static final int COLUMN_CONTACT_ID = 37; 3497 private static final int COLUMN_STARRED = 38; 3498 3499 public ContactsEntityIterator(ContactsProvider2 provider, String contactsIdString, Uri uri, 3500 String selection, String[] selectionArgs, String sortOrder) { 3501 mIsClosed = false; 3502 3503 final String updatedSortOrder = (sortOrder == null) 3504 ? Data.RAW_CONTACT_ID 3505 : (Data.RAW_CONTACT_ID + "," + sortOrder); 3506 3507 final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase(); 3508 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3509 qb.setTables(Tables.CONTACT_ENTITIES); 3510 if (contactsIdString != null) { 3511 qb.appendWhere(Data.RAW_CONTACT_ID + "=" + contactsIdString); 3512 } 3513 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 3514 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 3515 if (!TextUtils.isEmpty(accountName)) { 3516 qb.appendWhere(RawContacts.ACCOUNT_NAME + "=" 3517 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3518 + RawContacts.ACCOUNT_TYPE + "=" 3519 + DatabaseUtils.sqlEscapeString(accountType)); 3520 } 3521 mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs, 3522 null, null, updatedSortOrder); 3523 mEntityCursor.moveToFirst(); 3524 } 3525 3526 public void reset() throws RemoteException { 3527 if (mIsClosed) { 3528 throw new IllegalStateException("calling reset() when the iterator is closed"); 3529 } 3530 mEntityCursor.moveToFirst(); 3531 } 3532 3533 public void close() { 3534 if (mIsClosed) { 3535 throw new IllegalStateException("closing when already closed"); 3536 } 3537 mIsClosed = true; 3538 mEntityCursor.close(); 3539 } 3540 3541 public boolean hasNext() throws RemoteException { 3542 if (mIsClosed) { 3543 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 3544 } 3545 3546 return !mEntityCursor.isAfterLast(); 3547 } 3548 3549 public Entity next() throws RemoteException { 3550 if (mIsClosed) { 3551 throw new IllegalStateException("calling next() when the iterator is closed"); 3552 } 3553 if (!hasNext()) { 3554 throw new IllegalStateException("you may only call next() if hasNext() is true"); 3555 } 3556 3557 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 3558 3559 final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID); 3560 3561 // we expect the cursor is already at the row we need to read from 3562 ContentValues contactValues = new ContentValues(); 3563 contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 3564 contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 3565 contactValues.put(RawContacts._ID, rawContactId); 3566 contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY)); 3567 contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION)); 3568 contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 3569 contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1)); 3570 contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2)); 3571 contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3)); 3572 contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4)); 3573 contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED)); 3574 contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID)); 3575 contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED)); 3576 Entity contact = new Entity(contactValues); 3577 3578 // read data rows until the contact id changes 3579 do { 3580 if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) { 3581 break; 3582 } 3583 // add the data to to the contact 3584 ContentValues dataValues = new ContentValues(); 3585 dataValues.put(Data._ID, c.getString(COLUMN_DATA_ID)); 3586 dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE)); 3587 dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE)); 3588 dataValues.put(Data.IS_PRIMARY, c.getString(COLUMN_IS_PRIMARY)); 3589 dataValues.put(Data.IS_SUPER_PRIMARY, c.getString(COLUMN_IS_SUPER_PRIMARY)); 3590 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 3591 if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) { 3592 dataValues.put(GroupMembership.GROUP_SOURCE_ID, 3593 c.getString(COLUMN_GROUP_SOURCE_ID)); 3594 } 3595 dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION)); 3596 for (int i = 0; i < DATA_KEYS.length; i++) { 3597 final int columnIndex = i + COLUMN_DATA1; 3598 String key = DATA_KEYS[i]; 3599 if (c.isNull(columnIndex)) { 3600 // don't put anything 3601 } else if (c.isLong(columnIndex)) { 3602 dataValues.put(key, c.getLong(columnIndex)); 3603 } else if (c.isFloat(columnIndex)) { 3604 dataValues.put(key, c.getFloat(columnIndex)); 3605 } else if (c.isString(columnIndex)) { 3606 dataValues.put(key, c.getString(columnIndex)); 3607 } else if (c.isBlob(columnIndex)) { 3608 dataValues.put(key, c.getBlob(columnIndex)); 3609 } 3610 } 3611 contact.addSubValue(Data.CONTENT_URI, dataValues); 3612 } while (mEntityCursor.moveToNext()); 3613 3614 return contact; 3615 } 3616 } 3617 3618 /** 3619 * An implementation of EntityIterator that joins the contacts and data tables 3620 * and consumes all the data rows for a contact in order to build the Entity for a contact. 3621 */ 3622 private static class GroupsEntityIterator implements EntityIterator { 3623 private final Cursor mEntityCursor; 3624 private volatile boolean mIsClosed; 3625 3626 private static final String[] PROJECTION = new String[]{ 3627 Groups._ID, 3628 Groups.ACCOUNT_NAME, 3629 Groups.ACCOUNT_TYPE, 3630 Groups.SOURCE_ID, 3631 Groups.DIRTY, 3632 Groups.VERSION, 3633 Groups.RES_PACKAGE, 3634 Groups.TITLE, 3635 Groups.TITLE_RES, 3636 Groups.GROUP_VISIBLE, 3637 Groups.SYNC1, 3638 Groups.SYNC2, 3639 Groups.SYNC3, 3640 Groups.SYNC4, 3641 Groups.SYSTEM_ID, 3642 Groups.NOTES, 3643 Groups.DELETED}; 3644 3645 private static final int COLUMN_ID = 0; 3646 private static final int COLUMN_ACCOUNT_NAME = 1; 3647 private static final int COLUMN_ACCOUNT_TYPE = 2; 3648 private static final int COLUMN_SOURCE_ID = 3; 3649 private static final int COLUMN_DIRTY = 4; 3650 private static final int COLUMN_VERSION = 5; 3651 private static final int COLUMN_RES_PACKAGE = 6; 3652 private static final int COLUMN_TITLE = 7; 3653 private static final int COLUMN_TITLE_RES = 8; 3654 private static final int COLUMN_GROUP_VISIBLE = 9; 3655 private static final int COLUMN_SYNC1 = 10; 3656 private static final int COLUMN_SYNC2 = 11; 3657 private static final int COLUMN_SYNC3 = 12; 3658 private static final int COLUMN_SYNC4 = 13; 3659 private static final int COLUMN_SYSTEM_ID = 14; 3660 private static final int COLUMN_NOTES = 15; 3661 private static final int COLUMN_DELETED = 16; 3662 3663 public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri, 3664 String selection, String[] selectionArgs, String sortOrder) { 3665 mIsClosed = false; 3666 3667 final String updatedSortOrder = (sortOrder == null) 3668 ? Groups._ID 3669 : (Groups._ID + "," + sortOrder); 3670 3671 final SQLiteDatabase db = provider.mOpenHelper.getReadableDatabase(); 3672 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3673 qb.setTables(Tables.GROUPS_JOIN_PACKAGES); 3674 qb.setProjectionMap(sGroupsProjectionMap); 3675 if (groupIdString != null) { 3676 qb.appendWhere(Groups._ID + "=" + groupIdString); 3677 } 3678 final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME); 3679 final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE); 3680 if (!TextUtils.isEmpty(accountName)) { 3681 qb.appendWhere(Groups.ACCOUNT_NAME + "=" 3682 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 3683 + Groups.ACCOUNT_TYPE + "=" 3684 + DatabaseUtils.sqlEscapeString(accountType)); 3685 } 3686 mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs, 3687 null, null, updatedSortOrder); 3688 mEntityCursor.moveToFirst(); 3689 } 3690 3691 public void close() { 3692 if (mIsClosed) { 3693 throw new IllegalStateException("closing when already closed"); 3694 } 3695 mIsClosed = true; 3696 mEntityCursor.close(); 3697 } 3698 3699 public boolean hasNext() throws RemoteException { 3700 if (mIsClosed) { 3701 throw new IllegalStateException("calling hasNext() when the iterator is closed"); 3702 } 3703 3704 return !mEntityCursor.isAfterLast(); 3705 } 3706 3707 public void reset() throws RemoteException { 3708 if (mIsClosed) { 3709 throw new IllegalStateException("calling reset() when the iterator is closed"); 3710 } 3711 mEntityCursor.moveToFirst(); 3712 } 3713 3714 public Entity next() throws RemoteException { 3715 if (mIsClosed) { 3716 throw new IllegalStateException("calling next() when the iterator is closed"); 3717 } 3718 if (!hasNext()) { 3719 throw new IllegalStateException("you may only call next() if hasNext() is true"); 3720 } 3721 3722 final SQLiteCursor c = (SQLiteCursor) mEntityCursor; 3723 3724 final long groupId = c.getLong(COLUMN_ID); 3725 3726 // we expect the cursor is already at the row we need to read from 3727 ContentValues groupValues = new ContentValues(); 3728 groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME)); 3729 groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE)); 3730 groupValues.put(Groups._ID, groupId); 3731 groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY)); 3732 groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION)); 3733 groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID)); 3734 groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE)); 3735 groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE)); 3736 groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES)); 3737 groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE)); 3738 groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1)); 3739 groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2)); 3740 groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3)); 3741 groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4)); 3742 groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID)); 3743 groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED)); 3744 groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES)); 3745 Entity group = new Entity(groupValues); 3746 3747 mEntityCursor.moveToNext(); 3748 3749 return group; 3750 } 3751 } 3752 3753 @Override 3754 public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, 3755 String sortOrder) { 3756 waitForAccess(); 3757 3758 final int match = sUriMatcher.match(uri); 3759 switch (match) { 3760 case RAW_CONTACTS: 3761 case RAW_CONTACTS_ID: 3762 String contactsIdString = null; 3763 if (match == RAW_CONTACTS_ID) { 3764 contactsIdString = uri.getPathSegments().get(1); 3765 } 3766 3767 return new ContactsEntityIterator(this, contactsIdString, 3768 uri, selection, selectionArgs, sortOrder); 3769 case GROUPS: 3770 case GROUPS_ID: 3771 String idString = null; 3772 if (match == GROUPS_ID) { 3773 idString = uri.getPathSegments().get(1); 3774 } 3775 3776 return new GroupsEntityIterator(this, idString, 3777 uri, selection, selectionArgs, sortOrder); 3778 default: 3779 throw new UnsupportedOperationException("Unknown uri: " + uri); 3780 } 3781 } 3782 3783 @Override 3784 public String getType(Uri uri) { 3785 final int match = sUriMatcher.match(uri); 3786 switch (match) { 3787 case CONTACTS: 3788 case CONTACTS_LOOKUP: 3789 return Contacts.CONTENT_TYPE; 3790 case CONTACTS_ID: 3791 case CONTACTS_LOOKUP_ID: 3792 return Contacts.CONTENT_ITEM_TYPE; 3793 case RAW_CONTACTS: 3794 return RawContacts.CONTENT_TYPE; 3795 case RAW_CONTACTS_ID: 3796 return RawContacts.CONTENT_ITEM_TYPE; 3797 case DATA_ID: 3798 return mOpenHelper.getDataMimeType(ContentUris.parseId(uri)); 3799 case AGGREGATION_EXCEPTIONS: 3800 return AggregationExceptions.CONTENT_TYPE; 3801 case AGGREGATION_EXCEPTION_ID: 3802 return AggregationExceptions.CONTENT_ITEM_TYPE; 3803 case SETTINGS: 3804 return Settings.CONTENT_TYPE; 3805 case AGGREGATION_SUGGESTIONS: 3806 return Contacts.CONTENT_TYPE; 3807 case SEARCH_SUGGESTIONS: 3808 return SearchManager.SUGGEST_MIME_TYPE; 3809 case SEARCH_SHORTCUT: 3810 return SearchManager.SHORTCUT_MIME_TYPE; 3811 default: 3812 return mLegacyApiSupport.getType(uri); 3813 } 3814 } 3815 3816 private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) { 3817 if (displayName != null) { 3818 mRawContactDisplayNameUpdate.bindString(1, displayName); 3819 } else { 3820 mRawContactDisplayNameUpdate.bindNull(1); 3821 } 3822 mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource); 3823 mRawContactDisplayNameUpdate.bindLong(3, rawContactId); 3824 mRawContactDisplayNameUpdate.execute(); 3825 } 3826 3827 /** 3828 * Checks the {@link Data#MARK_AS_DIRTY} query parameter. 3829 * 3830 * Returns true if the parameter is missing or is either "true" or "1". 3831 */ 3832 private boolean shouldMarkRawContactAsDirty(Uri uri) { 3833 if (mImportMode) { 3834 return false; 3835 } 3836 3837 String param = uri.getQueryParameter(Data.MARK_AS_DIRTY); 3838 return param == null || (!param.equalsIgnoreCase("false") && !param.equals("0")); 3839 } 3840 3841 /** 3842 * Sets the {@link RawContacts#DIRTY} for the specified raw contact. 3843 */ 3844 private void setRawContactDirty(long rawContactId) { 3845 mRawContactDirtyUpdate.bindLong(1, rawContactId); 3846 mRawContactDirtyUpdate.execute(); 3847 } 3848 3849 /** 3850 * Checks the {@link Groups#MARK_AS_DIRTY} query parameter. 3851 * 3852 * Returns true if the parameter is missing or is either "true" or "1". 3853 */ 3854 private boolean shouldMarkGroupAsDirty(Uri uri) { 3855 if (mImportMode) { 3856 return false; 3857 } 3858 3859 return readBooleanQueryParameter(uri, Groups.MARK_AS_DIRTY, true); 3860 } 3861 3862 /* 3863 * Sets the given dataId record in the "data" table to primary, and resets all data records of 3864 * the same mimetype and under the same contact to not be primary. 3865 * 3866 * @param dataId the id of the data record to be set to primary. 3867 */ 3868 private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) { 3869 mSetPrimaryStatement.bindLong(1, dataId); 3870 mSetPrimaryStatement.bindLong(2, mimeTypeId); 3871 mSetPrimaryStatement.bindLong(3, rawContactId); 3872 mSetPrimaryStatement.execute(); 3873 } 3874 3875 /* 3876 * Sets the given dataId record in the "data" table to "super primary", and resets all data 3877 * records of the same mimetype and under the same aggregate to not be "super primary". 3878 * 3879 * @param dataId the id of the data record to be set to primary. 3880 */ 3881 private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) { 3882 mSetSuperPrimaryStatement.bindLong(1, dataId); 3883 mSetSuperPrimaryStatement.bindLong(2, mimeTypeId); 3884 mSetSuperPrimaryStatement.bindLong(3, rawContactId); 3885 mSetSuperPrimaryStatement.execute(); 3886 } 3887 3888 private void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) { 3889 sb.append("(SELECT DISTINCT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS 3890 + " JOIN name_lookup ON(" + RawContactsColumns.CONCRETE_ID + "=raw_contact_id)" 3891 + " WHERE normalized_name GLOB '"); 3892 sb.append(NameNormalizer.normalize(filterParam)); 3893 sb.append("*')"); 3894 } 3895 3896 public String getRawContactsByFilterAsNestedQuery(String filterParam) { 3897 StringBuilder sb = new StringBuilder(); 3898 appendRawContactsByFilterAsNestedQuery(sb, filterParam, null); 3899 return sb.toString(); 3900 } 3901 3902 public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam, 3903 String limit) { 3904 appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit); 3905 } 3906 3907 private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName, 3908 String limit) { 3909 sb.append("(SELECT DISTINCT raw_contact_id FROM name_lookup WHERE normalized_name GLOB '"); 3910 sb.append(normalizedName); 3911 sb.append("*'"); 3912 if (limit != null) { 3913 sb.append(" LIMIT ").append(limit); 3914 } 3915 sb.append(")"); 3916 } 3917 3918 /** 3919 * Inserts an argument at the beginning of the selection arg list. 3920 */ 3921 private String[] insertSelectionArg(String[] selectionArgs, String arg) { 3922 if (selectionArgs == null) { 3923 return new String[] {arg}; 3924 } else { 3925 int newLength = selectionArgs.length + 1; 3926 String[] newSelectionArgs = new String[newLength]; 3927 newSelectionArgs[0] = arg; 3928 System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length); 3929 return newSelectionArgs; 3930 } 3931 } 3932 3933 protected Account getDefaultAccount() { 3934 AccountManager accountManager = AccountManager.get(getContext()); 3935 try { 3936 Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE, 3937 new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult(); 3938 if (accounts != null && accounts.length > 0) { 3939 return accounts[0]; 3940 } 3941 } catch (Throwable e) { 3942 Log.e(TAG, "Cannot determine the default account for contacts compatibility", e); 3943 } 3944 return null; 3945 } 3946} 3947