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