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