1/* 2 * Copyright (C) 2006 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 android.app.SearchManager; 20import android.content.AbstractSyncableContentProvider; 21import android.content.AbstractTableMerger; 22import android.content.ContentProvider; 23import android.content.ContentResolver; 24import android.content.ContentUris; 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.SharedPreferences; 28import android.content.UriMatcher; 29import android.content.res.AssetFileDescriptor; 30import android.content.res.Resources; 31import android.database.Cursor; 32import android.database.CursorJoiner; 33import android.database.DatabaseUtils; 34import android.database.SQLException; 35import android.database.sqlite.SQLiteContentHelper; 36import android.database.sqlite.SQLiteCursor; 37import android.database.sqlite.SQLiteDatabase; 38import android.database.sqlite.SQLiteDoneException; 39import android.database.sqlite.SQLiteException; 40import android.database.sqlite.SQLiteQueryBuilder; 41import android.database.sqlite.SQLiteStatement; 42import android.net.Uri; 43import android.os.Bundle; 44import android.os.MemoryFile; 45import android.os.ParcelFileDescriptor; 46import android.provider.CallLog; 47import android.provider.Contacts; 48import android.provider.LiveFolders; 49import android.provider.SyncConstValue; 50import android.provider.CallLog.Calls; 51import android.provider.Contacts.ContactMethods; 52import android.provider.Contacts.Extensions; 53import android.provider.Contacts.GroupMembership; 54import android.provider.Contacts.Groups; 55import android.provider.Contacts.GroupsColumns; 56import android.provider.Contacts.Intents; 57import android.provider.Contacts.Organizations; 58import android.provider.Contacts.People; 59import android.provider.Contacts.PeopleColumns; 60import android.provider.Contacts.Phones; 61import android.provider.Contacts.Photos; 62import android.provider.Contacts.Presence; 63import android.provider.Contacts.PresenceColumns; 64import android.telephony.PhoneNumberUtils; 65import android.text.TextUtils; 66import android.util.Config; 67import android.util.Log; 68 69import com.google.android.collect.Maps; 70import com.google.android.collect.Sets; 71 72import com.android.internal.database.ArrayListCursor; 73 74import java.io.FileNotFoundException; 75import java.io.IOException; 76import java.util.ArrayList; 77import java.util.HashMap; 78import java.util.HashSet; 79import java.util.Locale; 80import java.util.Map; 81import java.util.Set; 82 83public class ContactsProvider extends AbstractSyncableContentProvider { 84 private static final String STREQUENT_ORDER_BY = "times_contacted DESC, display_name ASC"; 85 private static final String STREQUENT_LIMIT = 86 "(SELECT COUNT(*) FROM people WHERE starred = 1) + 25"; 87 88 private static final String PEOPLE_PHONES_JOIN = 89 "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id " 90 + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id)"; 91 92 private static final String PEOPLE_PHONES_PHOTOS_JOIN = 93 "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id " 94 + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id) " 95 + "LEFT OUTER JOIN photos ON (photos." + Photos.PERSON_ID + "=people._id)"; 96 97 private static final String PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN = 98 "people LEFT OUTER JOIN phones ON people.primary_phone=phones._id " 99 + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID + "=people._id) " 100 + "LEFT OUTER JOIN photos ON (photos." + Photos.PERSON_ID + "=people._id) " 101 + "LEFT OUTER JOIN organizations ON (organizations._id=people.primary_organization)"; 102 103 private static final String GTALK_PROTOCOL_STRING = 104 ContactMethods.encodePredefinedImProtocol(ContactMethods.PROTOCOL_GOOGLE_TALK); 105 106 private static final String[] ID_TYPE_PROJECTION = new String[]{"_id", "type"}; 107 108 private static final String[] sIsPrimaryProjectionWithoutKind = 109 new String[]{"isprimary", "person", "_id"}; 110 private static final String[] sIsPrimaryProjectionWithKind = 111 new String[]{"isprimary", "person", "_id", "kind"}; 112 113 private static final String WHERE_ID = "_id=?"; 114 115 private static final String sGroupsJoinString; 116 117 private static final String PREFS_NAME_OWNER = "owner-info"; 118 private static final String PREF_OWNER_ID = "owner-id"; 119 120 /** this is suitable for use by insert/update/delete/query and may be passed 121 * as a method call parameter. Only insert/update/delete/query should call .clear() on it */ 122 private final ContentValues mValues = new ContentValues(); 123 124 /** this is suitable for local use in methods and should never be passed as a parameter to 125 * other methods (other than the DB layer) */ 126 private final ContentValues mValuesLocal = new ContentValues(); 127 128 private String[] mAccounts = new String[0]; 129 private final Object mAccountsLock = new Object(); 130 131 private DatabaseUtils.InsertHelper mDeletedPeopleInserter; 132 private DatabaseUtils.InsertHelper mPeopleInserter; 133 private int mIndexPeopleSyncId; 134 private int mIndexPeopleSyncTime; 135 private int mIndexPeopleSyncVersion; 136 private int mIndexPeopleSyncDirty; 137 private int mIndexPeopleSyncAccount; 138 private int mIndexPeopleName; 139 private int mIndexPeoplePhoneticName; 140 private int mIndexPeopleNotes; 141 private DatabaseUtils.InsertHelper mGroupsInserter; 142 private DatabaseUtils.InsertHelper mPhotosInserter; 143 private int mIndexPhotosPersonId; 144 private int mIndexPhotosSyncId; 145 private int mIndexPhotosSyncTime; 146 private int mIndexPhotosSyncVersion; 147 private int mIndexPhotosSyncDirty; 148 private int mIndexPhotosSyncAccount; 149 private int mIndexPhotosExistsOnServer; 150 private int mIndexPhotosSyncError; 151 private DatabaseUtils.InsertHelper mContactMethodsInserter; 152 private int mIndexContactMethodsPersonId; 153 private int mIndexContactMethodsLabel; 154 private int mIndexContactMethodsKind; 155 private int mIndexContactMethodsType; 156 private int mIndexContactMethodsData; 157 private int mIndexContactMethodsAuxData; 158 private int mIndexContactMethodsIsPrimary; 159 private DatabaseUtils.InsertHelper mOrganizationsInserter; 160 private int mIndexOrganizationsPersonId; 161 private int mIndexOrganizationsLabel; 162 private int mIndexOrganizationsType; 163 private int mIndexOrganizationsCompany; 164 private int mIndexOrganizationsTitle; 165 private int mIndexOrganizationsIsPrimary; 166 private DatabaseUtils.InsertHelper mExtensionsInserter; 167 private int mIndexExtensionsPersonId; 168 private int mIndexExtensionsName; 169 private int mIndexExtensionsValue; 170 private DatabaseUtils.InsertHelper mGroupMembershipInserter; 171 private int mIndexGroupMembershipPersonId; 172 private int mIndexGroupMembershipGroupSyncAccount; 173 private int mIndexGroupMembershipGroupSyncId; 174 private DatabaseUtils.InsertHelper mCallsInserter; 175 private DatabaseUtils.InsertHelper mPhonesInserter; 176 private int mIndexPhonesPersonId; 177 private int mIndexPhonesLabel; 178 private int mIndexPhonesType; 179 private int mIndexPhonesNumber; 180 private int mIndexPhonesNumberKey; 181 private int mIndexPhonesIsPrimary; 182 183 private static HashMap<String, String> mSearchSuggestionsProjectionMap; 184 private static String mSearchSuggestionLanguage; 185 186 public ContactsProvider() { 187 super(DATABASE_NAME, DATABASE_VERSION, Contacts.CONTENT_URI); 188 mSearchSuggestionLanguage = Locale.getDefault().getLanguage(); 189 // Search suggestions projection map 190 mSearchSuggestionsProjectionMap = new HashMap<String, String>(); 191 updateSuggestColumnTexts(); 192 mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_ICON_1, 193 "(CASE WHEN " + Photos.DATA + " IS NOT NULL" 194 + " THEN '" + People.CONTENT_URI + "/' || people._id ||" 195 + " '/" + Photos.CONTENT_DIRECTORY + "/data'" 196 + " ELSE " + com.android.internal.R.drawable.ic_contact_picture 197 + " END) AS " + SearchManager.SUGGEST_COLUMN_ICON_1); 198 mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_ICON_2, 199 PRESENCE_ICON_SQL + " AS " + SearchManager.SUGGEST_COLUMN_ICON_2); 200 mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, 201 "people._id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 202 mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 203 "people._id AS " + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID); 204 mSearchSuggestionsProjectionMap.put(People._ID, "people._id AS " + People._ID); 205 } 206 207 @Override 208 protected void onDatabaseOpened(SQLiteDatabase db) { 209 maybeCreatePresenceTable(db); 210 211 // Mark all the tables as syncable 212 db.markTableSyncable(sPeopleTable, sDeletedPeopleTable); 213 db.markTableSyncable(sPhonesTable, Phones.PERSON_ID, sPeopleTable); 214 db.markTableSyncable(sContactMethodsTable, ContactMethods.PERSON_ID, sPeopleTable); 215 db.markTableSyncable(sOrganizationsTable, Organizations.PERSON_ID, sPeopleTable); 216 db.markTableSyncable(sGroupmembershipTable, GroupMembership.PERSON_ID, sPeopleTable); 217 db.markTableSyncable(sExtensionsTable, Extensions.PERSON_ID, sPeopleTable); 218 db.markTableSyncable(sGroupsTable, sDeletedGroupsTable); 219 220 mDeletedPeopleInserter = new DatabaseUtils.InsertHelper(db, sDeletedPeopleTable); 221 mPeopleInserter = new DatabaseUtils.InsertHelper(db, sPeopleTable); 222 mIndexPeopleSyncId = mPeopleInserter.getColumnIndex(People._SYNC_ID); 223 mIndexPeopleSyncTime = mPeopleInserter.getColumnIndex(People._SYNC_TIME); 224 mIndexPeopleSyncVersion = mPeopleInserter.getColumnIndex(People._SYNC_VERSION); 225 mIndexPeopleSyncDirty = mPeopleInserter.getColumnIndex(People._SYNC_DIRTY); 226 mIndexPeopleSyncAccount = mPeopleInserter.getColumnIndex(People._SYNC_ACCOUNT); 227 mIndexPeopleName = mPeopleInserter.getColumnIndex(People.NAME); 228 mIndexPeoplePhoneticName = mPeopleInserter.getColumnIndex(People.PHONETIC_NAME); 229 mIndexPeopleNotes = mPeopleInserter.getColumnIndex(People.NOTES); 230 231 mGroupsInserter = new DatabaseUtils.InsertHelper(db, sGroupsTable); 232 233 mPhotosInserter = new DatabaseUtils.InsertHelper(db, sPhotosTable); 234 mIndexPhotosPersonId = mPhotosInserter.getColumnIndex(Photos.PERSON_ID); 235 mIndexPhotosSyncId = mPhotosInserter.getColumnIndex(Photos._SYNC_ID); 236 mIndexPhotosSyncTime = mPhotosInserter.getColumnIndex(Photos._SYNC_TIME); 237 mIndexPhotosSyncVersion = mPhotosInserter.getColumnIndex(Photos._SYNC_VERSION); 238 mIndexPhotosSyncDirty = mPhotosInserter.getColumnIndex(Photos._SYNC_DIRTY); 239 mIndexPhotosSyncAccount = mPhotosInserter.getColumnIndex(Photos._SYNC_ACCOUNT); 240 mIndexPhotosSyncError = mPhotosInserter.getColumnIndex(Photos.SYNC_ERROR); 241 mIndexPhotosExistsOnServer = mPhotosInserter.getColumnIndex(Photos.EXISTS_ON_SERVER); 242 243 mContactMethodsInserter = new DatabaseUtils.InsertHelper(db, sContactMethodsTable); 244 mIndexContactMethodsPersonId = mContactMethodsInserter.getColumnIndex(ContactMethods.PERSON_ID); 245 mIndexContactMethodsLabel = mContactMethodsInserter.getColumnIndex(ContactMethods.LABEL); 246 mIndexContactMethodsKind = mContactMethodsInserter.getColumnIndex(ContactMethods.KIND); 247 mIndexContactMethodsType = mContactMethodsInserter.getColumnIndex(ContactMethods.TYPE); 248 mIndexContactMethodsData = mContactMethodsInserter.getColumnIndex(ContactMethods.DATA); 249 mIndexContactMethodsAuxData = mContactMethodsInserter.getColumnIndex(ContactMethods.AUX_DATA); 250 mIndexContactMethodsIsPrimary = mContactMethodsInserter.getColumnIndex(ContactMethods.ISPRIMARY); 251 252 mOrganizationsInserter = new DatabaseUtils.InsertHelper(db, sOrganizationsTable); 253 mIndexOrganizationsPersonId = mOrganizationsInserter.getColumnIndex(Organizations.PERSON_ID); 254 mIndexOrganizationsLabel = mOrganizationsInserter.getColumnIndex(Organizations.LABEL); 255 mIndexOrganizationsType = mOrganizationsInserter.getColumnIndex(Organizations.TYPE); 256 mIndexOrganizationsCompany = mOrganizationsInserter.getColumnIndex(Organizations.COMPANY); 257 mIndexOrganizationsTitle = mOrganizationsInserter.getColumnIndex(Organizations.TITLE); 258 mIndexOrganizationsIsPrimary = mOrganizationsInserter.getColumnIndex(Organizations.ISPRIMARY); 259 260 mExtensionsInserter = new DatabaseUtils.InsertHelper(db, sExtensionsTable); 261 mIndexExtensionsPersonId = mExtensionsInserter.getColumnIndex(Extensions.PERSON_ID); 262 mIndexExtensionsName = mExtensionsInserter.getColumnIndex(Extensions.NAME); 263 mIndexExtensionsValue = mExtensionsInserter.getColumnIndex(Extensions.VALUE); 264 265 mGroupMembershipInserter = new DatabaseUtils.InsertHelper(db, sGroupmembershipTable); 266 mIndexGroupMembershipPersonId = mGroupMembershipInserter.getColumnIndex(GroupMembership.PERSON_ID); 267 mIndexGroupMembershipGroupSyncAccount = mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ACCOUNT); 268 mIndexGroupMembershipGroupSyncId = mGroupMembershipInserter.getColumnIndex(GroupMembership.GROUP_SYNC_ID); 269 270 mCallsInserter = new DatabaseUtils.InsertHelper(db, sCallsTable); 271 272 mPhonesInserter = new DatabaseUtils.InsertHelper(db, sPhonesTable); 273 mIndexPhonesPersonId = mPhonesInserter.getColumnIndex(Phones.PERSON_ID); 274 mIndexPhonesLabel = mPhonesInserter.getColumnIndex(Phones.LABEL); 275 mIndexPhonesType = mPhonesInserter.getColumnIndex(Phones.TYPE); 276 mIndexPhonesNumber = mPhonesInserter.getColumnIndex(Phones.NUMBER); 277 mIndexPhonesNumberKey = mPhonesInserter.getColumnIndex(Phones.NUMBER_KEY); 278 mIndexPhonesIsPrimary = mPhonesInserter.getColumnIndex(Phones.ISPRIMARY); 279 } 280 281 @Override 282 protected boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion) { 283 boolean upgradeWasLossless = true; 284 if (oldVersion < 71) { 285 Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + 286 newVersion + ", which will destroy all old data"); 287 dropTables(db); 288 bootstrapDatabase(db); 289 return false; // this was lossy 290 } 291 if (oldVersion == 71) { 292 Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " + 293 newVersion + ", which will preserve existing data"); 294 295 db.delete("_sync_state", null, null); 296 mValuesLocal.clear(); 297 mValuesLocal.putNull(Photos._SYNC_VERSION); 298 mValuesLocal.putNull(Photos._SYNC_TIME); 299 db.update(sPhotosTable, mValuesLocal, null, null); 300 getContext().getContentResolver().startSync(Contacts.CONTENT_URI, new Bundle()); 301 oldVersion = 72; 302 } 303 if (oldVersion == 72) { 304 Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " + 305 newVersion + ", which will preserve existing data"); 306 307 // use new token format from 73 308 db.execSQL("delete from peopleLookup"); 309 try { 310 // With longForQuery(), _TOKERNIZE() is called just once, toward the first entry 311 // in "people" table. This may be a bug. Instead, we use rawQuery() for now. 312 // DatabaseUtils.longForQuery(db, query, null); 313 314 // Cursors objects are lazily executed. So we have to call some method which forces 315 // the cursor to run the query. 316 Cursor cursor = 317 db.rawQuery("SELECT _TOKENIZE('peopleLookup', _id, name, ' ') from people", 318 null); 319 try { 320 int rows = cursor.getCount(); 321 Log.i(TAG, "Processed " + rows + " contacts."); 322 } finally { 323 if (cursor != null) { 324 cursor.close(); 325 } 326 } 327 } catch (SQLiteDoneException ex) { 328 // it is ok to throw this, 329 // it just means you don't have data in people table 330 } 331 oldVersion = 73; 332 } 333 // There was a bug for a while in the upgrade logic where going from 72 to 74 would skip 334 // the step from 73 to 74, so 74 to 75 just tries the same steps, and gracefully handles 335 // errors in case the device was started freshly at 74. 336 if (oldVersion == 73 || oldVersion == 74) { 337 Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " + 338 newVersion + ", which will preserve existing data"); 339 340 try { 341 db.execSQL("ALTER TABLE calls ADD name TEXT;"); 342 db.execSQL("ALTER TABLE calls ADD numbertype INTEGER;"); 343 db.execSQL("ALTER TABLE calls ADD numberlabel TEXT;"); 344 } catch (SQLiteException sqle) { 345 // Maybe the table was altered already... Shouldn't be an issue. 346 } 347 oldVersion = 75; 348 } 349 // There were some indices added in version 76 350 if (oldVersion == 75) { 351 Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " + 352 newVersion + ", which will preserve existing data"); 353 354 // add the new indices 355 db.execSQL("CREATE INDEX IF NOT EXISTS groupsSyncDirtyIndex" 356 + " ON groups (" + Groups._SYNC_DIRTY + ");"); 357 db.execSQL("CREATE INDEX IF NOT EXISTS photosSyncDirtyIndex" 358 + " ON photos (" + Photos._SYNC_DIRTY + ");"); 359 db.execSQL("CREATE INDEX IF NOT EXISTS peopleSyncDirtyIndex" 360 + " ON people (" + People._SYNC_DIRTY + ");"); 361 oldVersion = 76; 362 } 363 364 if (oldVersion == 76 || oldVersion == 77) { 365 db.execSQL("DELETE FROM people"); 366 db.execSQL("DELETE FROM groups"); 367 db.execSQL("DELETE FROM photos"); 368 db.execSQL("DELETE FROM _deleted_people"); 369 db.execSQL("DELETE FROM _deleted_groups"); 370 upgradeWasLossless = false; 371 oldVersion = 78; 372 } 373 374 if (oldVersion == 78) { 375 db.execSQL("UPDATE photos SET _sync_dirty=0 where _sync_dirty is null;"); 376 oldVersion = 79; 377 } 378 379 if (oldVersion == 79) { 380 try { 381 db.execSQL("ALTER TABLE people ADD phonetic_name TEXT COLLATE LOCALIZED;"); 382 } catch (SQLiteException sqle) { 383 // Maybe the table was altered already... Shouldn't be an issue. 384 } 385 oldVersion = 80; 386 } 387 388 // Because of historical reason, version 81 have two types. 389 // 1) One type already has "peopleLookupWithPhoneticName" table but does not have 390 // "peopleLookup" table with token_index. 391 // 2) Another type has "peopleLookup" table with token_index but does not have 392 // "peopleLookupWithPhoneticName" table. 393 // For simplicity, both databases are once dropped here. 394 // This is slow but should be done just once anyway... 395 if (oldVersion == 80 || oldVersion == 81) { 396 Log.i(TAG, "Upgrading contacts database from version " + oldVersion + " to " + 397 newVersion + ", which will preserve existing data"); 398 399 recreatePeopleLookupTable(db); 400 try { 401 String query = "SELECT _TOKENIZE('peopleLookup', _id, name, ' ', 1) FROM people"; 402 Cursor cursor = db.rawQuery(query, null); 403 try { 404 int rows = cursor.getCount(); 405 Log.i(TAG, "Processed " + rows + " contacts."); 406 } finally { 407 if (cursor != null) { 408 cursor.close(); 409 } 410 } 411 } catch (SQLiteException e) { 412 Log.e(TAG, e.toString() + ": " + e.getMessage()); 413 } 414 415 recreatePeopleLookupWithPhoneticNameTable(db); 416 try { 417 String query = "SELECT _TOKENIZE('peopleLookupWithPhoneticName', _id, " 418 + PHONETIC_LOOKUP_SQL_SIMPLE + 419 ", ' ', 1) FROM people"; 420 Cursor cursor = db.rawQuery(query, null); 421 try { 422 int rows = cursor.getCount(); 423 Log.i(TAG, "Processed " + rows + " contacts."); 424 } finally { 425 if (cursor != null) { 426 cursor.close(); 427 } 428 } 429 } catch (SQLiteException e) { 430 Log.e(TAG, e.toString() + ": " + e.getMessage()); 431 } 432 433 oldVersion = 82; 434 } 435 436 return upgradeWasLossless; 437 } 438 439 protected void dropTables(SQLiteDatabase db) { 440 db.execSQL("DROP TABLE IF EXISTS people"); 441 db.execSQL("DROP TABLE IF EXISTS peopleLookup"); 442 db.execSQL("DROP TABLE IF EXISTS peopleLookupWithPhoneticName"); 443 db.execSQL("DROP TABLE IF EXISTS _deleted_people"); 444 db.execSQL("DROP TABLE IF EXISTS phones"); 445 db.execSQL("DROP TABLE IF EXISTS contact_methods"); 446 db.execSQL("DROP TABLE IF EXISTS calls"); 447 db.execSQL("DROP TABLE IF EXISTS organizations"); 448 db.execSQL("DROP TABLE IF EXISTS voice_dialer_timestamp"); 449 db.execSQL("DROP TABLE IF EXISTS groups"); 450 db.execSQL("DROP TABLE IF EXISTS _deleted_groups"); 451 db.execSQL("DROP TABLE IF EXISTS groupmembership"); 452 db.execSQL("DROP TABLE IF EXISTS photos"); 453 db.execSQL("DROP TABLE IF EXISTS extensions"); 454 db.execSQL("DROP TABLE IF EXISTS settings"); 455 } 456 457 private void recreatePeopleLookupTable(SQLiteDatabase db) { 458 db.execSQL("DROP TABLE IF EXISTS peopleLookup"); 459 db.execSQL("DROP INDEX IF EXISTS peopleLookupIndex"); 460 db.execSQL("DROP TRIGGER IF EXISTS peopleLookup_update"); 461 db.execSQL("DROP TRIGGER IF EXISTS peopleLookup_insert"); 462 463 db.execSQL("CREATE TABLE peopleLookup (" + 464 "token TEXT," + 465 "source INTEGER REFERENCES people(_id)," + 466 "token_index INTEGER" + 467 ");"); 468 db.execSQL("CREATE INDEX peopleLookupIndex ON peopleLookup (" + 469 "token," + 470 "source" + 471 ");"); 472 473 // Triggers to keep the peopleLookup table up to date 474 db.execSQL("CREATE TRIGGER peopleLookup_update UPDATE OF name ON people " + 475 "BEGIN " + 476 "DELETE FROM peopleLookup WHERE source = new._id;" + 477 "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" + 478 "END"); 479 db.execSQL("CREATE TRIGGER peopleLookup_insert AFTER INSERT ON people " + 480 "BEGIN " + 481 "SELECT _TOKENIZE('peopleLookup', new._id, new.name, ' ', 1);" + 482 "END"); 483 } 484 485 private void recreatePeopleLookupWithPhoneticNameTable(SQLiteDatabase db) { 486 db.execSQL("DROP TABLE IF EXISTS peopleLookupWithPhoneticName"); 487 db.execSQL("DROP INDEX IF EXISTS peopleLookupwithPhoneticNameIndex"); 488 db.execSQL("DROP TRIGGER IF EXISTS peopleLookupWithPhoneticName_update"); 489 db.execSQL("DROP TRIGGER IF EXISTS peopleLookupWithPhoneticName_insert"); 490 491 db.execSQL("CREATE TABLE peopleLookupWithPhoneticName (" + 492 "token TEXT," + 493 "source INTEGER REFERENCES people(_id)," + 494 "token_index INTEGER" + 495 ");"); 496 db.execSQL("CREATE INDEX peopleLookupWithPhoneticNameIndex ON " + 497 "peopleLookupWithPhoneticName (" + 498 "token," + 499 "source" + 500 ");"); 501 502 // Triggers to keep the peopleLookupWithPhoneticName table up to date 503 db.execSQL("CREATE TRIGGER peopleLookupWithPhoneticName_update UPDATE OF " + 504 "name, phonetic_name ON people " + 505 "BEGIN " + 506 "DELETE FROM peopleLookupWithPhoneticName WHERE source = new._id;" + 507 "SELECT _TOKENIZE('peopleLookupWithPhoneticName', new._id, " + 508 PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW + 509 ", ' ', 1);" + 510 "END"); 511 db.execSQL("CREATE TRIGGER peopleLookupWithPhoneticName_insert AFTER INSERT ON people " + 512 "BEGIN " + 513 "SELECT _TOKENIZE('peopleLookupWithPhoneticName', new._id, " + 514 PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW + 515 ", ' ', 1);" + 516 "END"); 517 } 518 519 @Override 520 protected void bootstrapDatabase(SQLiteDatabase db) { 521 super.bootstrapDatabase(db); 522 db.execSQL("CREATE TABLE people (" + 523 People._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 524 People._SYNC_ACCOUNT + " TEXT," + // From the sync source 525 People._SYNC_ID + " TEXT," + // From the sync source 526 People._SYNC_TIME + " TEXT," + // From the sync source 527 People._SYNC_VERSION + " TEXT," + // From the sync source 528 People._SYNC_LOCAL_ID + " INTEGER," + // Used while syncing, not persistent 529 People._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," + 530 // if syncable, non-zero if the record 531 // has local, unsynced, changes 532 People._SYNC_MARK + " INTEGER," + // Used to filter out new rows 533 534 People.NAME + " TEXT COLLATE LOCALIZED," + 535 People.NOTES + " TEXT COLLATE LOCALIZED," + 536 People.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," + 537 People.LAST_TIME_CONTACTED + " INTEGER," + 538 People.STARRED + " INTEGER NOT NULL DEFAULT 0," + 539 People.PRIMARY_PHONE_ID + " INTEGER REFERENCES phones(_id)," + 540 People.PRIMARY_ORGANIZATION_ID + " INTEGER REFERENCES organizations(_id)," + 541 People.PRIMARY_EMAIL_ID + " INTEGER REFERENCES contact_methods(_id)," + 542 People.PHOTO_VERSION + " TEXT," + 543 People.CUSTOM_RINGTONE + " TEXT," + 544 People.SEND_TO_VOICEMAIL + " INTEGER," + 545 People.PHONETIC_NAME + " TEXT COLLATE LOCALIZED" + 546 ");"); 547 548 db.execSQL("CREATE INDEX peopleNameIndex ON people (" + People.NAME + ");"); 549 db.execSQL("CREATE INDEX peopleSyncDirtyIndex ON people (" + People._SYNC_DIRTY + ");"); 550 db.execSQL("CREATE INDEX peopleSyncIdIndex ON people (" + People._SYNC_ID + ");"); 551 552 db.execSQL("CREATE TRIGGER people_timesContacted UPDATE OF last_time_contacted ON people " + 553 "BEGIN " + 554 "UPDATE people SET " 555 + People.TIMES_CONTACTED + " = (new." + People.TIMES_CONTACTED + " + 1)" 556 + " WHERE _id = new._id;" + 557 "END"); 558 559 // table of all the groups that exist for an account 560 db.execSQL("CREATE TABLE groups (" + 561 Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 562 Groups._SYNC_ACCOUNT + " TEXT," + // From the sync source 563 Groups._SYNC_ID + " TEXT," + // From the sync source 564 Groups._SYNC_TIME + " TEXT," + // From the sync source 565 Groups._SYNC_VERSION + " TEXT," + // From the sync source 566 Groups._SYNC_LOCAL_ID + " INTEGER," + // Used while syncing, not persistent 567 Groups._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," + 568 // if syncable, non-zero if the record 569 // has local, unsynced, changes 570 Groups._SYNC_MARK + " INTEGER," + // Used to filter out new rows 571 572 Groups.NAME + " TEXT NOT NULL," + 573 Groups.NOTES + " TEXT," + 574 Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 0," + 575 Groups.SYSTEM_ID + " TEXT," + 576 "UNIQUE(" + 577 Groups.NAME + "," + Groups.SYSTEM_ID + "," + Groups._SYNC_ACCOUNT + ")" + 578 ");"); 579 580 db.execSQL("CREATE INDEX groupsSyncDirtyIndex ON groups (" + Groups._SYNC_DIRTY + ");"); 581 582 if (!isTemporary()) { 583 // Add the system groups, since we always need them. 584 db.execSQL("INSERT INTO groups (" + Groups.NAME + ", " + Groups.SYSTEM_ID + ") VALUES " 585 + "('" + Groups.GROUP_MY_CONTACTS + "', '" + Groups.GROUP_MY_CONTACTS + "')"); 586 } 587 588 db.execSQL("CREATE TABLE peopleLookup (" + 589 "token TEXT," + 590 "source INTEGER REFERENCES people(_id)," + 591 "token_index INTEGER"+ 592 ");"); 593 db.execSQL("CREATE INDEX peopleLookupIndex ON peopleLookup (" + 594 "token," + 595 "source" + 596 ");"); 597 598 db.execSQL("CREATE TABLE photos (" 599 + Photos._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 600 + Photos.EXISTS_ON_SERVER + " INTEGER NOT NULL DEFAULT 0," 601 + Photos.PERSON_ID + " INTEGER REFERENCES people(_id), " 602 + Photos.LOCAL_VERSION + " TEXT," 603 + Photos.DATA + " BLOB," 604 + Photos.SYNC_ERROR + " TEXT," 605 + Photos._SYNC_ACCOUNT + " TEXT," 606 + Photos._SYNC_ID + " TEXT," 607 + Photos._SYNC_TIME + " TEXT," 608 + Photos._SYNC_VERSION + " TEXT," 609 + Photos._SYNC_LOCAL_ID + " INTEGER," 610 + Photos._SYNC_DIRTY + " INTEGER NOT NULL DEFAULT 0," 611 + Photos._SYNC_MARK + " INTEGER," 612 + "UNIQUE(" + Photos.PERSON_ID + ") " 613 + ")"); 614 615 db.execSQL("CREATE INDEX photosSyncDirtyIndex ON photos (" + Photos._SYNC_DIRTY + ");"); 616 db.execSQL("CREATE INDEX photoPersonIndex ON photos (person);"); 617 618 // Delete the photo row when the people row is deleted 619 db.execSQL("" 620 + " CREATE TRIGGER peopleDeleteAndPhotos DELETE ON people " 621 + " BEGIN" 622 + " DELETE FROM photos WHERE person=OLD._id;" 623 + " END"); 624 625 db.execSQL("CREATE TABLE _deleted_people (" + 626 "_sync_version TEXT," + // From the sync source 627 "_sync_id TEXT," + 628 (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing, 629 "_sync_account TEXT," + 630 "_sync_mark INTEGER)"); // Used to filter out new rows 631 632 db.execSQL("CREATE TABLE _deleted_groups (" + 633 "_sync_version TEXT," + // From the sync source 634 "_sync_id TEXT," + 635 (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing, 636 "_sync_account TEXT," + 637 "_sync_mark INTEGER)"); // Used to filter out new rows 638 639 db.execSQL("CREATE TABLE phones (" + 640 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 641 "person INTEGER REFERENCES people(_id)," + 642 "type INTEGER NOT NULL," + // kind specific (home, work, etc) 643 "number TEXT," + 644 "number_key TEXT," + 645 "label TEXT," + 646 "isprimary INTEGER NOT NULL DEFAULT 0" + 647 ");"); 648 db.execSQL("CREATE INDEX phonesIndex1 ON phones (person);"); 649 db.execSQL("CREATE INDEX phonesIndex2 ON phones (number_key);"); 650 651 db.execSQL("CREATE TABLE contact_methods (" + 652 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 653 "person INTEGER REFERENCES people(_id)," + 654 "kind INTEGER NOT NULL," + // the kind of contact method 655 "data TEXT," + 656 "aux_data TEXT," + 657 "type INTEGER NOT NULL," + // kind specific (home, work, etc) 658 "label TEXT," + 659 "isprimary INTEGER NOT NULL DEFAULT 0" + 660 ");"); 661 db.execSQL("CREATE INDEX contactMethodsPeopleIndex " 662 + "ON contact_methods (person);"); 663 664 // The table for recent calls is here so we can do table joins 665 // on people, phones, and calls all in one place. 666 db.execSQL("CREATE TABLE calls (" + 667 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 668 "number TEXT," + 669 "date INTEGER," + 670 "duration INTEGER," + 671 "type INTEGER," + 672 "new INTEGER," + 673 "name TEXT," + 674 "numbertype INTEGER," + 675 "numberlabel TEXT" + 676 ");"); 677 678 // Various settings for the contacts sync adapter. The _sync_account column may 679 // be null, but it must not be the empty string. 680 db.execSQL("CREATE TABLE settings (" + 681 "_id INTEGER PRIMARY KEY," + 682 "_sync_account TEXT," + 683 "key STRING NOT NULL," + 684 "value STRING " + 685 ");"); 686 687 // The table for the organizations of a person. 688 db.execSQL("CREATE TABLE organizations (" + 689 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 690 "company TEXT," + 691 "title TEXT," + 692 "isprimary INTEGER NOT NULL DEFAULT 0," + 693 "type INTEGER NOT NULL," + // kind specific (home, work, etc) 694 "label TEXT," + 695 "person INTEGER REFERENCES people(_id)" + 696 ");"); 697 db.execSQL("CREATE INDEX organizationsIndex1 ON organizations (person);"); 698 699 // The table for the extensions of a person. 700 db.execSQL("CREATE TABLE extensions (" + 701 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 702 "name TEXT NOT NULL," + 703 "value TEXT NOT NULL," + 704 "person INTEGER REFERENCES people(_id)," + 705 "UNIQUE(person, name)" + 706 ");"); 707 db.execSQL("CREATE INDEX extensionsIndex1 ON extensions (person, name);"); 708 709 // The table for the groups of a person. 710 db.execSQL("CREATE TABLE groupmembership (" + 711 "_id INTEGER PRIMARY KEY," + 712 "person INTEGER REFERENCES people(_id)," + 713 "group_id INTEGER REFERENCES groups(_id)," + 714 "group_sync_account STRING," + 715 "group_sync_id STRING" + 716 ");"); 717 db.execSQL("CREATE INDEX groupmembershipIndex1 ON groupmembership (person, group_id);"); 718 db.execSQL("CREATE INDEX groupmembershipIndex2 ON groupmembership (group_id, person);"); 719 db.execSQL("CREATE INDEX groupmembershipIndex3 ON groupmembership " 720 + "(group_sync_account, group_sync_id);"); 721 722 // Trigger to completely remove a contacts data when they're deleted 723 db.execSQL("CREATE TRIGGER contact_cleanup DELETE ON people " + 724 "BEGIN " + 725 "DELETE FROM peopleLookup WHERE source = old._id;" + 726 "DELETE FROM peopleLookupWithPhoneticName WHERE source = old._id;" + 727 "DELETE FROM phones WHERE person = old._id;" + 728 "DELETE FROM contact_methods WHERE person = old._id;" + 729 "DELETE FROM organizations WHERE person = old._id;" + 730 "DELETE FROM groupmembership WHERE person = old._id;" + 731 "DELETE FROM extensions WHERE person = old._id;" + 732 "END"); 733 734 // Trigger to disassociate the groupmembership from the groups when an 735 // groups entry is deleted 736 db.execSQL("CREATE TRIGGER groups_cleanup DELETE ON groups " + 737 "BEGIN " + 738 "UPDATE groupmembership SET group_id = null WHERE group_id = old._id;" + 739 "END"); 740 741 // Trigger to move an account_people row to _deleted_account_people when it is deleted 742 db.execSQL("CREATE TRIGGER groups_to_deleted DELETE ON groups " + 743 "WHEN old._sync_id is not null " + 744 "BEGIN " + 745 "INSERT INTO _deleted_groups " + 746 "(_sync_id, _sync_account, _sync_version) " + 747 "VALUES (old._sync_id, old._sync_account, " + 748 "old._sync_version);" + 749 "END"); 750 751 recreatePeopleLookupTable(db); 752 recreatePeopleLookupWithPhoneticNameTable(db); 753 754 // Triggers to set the _sync_dirty flag when a phone is changed, 755 // inserted or deleted 756 db.execSQL("CREATE TRIGGER phones_update UPDATE ON phones " + 757 "BEGIN " + 758 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" + 759 "END"); 760 db.execSQL("CREATE TRIGGER phones_insert INSERT ON phones " + 761 "BEGIN " + 762 "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person;" + 763 "END"); 764 db.execSQL("CREATE TRIGGER phones_delete DELETE ON phones " + 765 "BEGIN " + 766 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" + 767 "END"); 768 769 // Triggers to set the _sync_dirty flag when a contact_method is 770 // changed, inserted or deleted 771 db.execSQL("CREATE TRIGGER contact_methods_update UPDATE ON contact_methods " + 772 "BEGIN " + 773 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" + 774 "END"); 775 db.execSQL("CREATE TRIGGER contact_methods_insert INSERT ON contact_methods " + 776 "BEGIN " + 777 "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person;" + 778 "END"); 779 db.execSQL("CREATE TRIGGER contact_methods_delete DELETE ON contact_methods " + 780 "BEGIN " + 781 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" + 782 "END"); 783 784 // Triggers for when an organization is changed, inserted or deleted 785 db.execSQL("CREATE TRIGGER organizations_update AFTER UPDATE ON organizations " + 786 "BEGIN " + 787 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " + 788 "END"); 789 db.execSQL("CREATE TRIGGER organizations_insert INSERT ON organizations " + 790 "BEGIN " + 791 "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " + 792 "END"); 793 db.execSQL("CREATE TRIGGER organizations_delete DELETE ON organizations " + 794 "BEGIN " + 795 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" + 796 "END"); 797 798 // Triggers for when an groupmembership is changed, inserted or deleted 799 db.execSQL("CREATE TRIGGER groupmembership_update AFTER UPDATE ON groupmembership " + 800 "BEGIN " + 801 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " + 802 "END"); 803 db.execSQL("CREATE TRIGGER groupmembership_insert INSERT ON groupmembership " + 804 "BEGIN " + 805 "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " + 806 "END"); 807 db.execSQL("CREATE TRIGGER groupmembership_delete DELETE ON groupmembership " + 808 "BEGIN " + 809 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" + 810 "END"); 811 812 // Triggers for when an extension is changed, inserted or deleted 813 db.execSQL("CREATE TRIGGER extensions_update AFTER UPDATE ON extensions " + 814 "BEGIN " + 815 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person; " + 816 "END"); 817 db.execSQL("CREATE TRIGGER extensions_insert INSERT ON extensions " + 818 "BEGIN " + 819 "UPDATE people SET _sync_dirty=1 WHERE people._id=new.person; " + 820 "END"); 821 db.execSQL("CREATE TRIGGER extensions_delete DELETE ON extensions " + 822 "BEGIN " + 823 "UPDATE people SET _sync_dirty=1 WHERE people._id=old.person;" + 824 "END"); 825 826 createTypeLabelTrigger(db, sPhonesTable, "INSERT"); 827 createTypeLabelTrigger(db, sPhonesTable, "UPDATE"); 828 createTypeLabelTrigger(db, sOrganizationsTable, "INSERT"); 829 createTypeLabelTrigger(db, sOrganizationsTable, "UPDATE"); 830 createTypeLabelTrigger(db, sContactMethodsTable, "INSERT"); 831 createTypeLabelTrigger(db, sContactMethodsTable, "UPDATE"); 832 833 // Temporary table that holds a time stamp of the last time data the voice 834 // dialer is interested in has changed so the grammar won't need to be 835 // recompiled when unused data is changed. 836 db.execSQL("CREATE TABLE voice_dialer_timestamp (" + 837 "_id INTEGER PRIMARY KEY," + 838 "timestamp INTEGER" + 839 ");"); 840 db.execSQL("INSERT INTO voice_dialer_timestamp (_id, timestamp) VALUES " + 841 "(1, strftime('%s', 'now'));"); 842 db.execSQL("CREATE TRIGGER timestamp_trigger1 AFTER UPDATE ON phones " + 843 "BEGIN " + 844 "UPDATE voice_dialer_timestamp SET timestamp=strftime('%s', 'now') "+ 845 "WHERE _id=1;" + 846 "END"); 847 db.execSQL("CREATE TRIGGER timestamp_trigger2 AFTER UPDATE OF name ON people " + 848 "BEGIN " + 849 "UPDATE voice_dialer_timestamp SET timestamp=strftime('%s', 'now') " + 850 "WHERE _id=1;" + 851 "END"); 852 } 853 854 private void createTypeLabelTrigger(SQLiteDatabase db, String table, String operation) { 855 final String name = table + "_" + operation + "_typeAndLabel"; 856 db.execSQL("CREATE TRIGGER " + name + " AFTER " + operation + " ON " + table 857 + " WHEN (NEW.type != 0 AND NEW.label IS NOT NULL) OR " 858 + " (NEW.type = 0 AND NEW.label IS NULL)" 859 + " BEGIN " 860 + " SELECT RAISE (ABORT, 'exactly one of type or label must be set'); " 861 + " END"); 862 } 863 864 private void maybeCreatePresenceTable(SQLiteDatabase db) { 865 // Load the presence table from the presence_db. Just create the table 866 // if we are 867 String cpDbName; 868 if (!isTemporary()) { 869 db.execSQL("ATTACH DATABASE ':memory:' AS presence_db;"); 870 cpDbName = "presence_db."; 871 } else { 872 cpDbName = ""; 873 } 874 db.execSQL("CREATE TABLE IF NOT EXISTS " + cpDbName + "presence ("+ 875 Presence._ID + " INTEGER PRIMARY KEY," + 876 Presence.PERSON_ID + " INTEGER REFERENCES people(_id)," + 877 Presence.IM_PROTOCOL + " TEXT," + 878 Presence.IM_HANDLE + " TEXT," + 879 Presence.IM_ACCOUNT + " TEXT," + 880 Presence.PRESENCE_STATUS + " INTEGER," + 881 Presence.PRESENCE_CUSTOM_STATUS + " TEXT," + 882 "UNIQUE(" + Presence.IM_PROTOCOL + ", " + Presence.IM_HANDLE + ", " 883 + Presence.IM_ACCOUNT + ")" + 884 ");"); 885 886 db.execSQL("CREATE INDEX IF NOT EXISTS " + cpDbName + "presenceIndex ON presence (" 887 + Presence.PERSON_ID + ");"); 888 } 889 890 private String buildPeopleLookupWhereClauseCommon(String filterParam, String tableName) { 891 StringBuilder filter = new StringBuilder("people._id IN (SELECT source FROM "); 892 filter.append(tableName); 893 filter.append(" WHERE token GLOB "); 894 // NOTE: Query parameters won't work here since the SQL compiler 895 // needs to parse the actual string to know that it can use the 896 // index to do a prefix scan. 897 DatabaseUtils.appendEscapedSQLString(filter, 898 DatabaseUtils.getHexCollationKey(filterParam) + "*"); 899 filter.append(')'); 900 return filter.toString(); 901 } 902 903 private String buildPeopleLookupWhereClause(String filterParam) { 904 return buildPeopleLookupWhereClauseCommon(filterParam, "peopleLookup"); 905 } 906 907 private String buildPeopleLookupWhereClauseForSuggestion(String filterParam) { 908 return buildPeopleLookupWhereClauseCommon(filterParam, 909 usePhoneticNameForPeopleLookup() 910 ? "peopleLookupWithPhoneticName" 911 : "peopleLookup"); 912 } 913 914 @Override 915 public Cursor queryInternal(Uri url, String[] projectionIn, 916 String selection, String[] selectionArgs, String sort) { 917 918 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 919 Uri notificationUri = Contacts.CONTENT_URI; 920 String limit = getLimit(url); 921 StringBuilder whereClause; 922 String groupBy = null; 923 924 // Generate the body of the query 925 int match = sURIMatcher.match(url); 926 927 if (Config.LOGV) Log.v(TAG, "ContactsProvider.query: url=" + url + ", match is " + match); 928 929 switch (match) { 930 case DELETED_GROUPS: 931 if (!isTemporary()) { 932 throw new UnsupportedOperationException(); 933 } 934 935 qb.setTables(sDeletedGroupsTable); 936 break; 937 938 case GROUPS_ID: 939 qb.appendWhere("_id="); 940 qb.appendWhere(url.getPathSegments().get(1)); 941 // fall through 942 case GROUPS: 943 qb.setTables(sGroupsTable); 944 qb.setProjectionMap(sGroupsProjectionMap); 945 break; 946 947 case SETTINGS: 948 qb.setTables(sSettingsTable); 949 break; 950 951 case PEOPLE_GROUPMEMBERSHIP_ID: 952 qb.appendWhere("groupmembership._id="); 953 qb.appendWhere(url.getPathSegments().get(3)); 954 qb.appendWhere(" AND "); 955 // fall through 956 case PEOPLE_GROUPMEMBERSHIP: 957 qb.appendWhere(sGroupsJoinString + " AND "); 958 qb.appendWhere("person=" + url.getPathSegments().get(1)); 959 qb.setTables("groups, groupmembership"); 960 qb.setProjectionMap(sGroupMembershipProjectionMap); 961 break; 962 963 case GROUPMEMBERSHIP_ID: 964 qb.appendWhere("groupmembership._id="); 965 qb.appendWhere(url.getPathSegments().get(1)); 966 qb.appendWhere(" AND "); 967 // fall through 968 case GROUPMEMBERSHIP: 969 qb.setTables("groups, groupmembership"); 970 qb.setProjectionMap(sGroupMembershipProjectionMap); 971 qb.appendWhere(sGroupsJoinString); 972 break; 973 974 case GROUPMEMBERSHIP_RAW: 975 qb.setTables("groupmembership"); 976 break; 977 978 case GROUP_NAME_MEMBERS_FILTER: 979 if (url.getPathSegments().size() > 5) { 980 qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment())); 981 qb.appendWhere(" AND "); 982 } 983 // fall through 984 case GROUP_NAME_MEMBERS: 985 qb.setTables(PEOPLE_PHONES_JOIN); 986 qb.setProjectionMap(sPeopleProjectionMap); 987 qb.appendWhere(buildGroupNameMatchWhereClause(url.getPathSegments().get(2))); 988 break; 989 990 case GROUP_SYSTEM_ID_MEMBERS_FILTER: 991 if (url.getPathSegments().size() > 5) { 992 qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment())); 993 qb.appendWhere(" AND "); 994 } 995 // fall through 996 case GROUP_SYSTEM_ID_MEMBERS: 997 qb.setTables(PEOPLE_PHONES_JOIN); 998 qb.setProjectionMap(sPeopleProjectionMap); 999 qb.appendWhere(buildGroupSystemIdMatchWhereClause(url.getPathSegments().get(2))); 1000 break; 1001 1002 case PEOPLE: 1003 qb.setTables(PEOPLE_PHONES_JOIN); 1004 qb.setProjectionMap(sPeopleProjectionMap); 1005 break; 1006 case PEOPLE_RAW: 1007 qb.setTables(sPeopleTable); 1008 break; 1009 1010 case PEOPLE_OWNER: 1011 return queryOwner(projectionIn); 1012 1013 case PEOPLE_WITH_PHONES_FILTER: 1014 1015 qb.appendWhere("number IS NOT NULL AND "); 1016 1017 // Fall through. 1018 1019 case PEOPLE_FILTER: { 1020 qb.setTables(PEOPLE_PHONES_JOIN); 1021 qb.setProjectionMap(sPeopleProjectionMap); 1022 if (url.getPathSegments().size() > 2) { 1023 qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment())); 1024 } 1025 break; 1026 } 1027 1028 case PEOPLE_WITH_EMAIL_OR_IM_FILTER: 1029 String email = url.getPathSegments().get(2); 1030 whereClause = new StringBuilder(); 1031 1032 // Match any E-mail or IM contact methods where data exactly 1033 // matches the provided string. 1034 whereClause.append(ContactMethods.DATA); 1035 whereClause.append("="); 1036 DatabaseUtils.appendEscapedSQLString(whereClause, email); 1037 whereClause.append(" AND (kind = " + Contacts.KIND_EMAIL + 1038 " OR kind = " + Contacts.KIND_IM + ")"); 1039 qb.appendWhere(whereClause.toString()); 1040 1041 qb.setTables("people INNER JOIN contact_methods on (people._id = contact_methods.person)"); 1042 qb.setProjectionMap(sPeopleWithEmailOrImProjectionMap); 1043 1044 // Prevent returning the same person for multiple matches 1045 groupBy = "contact_methods.person"; 1046 1047 qb.setDistinct(true); 1048 break; 1049 1050 case PHOTOS_ID: 1051 qb.appendWhere("_id="+url.getPathSegments().get(1)); 1052 // Fall through. 1053 case PHOTOS: 1054 qb.setTables(sPhotosTable); 1055 qb.setProjectionMap(sPhotosProjectionMap); 1056 break; 1057 1058 case PEOPLE_PHOTO: 1059 qb.appendWhere("person="+url.getPathSegments().get(1)); 1060 qb.setTables(sPhotosTable); 1061 qb.setProjectionMap(sPhotosProjectionMap); 1062 break; 1063 1064 case SEARCH_SUGGESTIONS: { 1065 // Force the default sort order, since the SearchManage doesn't ask for things 1066 // sorted, though they should be 1067 if (sort != null && !People.DEFAULT_SORT_ORDER.equals(sort)) { 1068 throw new IllegalArgumentException("Sort ordering not allowed for this URI"); 1069 } 1070 sort = SearchManager.SUGGEST_COLUMN_TEXT_1 + " COLLATE LOCALIZED ASC"; 1071 1072 // This will either setup the query builder so we can run the proper query below 1073 // and return null, or it will return a cursor with the results already in it. 1074 Cursor c = handleSearchSuggestionsQuery(url, qb); 1075 if (c != null) { 1076 return c; 1077 } 1078 break; 1079 } 1080 case SEARCH_SHORTCUT: { 1081 qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN); 1082 qb.setProjectionMap(getCurrentSearchSuggestionsProjectionMap()); 1083 qb.appendWhere(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID + "="); 1084 qb.appendWhere(url.getPathSegments().get(1)); 1085 break; 1086 } 1087 case PEOPLE_STREQUENT: { 1088 // Build the first query for starred 1089 qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN); 1090 qb.setProjectionMap(sStrequentStarredProjectionMap); 1091 final String starredQuery = qb.buildQuery(projectionIn, "starred = 1", 1092 null, null, null, null, 1093 null /* limit */); 1094 1095 // Build the second query for frequent 1096 qb = new SQLiteQueryBuilder(); 1097 qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN); 1098 qb.setProjectionMap(sPeopleWithPhotoProjectionMap); 1099 final String frequentQuery = qb.buildQuery(projectionIn, 1100 "times_contacted > 0 AND starred = 0", null, null, null, null, null); 1101 1102 // Put them together 1103 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 1104 STREQUENT_ORDER_BY, STREQUENT_LIMIT); 1105 final SQLiteDatabase db = getDatabase(); 1106 Cursor c = db.rawQueryWithFactory(null, query, null, sPeopleTable); 1107 if ((c != null) && !isTemporary()) { 1108 c.setNotificationUri(getContext().getContentResolver(), notificationUri); 1109 } 1110 return c; 1111 } 1112 case PEOPLE_STREQUENT_FILTER: { 1113 // Build the first query for starred 1114 qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN); 1115 qb.setProjectionMap(sStrequentStarredProjectionMap); 1116 if (url.getPathSegments().size() > 3) { 1117 qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment())); 1118 } 1119 final String starredQuery = qb.buildQuery(projectionIn, "starred = 1", 1120 null, null, null, null, 1121 null /* limit */); 1122 1123 // Build the second query for frequent 1124 qb = new SQLiteQueryBuilder(); 1125 qb.setTables(PEOPLE_PHONES_PHOTOS_JOIN); 1126 qb.setProjectionMap(sPeopleWithPhotoProjectionMap); 1127 if (url.getPathSegments().size() > 3) { 1128 qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment())); 1129 } 1130 final String frequentQuery = qb.buildQuery(projectionIn, 1131 "times_contacted > 0 AND starred = 0", null, null, null, null, null); 1132 1133 // Put them together 1134 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, 1135 STREQUENT_ORDER_BY, null); 1136 final SQLiteDatabase db = getDatabase(); 1137 Cursor c = db.rawQueryWithFactory(null, query, null, sPeopleTable); 1138 if ((c != null) && !isTemporary()) { 1139 c.setNotificationUri(getContext().getContentResolver(), notificationUri); 1140 } 1141 return c; 1142 } 1143 case DELETED_PEOPLE: 1144 if (isTemporary()) { 1145 qb.setTables("_deleted_people"); 1146 break; 1147 } 1148 throw new UnsupportedOperationException(); 1149 case PEOPLE_ID: 1150 qb.setTables("people LEFT OUTER JOIN phones ON people.primary_phone=phones._id " 1151 + "LEFT OUTER JOIN presence ON (presence." + Presence.PERSON_ID 1152 + "=people._id)"); 1153 qb.setProjectionMap(sPeopleProjectionMap); 1154 qb.appendWhere("people._id="); 1155 qb.appendWhere(url.getPathSegments().get(1)); 1156 break; 1157 case PEOPLE_PHONES: 1158 qb.setTables("phones, people"); 1159 qb.setProjectionMap(sPhonesProjectionMap); 1160 qb.appendWhere("people._id = phones.person AND person="); 1161 qb.appendWhere(url.getPathSegments().get(1)); 1162 break; 1163 case PEOPLE_PHONES_ID: 1164 qb.setTables("phones, people"); 1165 qb.setProjectionMap(sPhonesProjectionMap); 1166 qb.appendWhere("people._id = phones.person AND person="); 1167 qb.appendWhere(url.getPathSegments().get(1)); 1168 qb.appendWhere(" AND phones._id="); 1169 qb.appendWhere(url.getPathSegments().get(3)); 1170 break; 1171 1172 case PEOPLE_PHONES_WITH_PRESENCE: 1173 qb.appendWhere("people._id=?"); 1174 selectionArgs = appendSelectionArg(selectionArgs, url.getPathSegments().get(1)); 1175 // Fall through. 1176 1177 case PHONES_WITH_PRESENCE: 1178 qb.setTables("phones JOIN people ON (phones.person = people._id)" 1179 + " LEFT OUTER JOIN presence ON (presence.person = people._id)"); 1180 qb.setProjectionMap(sPhonesWithPresenceProjectionMap); 1181 break; 1182 1183 case PEOPLE_CONTACTMETHODS: 1184 qb.setTables("contact_methods, people"); 1185 qb.setProjectionMap(sContactMethodsProjectionMap); 1186 qb.appendWhere("people._id = contact_methods.person AND person="); 1187 qb.appendWhere(url.getPathSegments().get(1)); 1188 break; 1189 case PEOPLE_CONTACTMETHODS_ID: 1190 qb.setTables("contact_methods, people"); 1191 qb.setProjectionMap(sContactMethodsProjectionMap); 1192 qb.appendWhere("people._id = contact_methods.person AND person="); 1193 qb.appendWhere(url.getPathSegments().get(1)); 1194 qb.appendWhere(" AND contact_methods._id="); 1195 qb.appendWhere(url.getPathSegments().get(3)); 1196 break; 1197 case PEOPLE_ORGANIZATIONS: 1198 qb.setTables("organizations, people"); 1199 qb.setProjectionMap(sOrganizationsProjectionMap); 1200 qb.appendWhere("people._id = organizations.person AND person="); 1201 qb.appendWhere(url.getPathSegments().get(1)); 1202 break; 1203 case PEOPLE_ORGANIZATIONS_ID: 1204 qb.setTables("organizations, people"); 1205 qb.setProjectionMap(sOrganizationsProjectionMap); 1206 qb.appendWhere("people._id = organizations.person AND person="); 1207 qb.appendWhere(url.getPathSegments().get(1)); 1208 qb.appendWhere(" AND organizations._id="); 1209 qb.appendWhere(url.getPathSegments().get(3)); 1210 break; 1211 case PHONES: 1212 qb.setTables("phones, people"); 1213 qb.appendWhere("people._id = phones.person"); 1214 qb.setProjectionMap(sPhonesProjectionMap); 1215 break; 1216 case PHONES_ID: 1217 qb.setTables("phones, people"); 1218 qb.appendWhere("people._id = phones.person AND phones._id=" 1219 + url.getPathSegments().get(1)); 1220 qb.setProjectionMap(sPhonesProjectionMap); 1221 break; 1222 case ORGANIZATIONS: 1223 qb.setTables("organizations, people"); 1224 qb.appendWhere("people._id = organizations.person"); 1225 qb.setProjectionMap(sOrganizationsProjectionMap); 1226 break; 1227 case ORGANIZATIONS_ID: 1228 qb.setTables("organizations, people"); 1229 qb.appendWhere("people._id = organizations.person AND organizations._id=" 1230 + url.getPathSegments().get(1)); 1231 qb.setProjectionMap(sOrganizationsProjectionMap); 1232 break; 1233 case PHONES_MOBILE_FILTER_NAME: 1234 qb.appendWhere("type=" + Contacts.PhonesColumns.TYPE_MOBILE + " AND "); 1235 1236 // Fall through. 1237 1238 case PHONES_FILTER_NAME: 1239 qb.setTables("phones JOIN people ON (people._id = phones.person)"); 1240 qb.setProjectionMap(sPhonesProjectionMap); 1241 if (url.getPathSegments().size() > 2) { 1242 qb.appendWhere(buildPeopleLookupWhereClause(url.getLastPathSegment())); 1243 } 1244 break; 1245 1246 case PHONES_FILTER: { 1247 String phoneNumber = url.getPathSegments().get(2); 1248 String indexable = PhoneNumberUtils.toCallerIDMinMatch(phoneNumber); 1249 StringBuilder subQuery = new StringBuilder(); 1250 if (TextUtils.isEmpty(sort)) { 1251 // Default the sort order to something reasonable so we get consistent 1252 // results when callers don't request an ordering 1253 sort = People.DEFAULT_SORT_ORDER; 1254 } 1255 1256 subQuery.append("people, (SELECT * FROM phones WHERE (phones.number_key GLOB '"); 1257 subQuery.append(indexable); 1258 subQuery.append("*')) AS phones"); 1259 qb.setTables(subQuery.toString()); 1260 qb.appendWhere("phones.person=people._id AND PHONE_NUMBERS_EQUAL(phones.number, "); 1261 qb.appendWhereEscapeString(phoneNumber); 1262 qb.appendWhere(")"); 1263 qb.setProjectionMap(sPhonesProjectionMap); 1264 break; 1265 } 1266 case CONTACTMETHODS: 1267 qb.setTables("contact_methods, people"); 1268 qb.setProjectionMap(sContactMethodsProjectionMap); 1269 qb.appendWhere("people._id = contact_methods.person"); 1270 break; 1271 case CONTACTMETHODS_ID: 1272 qb.setTables("contact_methods LEFT OUTER JOIN people ON contact_methods.person = people._id"); 1273 qb.setProjectionMap(sContactMethodsProjectionMap); 1274 qb.appendWhere("contact_methods._id="); 1275 qb.appendWhere(url.getPathSegments().get(1)); 1276 break; 1277 case CONTACTMETHODS_EMAIL_FILTER: 1278 String pattern = url.getPathSegments().get(2); 1279 whereClause = new StringBuilder(); 1280 1281 // TODO This is going to be REALLY slow. Come up with 1282 // something faster. 1283 whereClause.append(ContactMethods.KIND); 1284 whereClause.append('='); 1285 whereClause.append('\''); 1286 whereClause.append(Contacts.KIND_EMAIL); 1287 whereClause.append("' AND (UPPER("); 1288 whereClause.append(ContactMethods.NAME); 1289 whereClause.append(") GLOB "); 1290 DatabaseUtils.appendEscapedSQLString(whereClause, pattern + "*"); 1291 whereClause.append(" OR UPPER("); 1292 whereClause.append(ContactMethods.NAME); 1293 whereClause.append(") GLOB "); 1294 DatabaseUtils.appendEscapedSQLString(whereClause, "* " + pattern + "*"); 1295 whereClause.append(") AND "); 1296 qb.appendWhere(whereClause.toString()); 1297 1298 // Fall through. 1299 1300 case CONTACTMETHODS_EMAIL: 1301 qb.setTables("contact_methods INNER JOIN people on (contact_methods.person = people._id)"); 1302 qb.setProjectionMap(sEmailSearchProjectionMap); 1303 qb.appendWhere("kind = " + Contacts.KIND_EMAIL); 1304 qb.setDistinct(true); 1305 break; 1306 1307 case PEOPLE_CONTACTMETHODS_WITH_PRESENCE: 1308 qb.appendWhere("people._id=?"); 1309 selectionArgs = appendSelectionArg(selectionArgs, url.getPathSegments().get(1)); 1310 // Fall through. 1311 1312 case CONTACTMETHODS_WITH_PRESENCE: 1313 qb.setTables("contact_methods JOIN people ON (contact_methods.person = people._id)" 1314 + " LEFT OUTER JOIN presence ON " 1315 // Match gtalk presence items 1316 + "((kind=" + Contacts.KIND_EMAIL + 1317 " AND im_protocol='" 1318 + ContactMethods.encodePredefinedImProtocol( 1319 ContactMethods.PROTOCOL_GOOGLE_TALK) 1320 + "' AND data=im_handle)" 1321 + " OR " 1322 // Match IM presence items 1323 + "(kind=" + Contacts.KIND_IM 1324 + " AND data=im_handle AND aux_data=im_protocol))"); 1325 qb.setProjectionMap(sContactMethodsWithPresenceProjectionMap); 1326 break; 1327 1328 case CALLS: 1329 qb.setTables("calls"); 1330 qb.setProjectionMap(sCallsProjectionMap); 1331 notificationUri = CallLog.CONTENT_URI; 1332 break; 1333 case CALLS_ID: 1334 qb.setTables("calls"); 1335 qb.setProjectionMap(sCallsProjectionMap); 1336 qb.appendWhere("calls._id="); 1337 qb.appendWhere(url.getPathSegments().get(1)); 1338 notificationUri = CallLog.CONTENT_URI; 1339 break; 1340 case CALLS_FILTER: { 1341 qb.setTables("calls"); 1342 qb.setProjectionMap(sCallsProjectionMap); 1343 1344 String phoneNumber = url.getPathSegments().get(2); 1345 qb.appendWhere("PHONE_NUMBERS_EQUAL(number, "); 1346 qb.appendWhereEscapeString(phoneNumber); 1347 qb.appendWhere(")"); 1348 notificationUri = CallLog.CONTENT_URI; 1349 break; 1350 } 1351 1352 case PRESENCE: 1353 qb.setTables("presence LEFT OUTER JOIN people on (presence." + Presence.PERSON_ID 1354 + "= people._id)"); 1355 qb.setProjectionMap(sPresenceProjectionMap); 1356 break; 1357 case PRESENCE_ID: 1358 qb.setTables("presence LEFT OUTER JOIN people on (presence." + Presence.PERSON_ID 1359 + "= people._id)"); 1360 qb.appendWhere("presence._id="); 1361 qb.appendWhere(url.getLastPathSegment()); 1362 break; 1363 case VOICE_DIALER_TIMESTAMP: 1364 qb.setTables("voice_dialer_timestamp"); 1365 qb.appendWhere("_id=1"); 1366 break; 1367 1368 case PEOPLE_EXTENSIONS_ID: 1369 qb.appendWhere("extensions._id=" + url.getPathSegments().get(3) + " AND "); 1370 // fall through 1371 case PEOPLE_EXTENSIONS: 1372 qb.appendWhere("person=" + url.getPathSegments().get(1)); 1373 qb.setTables(sExtensionsTable); 1374 qb.setProjectionMap(sExtensionsProjectionMap); 1375 break; 1376 1377 case EXTENSIONS_ID: 1378 qb.appendWhere("extensions._id=" + url.getPathSegments().get(1)); 1379 // fall through 1380 case EXTENSIONS: 1381 qb.setTables(sExtensionsTable); 1382 qb.setProjectionMap(sExtensionsProjectionMap); 1383 break; 1384 1385 case LIVE_FOLDERS_PEOPLE: 1386 qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)"); 1387 qb.setProjectionMap(sLiveFoldersProjectionMap); 1388 break; 1389 1390 case LIVE_FOLDERS_PEOPLE_WITH_PHONES: 1391 qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)"); 1392 qb.setProjectionMap(sLiveFoldersProjectionMap); 1393 qb.appendWhere(People.PRIMARY_PHONE_ID + " IS NOT NULL"); 1394 break; 1395 1396 case LIVE_FOLDERS_PEOPLE_FAVORITES: 1397 qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)"); 1398 qb.setProjectionMap(sLiveFoldersProjectionMap); 1399 qb.appendWhere(People.STARRED + " <> 0"); 1400 break; 1401 1402 case LIVE_FOLDERS_PEOPLE_GROUP_NAME: 1403 qb.setTables("people LEFT OUTER JOIN photos ON (people._id = photos.person)"); 1404 qb.setProjectionMap(sLiveFoldersProjectionMap); 1405 qb.appendWhere(buildGroupNameMatchWhereClause(url.getLastPathSegment())); 1406 break; 1407 1408 default: 1409 throw new IllegalArgumentException("Unknown URL " + url); 1410 } 1411 1412 // run the query 1413 final SQLiteDatabase db = getDatabase(); 1414 Cursor c = qb.query(db, projectionIn, selection, selectionArgs, 1415 groupBy, null, sort, limit); 1416 if ((c != null) && !isTemporary()) { 1417 c.setNotificationUri(getContext().getContentResolver(), notificationUri); 1418 } 1419 return c; 1420 } 1421 1422 /** 1423 * Gets the value of the "limit" URI query parameter. 1424 * 1425 * @return A string containing a non-negative integer, or <code>null</code> if 1426 * the parameter is not set, or is set to an invalid value. 1427 */ 1428 private String getLimit(Uri url) { 1429 String limit = url.getQueryParameter("limit"); 1430 if (limit == null) { 1431 return null; 1432 } 1433 // make sure that the limit is a non-negative integer 1434 try { 1435 int l = Integer.parseInt(limit); 1436 if (l < 0) { 1437 Log.w(TAG, "Invalid limit parameter: " + limit); 1438 return null; 1439 } 1440 return String.valueOf(l); 1441 } catch (NumberFormatException ex) { 1442 Log.w(TAG, "Invalid limit parameter: " + limit); 1443 return null; 1444 } 1445 } 1446 1447 /** 1448 * Build a WHERE clause that restricts the query to match people that are a member of 1449 * a particular system group. The projection map of the query must include {@link People#_ID}. 1450 * 1451 * @param groupSystemId The system group id (e.g {@link Groups#GROUP_MY_CONTACTS}) 1452 * @return The where clause. 1453 */ 1454 private CharSequence buildGroupSystemIdMatchWhereClause(String groupSystemId) { 1455 return "people._id IN (SELECT person FROM groupmembership JOIN groups " + 1456 "ON (group_id=groups._id OR " + 1457 "(group_sync_id = groups._sync_id AND " + 1458 "group_sync_account = groups._sync_account)) "+ 1459 "WHERE " + Groups.SYSTEM_ID + "=" 1460 + DatabaseUtils.sqlEscapeString(groupSystemId) + ")"; 1461 } 1462 1463 /** 1464 * Build a WHERE clause that restricts the query to match people that are a member of 1465 * a group with a particular name. The projection map of the query must include 1466 * {@link People#_ID}. 1467 * 1468 * @param groupName The name of the group 1469 * @return The where clause. 1470 */ 1471 private CharSequence buildGroupNameMatchWhereClause(String groupName) { 1472 return "people._id IN (SELECT person FROM groupmembership JOIN groups " + 1473 "ON (group_id=groups._id OR " + 1474 "(group_sync_id = groups._sync_id AND " + 1475 "group_sync_account = groups._sync_account)) "+ 1476 "WHERE " + Groups.NAME + "=" 1477 + DatabaseUtils.sqlEscapeString(groupName) + ")"; 1478 } 1479 1480 private Cursor queryOwner(String[] projection) { 1481 // Check the permissions 1482 getContext().enforceCallingPermission("android.permission.READ_OWNER_DATA", 1483 "No permission to access owner info"); 1484 1485 // Read the owner id 1486 SharedPreferences prefs = getContext().getSharedPreferences(PREFS_NAME_OWNER, 1487 Context.MODE_PRIVATE); 1488 long ownerId = prefs.getLong(PREF_OWNER_ID, 0); 1489 1490 // Run the query 1491 return queryInternal(ContentUris.withAppendedId(People.CONTENT_URI, ownerId), projection, 1492 null, null, null); 1493 } 1494 1495 /** 1496 * Append a string to a selection args array 1497 * 1498 * @param selectionArgs the old arg 1499 * @param newArg the new arg to append 1500 * @return a new string array with all of the args 1501 */ 1502 private String[] appendSelectionArg(String[] selectionArgs, String newArg) { 1503 if (selectionArgs == null || selectionArgs.length == 0) { 1504 return new String[] { newArg }; 1505 } else { 1506 int length = selectionArgs.length; 1507 String[] newArgs = new String[length + 1]; 1508 System.arraycopy(selectionArgs, 0, newArgs, 0, length); 1509 newArgs[length] = newArg; 1510 return newArgs; 1511 } 1512 } 1513 1514 private HashMap<String, String> getCurrentSearchSuggestionsProjectionMap() { 1515 String currentLanguage = Locale.getDefault().getLanguage(); 1516 synchronized (this) { 1517 if (!currentLanguage.equals(mSearchSuggestionLanguage)) { 1518 mSearchSuggestionLanguage = currentLanguage; 1519 updateSuggestColumnTexts(); 1520 } 1521 } 1522 return mSearchSuggestionsProjectionMap; 1523 } 1524 1525 /** 1526 * Either sets up the query builder so we can run the proper query against the database 1527 * and returns null, or returns a cursor with the results already in it. 1528 * 1529 * @param url the URL passed for the suggestion 1530 * @param qb the query builder to use if a query needs to be run on the database 1531 * @return null with qb configured for a query, a cursor with the results already in it. 1532 */ 1533 private Cursor handleSearchSuggestionsQuery(Uri url, SQLiteQueryBuilder qb) { 1534 qb.setTables(PEOPLE_PHONES_PHOTOS_ORGANIZATIONS_JOIN); 1535 qb.setProjectionMap(getCurrentSearchSuggestionsProjectionMap()); 1536 if (url.getPathSegments().size() > 1) { 1537 // A search term was entered, use it to filter 1538 1539 // only match within 'my contacts' 1540 // TODO: match the 'display group' instead of hard coding 'my contacts' 1541 // once that information is factored out of the shared prefs of the contacts 1542 // app into this content provider. 1543 qb.appendWhere(buildGroupSystemIdMatchWhereClause(Groups.GROUP_MY_CONTACTS)); 1544 qb.appendWhere(" AND "); 1545 1546 // match the query 1547 final String searchClause = url.getLastPathSegment(); 1548 if (!TextUtils.isDigitsOnly(searchClause)) { 1549 qb.appendWhere(buildPeopleLookupWhereClauseForSuggestion(searchClause)); 1550 } else { 1551 final String[] columnNames = new String[] { 1552 "_id", 1553 SearchManager.SUGGEST_COLUMN_TEXT_1, 1554 SearchManager.SUGGEST_COLUMN_TEXT_2, 1555 SearchManager.SUGGEST_COLUMN_ICON_1, 1556 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 1557 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 1558 SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 1559 }; 1560 1561 Resources r = getContext().getResources(); 1562 String s; 1563 int i; 1564 1565 ArrayList<Object> dialNumber = new ArrayList<Object>(); 1566 dialNumber.add(0); // _id 1567 s = r.getString(com.android.internal.R.string.dial_number_using, searchClause); 1568 i = s.indexOf('\n'); 1569 if (i < 0) { 1570 dialNumber.add(s); 1571 dialNumber.add(""); 1572 } else { 1573 dialNumber.add(s.substring(0, i)); 1574 dialNumber.add(s.substring(i + 1)); 1575 } 1576 dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact)); 1577 dialNumber.add("tel:" + searchClause); 1578 dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED); 1579 dialNumber.add(null); 1580 1581 ArrayList<Object> createContact = new ArrayList<Object>(); 1582 createContact.add(1); // _id 1583 s = r.getString(com.android.internal.R.string.create_contact_using, searchClause); 1584 i = s.indexOf('\n'); 1585 if (i < 0) { 1586 createContact.add(s); 1587 createContact.add(""); 1588 } else { 1589 createContact.add(s.substring(0, i)); 1590 createContact.add(s.substring(i + 1)); 1591 } 1592 createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact)); 1593 createContact.add("tel:" + searchClause); 1594 createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED); 1595 createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT); 1596 1597 ArrayList<ArrayList> rows = new ArrayList<ArrayList>(); 1598 rows.add(dialNumber); 1599 rows.add(createContact); 1600 1601 ArrayListCursor cursor = new ArrayListCursor(columnNames, rows); 1602 return cursor; 1603 } 1604 } 1605 return null; 1606 } 1607 1608 @Override 1609 public String getType(Uri url) { 1610 int match = sURIMatcher.match(url); 1611 switch (match) { 1612 case EXTENSIONS: 1613 case PEOPLE_EXTENSIONS: 1614 return Extensions.CONTENT_TYPE; 1615 case EXTENSIONS_ID: 1616 case PEOPLE_EXTENSIONS_ID: 1617 return Extensions.CONTENT_ITEM_TYPE; 1618 case PEOPLE: 1619 return "vnd.android.cursor.dir/person"; 1620 case PEOPLE_ID: 1621 return "vnd.android.cursor.item/person"; 1622 case PEOPLE_PHONES: 1623 return "vnd.android.cursor.dir/phone"; 1624 case PEOPLE_PHONES_ID: 1625 return "vnd.android.cursor.item/phone"; 1626 case PEOPLE_CONTACTMETHODS: 1627 return "vnd.android.cursor.dir/contact-methods"; 1628 case PEOPLE_CONTACTMETHODS_ID: 1629 return getContactMethodType(url); 1630 case PHONES: 1631 return "vnd.android.cursor.dir/phone"; 1632 case PHONES_ID: 1633 return "vnd.android.cursor.item/phone"; 1634 case PHONES_FILTER: 1635 case PHONES_FILTER_NAME: 1636 case PHONES_MOBILE_FILTER_NAME: 1637 return "vnd.android.cursor.dir/phone"; 1638 case PHOTOS_ID: 1639 return "vnd.android.cursor.item/photo"; 1640 case PHOTOS: 1641 return "vnd.android.cursor.dir/photo"; 1642 case PEOPLE_PHOTO: 1643 return "vnd.android.cursor.item/photo"; 1644 case PEOPLE_PHOTO_DATA: 1645 return "image/png"; 1646 case CONTACTMETHODS: 1647 return "vnd.android.cursor.dir/contact-methods"; 1648 case CONTACTMETHODS_ID: 1649 return getContactMethodType(url); 1650 case CONTACTMETHODS_EMAIL: 1651 case CONTACTMETHODS_EMAIL_FILTER: 1652 return "vnd.android.cursor.dir/email"; 1653 case CALLS: 1654 return "vnd.android.cursor.dir/calls"; 1655 case CALLS_ID: 1656 return "vnd.android.cursor.item/calls"; 1657 case ORGANIZATIONS: 1658 return "vnd.android.cursor.dir/organizations"; 1659 case ORGANIZATIONS_ID: 1660 return "vnd.android.cursor.item/organization"; 1661 case CALLS_FILTER: 1662 return "vnd.android.cursor.dir/calls"; 1663 case SEARCH_SUGGESTIONS: 1664 return SearchManager.SUGGEST_MIME_TYPE; 1665 case SEARCH_SHORTCUT: 1666 return SearchManager.SHORTCUT_MIME_TYPE; 1667 default: 1668 throw new IllegalArgumentException("Unknown URL"); 1669 } 1670 } 1671 1672 private String getContactMethodType(Uri url) 1673 { 1674 String mime = null; 1675 1676 Cursor c = query(url, new String[] {ContactMethods.KIND}, null, null, null); 1677 if (c != null) { 1678 try { 1679 if (c.moveToFirst()) { 1680 int kind = c.getInt(0); 1681 switch (kind) { 1682 case Contacts.KIND_EMAIL: 1683 mime = "vnd.android.cursor.item/email"; 1684 break; 1685 1686 case Contacts.KIND_IM: 1687 mime = "vnd.android.cursor.item/jabber-im"; 1688 break; 1689 1690 case Contacts.KIND_POSTAL: 1691 mime = "vnd.android.cursor.item/postal-address"; 1692 break; 1693 } 1694 } 1695 } finally { 1696 c.close(); 1697 } 1698 } 1699 return mime; 1700 } 1701 1702 private ContentValues queryAndroidStarredGroupId(String account) { 1703 String whereString; 1704 String[] whereArgs; 1705 if (!TextUtils.isEmpty(account)) { 1706 whereString = "_sync_account=? AND name=?"; 1707 whereArgs = new String[]{account, Groups.GROUP_ANDROID_STARRED}; 1708 } else { 1709 whereString = "_sync_account is null AND name=?"; 1710 whereArgs = new String[]{Groups.GROUP_ANDROID_STARRED}; 1711 } 1712 Cursor cursor = getDatabase().query(sGroupsTable, 1713 new String[]{Groups._ID, Groups._SYNC_ID, Groups._SYNC_ACCOUNT}, 1714 whereString, whereArgs, null, null, null); 1715 try { 1716 if (cursor.moveToNext()) { 1717 ContentValues result = new ContentValues(); 1718 result.put(Groups._ID, cursor.getLong(0)); 1719 result.put(Groups._SYNC_ID, cursor.getString(1)); 1720 result.put(Groups._SYNC_ACCOUNT, cursor.getString(2)); 1721 return result; 1722 } 1723 return null; 1724 } finally { 1725 cursor.close(); 1726 } 1727 } 1728 1729 @Override 1730 public Uri insertInternal(Uri url, ContentValues initialValues) { 1731 Uri resultUri = null; 1732 long rowID; 1733 1734 final SQLiteDatabase db = getDatabase(); 1735 int match = sURIMatcher.match(url); 1736 switch (match) { 1737 case PEOPLE_GROUPMEMBERSHIP: 1738 case GROUPMEMBERSHIP: { 1739 mValues.clear(); 1740 mValues.putAll(initialValues); 1741 if (match == PEOPLE_GROUPMEMBERSHIP) { 1742 mValues.put(GroupMembership.PERSON_ID, 1743 Long.valueOf(url.getPathSegments().get(1))); 1744 } 1745 resultUri = insertIntoGroupmembership(mValues); 1746 } 1747 break; 1748 1749 case PEOPLE_OWNER: 1750 return insertOwner(initialValues); 1751 1752 case PEOPLE_EXTENSIONS: 1753 case EXTENSIONS: { 1754 ContentValues newMap = new ContentValues(initialValues); 1755 if (match == PEOPLE_EXTENSIONS) { 1756 newMap.put(Extensions.PERSON_ID, 1757 Long.valueOf(url.getPathSegments().get(1))); 1758 } 1759 rowID = mExtensionsInserter.insert(newMap); 1760 if (rowID > 0) { 1761 resultUri = ContentUris.withAppendedId(Extensions.CONTENT_URI, rowID); 1762 } 1763 } 1764 break; 1765 1766 case PHOTOS: { 1767 if (!isTemporary()) { 1768 throw new UnsupportedOperationException(); 1769 } 1770 rowID = mPhotosInserter.insert(initialValues); 1771 if (rowID > 0) { 1772 resultUri = ContentUris.withAppendedId(Photos.CONTENT_URI, rowID); 1773 } 1774 } 1775 break; 1776 1777 case GROUPS: { 1778 ContentValues newMap = new ContentValues(initialValues); 1779 ensureSyncAccountIsSet(newMap); 1780 newMap.put(Groups._SYNC_DIRTY, 1); 1781 // Insert into the groups table 1782 rowID = mGroupsInserter.insert(newMap); 1783 if (rowID > 0) { 1784 resultUri = ContentUris.withAppendedId(Groups.CONTENT_URI, rowID); 1785 if (!isTemporary() && newMap.containsKey(Groups.SHOULD_SYNC)) { 1786 final String account = newMap.getAsString(Groups._SYNC_ACCOUNT); 1787 if (!TextUtils.isEmpty(account)) { 1788 final ContentResolver cr = getContext().getContentResolver(); 1789 onLocalChangesForAccount(cr, account, false); 1790 } 1791 } 1792 } 1793 } 1794 break; 1795 1796 case PEOPLE_RAW: 1797 case PEOPLE: { 1798 mValues.clear(); 1799 mValues.putAll(initialValues); 1800 ensureSyncAccountIsSet(mValues); 1801 mValues.put(People._SYNC_DIRTY, 1); 1802 // Insert into the people table 1803 rowID = mPeopleInserter.insert(mValues); 1804 if (rowID > 0) { 1805 resultUri = ContentUris.withAppendedId(People.CONTENT_URI, rowID); 1806 if (!isTemporary()) { 1807 String account = mValues.getAsString(People._SYNC_ACCOUNT); 1808 Long starredValue = mValues.getAsLong(People.STARRED); 1809 final String syncId = mValues.getAsString(People._SYNC_ID); 1810 boolean isStarred = starredValue != null && starredValue != 0; 1811 fixupGroupMembershipAfterPeopleUpdate(account, rowID, isStarred); 1812 // create a photo row for this person 1813 mDb.delete(sPhotosTable, "person=" + rowID, null); 1814 mValues.clear(); 1815 mValues.put(Photos.PERSON_ID, rowID); 1816 mValues.put(Photos._SYNC_ACCOUNT, account); 1817 mValues.put(Photos._SYNC_ID, syncId); 1818 mValues.put(Photos._SYNC_DIRTY, 0); 1819 mPhotosInserter.insert(mValues); 1820 } 1821 } 1822 } 1823 break; 1824 1825 case DELETED_PEOPLE: { 1826 if (isTemporary()) { 1827 // Insert into the people table 1828 rowID = db.insert("_deleted_people", "_sync_id", initialValues); 1829 if (rowID > 0) { 1830 resultUri = Uri.parse("content://contacts/_deleted_people/" + rowID); 1831 } 1832 } else { 1833 throw new UnsupportedOperationException(); 1834 } 1835 } 1836 break; 1837 1838 case DELETED_GROUPS: { 1839 if (isTemporary()) { 1840 rowID = db.insert(sDeletedGroupsTable, Groups._SYNC_ID, 1841 initialValues); 1842 if (rowID > 0) { 1843 resultUri =ContentUris.withAppendedId( 1844 Groups.DELETED_CONTENT_URI, rowID); 1845 } 1846 } else { 1847 throw new UnsupportedOperationException(); 1848 } 1849 } 1850 break; 1851 1852 case PEOPLE_PHONES: 1853 case PHONES: { 1854 mValues.clear(); 1855 mValues.putAll(initialValues); 1856 if (match == PEOPLE_PHONES) { 1857 mValues.put(Contacts.Phones.PERSON_ID, 1858 Long.valueOf(url.getPathSegments().get(1))); 1859 } 1860 String number = mValues.getAsString(Contacts.Phones.NUMBER); 1861 if (number != null) { 1862 mValues.put("number_key", PhoneNumberUtils.getStrippedReversed(number)); 1863 } 1864 1865 rowID = insertAndFixupPrimary(Contacts.KIND_PHONE, mValues); 1866 resultUri = ContentUris.withAppendedId(Phones.CONTENT_URI, rowID); 1867 } 1868 break; 1869 1870 case CONTACTMETHODS: 1871 case PEOPLE_CONTACTMETHODS: { 1872 mValues.clear(); 1873 mValues.putAll(initialValues); 1874 if (match == PEOPLE_CONTACTMETHODS) { 1875 mValues.put("person", url.getPathSegments().get(1)); 1876 } 1877 Integer kind = mValues.getAsInteger(ContactMethods.KIND); 1878 if (kind == null) { 1879 throw new IllegalArgumentException("you must specify the ContactMethods.KIND"); 1880 } 1881 rowID = insertAndFixupPrimary(kind, mValues); 1882 if (rowID > 0) { 1883 resultUri = ContentUris.withAppendedId(ContactMethods.CONTENT_URI, rowID); 1884 } 1885 } 1886 break; 1887 1888 case CALLS: { 1889 rowID = mCallsInserter.insert(initialValues); 1890 if (rowID > 0) { 1891 resultUri = Uri.parse("content://call_log/calls/" + rowID); 1892 } 1893 } 1894 break; 1895 1896 case PRESENCE: { 1897 final String handle = initialValues.getAsString(Presence.IM_HANDLE); 1898 final String protocol = initialValues.getAsString(Presence.IM_PROTOCOL); 1899 if (TextUtils.isEmpty(handle) || TextUtils.isEmpty(protocol)) { 1900 throw new IllegalArgumentException("IM_PROTOCOL and IM_HANDLE are required"); 1901 } 1902 1903 // Look for the contact for this presence update 1904 StringBuilder query = new StringBuilder("SELECT "); 1905 query.append(ContactMethods.PERSON_ID); 1906 query.append(" FROM contact_methods WHERE (kind="); 1907 query.append(Contacts.KIND_IM); 1908 query.append(" AND "); 1909 query.append(ContactMethods.DATA); 1910 query.append("=? AND "); 1911 query.append(ContactMethods.AUX_DATA); 1912 query.append("=?)"); 1913 1914 String[] selectionArgs; 1915 if (GTALK_PROTOCOL_STRING.equals(protocol)) { 1916 // For gtalk accounts we usually don't have an explicit IM 1917 // entry, so also look for the email address as well 1918 query.append(" OR ("); 1919 query.append("kind="); 1920 query.append(Contacts.KIND_EMAIL); 1921 query.append(" AND "); 1922 query.append(ContactMethods.DATA); 1923 query.append("=?)"); 1924 selectionArgs = new String[] { handle, protocol, handle }; 1925 } else { 1926 selectionArgs = new String[] { handle, protocol }; 1927 } 1928 1929 Cursor c = db.rawQueryWithFactory(null, query.toString(), selectionArgs, null); 1930 1931 long personId = 0; 1932 try { 1933 if (c.moveToFirst()) { 1934 personId = c.getLong(0); 1935 } else { 1936 // No contact found, return a null URI 1937 return null; 1938 } 1939 } finally { 1940 c.close(); 1941 } 1942 1943 mValues.clear(); 1944 mValues.putAll(initialValues); 1945 mValues.put(Presence.PERSON_ID, personId); 1946 1947 // Insert the presence update 1948 rowID = db.replace("presence", null, mValues); 1949 if (rowID > 0) { 1950 resultUri = Uri.parse("content://contacts/presence/" + rowID); 1951 } 1952 } 1953 break; 1954 1955 case PEOPLE_ORGANIZATIONS: 1956 case ORGANIZATIONS: { 1957 ContentValues newMap = new ContentValues(initialValues); 1958 if (match == PEOPLE_ORGANIZATIONS) { 1959 newMap.put(Contacts.Phones.PERSON_ID, 1960 Long.valueOf(url.getPathSegments().get(1))); 1961 } 1962 rowID = insertAndFixupPrimary(Contacts.KIND_ORGANIZATION, newMap); 1963 if (rowID > 0) { 1964 resultUri = Uri.parse("content://contacts/organizations/" + rowID); 1965 } 1966 } 1967 break; 1968 default: 1969 throw new UnsupportedOperationException("Cannot insert into URL: " + url); 1970 } 1971 1972 return resultUri; 1973 } 1974 1975 @Override 1976 protected void onAccountsChanged(String[] accountsArray) { 1977 super.onAccountsChanged(accountsArray); 1978 synchronized (mAccountsLock) { 1979 mAccounts = new String[accountsArray.length]; 1980 System.arraycopy(accountsArray, 0, mAccounts, 0, mAccounts.length); 1981 } 1982 } 1983 1984 private void ensureSyncAccountIsSet(ContentValues values) { 1985 synchronized (mAccountsLock) { 1986 String account = values.getAsString(SyncConstValue._SYNC_ACCOUNT); 1987 if (account == null && mAccounts.length > 0) { 1988 values.put(SyncConstValue._SYNC_ACCOUNT, mAccounts[0]); 1989 } 1990 } 1991 } 1992 1993 private Uri insertOwner(ContentValues values) { 1994 // Check the permissions 1995 getContext().enforceCallingPermission("android.permission.WRITE_OWNER_DATA", 1996 "No permission to set owner info"); 1997 1998 // Insert the owner info 1999 Uri uri = insertInternal(People.CONTENT_URI, values); 2000 2001 // Record which person is the owner 2002 long id = ContentUris.parseId(uri); 2003 SharedPreferences.Editor prefs = getContext().getSharedPreferences(PREFS_NAME_OWNER, 2004 Context.MODE_PRIVATE).edit(); 2005 prefs.putLong(PREF_OWNER_ID, id); 2006 prefs.commit(); 2007 return uri; 2008 } 2009 2010 private Uri insertIntoGroupmembership(ContentValues values) { 2011 String groupSyncAccount = values.getAsString(GroupMembership.GROUP_SYNC_ACCOUNT); 2012 String groupSyncId = values.getAsString(GroupMembership.GROUP_SYNC_ID); 2013 final Long personId = values.getAsLong(GroupMembership.PERSON_ID); 2014 if (!values.containsKey(GroupMembership.GROUP_ID)) { 2015 if (TextUtils.isEmpty(groupSyncAccount) || TextUtils.isEmpty(groupSyncId)) { 2016 throw new IllegalArgumentException( 2017 "insertIntoGroupmembership: no GROUP_ID wasn't specified and non-empty " 2018 + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT fields weren't specifid, " 2019 + values); 2020 } 2021 if (0 != DatabaseUtils.longForQuery(getDatabase(), "" 2022 + "SELECT COUNT(*) " 2023 + "FROM groupmembership " 2024 + "WHERE group_sync_id=? AND person=?", 2025 new String[]{groupSyncId, String.valueOf(personId)})) { 2026 final String errorMessage = 2027 "insertIntoGroupmembership: a row with this server key already exists, " 2028 + values; 2029 if (Config.LOGD) Log.d(TAG, errorMessage); 2030 return null; 2031 } 2032 } else { 2033 long groupId = values.getAsLong(GroupMembership.GROUP_ID); 2034 if (!TextUtils.isEmpty(groupSyncAccount) || !TextUtils.isEmpty(groupSyncId)) { 2035 throw new IllegalArgumentException( 2036 "insertIntoGroupmembership: GROUP_ID was specified but " 2037 + "GROUP_SYNC_ID and GROUP_SYNC_ACCOUNT fields were also specifid, " 2038 + values); 2039 } 2040 if (0 != DatabaseUtils.longForQuery(getDatabase(), 2041 "SELECT COUNT(*) FROM groupmembership where group_id=? AND person=?", 2042 new String[]{String.valueOf(groupId), String.valueOf(personId)})) { 2043 final String errorMessage = 2044 "insertIntoGroupmembership: a row with this local key already exists, " 2045 + values; 2046 if (Config.LOGD) Log.d(TAG, errorMessage); 2047 return null; 2048 } 2049 } 2050 2051 long rowId = mGroupMembershipInserter.insert(values); 2052 if (rowId <= 0) { 2053 final String errorMessage = "insertIntoGroupmembership: the insert failed, values are " 2054 + values; 2055 if (Config.LOGD) Log.d(TAG, errorMessage); 2056 return null; 2057 } 2058 2059 // set the STARRED column in the people row if this group is the GROUP_ANDROID_STARRED 2060 if (!isTemporary() && queryGroupMembershipContainsStarred(personId)) { 2061 fixupPeopleStarred(personId, true); 2062 } 2063 2064 return ContentUris.withAppendedId(GroupMembership.CONTENT_URI, rowId); 2065 } 2066 2067 private void fixupGroupMembershipAfterPeopleUpdate(String account, long personId, 2068 boolean makeStarred) { 2069 ContentValues starredGroupInfo = queryAndroidStarredGroupId(account); 2070 if (makeStarred) { 2071 if (starredGroupInfo == null) { 2072 // we need to add the starred group 2073 mValuesLocal.clear(); 2074 mValuesLocal.put(Groups.NAME, Groups.GROUP_ANDROID_STARRED); 2075 mValuesLocal.put(Groups._SYNC_DIRTY, 1); 2076 mValuesLocal.put(Groups._SYNC_ACCOUNT, account); 2077 long groupId = mGroupsInserter.insert(mValuesLocal); 2078 starredGroupInfo = new ContentValues(); 2079 starredGroupInfo.put(Groups._ID, groupId); 2080 starredGroupInfo.put(Groups._SYNC_ACCOUNT, account); 2081 // don't put the _SYNC_ID in here since we don't know it yet 2082 } 2083 2084 final Long groupId = starredGroupInfo.getAsLong(Groups._ID); 2085 final String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID); 2086 final String syncAccount = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT); 2087 2088 // check that either groupId is set or the syncId/Account is set 2089 final boolean hasSyncId = !TextUtils.isEmpty(syncId); 2090 final boolean hasGroupId = groupId != null; 2091 if (!hasGroupId && !hasSyncId) { 2092 throw new IllegalStateException("at least one of the groupId or " 2093 + "the syncId must be set, " + starredGroupInfo); 2094 } 2095 2096 // now add this person to the group 2097 mValuesLocal.clear(); 2098 mValuesLocal.put(GroupMembership.PERSON_ID, personId); 2099 mValuesLocal.put(GroupMembership.GROUP_ID, groupId); 2100 mValuesLocal.put(GroupMembership.GROUP_SYNC_ID, syncId); 2101 mValuesLocal.put(GroupMembership.GROUP_SYNC_ACCOUNT, syncAccount); 2102 mGroupMembershipInserter.insert(mValuesLocal); 2103 } else { 2104 if (starredGroupInfo != null) { 2105 // delete the groupmembership rows for this person that match the starred group id 2106 String syncAccount = starredGroupInfo.getAsString(Groups._SYNC_ACCOUNT); 2107 String syncId = starredGroupInfo.getAsString(Groups._SYNC_ID); 2108 if (!TextUtils.isEmpty(syncId)) { 2109 mDb.delete(sGroupmembershipTable, 2110 "person=? AND group_sync_id=? AND group_sync_account=?", 2111 new String[]{String.valueOf(personId), syncId, syncAccount}); 2112 } else { 2113 mDb.delete(sGroupmembershipTable, "person=? AND group_id=?", 2114 new String[]{ 2115 Long.toString(personId), 2116 Long.toString(starredGroupInfo.getAsLong(Groups._ID))}); 2117 } 2118 } 2119 } 2120 } 2121 2122 private int fixupPeopleStarred(long personId, boolean inStarredGroup) { 2123 mValuesLocal.clear(); 2124 mValuesLocal.put(People.STARRED, inStarredGroup ? 1 : 0); 2125 return getDatabase().update(sPeopleTable, mValuesLocal, WHERE_ID, 2126 new String[]{String.valueOf(personId)}); 2127 } 2128 2129 private String kindToTable(int kind) { 2130 switch (kind) { 2131 case Contacts.KIND_EMAIL: return sContactMethodsTable; 2132 case Contacts.KIND_POSTAL: return sContactMethodsTable; 2133 case Contacts.KIND_IM: return sContactMethodsTable; 2134 case Contacts.KIND_PHONE: return sPhonesTable; 2135 case Contacts.KIND_ORGANIZATION: return sOrganizationsTable; 2136 default: throw new IllegalArgumentException("unknown kind, " + kind); 2137 } 2138 } 2139 2140 private DatabaseUtils.InsertHelper kindToInserter(int kind) { 2141 switch (kind) { 2142 case Contacts.KIND_EMAIL: return mContactMethodsInserter; 2143 case Contacts.KIND_POSTAL: return mContactMethodsInserter; 2144 case Contacts.KIND_IM: return mContactMethodsInserter; 2145 case Contacts.KIND_PHONE: return mPhonesInserter; 2146 case Contacts.KIND_ORGANIZATION: return mOrganizationsInserter; 2147 default: throw new IllegalArgumentException("unknown kind, " + kind); 2148 } 2149 } 2150 2151 private long insertAndFixupPrimary(int kind, ContentValues values) { 2152 final String table = kindToTable(kind); 2153 boolean isPrimary = false; 2154 Long personId = null; 2155 2156 if (!isTemporary()) { 2157 // when you add a item, if isPrimary or if there is no primary, 2158 // make this it, set the isPrimary flag, and clear other primary flags 2159 isPrimary = values.containsKey("isprimary") 2160 && (values.getAsInteger("isprimary") != 0); 2161 personId = values.getAsLong("person"); 2162 if (!isPrimary) { 2163 // make it primary anyway if this person doesn't have any rows of this type yet 2164 StringBuilder sb = new StringBuilder("person=" + personId); 2165 if (sContactMethodsTable.equals(table)) { 2166 sb.append(" AND kind="); 2167 sb.append(kind); 2168 } 2169 final boolean isFirstRowOfType = DatabaseUtils.longForQuery(getDatabase(), 2170 "SELECT count(*) FROM " + table + " where " + sb.toString(), null) == 0; 2171 isPrimary = isFirstRowOfType; 2172 } 2173 2174 values.put("isprimary", isPrimary ? 1 : 0); 2175 } 2176 2177 // do the actual insert 2178 long newRowId = kindToInserter(kind).insert(values); 2179 2180 if (newRowId <= 0) { 2181 throw new RuntimeException("error while inserting into " + table + ", " + values); 2182 } 2183 2184 if (!isTemporary()) { 2185 // If this row was made the primary then clear the other isprimary flags and update 2186 // corresponding people row, if necessary. 2187 if (isPrimary) { 2188 clearOtherIsPrimary(kind, personId, newRowId); 2189 if (kind == Contacts.KIND_PHONE) { 2190 updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newRowId); 2191 } else if (kind == Contacts.KIND_EMAIL) { 2192 updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newRowId); 2193 } else if (kind == Contacts.KIND_ORGANIZATION) { 2194 updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newRowId); 2195 } 2196 } 2197 } 2198 2199 return newRowId; 2200 } 2201 2202 @Override 2203 public int deleteInternal(Uri url, String userWhere, String[] whereArgs) { 2204 String tableToChange; 2205 String changedItemId; 2206 2207 final int matchedUriId = sURIMatcher.match(url); 2208 switch (matchedUriId) { 2209 case GROUPMEMBERSHIP_ID: 2210 return deleteFromGroupMembership(Long.parseLong(url.getPathSegments().get(1)), 2211 userWhere, whereArgs); 2212 case GROUPS: 2213 return deleteFromGroups(userWhere, whereArgs); 2214 case GROUPS_ID: 2215 changedItemId = url.getPathSegments().get(1); 2216 return deleteFromGroups(addIdToWhereClause(changedItemId, userWhere), whereArgs); 2217 case EXTENSIONS: 2218 tableToChange = sExtensionsTable; 2219 changedItemId = null; 2220 break; 2221 case EXTENSIONS_ID: 2222 tableToChange = sExtensionsTable; 2223 changedItemId = url.getPathSegments().get(1); 2224 break; 2225 case PEOPLE_RAW: 2226 case PEOPLE: 2227 return deleteFromPeople(null, userWhere, whereArgs); 2228 case PEOPLE_ID: 2229 return deleteFromPeople(url.getPathSegments().get(1), userWhere, whereArgs); 2230 case PEOPLE_PHONES_ID: 2231 tableToChange = sPhonesTable; 2232 changedItemId = url.getPathSegments().get(3); 2233 break; 2234 case PEOPLE_CONTACTMETHODS_ID: 2235 tableToChange = sContactMethodsTable; 2236 changedItemId = url.getPathSegments().get(3); 2237 break; 2238 case PHONES_ID: 2239 tableToChange = sPhonesTable; 2240 changedItemId = url.getPathSegments().get(1); 2241 break; 2242 case ORGANIZATIONS_ID: 2243 tableToChange = sOrganizationsTable; 2244 changedItemId = url.getPathSegments().get(1); 2245 break; 2246 case CONTACTMETHODS_ID: 2247 tableToChange = sContactMethodsTable; 2248 changedItemId = url.getPathSegments().get(1); 2249 break; 2250 case PRESENCE: 2251 tableToChange = "presence"; 2252 changedItemId = null; 2253 break; 2254 case CALLS: 2255 tableToChange = "calls"; 2256 changedItemId = null; 2257 break; 2258 default: 2259 throw new UnsupportedOperationException("Cannot delete that URL: " + url); 2260 } 2261 2262 String where = addIdToWhereClause(changedItemId, userWhere); 2263 IsPrimaryInfo oldPrimaryInfo = null; 2264 switch (matchedUriId) { 2265 case PEOPLE_PHONES_ID: 2266 case PHONES_ID: 2267 case ORGANIZATIONS_ID: 2268 oldPrimaryInfo = lookupIsPrimaryInfo(tableToChange, 2269 sIsPrimaryProjectionWithoutKind, where, whereArgs); 2270 break; 2271 2272 case PEOPLE_CONTACTMETHODS_ID: 2273 case CONTACTMETHODS_ID: 2274 oldPrimaryInfo = lookupIsPrimaryInfo(tableToChange, 2275 sIsPrimaryProjectionWithKind, where, whereArgs); 2276 break; 2277 } 2278 2279 final SQLiteDatabase db = getDatabase(); 2280 int count = db.delete(tableToChange, where, whereArgs); 2281 if (count > 0) { 2282 if (oldPrimaryInfo != null && oldPrimaryInfo.isPrimary) { 2283 fixupPrimaryAfterDelete(oldPrimaryInfo.kind, 2284 oldPrimaryInfo.id, oldPrimaryInfo.person); 2285 } 2286 } 2287 2288 return count; 2289 } 2290 2291 @Override 2292 public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException { 2293 int match = sURIMatcher.match(uri); 2294 switch (match) { 2295 case PEOPLE_PHOTO_DATA: 2296 if (!"r".equals(mode)) { 2297 throw new FileNotFoundException("Mode " + mode + " not supported."); 2298 } 2299 String person = uri.getPathSegments().get(1); 2300 String sql = "SELECT " + Photos.DATA + " FROM " + sPhotosTable 2301 + " WHERE " + Photos.PERSON_ID + "=?"; 2302 String[] selectionArgs = { person }; 2303 return SQLiteContentHelper.getBlobColumnAsAssetFile(getDatabase(), sql, 2304 selectionArgs); 2305 default: 2306 throw new FileNotFoundException("No file at: " + uri); 2307 } 2308 } 2309 2310 @Override 2311 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 2312 int match = sURIMatcher.match(uri); 2313 switch (match) { 2314 default: 2315 throw new UnsupportedOperationException(uri.toString()); 2316 } 2317 } 2318 2319 private int deleteFromGroupMembership(long rowId, String where, String[] whereArgs) { 2320 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 2321 qb.setTables("groups, groupmembership"); 2322 qb.setProjectionMap(sGroupMembershipProjectionMap); 2323 qb.appendWhere(sGroupsJoinString); 2324 qb.appendWhere(" AND groupmembership._id=" + rowId); 2325 Cursor cursor = qb.query(getDatabase(), null, where, whereArgs, null, null, null); 2326 try { 2327 final int indexPersonId = cursor.getColumnIndexOrThrow(GroupMembership.PERSON_ID); 2328 final int indexName = cursor.getColumnIndexOrThrow(GroupMembership.NAME); 2329 while (cursor.moveToNext()) { 2330 if (Groups.GROUP_ANDROID_STARRED.equals(cursor.getString(indexName))) { 2331 fixupPeopleStarred(cursor.getLong(indexPersonId), false); 2332 } 2333 } 2334 } finally { 2335 cursor.close(); 2336 } 2337 2338 return mDb.delete(sGroupmembershipTable, 2339 addIdToWhereClause(String.valueOf(rowId), where), 2340 whereArgs); 2341 } 2342 2343 private int deleteFromPeople(String rowId, String where, String[] whereArgs) { 2344 final SQLiteDatabase db = getDatabase(); 2345 where = addIdToWhereClause(rowId, where); 2346 Cursor cursor = db.query(sPeopleTable, null, where, whereArgs, null, null, null); 2347 try { 2348 final int idxSyncId = cursor.getColumnIndexOrThrow(People._SYNC_ID); 2349 final int idxSyncAccount = cursor.getColumnIndexOrThrow(People._SYNC_ACCOUNT); 2350 final int idxSyncVersion = cursor.getColumnIndexOrThrow(People._SYNC_VERSION); 2351 final int dstIdxSyncId = mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ID); 2352 final int dstIdxSyncAccount = 2353 mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_ACCOUNT); 2354 final int dstIdxSyncVersion = 2355 mDeletedPeopleInserter.getColumnIndex(SyncConstValue._SYNC_VERSION); 2356 while (cursor.moveToNext()) { 2357 final String syncId = cursor.getString(idxSyncId); 2358 if (TextUtils.isEmpty(syncId)) continue; 2359 // insert into deleted table 2360 mDeletedPeopleInserter.prepareForInsert(); 2361 mDeletedPeopleInserter.bind(dstIdxSyncId, syncId); 2362 mDeletedPeopleInserter.bind(dstIdxSyncAccount, cursor.getString(idxSyncAccount)); 2363 mDeletedPeopleInserter.bind(dstIdxSyncVersion, cursor.getString(idxSyncVersion)); 2364 mDeletedPeopleInserter.execute(); 2365 } 2366 } finally { 2367 cursor.close(); 2368 } 2369 2370 // perform the actual delete 2371 return db.delete(sPeopleTable, where, whereArgs); 2372 } 2373 2374 private int deleteFromGroups(String where, String[] whereArgs) { 2375 HashSet<String> modifiedAccounts = Sets.newHashSet(); 2376 Cursor cursor = getDatabase().query(sGroupsTable, null, where, whereArgs, 2377 null, null, null); 2378 try { 2379 final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME); 2380 final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT); 2381 final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID); 2382 final int indexId = cursor.getColumnIndexOrThrow(Groups._ID); 2383 final int indexShouldSync = cursor.getColumnIndexOrThrow(Groups.SHOULD_SYNC); 2384 while (cursor.moveToNext()) { 2385 String oldName = cursor.getString(indexName); 2386 String syncAccount = cursor.getString(indexSyncAccount); 2387 String syncId = cursor.getString(indexSyncId); 2388 boolean shouldSync = cursor.getLong(indexShouldSync) != 0; 2389 long id = cursor.getLong(indexId); 2390 fixupPeopleStarredOnGroupRename(oldName, null, id); 2391 if (!TextUtils.isEmpty(syncAccount) && !TextUtils.isEmpty(syncId)) { 2392 fixupPeopleStarredOnGroupRename(oldName, null, syncAccount, syncId); 2393 } 2394 if (!TextUtils.isEmpty(syncAccount) && shouldSync) { 2395 modifiedAccounts.add(syncAccount); 2396 } 2397 } 2398 } finally { 2399 cursor.close(); 2400 } 2401 2402 int numRows = mDb.delete(sGroupsTable, where, whereArgs); 2403 if (numRows > 0) { 2404 if (!isTemporary()) { 2405 final ContentResolver cr = getContext().getContentResolver(); 2406 for (String account : modifiedAccounts) { 2407 onLocalChangesForAccount(cr, account, true); 2408 } 2409 } 2410 } 2411 return numRows; 2412 } 2413 2414 /** 2415 * Called when local changes are made, so subclasses have 2416 * an opportunity to react as they see fit. 2417 * 2418 * @param resolver the content resolver to use 2419 * @param account the account the changes are tied to 2420 */ 2421 protected void onLocalChangesForAccount(final ContentResolver resolver, String account, 2422 boolean groupsModified) { 2423 // Do nothing 2424 } 2425 2426 private void fixupPrimaryAfterDelete(int kind, Long itemId, Long personId) { 2427 final String table = kindToTable(kind); 2428 // when you delete an item with isPrimary, 2429 // select a new one as isPrimary and clear the primary if no more items 2430 Long newPrimaryId = findNewPrimary(kind, personId, itemId); 2431 2432 // we found a new primary, set its isprimary flag 2433 if (newPrimaryId != null) { 2434 mValuesLocal.clear(); 2435 mValuesLocal.put("isprimary", 1); 2436 if (getDatabase().update(table, mValuesLocal, "_id=" + newPrimaryId, null) != 1) { 2437 throw new RuntimeException("error updating " + table + ", _id " 2438 + newPrimaryId + ", values " + mValuesLocal); 2439 } 2440 } 2441 2442 // if this kind's primary status should be reflected in the people row, update it 2443 if (kind == Contacts.KIND_PHONE) { 2444 updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newPrimaryId); 2445 } else if (kind == Contacts.KIND_EMAIL) { 2446 updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newPrimaryId); 2447 } else if (kind == Contacts.KIND_ORGANIZATION) { 2448 updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newPrimaryId); 2449 } 2450 } 2451 2452 @Override 2453 public int updateInternal(Uri url, ContentValues values, String userWhere, String[] whereArgs) { 2454 final SQLiteDatabase db = getDatabase(); 2455 String tableToChange; 2456 String changedItemId; 2457 final int matchedUriId = sURIMatcher.match(url); 2458 switch (matchedUriId) { 2459 case GROUPS_ID: 2460 changedItemId = url.getPathSegments().get(1); 2461 return updateGroups(values, 2462 addIdToWhereClause(changedItemId, userWhere), whereArgs); 2463 2464 case PEOPLE_EXTENSIONS_ID: 2465 tableToChange = sExtensionsTable; 2466 changedItemId = url.getPathSegments().get(3); 2467 break; 2468 2469 case EXTENSIONS_ID: 2470 tableToChange = sExtensionsTable; 2471 changedItemId = url.getPathSegments().get(1); 2472 break; 2473 2474 case PEOPLE_UPDATE_CONTACT_TIME: 2475 if (values.size() != 1 || !values.containsKey(People.LAST_TIME_CONTACTED)) { 2476 throw new IllegalArgumentException( 2477 "You may only use " + url + " to update People.LAST_TIME_CONTACTED"); 2478 } 2479 tableToChange = sPeopleTable; 2480 changedItemId = url.getPathSegments().get(1); 2481 break; 2482 2483 case PEOPLE_ID: 2484 mValues.clear(); 2485 mValues.putAll(values); 2486 mValues.put(Photos._SYNC_DIRTY, 1); 2487 values = mValues; 2488 tableToChange = sPeopleTable; 2489 changedItemId = url.getPathSegments().get(1); 2490 break; 2491 2492 case PEOPLE_PHONES_ID: 2493 tableToChange = sPhonesTable; 2494 changedItemId = url.getPathSegments().get(3); 2495 break; 2496 2497 case PEOPLE_CONTACTMETHODS_ID: 2498 tableToChange = sContactMethodsTable; 2499 changedItemId = url.getPathSegments().get(3); 2500 break; 2501 2502 case PHONES_ID: 2503 tableToChange = sPhonesTable; 2504 changedItemId = url.getPathSegments().get(1); 2505 break; 2506 2507 case PEOPLE_PHOTO: 2508 case PHOTOS_ID: 2509 mValues.clear(); 2510 mValues.putAll(values); 2511 2512 // The _SYNC_DIRTY flag should only be set if the data was modified and if 2513 // it isn't already provided. 2514 if (!mValues.containsKey(Photos._SYNC_DIRTY) && mValues.containsKey(Photos.DATA)) { 2515 mValues.put(Photos._SYNC_DIRTY, 1); 2516 } 2517 StringBuilder where; 2518 if (matchedUriId == PEOPLE_PHOTO) { 2519 where = new StringBuilder("_id=" + url.getPathSegments().get(1)); 2520 } else { 2521 where = new StringBuilder("person=" + url.getPathSegments().get(1)); 2522 } 2523 if (!TextUtils.isEmpty(userWhere)) { 2524 where.append(" AND ("); 2525 where.append(userWhere); 2526 where.append(')'); 2527 } 2528 return db.update(sPhotosTable, mValues, where.toString(), whereArgs); 2529 2530 case ORGANIZATIONS_ID: 2531 tableToChange = sOrganizationsTable; 2532 changedItemId = url.getPathSegments().get(1); 2533 break; 2534 2535 case CONTACTMETHODS_ID: 2536 tableToChange = sContactMethodsTable; 2537 changedItemId = url.getPathSegments().get(1); 2538 break; 2539 2540 case SETTINGS: 2541 if (whereArgs != null) { 2542 throw new IllegalArgumentException( 2543 "you aren't allowed to specify where args when updating settings"); 2544 } 2545 if (userWhere != null) { 2546 throw new IllegalArgumentException( 2547 "you aren't allowed to specify a where string when updating settings"); 2548 } 2549 return updateSettings(values); 2550 2551 case CALLS: 2552 tableToChange = "calls"; 2553 changedItemId = null; 2554 break; 2555 2556 case CALLS_ID: 2557 tableToChange = "calls"; 2558 changedItemId = url.getPathSegments().get(1); 2559 break; 2560 2561 default: 2562 throw new UnsupportedOperationException("Cannot update URL: " + url); 2563 } 2564 2565 String where = addIdToWhereClause(changedItemId, userWhere); 2566 int numRowsUpdated = db.update(tableToChange, values, where, whereArgs); 2567 2568 if (numRowsUpdated > 0 && changedItemId != null) { 2569 long itemId = Long.parseLong(changedItemId); 2570 switch (matchedUriId) { 2571 case ORGANIZATIONS_ID: 2572 fixupPrimaryAfterUpdate( 2573 Contacts.KIND_ORGANIZATION, null, itemId, 2574 values.getAsInteger(Organizations.ISPRIMARY)); 2575 break; 2576 2577 case PHONES_ID: 2578 case PEOPLE_PHONES_ID: 2579 fixupPrimaryAfterUpdate( 2580 Contacts.KIND_PHONE, matchedUriId == PEOPLE_PHONES_ID 2581 ? Long.parseLong(url.getPathSegments().get(1)) 2582 : null, itemId, 2583 values.getAsInteger(Phones.ISPRIMARY)); 2584 break; 2585 2586 case CONTACTMETHODS_ID: 2587 case PEOPLE_CONTACTMETHODS_ID: 2588 IsPrimaryInfo isPrimaryInfo = lookupIsPrimaryInfo(sContactMethodsTable, 2589 sIsPrimaryProjectionWithKind, where, whereArgs); 2590 fixupPrimaryAfterUpdate( 2591 isPrimaryInfo.kind, isPrimaryInfo.person, itemId, 2592 values.getAsInteger(ContactMethods.ISPRIMARY)); 2593 break; 2594 2595 case PEOPLE_ID: 2596 boolean hasStarred = values.containsKey(People.STARRED); 2597 boolean hasPrimaryPhone = values.containsKey(People.PRIMARY_PHONE_ID); 2598 boolean hasPrimaryOrganization = 2599 values.containsKey(People.PRIMARY_ORGANIZATION_ID); 2600 boolean hasPrimaryEmail = values.containsKey(People.PRIMARY_EMAIL_ID); 2601 if (hasStarred || hasPrimaryPhone || hasPrimaryOrganization 2602 || hasPrimaryEmail) { 2603 Cursor c = mDb.query(sPeopleTable, null, 2604 where, whereArgs, null, null, null); 2605 try { 2606 int indexAccount = c.getColumnIndexOrThrow(People._SYNC_ACCOUNT); 2607 int indexId = c.getColumnIndexOrThrow(People._ID); 2608 Long starredValue = values.getAsLong(People.STARRED); 2609 Long primaryPhone = values.getAsLong(People.PRIMARY_PHONE_ID); 2610 Long primaryOrganization = 2611 values.getAsLong(People.PRIMARY_ORGANIZATION_ID); 2612 Long primaryEmail = values.getAsLong(People.PRIMARY_EMAIL_ID); 2613 while (c.moveToNext()) { 2614 final long personId = c.getLong(indexId); 2615 if (hasStarred) { 2616 fixupGroupMembershipAfterPeopleUpdate(c.getString(indexAccount), 2617 personId, starredValue != null && starredValue != 0); 2618 } 2619 2620 if (hasPrimaryPhone) { 2621 if (primaryPhone == null) { 2622 throw new IllegalArgumentException( 2623 "the value of PRIMARY_PHONE_ID must not be null"); 2624 } 2625 setIsPrimary(Contacts.KIND_PHONE, personId, primaryPhone); 2626 } 2627 if (hasPrimaryOrganization) { 2628 if (primaryOrganization == null) { 2629 throw new IllegalArgumentException( 2630 "the value of PRIMARY_ORGANIZATION_ID must " 2631 + "not be null"); 2632 } 2633 setIsPrimary(Contacts.KIND_ORGANIZATION, personId, 2634 primaryOrganization); 2635 } 2636 if (hasPrimaryEmail) { 2637 if (primaryEmail == null) { 2638 throw new IllegalArgumentException( 2639 "the value of PRIMARY_EMAIL_ID must not be null"); 2640 } 2641 setIsPrimary(Contacts.KIND_EMAIL, personId, primaryEmail); 2642 } 2643 } 2644 } finally { 2645 c.close(); 2646 } 2647 } 2648 break; 2649 } 2650 } 2651 2652 return numRowsUpdated; 2653 } 2654 2655 private int updateSettings(ContentValues values) { 2656 final SQLiteDatabase db = getDatabase(); 2657 final String account = values.getAsString(Contacts.Settings._SYNC_ACCOUNT); 2658 final String key = values.getAsString(Contacts.Settings.KEY); 2659 if (key == null) { 2660 throw new IllegalArgumentException("you must specify the key when updating settings"); 2661 } 2662 if (account == null) { 2663 db.delete(sSettingsTable, "_sync_account IS NULL AND key=?", new String[]{key}); 2664 } else { 2665 if (TextUtils.isEmpty(account)) { 2666 throw new IllegalArgumentException("account cannot be the empty string, " + values); 2667 } 2668 db.delete(sSettingsTable, "_sync_account=? AND key=?", new String[]{account, key}); 2669 } 2670 long rowId = db.insert(sSettingsTable, Contacts.Settings.KEY, values); 2671 if (rowId < 0) { 2672 throw new SQLException("error updating settings with " + values); 2673 } 2674 return 1; 2675 } 2676 2677 private int updateGroups(ContentValues values, String where, String[] whereArgs) { 2678 for (Map.Entry<String, Object> entry : values.valueSet()) { 2679 final String column = entry.getKey(); 2680 if (!Groups.NAME.equals(column) && !Groups.NOTES.equals(column) 2681 && !Groups.SYSTEM_ID.equals(column) && !Groups.SHOULD_SYNC.equals(column)) { 2682 throw new IllegalArgumentException( 2683 "you are not allowed to change column " + column); 2684 } 2685 } 2686 2687 Set<String> modifiedAccounts = Sets.newHashSet(); 2688 final SQLiteDatabase db = getDatabase(); 2689 if (values.containsKey(Groups.NAME) || values.containsKey(Groups.SHOULD_SYNC)) { 2690 String newName = values.getAsString(Groups.NAME); 2691 Cursor cursor = db.query(sGroupsTable, null, where, whereArgs, null, null, null); 2692 try { 2693 final int indexName = cursor.getColumnIndexOrThrow(Groups.NAME); 2694 final int indexSyncAccount = cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT); 2695 final int indexSyncId = cursor.getColumnIndexOrThrow(Groups._SYNC_ID); 2696 final int indexId = cursor.getColumnIndexOrThrow(Groups._ID); 2697 while (cursor.moveToNext()) { 2698 String syncAccount = cursor.getString(indexSyncAccount); 2699 if (values.containsKey(Groups.NAME)) { 2700 String oldName = cursor.getString(indexName); 2701 String syncId = cursor.getString(indexSyncId); 2702 long id = cursor.getLong(indexId); 2703 fixupPeopleStarredOnGroupRename(oldName, newName, id); 2704 if (!TextUtils.isEmpty(syncAccount) && !TextUtils.isEmpty(syncId)) { 2705 fixupPeopleStarredOnGroupRename(oldName, newName, syncAccount, syncId); 2706 } 2707 } 2708 if (!TextUtils.isEmpty(syncAccount) && values.containsKey(Groups.SHOULD_SYNC)) { 2709 modifiedAccounts.add(syncAccount); 2710 } 2711 } 2712 } finally { 2713 cursor.close(); 2714 } 2715 } 2716 2717 int numRows = db.update(sGroupsTable, values, where, whereArgs); 2718 if (numRows > 0) { 2719 if (!isTemporary()) { 2720 final ContentResolver cr = getContext().getContentResolver(); 2721 for (String account : modifiedAccounts) { 2722 onLocalChangesForAccount(cr, account, true); 2723 } 2724 } 2725 } 2726 return numRows; 2727 } 2728 2729 void fixupPeopleStarredOnGroupRename(String oldName, String newName, 2730 String where, String[] whereArgs) { 2731 if (TextUtils.equals(oldName, newName)) return; 2732 2733 int starredValue; 2734 if (Groups.GROUP_ANDROID_STARRED.equals(newName)) { 2735 starredValue = 1; 2736 } else if (Groups.GROUP_ANDROID_STARRED.equals(oldName)) { 2737 starredValue = 0; 2738 } else { 2739 return; 2740 } 2741 2742 getDatabase().execSQL("UPDATE people SET starred=" + starredValue + " WHERE _id in (" 2743 + "SELECT person " 2744 + "FROM groups, groupmembership " 2745 + "WHERE " + where + " AND " + sGroupsJoinString + ")", 2746 whereArgs); 2747 } 2748 2749 void fixupPeopleStarredOnGroupRename(String oldName, String newName, 2750 String syncAccount, String syncId) { 2751 fixupPeopleStarredOnGroupRename(oldName, newName, "_sync_account=? AND _sync_id=?", 2752 new String[]{syncAccount, syncId}); 2753 } 2754 2755 void fixupPeopleStarredOnGroupRename(String oldName, String newName, long groupId) { 2756 fixupPeopleStarredOnGroupRename(oldName, newName, "group_id=?", 2757 new String[]{String.valueOf(groupId)}); 2758 } 2759 2760 private void fixupPrimaryAfterUpdate(int kind, Long personId, Long changedItemId, 2761 Integer isPrimaryValue) { 2762 final String table = kindToTable(kind); 2763 2764 // - when you update isPrimary to true, 2765 // make the changed item the primary, clear others 2766 // - when you update isPrimary to false, 2767 // select a new one as isPrimary, clear the primary if no more phones 2768 if (isPrimaryValue != null) { 2769 if (personId == null) { 2770 personId = lookupPerson(table, changedItemId); 2771 } 2772 2773 boolean isPrimary = isPrimaryValue != 0; 2774 Long newPrimary = changedItemId; 2775 if (!isPrimary) { 2776 newPrimary = findNewPrimary(kind, personId, changedItemId); 2777 } 2778 clearOtherIsPrimary(kind, personId, changedItemId); 2779 2780 if (kind == Contacts.KIND_PHONE) { 2781 updatePeoplePrimary(personId, People.PRIMARY_PHONE_ID, newPrimary); 2782 } else if (kind == Contacts.KIND_EMAIL) { 2783 updatePeoplePrimary(personId, People.PRIMARY_EMAIL_ID, newPrimary); 2784 } else if (kind == Contacts.KIND_ORGANIZATION) { 2785 updatePeoplePrimary(personId, People.PRIMARY_ORGANIZATION_ID, newPrimary); 2786 } 2787 } 2788 } 2789 2790 /** 2791 * Queries table to find the value of the person column for the row with _id. There must 2792 * be exactly one row that matches this id. 2793 * @param table the table to query 2794 * @param id the id of the row to query 2795 * @return the value of the person column for the specified row, returned as a String. 2796 */ 2797 private long lookupPerson(String table, long id) { 2798 return DatabaseUtils.longForQuery( 2799 getDatabase(), 2800 "SELECT person FROM " + table + " where _id=" + id, 2801 null); 2802 } 2803 2804 /** 2805 * Used to pass around information about a row that has the isprimary column. 2806 */ 2807 private class IsPrimaryInfo { 2808 boolean isPrimary; 2809 Long person; 2810 Long id; 2811 Integer kind; 2812 } 2813 2814 /** 2815 * Queries the table to determine the state of the row's isprimary column and the kind. 2816 * The where and whereArgs must be sufficient to match either 0 or 1 row. 2817 * @param table the table of rows to consider, supports "phones" and "contact_methods" 2818 * @param projection the projection to use to get the columns that pertain to table 2819 * @param where used in conjunction with the whereArgs to identify the row 2820 * @param where used in conjunction with the where string to identify the row 2821 * @return the IsPrimaryInfo about the matched row, or null if no row was matched 2822 */ 2823 private IsPrimaryInfo lookupIsPrimaryInfo(String table, String[] projection, String where, 2824 String[] whereArgs) { 2825 Cursor cursor = getDatabase().query(table, projection, where, whereArgs, null, null, null); 2826 try { 2827 if (!(cursor.getCount() <= 1)) { 2828 throw new IllegalArgumentException("expected only zero or one rows, got " 2829 + DatabaseUtils.dumpCursorToString(cursor)); 2830 } 2831 if (!cursor.moveToFirst()) return null; 2832 IsPrimaryInfo info = new IsPrimaryInfo(); 2833 info.isPrimary = cursor.getInt(0) != 0; 2834 info.person = cursor.getLong(1); 2835 info.id = cursor.getLong(2); 2836 if (projection == sIsPrimaryProjectionWithKind) { 2837 info.kind = cursor.getInt(3); 2838 } else { 2839 if (sPhonesTable.equals(table)) { 2840 info.kind = Contacts.KIND_PHONE; 2841 } else if (sOrganizationsTable.equals(table)) { 2842 info.kind = Contacts.KIND_ORGANIZATION; 2843 } else { 2844 throw new IllegalArgumentException("unexpected table, " + table); 2845 } 2846 } 2847 return info; 2848 } finally { 2849 cursor.close(); 2850 } 2851 } 2852 2853 /** 2854 * Returns the rank of the table-specific type, used when deciding which row 2855 * should be primary when none are primary. The lower the rank the better the type. 2856 * @param table supports "phones", "contact_methods" and "organizations" 2857 * @param type the table-specific type from the TYPE column 2858 * @return the rank of the table-specific type, the lower the better 2859 */ 2860 private int getRankOfType(String table, int type) { 2861 if (table.equals(sPhonesTable)) { 2862 switch (type) { 2863 case Contacts.Phones.TYPE_MOBILE: return 0; 2864 case Contacts.Phones.TYPE_WORK: return 1; 2865 case Contacts.Phones.TYPE_HOME: return 2; 2866 case Contacts.Phones.TYPE_PAGER: return 3; 2867 case Contacts.Phones.TYPE_CUSTOM: return 4; 2868 case Contacts.Phones.TYPE_OTHER: return 5; 2869 case Contacts.Phones.TYPE_FAX_WORK: return 6; 2870 case Contacts.Phones.TYPE_FAX_HOME: return 7; 2871 default: return 1000; 2872 } 2873 } 2874 2875 if (table.equals(sContactMethodsTable)) { 2876 switch (type) { 2877 case Contacts.ContactMethods.TYPE_HOME: return 0; 2878 case Contacts.ContactMethods.TYPE_WORK: return 1; 2879 case Contacts.ContactMethods.TYPE_CUSTOM: return 2; 2880 case Contacts.ContactMethods.TYPE_OTHER: return 3; 2881 default: return 1000; 2882 } 2883 } 2884 2885 if (table.equals(sOrganizationsTable)) { 2886 switch (type) { 2887 case Organizations.TYPE_WORK: return 0; 2888 case Organizations.TYPE_CUSTOM: return 1; 2889 case Organizations.TYPE_OTHER: return 2; 2890 default: return 1000; 2891 } 2892 } 2893 2894 throw new IllegalArgumentException("unexpected table, " + table); 2895 } 2896 2897 /** 2898 * Determines which of the rows in table for the personId should be picked as the primary 2899 * row based on the rank of the row's type. 2900 * @param kind the kind of contact 2901 * @param personId used to limit the rows to those pertaining to this person 2902 * @param itemId optional, a row to ignore 2903 * @return the _id of the row that should be the new primary. Is null if there are no 2904 * matching rows. 2905 */ 2906 private Long findNewPrimary(int kind, Long personId, Long itemId) { 2907 final String table = kindToTable(kind); 2908 if (personId == null) throw new IllegalArgumentException("personId must not be null"); 2909 StringBuilder sb = new StringBuilder(); 2910 sb.append("person="); 2911 sb.append(personId); 2912 if (itemId != null) { 2913 sb.append(" and _id!="); 2914 sb.append(itemId); 2915 } 2916 if (sContactMethodsTable.equals(table)) { 2917 sb.append(" and "); 2918 sb.append(ContactMethods.KIND); 2919 sb.append("="); 2920 sb.append(kind); 2921 } 2922 2923 Cursor cursor = getDatabase().query(table, ID_TYPE_PROJECTION, sb.toString(), 2924 null, null, null, null); 2925 try { 2926 Long newPrimaryId = null; 2927 int bestRank = -1; 2928 while (cursor.moveToNext()) { 2929 final int rank = getRankOfType(table, cursor.getInt(1)); 2930 if (bestRank == -1 || rank < bestRank) { 2931 newPrimaryId = cursor.getLong(0); 2932 bestRank = rank; 2933 } 2934 } 2935 return newPrimaryId; 2936 } finally { 2937 cursor.close(); 2938 } 2939 } 2940 2941 private void setIsPrimary(int kind, long personId, long itemId) { 2942 final String table = kindToTable(kind); 2943 StringBuilder sb = new StringBuilder(); 2944 sb.append("person="); 2945 sb.append(personId); 2946 2947 if (sContactMethodsTable.equals(table)) { 2948 sb.append(" and "); 2949 sb.append(ContactMethods.KIND); 2950 sb.append("="); 2951 sb.append(kind); 2952 } 2953 2954 final String where = sb.toString(); 2955 getDatabase().execSQL( 2956 "UPDATE " + table + " SET isprimary=(_id=" + itemId + ") WHERE " + where); 2957 } 2958 2959 /** 2960 * Clears the isprimary flag for all rows other than the itemId. 2961 * @param kind the kind of item 2962 * @param personId used to limit the updates to rows pertaining to this person 2963 * @param itemId which row to leave untouched 2964 */ 2965 private void clearOtherIsPrimary(int kind, Long personId, Long itemId) { 2966 final String table = kindToTable(kind); 2967 if (personId == null) throw new IllegalArgumentException("personId must not be null"); 2968 StringBuilder sb = new StringBuilder(); 2969 sb.append("person="); 2970 sb.append(personId); 2971 if (itemId != null) { 2972 sb.append(" and _id!="); 2973 sb.append(itemId); 2974 } 2975 if (sContactMethodsTable.equals(table)) { 2976 sb.append(" and "); 2977 sb.append(ContactMethods.KIND); 2978 sb.append("="); 2979 sb.append(kind); 2980 } 2981 2982 mValuesLocal.clear(); 2983 mValuesLocal.put("isprimary", 0); 2984 getDatabase().update(table, mValuesLocal, sb.toString(), null); 2985 } 2986 2987 /** 2988 * Set the specified primary column for the person. This is used to make the people 2989 * row reflect the isprimary flag in the people or contactmethods tables, which is 2990 * authoritative. 2991 * @param personId the person to modify 2992 * @param column the name of the primary column (phone or email) 2993 * @param primaryId the new value to write into the primary column 2994 */ 2995 private void updatePeoplePrimary(Long personId, String column, Long primaryId) { 2996 mValuesLocal.clear(); 2997 mValuesLocal.put(column, primaryId); 2998 getDatabase().update(sPeopleTable, mValuesLocal, "_id=" + personId, null); 2999 } 3000 3001 private static String addIdToWhereClause(String id, String where) { 3002 if (id != null) { 3003 StringBuilder whereSb = new StringBuilder("_id="); 3004 whereSb.append(id); 3005 if (!TextUtils.isEmpty(where)) { 3006 whereSb.append(" AND ("); 3007 whereSb.append(where); 3008 whereSb.append(')'); 3009 } 3010 return whereSb.toString(); 3011 } else { 3012 return where; 3013 } 3014 } 3015 3016 private boolean queryGroupMembershipContainsStarred(long personId) { 3017 // TODO: Part 1 of 2 part hack to work around a bug in reusing SQLiteStatements 3018 SQLiteStatement mGroupsMembershipQuery = null; 3019 3020 if (mGroupsMembershipQuery == null) { 3021 String query = 3022 "SELECT COUNT(*) FROM groups, groupmembership WHERE " 3023 + sGroupsJoinString + " AND person=? AND groups.name=?"; 3024 mGroupsMembershipQuery = getDatabase().compileStatement(query); 3025 } 3026 long result = DatabaseUtils.longForQuery(mGroupsMembershipQuery, 3027 new String[]{String.valueOf(personId), Groups.GROUP_ANDROID_STARRED}); 3028 3029 // TODO: Part 2 of 2 part hack to work around a bug in reusing SQLiteStatements 3030 mGroupsMembershipQuery.close(); 3031 3032 return result != 0; 3033 } 3034 3035 @Override 3036 public boolean changeRequiresLocalSync(Uri uri) { 3037 final int match = sURIMatcher.match(uri); 3038 switch (match) { 3039 // Changes to these URIs cannot cause syncable data to be changed, so don't 3040 // bother trying to sync them. 3041 case CALLS: 3042 case CALLS_FILTER: 3043 case CALLS_ID: 3044 case PRESENCE: 3045 case PRESENCE_ID: 3046 case PEOPLE_UPDATE_CONTACT_TIME: 3047 return false; 3048 3049 default: 3050 return true; 3051 } 3052 } 3053 3054 @Override 3055 protected Iterable<? extends AbstractTableMerger> getMergers() { 3056 ArrayList<AbstractTableMerger> list = new ArrayList<AbstractTableMerger> (); 3057 list.add(new PersonMerger()); 3058 list.add(new GroupMerger()); 3059 list.add(new PhotoMerger()); 3060 return list; 3061 } 3062 3063 protected static String sPeopleTable = "people"; 3064 protected static Uri sPeopleRawURL = Uri.parse("content://contacts/people/raw/"); 3065 protected static String sDeletedPeopleTable = "_deleted_people"; 3066 protected static Uri sDeletedPeopleURL = Uri.parse("content://contacts/deleted_people/"); 3067 protected static String sGroupsTable = "groups"; 3068 protected static String sSettingsTable = "settings"; 3069 protected static Uri sGroupsURL = Uri.parse("content://contacts/groups/"); 3070 protected static String sDeletedGroupsTable = "_deleted_groups"; 3071 protected static Uri sDeletedGroupsURL = 3072 Uri.parse("content://contacts/deleted_groups/"); 3073 protected static String sPhonesTable = "phones"; 3074 protected static String sOrganizationsTable = "organizations"; 3075 protected static String sContactMethodsTable = "contact_methods"; 3076 protected static String sGroupmembershipTable = "groupmembership"; 3077 protected static String sPhotosTable = "photos"; 3078 protected static Uri sPhotosURL = Uri.parse("content://contacts/photos/"); 3079 protected static String sExtensionsTable = "extensions"; 3080 protected static String sCallsTable = "calls"; 3081 3082 protected class PersonMerger extends AbstractTableMerger 3083 { 3084 private ContentValues mValues = new ContentValues(); 3085 Map<String, SQLiteCursor> mCursorMap = Maps.newHashMap(); 3086 public PersonMerger() 3087 { 3088 super(getDatabase(), 3089 sPeopleTable, sPeopleRawURL, sDeletedPeopleTable, sDeletedPeopleURL); 3090 } 3091 3092 @Override 3093 protected void notifyChanges() { 3094 // notify that a change has occurred. 3095 getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI, 3096 null /* observer */, false /* do not sync to network */); 3097 } 3098 3099 @Override 3100 public void insertRow(ContentProvider diffs, Cursor diffsCursor) { 3101 final SQLiteDatabase db = getDatabase(); 3102 3103 Long localPrimaryPhoneId = null; 3104 Long localPrimaryEmailId = null; 3105 Long localPrimaryOrganizationId = null; 3106 3107 // Copy the person 3108 mPeopleInserter.prepareForInsert(); 3109 DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_ID, mPeopleInserter, mIndexPeopleSyncId); 3110 DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_TIME, mPeopleInserter, mIndexPeopleSyncTime); 3111 DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_VERSION, mPeopleInserter, mIndexPeopleSyncVersion); 3112 DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_DIRTY, mPeopleInserter, mIndexPeopleSyncDirty); 3113 DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People._SYNC_ACCOUNT, mPeopleInserter, mIndexPeopleSyncAccount); 3114 DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.NAME, mPeopleInserter, mIndexPeopleName); 3115 DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.PHONETIC_NAME, mPeopleInserter, mIndexPeoplePhoneticName); 3116 DatabaseUtils.cursorStringToInsertHelper(diffsCursor, People.NOTES, mPeopleInserter, mIndexPeopleNotes); 3117 long localPersonID = mPeopleInserter.execute(); 3118 3119 Cursor c; 3120 final SQLiteDatabase diffsDb = ((ContactsProvider) diffs).getDatabase(); 3121 long diffsPersonID = diffsCursor.getLong(diffsCursor.getColumnIndexOrThrow(People._ID)); 3122 3123 // Copy the Photo info 3124 c = doSubQuery(diffsDb, sPhotosTable, null, diffsPersonID, null); 3125 try { 3126 if (c.moveToNext()) { 3127 mDb.delete(sPhotosTable, "person=" + localPersonID, null); 3128 mPhotosInserter.prepareForInsert(); 3129 DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ID, 3130 mPhotosInserter, mIndexPhotosSyncId); 3131 DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_TIME, 3132 mPhotosInserter, mIndexPhotosSyncTime); 3133 DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_VERSION, 3134 mPhotosInserter, mIndexPhotosSyncVersion); 3135 DatabaseUtils.cursorStringToInsertHelper(c, Photos._SYNC_ACCOUNT, 3136 mPhotosInserter, mIndexPhotosSyncAccount); 3137 DatabaseUtils.cursorStringToInsertHelper(c, Photos.EXISTS_ON_SERVER, 3138 mPhotosInserter, mIndexPhotosExistsOnServer); 3139 mPhotosInserter.bind(mIndexPhotosSyncError, (String)null); 3140 mPhotosInserter.bind(mIndexPhotosSyncDirty, 0); 3141 mPhotosInserter.bind(mIndexPhotosPersonId, localPersonID); 3142 mPhotosInserter.execute(); 3143 } 3144 } finally { 3145 c.deactivate(); 3146 } 3147 3148 // Copy all phones 3149 c = doSubQuery(diffsDb, sPhonesTable, null, diffsPersonID, sPhonesTable + "._id"); 3150 if (c != null) { 3151 Long newPrimaryId = null; 3152 int bestRank = -1; 3153 final int labelIndex = c.getColumnIndexOrThrow(Phones.LABEL); 3154 final int typeIndex = c.getColumnIndexOrThrow(Phones.TYPE); 3155 final int numberIndex = c.getColumnIndexOrThrow(Phones.NUMBER); 3156 final int keyIndex = c.getColumnIndexOrThrow(Phones.NUMBER_KEY); 3157 final int primaryIndex = c.getColumnIndexOrThrow(Phones.ISPRIMARY); 3158 while(c.moveToNext()) { 3159 final int type = c.getInt(typeIndex); 3160 final int isPrimaryValue = c.getInt(primaryIndex); 3161 mPhonesInserter.prepareForInsert(); 3162 mPhonesInserter.bind(mIndexPhonesPersonId, localPersonID); 3163 mPhonesInserter.bind(mIndexPhonesLabel, c.getString(labelIndex)); 3164 mPhonesInserter.bind(mIndexPhonesType, type); 3165 mPhonesInserter.bind(mIndexPhonesNumber, c.getString(numberIndex)); 3166 mPhonesInserter.bind(mIndexPhonesNumberKey, c.getString(keyIndex)); 3167 mPhonesInserter.bind(mIndexPhonesIsPrimary, isPrimaryValue); 3168 long rowId = mPhonesInserter.execute(); 3169 3170 if (isPrimaryValue != 0) { 3171 if (localPrimaryPhoneId != null) { 3172 throw new IllegalArgumentException( 3173 "more than one phone was marked as primary, " 3174 + DatabaseUtils.dumpCursorToString(c)); 3175 } 3176 localPrimaryPhoneId = rowId; 3177 } 3178 3179 if (localPrimaryPhoneId == null) { 3180 final int rank = getRankOfType(sPhonesTable, type); 3181 if (bestRank == -1 || rank < bestRank) { 3182 newPrimaryId = rowId; 3183 bestRank = rank; 3184 } 3185 } 3186 } 3187 c.deactivate(); 3188 3189 if (localPrimaryPhoneId == null) { 3190 localPrimaryPhoneId = newPrimaryId; 3191 } 3192 } 3193 3194 // Copy all contact_methods 3195 c = doSubQuery(diffsDb, sContactMethodsTable, null, diffsPersonID, 3196 sContactMethodsTable + "._id"); 3197 if (c != null) { 3198 Long newPrimaryId = null; 3199 int bestRank = -1; 3200 final int labelIndex = c.getColumnIndexOrThrow(ContactMethods.LABEL); 3201 final int kindIndex = c.getColumnIndexOrThrow(ContactMethods.KIND); 3202 final int typeIndex = c.getColumnIndexOrThrow(ContactMethods.TYPE); 3203 final int dataIndex = c.getColumnIndexOrThrow(ContactMethods.DATA); 3204 final int auxDataIndex = c.getColumnIndexOrThrow(ContactMethods.AUX_DATA); 3205 final int primaryIndex = c.getColumnIndexOrThrow(ContactMethods.ISPRIMARY); 3206 while(c.moveToNext()) { 3207 final int type = c.getInt(typeIndex); 3208 final int kind = c.getInt(kindIndex); 3209 final int isPrimaryValue = c.getInt(primaryIndex); 3210 mContactMethodsInserter.prepareForInsert(); 3211 mContactMethodsInserter.bind(mIndexContactMethodsPersonId, localPersonID); 3212 mContactMethodsInserter.bind(mIndexContactMethodsLabel, c.getString(labelIndex)); 3213 mContactMethodsInserter.bind(mIndexContactMethodsKind, kind); 3214 mContactMethodsInserter.bind(mIndexContactMethodsType, type); 3215 mContactMethodsInserter.bind(mIndexContactMethodsData, c.getString(dataIndex)); 3216 mContactMethodsInserter.bind(mIndexContactMethodsAuxData, c.getString(auxDataIndex)); 3217 mContactMethodsInserter.bind(mIndexContactMethodsIsPrimary, isPrimaryValue); 3218 long rowId = mContactMethodsInserter.execute(); 3219 if ((kind == Contacts.KIND_EMAIL) && (isPrimaryValue != 0)) { 3220 if (localPrimaryEmailId != null) { 3221 throw new IllegalArgumentException( 3222 "more than one email was marked as primary, " 3223 + DatabaseUtils.dumpCursorToString(c)); 3224 } 3225 localPrimaryEmailId = rowId; 3226 } 3227 3228 if (localPrimaryEmailId == null) { 3229 final int rank = getRankOfType(sContactMethodsTable, type); 3230 if (bestRank == -1 || rank < bestRank) { 3231 newPrimaryId = rowId; 3232 bestRank = rank; 3233 } 3234 } 3235 } 3236 c.deactivate(); 3237 3238 if (localPrimaryEmailId == null) { 3239 localPrimaryEmailId = newPrimaryId; 3240 } 3241 } 3242 3243 // Copy all organizations 3244 c = doSubQuery(diffsDb, sOrganizationsTable, null, diffsPersonID, 3245 sOrganizationsTable + "._id"); 3246 try { 3247 Long newPrimaryId = null; 3248 int bestRank = -1; 3249 final int labelIndex = c.getColumnIndexOrThrow(Organizations.LABEL); 3250 final int typeIndex = c.getColumnIndexOrThrow(Organizations.TYPE); 3251 final int companyIndex = c.getColumnIndexOrThrow(Organizations.COMPANY); 3252 final int titleIndex = c.getColumnIndexOrThrow(Organizations.TITLE); 3253 final int primaryIndex = c.getColumnIndexOrThrow(Organizations.ISPRIMARY); 3254 while(c.moveToNext()) { 3255 final int type = c.getInt(typeIndex); 3256 final int isPrimaryValue = c.getInt(primaryIndex); 3257 mOrganizationsInserter.prepareForInsert(); 3258 mOrganizationsInserter.bind(mIndexOrganizationsPersonId, localPersonID); 3259 mOrganizationsInserter.bind(mIndexOrganizationsLabel, c.getString(labelIndex)); 3260 mOrganizationsInserter.bind(mIndexOrganizationsType, type); 3261 mOrganizationsInserter.bind(mIndexOrganizationsCompany, c.getString(companyIndex)); 3262 mOrganizationsInserter.bind(mIndexOrganizationsTitle, c.getString(titleIndex)); 3263 mOrganizationsInserter.bind(mIndexOrganizationsIsPrimary, isPrimaryValue); 3264 long rowId = mOrganizationsInserter.execute(); 3265 if (isPrimaryValue != 0) { 3266 if (localPrimaryOrganizationId != null) { 3267 throw new IllegalArgumentException( 3268 "more than one organization was marked as primary, " 3269 + DatabaseUtils.dumpCursorToString(c)); 3270 } 3271 localPrimaryOrganizationId = rowId; 3272 } 3273 3274 if (localPrimaryOrganizationId == null) { 3275 final int rank = getRankOfType(sOrganizationsTable, type); 3276 if (bestRank == -1 || rank < bestRank) { 3277 newPrimaryId = rowId; 3278 bestRank = rank; 3279 } 3280 } 3281 } 3282 3283 if (localPrimaryOrganizationId == null) { 3284 localPrimaryOrganizationId = newPrimaryId; 3285 } 3286 } finally { 3287 c.deactivate(); 3288 } 3289 3290 // Copy all groupmembership rows 3291 c = doSubQuery(diffsDb, sGroupmembershipTable, null, diffsPersonID, 3292 sGroupmembershipTable + "._id"); 3293 try { 3294 final int accountIndex = 3295 c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ACCOUNT); 3296 final int idIndex = c.getColumnIndexOrThrow(GroupMembership.GROUP_SYNC_ID); 3297 while(c.moveToNext()) { 3298 mGroupMembershipInserter.prepareForInsert(); 3299 mGroupMembershipInserter.bind(mIndexGroupMembershipPersonId, localPersonID); 3300 mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncAccount, c.getString(accountIndex)); 3301 mGroupMembershipInserter.bind(mIndexGroupMembershipGroupSyncId, c.getString(idIndex)); 3302 mGroupMembershipInserter.execute(); 3303 } 3304 } finally { 3305 c.deactivate(); 3306 } 3307 3308 // Copy all extensions rows 3309 c = doSubQuery(diffsDb, sExtensionsTable, null, diffsPersonID, sExtensionsTable + "._id"); 3310 try { 3311 final int nameIndex = c.getColumnIndexOrThrow(Extensions.NAME); 3312 final int valueIndex = c.getColumnIndexOrThrow(Extensions.VALUE); 3313 while(c.moveToNext()) { 3314 mExtensionsInserter.prepareForInsert(); 3315 mExtensionsInserter.bind(mIndexExtensionsPersonId, localPersonID); 3316 mExtensionsInserter.bind(mIndexExtensionsName, c.getString(nameIndex)); 3317 mExtensionsInserter.bind(mIndexExtensionsValue, c.getString(valueIndex)); 3318 mExtensionsInserter.execute(); 3319 } 3320 } finally { 3321 c.deactivate(); 3322 } 3323 3324 // Update the _SYNC_DIRTY flag of the person. We have to do this 3325 // after inserting since the updated of the phones, contact 3326 // methods and organizations will fire a sql trigger that will 3327 // cause this flag to be set. 3328 mValues.clear(); 3329 mValues.put(People._SYNC_DIRTY, 0); 3330 mValues.put(People.PRIMARY_PHONE_ID, localPrimaryPhoneId); 3331 mValues.put(People.PRIMARY_EMAIL_ID, localPrimaryEmailId); 3332 mValues.put(People.PRIMARY_ORGANIZATION_ID, localPrimaryOrganizationId); 3333 final boolean isStarred = queryGroupMembershipContainsStarred(localPersonID); 3334 mValues.put(People.STARRED, isStarred ? 1 : 0); 3335 db.update(mTable, mValues, People._ID + '=' + localPersonID, null); 3336 } 3337 3338 @Override 3339 public void updateRow(long localPersonID, ContentProvider diffs, Cursor diffsCursor) { 3340 updateOrResolveRow(localPersonID, null, diffs, diffsCursor, false); 3341 } 3342 3343 @Override 3344 public void resolveRow(long localPersonID, String syncID, 3345 ContentProvider diffs, Cursor diffsCursor) { 3346 updateOrResolveRow(localPersonID, syncID, diffs, diffsCursor, true); 3347 } 3348 3349 protected void updateOrResolveRow(long localPersonID, String syncID, 3350 ContentProvider diffs, Cursor diffsCursor, boolean conflicts) { 3351 final SQLiteDatabase db = getDatabase(); 3352 // The local version of localPersonId's record has changed. This 3353 // person also has a changed record in the diffs. Merge the changes 3354 // in the following way: 3355 // - if any fields in the people table changed use the server's 3356 // version 3357 // - for phones, emails, addresses, compute the join of all unique 3358 // subrecords. If any of the subrecords has changes in both 3359 // places then choose the server version of the subrecord 3360 // 3361 // Limitation: deletes of phones, emails, or addresses are ignored 3362 // when the record has changed on both the client and the server 3363 3364 long diffsPersonID = diffsCursor.getLong(diffsCursor.getColumnIndexOrThrow("_id")); 3365 3366 // Join the server phones, organizations, and contact_methods with the local ones. 3367 // - Add locally any that exist only on the server. 3368 // - If the row conflicts, delete locally any that exist only on the client. 3369 // - If the row doesn't conflict, ignore any that exist only on the client. 3370 // - Update any that exist in both places. 3371 3372 Map<Integer, Long> primaryLocal = new HashMap<Integer, Long>(); 3373 Map<Integer, Long> primaryDiffs = new HashMap<Integer, Long>(); 3374 3375 Cursor cRemote; 3376 Cursor cLocal; 3377 3378 // Phones 3379 cRemote = null; 3380 cLocal = null; 3381 final SQLiteDatabase diffsDb = ((ContactsProvider) diffs).getDatabase(); 3382 try { 3383 cLocal = doSubQuery(db, sPhonesTable, null, localPersonID, sPhonesKeyOrderBy); 3384 cRemote = doSubQuery(diffsDb, sPhonesTable, 3385 null, diffsPersonID, sPhonesKeyOrderBy); 3386 3387 final int idColLocal = cLocal.getColumnIndexOrThrow(Phones._ID); 3388 final int isPrimaryColLocal = cLocal.getColumnIndexOrThrow(Phones.ISPRIMARY); 3389 final int isPrimaryColRemote = cRemote.getColumnIndexOrThrow(Phones.ISPRIMARY); 3390 3391 CursorJoiner joiner = 3392 new CursorJoiner(cLocal, sPhonesKeyColumns, cRemote, sPhonesKeyColumns); 3393 for (CursorJoiner.Result joinResult : joiner) { 3394 switch(joinResult) { 3395 case LEFT: 3396 if (!conflicts) { 3397 db.delete(sPhonesTable, 3398 Phones._ID + "=" + cLocal.getLong(idColLocal), null); 3399 } else { 3400 if (cLocal.getLong(isPrimaryColLocal) != 0) { 3401 savePrimaryId(primaryLocal, Contacts.KIND_PHONE, 3402 cLocal.getLong(idColLocal)); 3403 } 3404 } 3405 break; 3406 3407 case RIGHT: 3408 case BOTH: 3409 mValues.clear(); 3410 DatabaseUtils.cursorIntToContentValues( 3411 cRemote, Phones.TYPE, mValues); 3412 DatabaseUtils.cursorStringToContentValues( 3413 cRemote, Phones.LABEL, mValues); 3414 DatabaseUtils.cursorStringToContentValues( 3415 cRemote, Phones.NUMBER, mValues); 3416 DatabaseUtils.cursorStringToContentValues( 3417 cRemote, Phones.NUMBER_KEY, mValues); 3418 DatabaseUtils.cursorIntToContentValues( 3419 cRemote, Phones.ISPRIMARY, mValues); 3420 3421 long localId; 3422 if (joinResult == CursorJoiner.Result.RIGHT) { 3423 mValues.put(Phones.PERSON_ID, localPersonID); 3424 localId = mPhonesInserter.insert(mValues); 3425 } else { 3426 localId = cLocal.getLong(idColLocal); 3427 db.update(sPhonesTable, mValues, "_id =" + localId, null); 3428 } 3429 if (cRemote.getLong(isPrimaryColRemote) != 0) { 3430 savePrimaryId(primaryDiffs, Contacts.KIND_PHONE, localId); 3431 } 3432 break; 3433 } 3434 } 3435 } finally { 3436 if (cRemote != null) cRemote.deactivate(); 3437 if (cLocal != null) cLocal.deactivate(); 3438 } 3439 3440 // Contact methods 3441 cRemote = null; 3442 cLocal = null; 3443 try { 3444 cLocal = doSubQuery(db, 3445 sContactMethodsTable, null, localPersonID, sContactMethodsKeyOrderBy); 3446 cRemote = doSubQuery(diffsDb, 3447 sContactMethodsTable, null, diffsPersonID, sContactMethodsKeyOrderBy); 3448 3449 final int idColLocal = cLocal.getColumnIndexOrThrow(ContactMethods._ID); 3450 final int kindColLocal = cLocal.getColumnIndexOrThrow(ContactMethods.KIND); 3451 final int kindColRemote = cRemote.getColumnIndexOrThrow(ContactMethods.KIND); 3452 final int isPrimaryColLocal = 3453 cLocal.getColumnIndexOrThrow(ContactMethods.ISPRIMARY); 3454 final int isPrimaryColRemote = 3455 cRemote.getColumnIndexOrThrow(ContactMethods.ISPRIMARY); 3456 3457 CursorJoiner joiner = new CursorJoiner( 3458 cLocal, sContactMethodsKeyColumns, cRemote, sContactMethodsKeyColumns); 3459 for (CursorJoiner.Result joinResult : joiner) { 3460 switch(joinResult) { 3461 case LEFT: 3462 if (!conflicts) { 3463 db.delete(sContactMethodsTable, ContactMethods._ID + "=" 3464 + cLocal.getLong(idColLocal), null); 3465 } else { 3466 if (cLocal.getLong(isPrimaryColLocal) != 0) { 3467 savePrimaryId(primaryLocal, cLocal.getInt(kindColLocal), 3468 cLocal.getLong(idColLocal)); 3469 } 3470 } 3471 break; 3472 3473 case RIGHT: 3474 case BOTH: 3475 mValues.clear(); 3476 DatabaseUtils.cursorStringToContentValues(cRemote, 3477 ContactMethods.LABEL, mValues); 3478 DatabaseUtils.cursorIntToContentValues(cRemote, 3479 ContactMethods.TYPE, mValues); 3480 DatabaseUtils.cursorIntToContentValues(cRemote, 3481 ContactMethods.KIND, mValues); 3482 DatabaseUtils.cursorStringToContentValues(cRemote, 3483 ContactMethods.DATA, mValues); 3484 DatabaseUtils.cursorStringToContentValues(cRemote, 3485 ContactMethods.AUX_DATA, mValues); 3486 DatabaseUtils.cursorIntToContentValues(cRemote, 3487 ContactMethods.ISPRIMARY, mValues); 3488 3489 long localId; 3490 if (joinResult == CursorJoiner.Result.RIGHT) { 3491 mValues.put(ContactMethods.PERSON_ID, localPersonID); 3492 localId = mContactMethodsInserter.insert(mValues); 3493 } else { 3494 localId = cLocal.getLong(idColLocal); 3495 db.update(sContactMethodsTable, mValues, "_id =" + localId, null); 3496 } 3497 if (cRemote.getLong(isPrimaryColRemote) != 0) { 3498 savePrimaryId(primaryDiffs, cRemote.getInt(kindColRemote), localId); 3499 } 3500 break; 3501 } 3502 } 3503 } finally { 3504 if (cRemote != null) cRemote.deactivate(); 3505 if (cLocal != null) cLocal.deactivate(); 3506 } 3507 3508 // Organizations 3509 cRemote = null; 3510 cLocal = null; 3511 try { 3512 cLocal = doSubQuery(db, 3513 sOrganizationsTable, null, localPersonID, sOrganizationsKeyOrderBy); 3514 cRemote = doSubQuery(diffsDb, 3515 sOrganizationsTable, null, diffsPersonID, sOrganizationsKeyOrderBy); 3516 3517 final int idColLocal = cLocal.getColumnIndexOrThrow(Organizations._ID); 3518 final int isPrimaryColLocal = 3519 cLocal.getColumnIndexOrThrow(ContactMethods.ISPRIMARY); 3520 final int isPrimaryColRemote = 3521 cRemote.getColumnIndexOrThrow(ContactMethods.ISPRIMARY); 3522 CursorJoiner joiner = new CursorJoiner( 3523 cLocal, sOrganizationsKeyColumns, cRemote, sOrganizationsKeyColumns); 3524 for (CursorJoiner.Result joinResult : joiner) { 3525 switch(joinResult) { 3526 case LEFT: 3527 if (!conflicts) { 3528 db.delete(sOrganizationsTable, 3529 Phones._ID + "=" + cLocal.getLong(idColLocal), null); 3530 } else { 3531 if (cLocal.getLong(isPrimaryColLocal) != 0) { 3532 savePrimaryId(primaryLocal, Contacts.KIND_ORGANIZATION, 3533 cLocal.getLong(idColLocal)); 3534 } 3535 } 3536 break; 3537 3538 case RIGHT: 3539 case BOTH: 3540 mValues.clear(); 3541 DatabaseUtils.cursorStringToContentValues(cRemote, 3542 Organizations.LABEL, mValues); 3543 DatabaseUtils.cursorIntToContentValues(cRemote, 3544 Organizations.TYPE, mValues); 3545 DatabaseUtils.cursorStringToContentValues(cRemote, 3546 Organizations.COMPANY, mValues); 3547 DatabaseUtils.cursorStringToContentValues(cRemote, 3548 Organizations.TITLE, mValues); 3549 DatabaseUtils.cursorIntToContentValues(cRemote, 3550 Organizations.ISPRIMARY, mValues); 3551 long localId; 3552 if (joinResult == CursorJoiner.Result.RIGHT) { 3553 mValues.put(Organizations.PERSON_ID, localPersonID); 3554 localId = mOrganizationsInserter.insert(mValues); 3555 } else { 3556 localId = cLocal.getLong(idColLocal); 3557 db.update(sOrganizationsTable, mValues, 3558 "_id =" + localId, null /* whereArgs */); 3559 } 3560 if (cRemote.getLong(isPrimaryColRemote) != 0) { 3561 savePrimaryId(primaryDiffs, Contacts.KIND_ORGANIZATION, localId); 3562 } 3563 break; 3564 } 3565 } 3566 } finally { 3567 if (cRemote != null) cRemote.deactivate(); 3568 if (cLocal != null) cLocal.deactivate(); 3569 } 3570 3571 // Groupmembership 3572 cRemote = null; 3573 cLocal = null; 3574 try { 3575 cLocal = doSubQuery(db, 3576 sGroupmembershipTable, null, localPersonID, sGroupmembershipKeyOrderBy); 3577 cRemote = doSubQuery(diffsDb, 3578 sGroupmembershipTable, null, diffsPersonID, sGroupmembershipKeyOrderBy); 3579 3580 final int idColLocal = cLocal.getColumnIndexOrThrow(GroupMembership._ID); 3581 CursorJoiner joiner = new CursorJoiner( 3582 cLocal, sGroupmembershipKeyColumns, cRemote, sGroupmembershipKeyColumns); 3583 for (CursorJoiner.Result joinResult : joiner) { 3584 switch(joinResult) { 3585 case LEFT: 3586 if (!conflicts) { 3587 db.delete(sGroupmembershipTable, 3588 Phones._ID + "=" + cLocal.getLong(idColLocal), null); 3589 } 3590 break; 3591 3592 case RIGHT: 3593 case BOTH: 3594 mValues.clear(); 3595 DatabaseUtils.cursorStringToContentValues(cRemote, 3596 GroupMembership.GROUP_SYNC_ACCOUNT, mValues); 3597 DatabaseUtils.cursorStringToContentValues(cRemote, 3598 GroupMembership.GROUP_SYNC_ID, mValues); 3599 if (joinResult == CursorJoiner.Result.RIGHT) { 3600 mValues.put(GroupMembership.PERSON_ID, localPersonID); 3601 mGroupMembershipInserter.insert(mValues); 3602 } else { 3603 db.update(sGroupmembershipTable, mValues, 3604 "_id =" + cLocal.getLong(idColLocal), null /* whereArgs */); 3605 } 3606 break; 3607 } 3608 } 3609 } finally { 3610 if (cRemote != null) cRemote.deactivate(); 3611 if (cLocal != null) cLocal.deactivate(); 3612 } 3613 3614 // Extensions 3615 cRemote = null; 3616 cLocal = null; 3617 try { 3618 cLocal = doSubQuery(db, 3619 sExtensionsTable, null, localPersonID, Extensions.NAME); 3620 cRemote = doSubQuery(diffsDb, 3621 sExtensionsTable, null, diffsPersonID, Extensions.NAME); 3622 3623 final int idColLocal = cLocal.getColumnIndexOrThrow(Extensions._ID); 3624 CursorJoiner joiner = new CursorJoiner( 3625 cLocal, sExtensionsKeyColumns, cRemote, sExtensionsKeyColumns); 3626 for (CursorJoiner.Result joinResult : joiner) { 3627 switch(joinResult) { 3628 case LEFT: 3629 if (!conflicts) { 3630 db.delete(sExtensionsTable, 3631 Phones._ID + "=" + cLocal.getLong(idColLocal), null); 3632 } 3633 break; 3634 3635 case RIGHT: 3636 case BOTH: 3637 mValues.clear(); 3638 DatabaseUtils.cursorStringToContentValues(cRemote, 3639 Extensions.NAME, mValues); 3640 DatabaseUtils.cursorStringToContentValues(cRemote, 3641 Extensions.VALUE, mValues); 3642 if (joinResult == CursorJoiner.Result.RIGHT) { 3643 mValues.put(Extensions.PERSON_ID, localPersonID); 3644 mExtensionsInserter.insert(mValues); 3645 } else { 3646 db.update(sExtensionsTable, mValues, 3647 "_id =" + cLocal.getLong(idColLocal), null /* whereArgs */); 3648 } 3649 break; 3650 } 3651 } 3652 } finally { 3653 if (cRemote != null) cRemote.deactivate(); 3654 if (cLocal != null) cLocal.deactivate(); 3655 } 3656 3657 // Copy the Photo's server id and account so that the merger will find it 3658 cRemote = doSubQuery(diffsDb, sPhotosTable, null, diffsPersonID, null); 3659 try { 3660 if(cRemote.moveToNext()) { 3661 mValues.clear(); 3662 DatabaseUtils.cursorStringToContentValues(cRemote, Photos._SYNC_ID, mValues); 3663 DatabaseUtils.cursorStringToContentValues(cRemote, Photos._SYNC_ACCOUNT, mValues); 3664 db.update(sPhotosTable, mValues, Photos.PERSON_ID + '=' + localPersonID, null); 3665 } 3666 } finally { 3667 cRemote.deactivate(); 3668 } 3669 3670 // make sure there is exactly one primary set for each of these types 3671 Long primaryPhoneId = setSinglePrimary( 3672 primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_PHONE); 3673 3674 Long primaryEmailId = setSinglePrimary( 3675 primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_EMAIL); 3676 3677 Long primaryOrganizationId = setSinglePrimary( 3678 primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_ORGANIZATION); 3679 3680 setSinglePrimary(primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_IM); 3681 3682 setSinglePrimary(primaryDiffs, primaryLocal, localPersonID, Contacts.KIND_POSTAL); 3683 3684 // Update the person 3685 mValues.clear(); 3686 DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ID, mValues); 3687 DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_TIME, mValues); 3688 DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_VERSION, mValues); 3689 DatabaseUtils.cursorStringToContentValues(diffsCursor, People._SYNC_ACCOUNT, mValues); 3690 DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NAME, mValues); 3691 DatabaseUtils.cursorStringToContentValues(diffsCursor, People.PHONETIC_NAME, mValues); 3692 DatabaseUtils.cursorStringToContentValues(diffsCursor, People.NOTES, mValues); 3693 mValues.put(People.PRIMARY_PHONE_ID, primaryPhoneId); 3694 mValues.put(People.PRIMARY_EMAIL_ID, primaryEmailId); 3695 mValues.put(People.PRIMARY_ORGANIZATION_ID, primaryOrganizationId); 3696 final boolean isStarred = queryGroupMembershipContainsStarred(localPersonID); 3697 mValues.put(People.STARRED, isStarred ? 1 : 0); 3698 mValues.put(People._SYNC_DIRTY, conflicts ? 1 : 0); 3699 db.update(mTable, mValues, People._ID + '=' + localPersonID, null); 3700 } 3701 3702 private void savePrimaryId(Map<Integer, Long> primaryDiffs, Integer kind, long localId) { 3703 if (primaryDiffs.containsKey(kind)) { 3704 throw new IllegalArgumentException("more than one of kind " 3705 + kind + " was marked as primary"); 3706 } 3707 primaryDiffs.put(kind, localId); 3708 } 3709 3710 private Long setSinglePrimary( 3711 Map<Integer, Long> diffsMap, 3712 Map<Integer, Long> localMap, 3713 long localPersonID, int kind) { 3714 Long primaryId = diffsMap.containsKey(kind) ? diffsMap.get(kind) : null; 3715 if (primaryId == null) { 3716 primaryId = localMap.containsKey(kind) ? localMap.get(kind) : null; 3717 } 3718 if (primaryId == null) { 3719 primaryId = findNewPrimary(kind, localPersonID, null); 3720 } 3721 clearOtherIsPrimary(kind, localPersonID, primaryId); 3722 return primaryId; 3723 } 3724 3725 /** 3726 * Returns a cursor on the specified table that selects rows where 3727 * the "person" column is equal to the personId parameter. The cursor 3728 * is also saved and may be returned in future calls where db and table 3729 * parameter are the same. In that case the projection and orderBy parameters 3730 * are ignored, so one must take care to not change those parameters across 3731 * multiple calls to the same db/table. 3732 * <p> 3733 * Since the cursor may be saced by this call, the caller must be sure to not 3734 * close the cursor, though they still must deactivate it when they are done 3735 * with it. 3736 */ 3737 private Cursor doSubQuery(SQLiteDatabase db, String table, String[] projection, 3738 long personId, String orderBy) { 3739 final String[] selectArgs = new String[]{Long.toString(personId)}; 3740 final String key = (db == getDatabase() ? "local_" : "remote_") + table; 3741 SQLiteCursor cursor = mCursorMap.get(key); 3742 3743 // don't use the cached cursor if it is from a different DB 3744 if (cursor != null && cursor.getDatabase() != db) { 3745 cursor.close(); 3746 cursor = null; 3747 } 3748 3749 // If we can't find a cached cursor then create a new one and add it to the cache. 3750 // Otherwise just change the selection arguments and requery it. 3751 if (cursor == null) { 3752 cursor = (SQLiteCursor)db.query(table, projection, "person=?", selectArgs, 3753 null, null, orderBy); 3754 mCursorMap.put(key, cursor); 3755 } else { 3756 cursor.setSelectionArguments(selectArgs); 3757 cursor.requery(); 3758 } 3759 return cursor; 3760 } 3761 } 3762 3763 protected class GroupMerger extends AbstractTableMerger { 3764 private ContentValues mValues = new ContentValues(); 3765 3766 private static final String UNSYNCED_GROUP_BY_NAME_WHERE_CLAUSE = 3767 Groups._SYNC_ID + " is null AND " 3768 + Groups._SYNC_ACCOUNT + " is null AND " 3769 + Groups.NAME + "=?"; 3770 3771 private static final String UNSYNCED_GROUP_BY_SYSTEM_ID_WHERE_CLAUSE = 3772 Groups._SYNC_ID + " is null AND " 3773 + Groups._SYNC_ACCOUNT + " is null AND " 3774 + Groups.SYSTEM_ID + "=?"; 3775 3776 public GroupMerger() 3777 { 3778 super(getDatabase(), sGroupsTable, sGroupsURL, sDeletedGroupsTable, sDeletedGroupsURL); 3779 } 3780 3781 @Override 3782 protected void notifyChanges() { 3783 // notify that a change has occurred. 3784 getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI, 3785 null /* observer */, false /* do not sync to network */); 3786 } 3787 3788 @Override 3789 public void insertRow(ContentProvider diffs, Cursor cursor) { 3790 // if an unsynced group with this name already exists then update it, otherwise 3791 // insert a new group 3792 mValues.clear(); 3793 DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues); 3794 DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues); 3795 DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues); 3796 DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues); 3797 DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues); 3798 DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues); 3799 DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues); 3800 mValues.put(Groups._SYNC_DIRTY, 0); 3801 3802 final String systemId = mValues.getAsString(Groups.SYSTEM_ID); 3803 boolean rowUpdated = false; 3804 if (TextUtils.isEmpty(systemId)) { 3805 rowUpdated = getDatabase().update(mTable, mValues, 3806 UNSYNCED_GROUP_BY_NAME_WHERE_CLAUSE, 3807 new String[]{mValues.getAsString(Groups.NAME)}) > 0; 3808 } else { 3809 rowUpdated = getDatabase().update(mTable, mValues, 3810 UNSYNCED_GROUP_BY_SYSTEM_ID_WHERE_CLAUSE, 3811 new String[]{systemId}) > 0; 3812 } 3813 if (!rowUpdated) { 3814 mGroupsInserter.insert(mValues); 3815 } else { 3816 // We may have just synced the metadata for a groups we previously marked for 3817 // syncing. 3818 final ContentResolver cr = getContext().getContentResolver(); 3819 final String account = mValues.getAsString(Groups._SYNC_ACCOUNT); 3820 onLocalChangesForAccount(cr, account, false); 3821 } 3822 3823 String oldName = null; 3824 String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME)); 3825 String account = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT)); 3826 String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID)); 3827 // this must come after the insert, otherwise the join won't work 3828 fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId); 3829 } 3830 3831 @Override 3832 public void updateRow(long localId, ContentProvider diffs, Cursor diffsCursor) { 3833 updateOrResolveRow(localId, null, diffs, diffsCursor, false); 3834 } 3835 3836 @Override 3837 public void resolveRow(long localId, String syncID, 3838 ContentProvider diffs, Cursor diffsCursor) { 3839 updateOrResolveRow(localId, syncID, diffs, diffsCursor, true); 3840 } 3841 3842 protected void updateOrResolveRow(long localRowId, String syncID, 3843 ContentProvider diffs, Cursor cursor, boolean conflicts) { 3844 final SQLiteDatabase db = getDatabase(); 3845 3846 String oldName = DatabaseUtils.stringForQuery(db, 3847 "select name from groups where _id=" + localRowId, null); 3848 String newName = cursor.getString(cursor.getColumnIndexOrThrow(Groups.NAME)); 3849 String account = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT)); 3850 String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Groups._SYNC_ID)); 3851 // this can come before or after the delete 3852 fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId); 3853 3854 mValues.clear(); 3855 DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ID, mValues); 3856 DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_TIME, mValues); 3857 DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_VERSION, mValues); 3858 DatabaseUtils.cursorStringToContentValues(cursor, Groups._SYNC_ACCOUNT, mValues); 3859 DatabaseUtils.cursorStringToContentValues(cursor, Groups.NAME, mValues); 3860 DatabaseUtils.cursorStringToContentValues(cursor, Groups.NOTES, mValues); 3861 DatabaseUtils.cursorStringToContentValues(cursor, Groups.SYSTEM_ID, mValues); 3862 mValues.put(Groups._SYNC_DIRTY, 0); 3863 db.update(mTable, mValues, Groups._ID + '=' + localRowId, null); 3864 } 3865 3866 @Override 3867 public void deleteRow(Cursor cursor) { 3868 // we have to read this row from the DB since the projection that is used 3869 // by cursor doesn't necessarily contain the columns we need 3870 Cursor c = getDatabase().query(sGroupsTable, null, 3871 "_id=" + cursor.getLong(cursor.getColumnIndexOrThrow(Groups._ID)), 3872 null, null, null, null); 3873 try { 3874 c.moveToNext(); 3875 String oldName = c.getString(c.getColumnIndexOrThrow(Groups.NAME)); 3876 String newName = null; 3877 String account = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ACCOUNT)); 3878 String syncId = c.getString(c.getColumnIndexOrThrow(Groups._SYNC_ID)); 3879 String systemId = c.getString(c.getColumnIndexOrThrow(Groups.SYSTEM_ID)); 3880 if (!TextUtils.isEmpty(systemId)) { 3881 // We don't support deleting of system groups, but due to a server bug they 3882 // occasionally get sent. Ignore the delete. 3883 Log.w(TAG, "ignoring a delete for a system group: " + 3884 DatabaseUtils.dumpCurrentRowToString(c)); 3885 cursor.moveToNext(); 3886 return; 3887 } 3888 3889 // this must come before the delete, since the join won't work once this row is gone 3890 fixupPeopleStarredOnGroupRename(oldName, newName, account, syncId); 3891 } finally { 3892 c.close(); 3893 } 3894 3895 cursor.deleteRow(); 3896 } 3897 } 3898 3899 protected class PhotoMerger extends AbstractTableMerger { 3900 private ContentValues mValues = new ContentValues(); 3901 3902 public PhotoMerger() { 3903 super(getDatabase(), sPhotosTable, sPhotosURL, null, null); 3904 } 3905 3906 @Override 3907 protected void notifyChanges() { 3908 // notify that a change has occurred. 3909 getContext().getContentResolver().notifyChange(Contacts.CONTENT_URI, 3910 null /* observer */, false /* do not sync to network */); 3911 } 3912 3913 @Override 3914 public void insertRow(ContentProvider diffs, Cursor cursor) { 3915 // This photo may correspond to a contact that is in the delete table. If so then 3916 // ignore this insert. 3917 String syncId = cursor.getString(cursor.getColumnIndexOrThrow(Photos._SYNC_ID)); 3918 boolean contactIsDeleted = DatabaseUtils.longForQuery(getDatabase(), 3919 "select count(*) from _deleted_people where _sync_id=?", 3920 new String[]{syncId}) > 0; 3921 if (contactIsDeleted) { 3922 return; 3923 } 3924 3925 throw new UnsupportedOperationException( 3926 "the photo row is inserted by PersonMerger.insertRow"); 3927 } 3928 3929 @Override 3930 public void updateRow(long localId, ContentProvider diffs, Cursor diffsCursor) { 3931 updateOrResolveRow(localId, null, diffs, diffsCursor, false); 3932 } 3933 3934 @Override 3935 public void resolveRow(long localId, String syncID, 3936 ContentProvider diffs, Cursor diffsCursor) { 3937 updateOrResolveRow(localId, syncID, diffs, diffsCursor, true); 3938 } 3939 3940 protected void updateOrResolveRow(long localRowId, String syncID, 3941 ContentProvider diffs, Cursor cursor, boolean conflicts) { 3942 if (Log.isLoggable(TAG, Log.VERBOSE)) { 3943 Log.v(TAG, "PhotoMerger.updateOrResolveRow: localRowId " + localRowId 3944 + ", syncId " + syncID + ", conflicts " + conflicts 3945 + ", server row " + DatabaseUtils.dumpCurrentRowToString(cursor)); 3946 } 3947 mValues.clear(); 3948 DatabaseUtils.cursorStringToContentValues(cursor, Photos._SYNC_TIME, mValues); 3949 DatabaseUtils.cursorStringToContentValues(cursor, Photos._SYNC_VERSION, mValues); 3950 DatabaseUtils.cursorStringToContentValues(cursor, Photos.EXISTS_ON_SERVER, mValues); 3951 // reset the error field to allow the phone to attempt to redownload the photo. 3952 mValues.put(Photos.SYNC_ERROR, (String)null); 3953 3954 // If the photo didn't change locally and the server doesn't have a photo for this 3955 // contact then delete the local photo. 3956 long syncDirty = DatabaseUtils.longForQuery(getDatabase(), 3957 "SELECT _sync_dirty FROM photos WHERE _id=" + localRowId 3958 + " UNION SELECT 0 AS _sync_dirty ORDER BY _sync_dirty DESC LIMIT 1", 3959 null); 3960 if (syncDirty == 0) { 3961 if (mValues.getAsInteger(Photos.EXISTS_ON_SERVER) == 0) { 3962 mValues.put(Photos.DATA, (String)null); 3963 mValues.put(Photos.LOCAL_VERSION, mValues.getAsString(Photos.LOCAL_VERSION)); 3964 } 3965 // if it does exist on the server then we will attempt to download it later 3966 } 3967 // if it does conflict then we will send the client version of the photo to 3968 // the server later. That will trigger a new sync of the photo data which will 3969 // cause this method to be called again, at which time the row will no longer 3970 // conflict. We will then download the photo we just sent to the server and 3971 // set the LOCAL_VERSION to match the data we just downloaded. 3972 3973 getDatabase().update(mTable, mValues, Photos._ID + '=' + localRowId, null); 3974 } 3975 3976 @Override 3977 public void deleteRow(Cursor cursor) { 3978 // this row is never deleted explicitly, instead it is deleted by a trigger on 3979 // the people table 3980 cursor.moveToNext(); 3981 } 3982 } 3983 3984 private static final String TAG = "ContactsProvider"; 3985 3986 /* package private */ static final String DATABASE_NAME = "contacts.db"; 3987 /* package private */ static final int DATABASE_VERSION = 82; 3988 3989 protected static final String CONTACTS_AUTHORITY = "contacts"; 3990 protected static final String CALL_LOG_AUTHORITY = "call_log"; 3991 3992 private static final int PEOPLE_BASE = 0; 3993 private static final int PEOPLE = PEOPLE_BASE; 3994 private static final int PEOPLE_FILTER = PEOPLE_BASE + 1; 3995 private static final int PEOPLE_ID = PEOPLE_BASE + 2; 3996 private static final int PEOPLE_PHONES = PEOPLE_BASE + 3; 3997 private static final int PEOPLE_PHONES_ID = PEOPLE_BASE + 4; 3998 private static final int PEOPLE_CONTACTMETHODS = PEOPLE_BASE + 5; 3999 private static final int PEOPLE_CONTACTMETHODS_ID = PEOPLE_BASE + 6; 4000 private static final int PEOPLE_RAW = PEOPLE_BASE + 7; 4001 private static final int PEOPLE_WITH_PHONES_FILTER = PEOPLE_BASE + 8; 4002 private static final int PEOPLE_STREQUENT = PEOPLE_BASE + 9; 4003 private static final int PEOPLE_STREQUENT_FILTER = PEOPLE_BASE + 10; 4004 private static final int PEOPLE_ORGANIZATIONS = PEOPLE_BASE + 11; 4005 private static final int PEOPLE_ORGANIZATIONS_ID = PEOPLE_BASE + 12; 4006 private static final int PEOPLE_GROUPMEMBERSHIP = PEOPLE_BASE + 13; 4007 private static final int PEOPLE_GROUPMEMBERSHIP_ID = PEOPLE_BASE + 14; 4008 private static final int PEOPLE_PHOTO = PEOPLE_BASE + 15; 4009 private static final int PEOPLE_EXTENSIONS = PEOPLE_BASE + 16; 4010 private static final int PEOPLE_EXTENSIONS_ID = PEOPLE_BASE + 17; 4011 private static final int PEOPLE_CONTACTMETHODS_WITH_PRESENCE = PEOPLE_BASE + 18; 4012 private static final int PEOPLE_OWNER = PEOPLE_BASE + 19; 4013 private static final int PEOPLE_UPDATE_CONTACT_TIME = PEOPLE_BASE + 20; 4014 private static final int PEOPLE_PHONES_WITH_PRESENCE = PEOPLE_BASE + 21; 4015 private static final int PEOPLE_WITH_EMAIL_OR_IM_FILTER = PEOPLE_BASE + 22; 4016 private static final int PEOPLE_PHOTO_DATA = PEOPLE_BASE + 23; 4017 4018 private static final int DELETED_BASE = 1000; 4019 private static final int DELETED_PEOPLE = DELETED_BASE; 4020 private static final int DELETED_GROUPS = DELETED_BASE + 1; 4021 4022 private static final int PHONES_BASE = 2000; 4023 private static final int PHONES = PHONES_BASE; 4024 private static final int PHONES_ID = PHONES_BASE + 1; 4025 private static final int PHONES_FILTER = PHONES_BASE + 2; 4026 private static final int PHONES_FILTER_NAME = PHONES_BASE + 3; 4027 private static final int PHONES_MOBILE_FILTER_NAME = PHONES_BASE + 4; 4028 private static final int PHONES_WITH_PRESENCE = PHONES_BASE + 5; 4029 4030 private static final int CONTACTMETHODS_BASE = 3000; 4031 private static final int CONTACTMETHODS = CONTACTMETHODS_BASE; 4032 private static final int CONTACTMETHODS_ID = CONTACTMETHODS_BASE + 1; 4033 private static final int CONTACTMETHODS_EMAIL = CONTACTMETHODS_BASE + 2; 4034 private static final int CONTACTMETHODS_EMAIL_FILTER = CONTACTMETHODS_BASE + 3; 4035 private static final int CONTACTMETHODS_WITH_PRESENCE = CONTACTMETHODS_BASE + 4; 4036 4037 private static final int CALLS_BASE = 4000; 4038 private static final int CALLS = CALLS_BASE; 4039 private static final int CALLS_ID = CALLS_BASE + 1; 4040 private static final int CALLS_FILTER = CALLS_BASE + 2; 4041 4042 private static final int PRESENCE_BASE = 5000; 4043 private static final int PRESENCE = PRESENCE_BASE; 4044 private static final int PRESENCE_ID = PRESENCE_BASE + 1; 4045 4046 private static final int ORGANIZATIONS_BASE = 6000; 4047 private static final int ORGANIZATIONS = ORGANIZATIONS_BASE; 4048 private static final int ORGANIZATIONS_ID = ORGANIZATIONS_BASE + 1; 4049 4050 private static final int VOICE_DIALER_TIMESTAMP = 7000; 4051 private static final int SEARCH_SUGGESTIONS = 7001; 4052 private static final int SEARCH_SHORTCUT = 7002; 4053 4054 private static final int GROUPS_BASE = 8000; 4055 private static final int GROUPS = GROUPS_BASE; 4056 private static final int GROUPS_ID = GROUPS_BASE + 2; 4057 private static final int GROUP_NAME_MEMBERS = GROUPS_BASE + 3; 4058 private static final int GROUP_NAME_MEMBERS_FILTER = GROUPS_BASE + 4; 4059 private static final int GROUP_SYSTEM_ID_MEMBERS = GROUPS_BASE + 5; 4060 private static final int GROUP_SYSTEM_ID_MEMBERS_FILTER = GROUPS_BASE + 6; 4061 4062 private static final int GROUPMEMBERSHIP_BASE = 9000; 4063 private static final int GROUPMEMBERSHIP = GROUPMEMBERSHIP_BASE; 4064 private static final int GROUPMEMBERSHIP_ID = GROUPMEMBERSHIP_BASE + 2; 4065 private static final int GROUPMEMBERSHIP_RAW = GROUPMEMBERSHIP_BASE + 3; 4066 4067 private static final int PHOTOS_BASE = 10000; 4068 private static final int PHOTOS = PHOTOS_BASE; 4069 private static final int PHOTOS_ID = PHOTOS_BASE + 1; 4070 4071 private static final int EXTENSIONS_BASE = 11000; 4072 private static final int EXTENSIONS = EXTENSIONS_BASE; 4073 private static final int EXTENSIONS_ID = EXTENSIONS_BASE + 2; 4074 4075 private static final int SETTINGS = 12000; 4076 4077 private static final int LIVE_FOLDERS_BASE = 13000; 4078 private static final int LIVE_FOLDERS_PEOPLE = LIVE_FOLDERS_BASE + 1; 4079 private static final int LIVE_FOLDERS_PEOPLE_GROUP_NAME = LIVE_FOLDERS_BASE + 2; 4080 private static final int LIVE_FOLDERS_PEOPLE_WITH_PHONES = LIVE_FOLDERS_BASE + 3; 4081 private static final int LIVE_FOLDERS_PEOPLE_FAVORITES = LIVE_FOLDERS_BASE + 4; 4082 4083 private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 4084 4085 private static final HashMap<String, String> sGroupsProjectionMap; 4086 private static final HashMap<String, String> sPeopleProjectionMap; 4087 private static final HashMap<String, String> sPeopleWithPhotoProjectionMap; 4088 private static final HashMap<String, String> sPeopleWithEmailOrImProjectionMap; 4089 /** Used to force items to the top of a times_contacted list */ 4090 private static final HashMap<String, String> sStrequentStarredProjectionMap; 4091 private static final HashMap<String, String> sCallsProjectionMap; 4092 private static final HashMap<String, String> sPhonesProjectionMap; 4093 private static final HashMap<String, String> sPhonesWithPresenceProjectionMap; 4094 private static final HashMap<String, String> sContactMethodsProjectionMap; 4095 private static final HashMap<String, String> sContactMethodsWithPresenceProjectionMap; 4096 private static final HashMap<String, String> sPresenceProjectionMap; 4097 private static final HashMap<String, String> sEmailSearchProjectionMap; 4098 private static final HashMap<String, String> sOrganizationsProjectionMap; 4099 private static final HashMap<String, String> sGroupMembershipProjectionMap; 4100 private static final HashMap<String, String> sPhotosProjectionMap; 4101 private static final HashMap<String, String> sExtensionsProjectionMap; 4102 private static final HashMap<String, String> sLiveFoldersProjectionMap; 4103 4104 private static final String sPhonesKeyOrderBy; 4105 private static final String sContactMethodsKeyOrderBy; 4106 private static final String sOrganizationsKeyOrderBy; 4107 private static final String sGroupmembershipKeyOrderBy; 4108 4109 private static final String DISPLAY_NAME_SQL 4110 = "(CASE WHEN (name IS NOT NULL AND name != '') " 4111 + "THEN name " 4112 + "ELSE " 4113 + "(CASE WHEN primary_organization is NOT NULL THEN " 4114 + "(SELECT company FROM organizations WHERE " 4115 + "organizations._id = primary_organization) " 4116 + "ELSE " 4117 + "(CASE WHEN primary_phone IS NOT NULL THEN " 4118 +"(SELECT number FROM phones WHERE phones._id = primary_phone) " 4119 + "ELSE " 4120 + "(CASE WHEN primary_email IS NOT NULL THEN " 4121 + "(SELECT data FROM contact_methods WHERE " 4122 + "contact_methods._id = primary_email) " 4123 + "ELSE " 4124 + "null " 4125 + "END) " 4126 + "END) " 4127 + "END) " 4128 + "END) "; 4129 4130 private static final String PHONETICALLY_SORTABLE_STRING_SQL = 4131 "GET_PHONETICALLY_SORTABLE_STRING(" 4132 + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') " 4133 + "THEN phonetic_name " 4134 + "ELSE " 4135 + "(CASE WHEN (name is NOT NULL AND name != '') " 4136 + "THEN name " 4137 + "ELSE " 4138 + "(CASE WHEN primary_email IS NOT NULL THEN " 4139 + "(SELECT data FROM contact_methods WHERE " 4140 + "contact_methods._id = primary_email) " 4141 + "ELSE " 4142 + "(CASE WHEN primary_phone IS NOT NULL THEN " 4143 + "(SELECT number FROM phones WHERE phones._id = primary_phone) " 4144 + "ELSE " 4145 + "null " 4146 + "END) " 4147 + "END) " 4148 + "END) " 4149 + "END" 4150 + ")"; 4151 4152 private static final String NAME_WHEN_SQL 4153 = " WHEN name is NOT NULL ANd name != '' THEN name"; 4154 4155 private static final String PRIMARY_ORGANIZATION_WHEN_SQL 4156 = " WHEN primary_organization is NOT NULL THEN " 4157 + "(SELECT company FROM organizations WHERE organizations._id = primary_organization)"; 4158 4159 private static final String PRIMARY_PHONE_WHEN_SQL 4160 = " WHEN primary_phone IS NOT NULL THEN " 4161 + "(SELECT number FROM phones WHERE phones._id = primary_phone)"; 4162 4163 private static final String PRIMARY_EMAIL_WHEN_SQL 4164 = " WHEN primary_email IS NOT NULL THEN " 4165 + "(SELECT data FROM contact_methods WHERE contact_methods._id = primary_email)"; 4166 4167 // The outer CASE is for figuring out what info DISPLAY_NAME_SQL returned. 4168 // We then pick the next piece of info, to avoid the two lines in the search 4169 // suggestion being identical. 4170 private static final String SUGGEST_DESCRIPTION_SQL 4171 = "(CASE" 4172 // DISPLAY_NAME_SQL returns name, try org, phone, email 4173 + " WHEN (name IS NOT NULL AND name != '') THEN " 4174 + "(CASE" 4175 + PRIMARY_ORGANIZATION_WHEN_SQL 4176 + PRIMARY_PHONE_WHEN_SQL 4177 + PRIMARY_EMAIL_WHEN_SQL 4178 + " ELSE null END)" 4179 // DISPLAY_NAME_SQL returns org, try phone, email 4180 + " WHEN primary_organization is NOT NULL THEN " 4181 + "(CASE" 4182 + PRIMARY_PHONE_WHEN_SQL 4183 + PRIMARY_EMAIL_WHEN_SQL 4184 + " ELSE null END)" 4185 // DISPLAY_NAME_SQL returns phone, try email 4186 + " WHEN primary_phone IS NOT NULL THEN " 4187 + "(CASE" 4188 + PRIMARY_EMAIL_WHEN_SQL 4189 + " ELSE null END)" 4190 // DISPLAY_NAME_SQL returns email or NULL, return NULL 4191 + " ELSE null END)"; 4192 4193 private static final String PRESENCE_ICON_SQL 4194 = "(CASE" 4195 + buildPresenceStatusWhen(People.OFFLINE) 4196 + buildPresenceStatusWhen(People.INVISIBLE) 4197 + buildPresenceStatusWhen(People.AWAY) 4198 + buildPresenceStatusWhen(People.IDLE) 4199 + buildPresenceStatusWhen(People.DO_NOT_DISTURB) 4200 + buildPresenceStatusWhen(People.AVAILABLE) 4201 + " ELSE null END)"; 4202 4203 private static String buildPresenceStatusWhen(int status) { 4204 return " WHEN " + Presence.PRESENCE_STATUS + " = " + status 4205 + " THEN " + Presence.getPresenceIconResourceId(status); 4206 } 4207 4208 4209 // This is similar to DISPLAY_NAME_SQL. Only difference is that this prioritize 4210 // phonetic_name. 4211 private static final String PHONETIC_LOOKUP_STRING_SQL = 4212 "GET_NORMALIZED_STRING(" 4213 + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') " 4214 + "THEN phonetic_name " 4215 + "ELSE " 4216 + "(CASE WHEN (name is NOT NULL AND name != '') " 4217 + "THEN name " 4218 + "ELSE " 4219 + "(CASE WHEN primary_organization is NOT NULL THEN " 4220 + "(SELECT company FROM organizations WHERE " 4221 + "organizations._id = primary_organization) " 4222 + "ELSE " 4223 + "(CASE WHEN primary_phone IS NOT NULL THEN " 4224 +"(SELECT number FROM phones WHERE phones._id = primary_phone) " 4225 + "ELSE " 4226 + "(CASE WHEN primary_email IS NOT NULL THEN " 4227 + "(SELECT data FROM contact_methods WHERE " 4228 + "contact_methods._id = primary_email) " 4229 + "ELSE " 4230 + "null " 4231 + "END) " 4232 + "END) " 4233 + "END) " 4234 + "END) " 4235 + "END)"; 4236 4237 private static final String PHONETIC_SUGGEST_DESCRIPTION_SQL = 4238 "(CASE" 4239 + " WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') THEN " 4240 // PHONETIC_LOOKUP_STRING_SQL returns phonetic_name. try name, org, phone, email 4241 + "(CASE" 4242 + NAME_WHEN_SQL 4243 + PRIMARY_ORGANIZATION_WHEN_SQL 4244 + PRIMARY_PHONE_WHEN_SQL 4245 + PRIMARY_EMAIL_WHEN_SQL 4246 + " ELSE null END)" 4247 // PHONETIC_LOOKUP_STRING_SQL returns name, try org, phone, email 4248 + " WHEN (name IS NOT NULL AND name != '') THEN " 4249 + "(CASE" 4250 + PRIMARY_ORGANIZATION_WHEN_SQL 4251 + PRIMARY_PHONE_WHEN_SQL 4252 + PRIMARY_EMAIL_WHEN_SQL 4253 + " ELSE null END)" 4254 // PHONETIC_LOOKUP_STRING_SQL returns org, try phone, email 4255 + " WHEN primary_organization is NOT NULL THEN " 4256 + "(CASE" 4257 + PRIMARY_PHONE_WHEN_SQL 4258 + PRIMARY_EMAIL_WHEN_SQL 4259 + " ELSE null END)" 4260 // PHONETIC_LOOKUP_STRING_SQL returns phone, try email 4261 + " WHEN primary_phone IS NOT NULL THEN " 4262 + "(CASE" 4263 + PRIMARY_EMAIL_WHEN_SQL 4264 + " ELSE null END)" 4265 // PHONETIC_LOOKUP_STRING_SQL returns email or NULL, return NULL 4266 + " ELSE null END)"; 4267 4268 // "primary_organization" etc. are not considered here, since peopleLookup does not 4269 // consider them either. 4270 private static final String PHONETIC_LOOKUP_SQL_SIMPLE = 4271 "GET_NORMALIZED_STRING(" 4272 + "CASE WHEN (phonetic_name IS NOT NULL AND phonetic_name != '') " 4273 + "THEN phonetic_name " 4274 + "ELSE " 4275 + "(CASE WHEN (name is NOT NULL AND name != '') " 4276 + "THEN name " 4277 + "ELSE " 4278 + "'' " 4279 + "END) " 4280 + "END)"; 4281 4282 private static final String PHONETIC_LOOKUP_SQL_SIMPLE_WITH_NEW = 4283 "GET_NORMALIZED_STRING(" 4284 + "CASE WHEN (new.phonetic_name IS NOT NULL AND new.phonetic_name != '') " 4285 + "THEN new.phonetic_name " 4286 + "ELSE " 4287 + "(CASE WHEN (new.name is NOT NULL AND new.name != '') " 4288 + "THEN new.name " 4289 + "ELSE " 4290 + "'' " 4291 + "END) " 4292 + "END)"; 4293 4294 private static final String[] sPhonesKeyColumns; 4295 private static final String[] sContactMethodsKeyColumns; 4296 private static final String[] sOrganizationsKeyColumns; 4297 private static final String[] sGroupmembershipKeyColumns; 4298 private static final String[] sExtensionsKeyColumns; 4299 4300 static private String buildOrderBy(String table, String... columns) { 4301 StringBuilder sb = null; 4302 for (String column : columns) { 4303 if (sb == null) { 4304 sb = new StringBuilder(); 4305 } else { 4306 sb.append(", "); 4307 } 4308 sb.append(table); 4309 sb.append('.'); 4310 sb.append(column); 4311 } 4312 return (sb == null) ? "" : sb.toString(); 4313 } 4314 4315 /** 4316 * @return true when phonetic_name should be considered when looking up people's names. 4317 */ 4318 private synchronized boolean usePhoneticNameForPeopleLookup() { 4319 return mSearchSuggestionLanguage.equals(Locale.JAPAN.getLanguage()); 4320 } 4321 4322 private void updateSuggestColumnTexts() { 4323 if (usePhoneticNameForPeopleLookup()) { 4324 mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_1, 4325 PHONETIC_LOOKUP_STRING_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1); 4326 mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_2, 4327 PHONETIC_SUGGEST_DESCRIPTION_SQL + " AS " + 4328 SearchManager.SUGGEST_COLUMN_TEXT_2); 4329 } else { 4330 mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_1, 4331 DISPLAY_NAME_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1); 4332 mSearchSuggestionsProjectionMap.put(SearchManager.SUGGEST_COLUMN_TEXT_2, 4333 SUGGEST_DESCRIPTION_SQL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2); 4334 } 4335 } 4336 4337 static { 4338 // Contacts URI matching table 4339 UriMatcher matcher = sURIMatcher; 4340 matcher.addURI(CONTACTS_AUTHORITY, "extensions", EXTENSIONS); 4341 matcher.addURI(CONTACTS_AUTHORITY, "extensions/#", EXTENSIONS_ID); 4342 matcher.addURI(CONTACTS_AUTHORITY, "groups", GROUPS); 4343 matcher.addURI(CONTACTS_AUTHORITY, "groups/#", GROUPS_ID); 4344 matcher.addURI(CONTACTS_AUTHORITY, "groups/name/*/members", GROUP_NAME_MEMBERS); 4345 matcher.addURI(CONTACTS_AUTHORITY, "groups/name/*/members/filter/*", 4346 GROUP_NAME_MEMBERS_FILTER); 4347 matcher.addURI(CONTACTS_AUTHORITY, "groups/system_id/*/members", GROUP_SYSTEM_ID_MEMBERS); 4348 matcher.addURI(CONTACTS_AUTHORITY, "groups/system_id/*/members/filter/*", 4349 GROUP_SYSTEM_ID_MEMBERS_FILTER); 4350 matcher.addURI(CONTACTS_AUTHORITY, "groupmembership", GROUPMEMBERSHIP); 4351 matcher.addURI(CONTACTS_AUTHORITY, "groupmembership/#", GROUPMEMBERSHIP_ID); 4352 matcher.addURI(CONTACTS_AUTHORITY, "groupmembershipraw", GROUPMEMBERSHIP_RAW); 4353 matcher.addURI(CONTACTS_AUTHORITY, "people", PEOPLE); 4354 matcher.addURI(CONTACTS_AUTHORITY, "people/strequent", PEOPLE_STREQUENT); 4355 matcher.addURI(CONTACTS_AUTHORITY, "people/strequent/filter/*", PEOPLE_STREQUENT_FILTER); 4356 matcher.addURI(CONTACTS_AUTHORITY, "people/filter/*", PEOPLE_FILTER); 4357 matcher.addURI(CONTACTS_AUTHORITY, "people/with_phones_filter/*", 4358 PEOPLE_WITH_PHONES_FILTER); 4359 matcher.addURI(CONTACTS_AUTHORITY, "people/with_email_or_im_filter/*", 4360 PEOPLE_WITH_EMAIL_OR_IM_FILTER); 4361 matcher.addURI(CONTACTS_AUTHORITY, "people/#", PEOPLE_ID); 4362 matcher.addURI(CONTACTS_AUTHORITY, "people/#/extensions", PEOPLE_EXTENSIONS); 4363 matcher.addURI(CONTACTS_AUTHORITY, "people/#/extensions/#", PEOPLE_EXTENSIONS_ID); 4364 matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones", PEOPLE_PHONES); 4365 matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones_with_presence", 4366 PEOPLE_PHONES_WITH_PRESENCE); 4367 matcher.addURI(CONTACTS_AUTHORITY, "people/#/photo", PEOPLE_PHOTO); 4368 matcher.addURI(CONTACTS_AUTHORITY, "people/#/photo/data", PEOPLE_PHOTO_DATA); 4369 matcher.addURI(CONTACTS_AUTHORITY, "people/#/phones/#", PEOPLE_PHONES_ID); 4370 matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods", PEOPLE_CONTACTMETHODS); 4371 matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods_with_presence", 4372 PEOPLE_CONTACTMETHODS_WITH_PRESENCE); 4373 matcher.addURI(CONTACTS_AUTHORITY, "people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID); 4374 matcher.addURI(CONTACTS_AUTHORITY, "people/#/organizations", PEOPLE_ORGANIZATIONS); 4375 matcher.addURI(CONTACTS_AUTHORITY, "people/#/organizations/#", PEOPLE_ORGANIZATIONS_ID); 4376 matcher.addURI(CONTACTS_AUTHORITY, "people/#/groupmembership", PEOPLE_GROUPMEMBERSHIP); 4377 matcher.addURI(CONTACTS_AUTHORITY, "people/#/groupmembership/#", PEOPLE_GROUPMEMBERSHIP_ID); 4378 matcher.addURI(CONTACTS_AUTHORITY, "people/raw", PEOPLE_RAW); 4379 matcher.addURI(CONTACTS_AUTHORITY, "people/owner", PEOPLE_OWNER); 4380 matcher.addURI(CONTACTS_AUTHORITY, "people/#/update_contact_time", 4381 PEOPLE_UPDATE_CONTACT_TIME); 4382 matcher.addURI(CONTACTS_AUTHORITY, "deleted_people", DELETED_PEOPLE); 4383 matcher.addURI(CONTACTS_AUTHORITY, "deleted_groups", DELETED_GROUPS); 4384 matcher.addURI(CONTACTS_AUTHORITY, "phones", PHONES); 4385 matcher.addURI(CONTACTS_AUTHORITY, "phones_with_presence", PHONES_WITH_PRESENCE); 4386 matcher.addURI(CONTACTS_AUTHORITY, "phones/filter/*", PHONES_FILTER); 4387 matcher.addURI(CONTACTS_AUTHORITY, "phones/filter_name/*", PHONES_FILTER_NAME); 4388 matcher.addURI(CONTACTS_AUTHORITY, "phones/mobile_filter_name/*", 4389 PHONES_MOBILE_FILTER_NAME); 4390 matcher.addURI(CONTACTS_AUTHORITY, "phones/#", PHONES_ID); 4391 matcher.addURI(CONTACTS_AUTHORITY, "photos", PHOTOS); 4392 matcher.addURI(CONTACTS_AUTHORITY, "photos/#", PHOTOS_ID); 4393 matcher.addURI(CONTACTS_AUTHORITY, "contact_methods", CONTACTMETHODS); 4394 matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/email", CONTACTMETHODS_EMAIL); 4395 matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/email/*", CONTACTMETHODS_EMAIL_FILTER); 4396 matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/#", CONTACTMETHODS_ID); 4397 matcher.addURI(CONTACTS_AUTHORITY, "contact_methods/with_presence", 4398 CONTACTMETHODS_WITH_PRESENCE); 4399 matcher.addURI(CONTACTS_AUTHORITY, "presence", PRESENCE); 4400 matcher.addURI(CONTACTS_AUTHORITY, "presence/#", PRESENCE_ID); 4401 matcher.addURI(CONTACTS_AUTHORITY, "organizations", ORGANIZATIONS); 4402 matcher.addURI(CONTACTS_AUTHORITY, "organizations/#", ORGANIZATIONS_ID); 4403 matcher.addURI(CONTACTS_AUTHORITY, "voice_dialer_timestamp", VOICE_DIALER_TIMESTAMP); 4404 matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, 4405 SEARCH_SUGGESTIONS); 4406 matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 4407 SEARCH_SUGGESTIONS); 4408 matcher.addURI(CONTACTS_AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#", 4409 SEARCH_SHORTCUT); 4410 matcher.addURI(CONTACTS_AUTHORITY, "settings", SETTINGS); 4411 4412 matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people", LIVE_FOLDERS_PEOPLE); 4413 matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people/*", 4414 LIVE_FOLDERS_PEOPLE_GROUP_NAME); 4415 matcher.addURI(CONTACTS_AUTHORITY, "live_folders/people_with_phones", 4416 LIVE_FOLDERS_PEOPLE_WITH_PHONES); 4417 matcher.addURI(CONTACTS_AUTHORITY, "live_folders/favorites", 4418 LIVE_FOLDERS_PEOPLE_FAVORITES); 4419 4420 // Call log URI matching table 4421 matcher.addURI(CALL_LOG_AUTHORITY, "calls", CALLS); 4422 matcher.addURI(CALL_LOG_AUTHORITY, "calls/filter/*", CALLS_FILTER); 4423 matcher.addURI(CALL_LOG_AUTHORITY, "calls/#", CALLS_ID); 4424 4425 HashMap<String, String> map; 4426 4427 // Create the common people columns 4428 HashMap<String, String> peopleColumns = new HashMap<String, String>(); 4429 peopleColumns.put(PeopleColumns.NAME, People.NAME); 4430 peopleColumns.put(PeopleColumns.NOTES, People.NOTES); 4431 peopleColumns.put(PeopleColumns.TIMES_CONTACTED, People.TIMES_CONTACTED); 4432 peopleColumns.put(PeopleColumns.LAST_TIME_CONTACTED, People.LAST_TIME_CONTACTED); 4433 peopleColumns.put(PeopleColumns.STARRED, People.STARRED); 4434 peopleColumns.put(PeopleColumns.CUSTOM_RINGTONE, People.CUSTOM_RINGTONE); 4435 peopleColumns.put(PeopleColumns.SEND_TO_VOICEMAIL, People.SEND_TO_VOICEMAIL); 4436 peopleColumns.put(PeopleColumns.PHONETIC_NAME, People.PHONETIC_NAME); 4437 peopleColumns.put(PeopleColumns.DISPLAY_NAME, 4438 DISPLAY_NAME_SQL + " AS " + People.DISPLAY_NAME); 4439 peopleColumns.put(PeopleColumns.SORT_STRING, 4440 PHONETICALLY_SORTABLE_STRING_SQL + " AS " + People.SORT_STRING); 4441 4442 // Create the common groups columns 4443 HashMap<String, String> groupsColumns = new HashMap<String, String>(); 4444 groupsColumns.put(GroupsColumns.NAME, Groups.NAME); 4445 groupsColumns.put(GroupsColumns.NOTES, Groups.NOTES); 4446 groupsColumns.put(GroupsColumns.SYSTEM_ID, Groups.SYSTEM_ID); 4447 groupsColumns.put(GroupsColumns.SHOULD_SYNC, Groups.SHOULD_SYNC); 4448 4449 // Create the common presence columns 4450 HashMap<String, String> presenceColumns = new HashMap<String, String>(); 4451 presenceColumns.put(PresenceColumns.IM_PROTOCOL, PresenceColumns.IM_PROTOCOL); 4452 presenceColumns.put(PresenceColumns.IM_HANDLE, PresenceColumns.IM_HANDLE); 4453 presenceColumns.put(PresenceColumns.IM_ACCOUNT, PresenceColumns.IM_ACCOUNT); 4454 presenceColumns.put(PresenceColumns.PRESENCE_STATUS, PresenceColumns.PRESENCE_STATUS); 4455 presenceColumns.put(PresenceColumns.PRESENCE_CUSTOM_STATUS, 4456 PresenceColumns.PRESENCE_CUSTOM_STATUS); 4457 4458 // Create the common sync columns 4459 HashMap<String, String> syncColumns = new HashMap<String, String>(); 4460 syncColumns.put(SyncConstValue._SYNC_ID, SyncConstValue._SYNC_ID); 4461 syncColumns.put(SyncConstValue._SYNC_TIME, SyncConstValue._SYNC_TIME); 4462 syncColumns.put(SyncConstValue._SYNC_VERSION, SyncConstValue._SYNC_VERSION); 4463 syncColumns.put(SyncConstValue._SYNC_LOCAL_ID, SyncConstValue._SYNC_LOCAL_ID); 4464 syncColumns.put(SyncConstValue._SYNC_DIRTY, SyncConstValue._SYNC_DIRTY); 4465 syncColumns.put(SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT); 4466 4467 // Phones columns 4468 HashMap<String, String> phonesColumns = new HashMap<String, String>(); 4469 phonesColumns.put(Phones.NUMBER, Phones.NUMBER); 4470 phonesColumns.put(Phones.NUMBER_KEY, Phones.NUMBER_KEY); 4471 phonesColumns.put(Phones.TYPE, Phones.TYPE); 4472 phonesColumns.put(Phones.LABEL, Phones.LABEL); 4473 4474 // People projection map 4475 map = new HashMap<String, String>(); 4476 map.put(People._ID, "people._id AS " + People._ID); 4477 peopleColumns.put(People.PRIMARY_PHONE_ID, People.PRIMARY_PHONE_ID); 4478 peopleColumns.put(People.PRIMARY_EMAIL_ID, People.PRIMARY_EMAIL_ID); 4479 peopleColumns.put(People.PRIMARY_ORGANIZATION_ID, People.PRIMARY_ORGANIZATION_ID); 4480 map.putAll(peopleColumns); 4481 map.putAll(phonesColumns); 4482 map.putAll(syncColumns); 4483 map.putAll(presenceColumns); 4484 sPeopleProjectionMap = map; 4485 4486 // People with photo projection map 4487 map = new HashMap<String, String>(sPeopleProjectionMap); 4488 map.put("photo_data", "photos.data AS photo_data"); 4489 sPeopleWithPhotoProjectionMap = map; 4490 4491 // People with E-mail or IM projection map 4492 map = new HashMap<String, String>(); 4493 map.put(People._ID, "people._id AS " + People._ID); 4494 map.put(ContactMethods.DATA, "contact_methods." + ContactMethods.DATA + " AS " + ContactMethods.DATA); 4495 map.put(ContactMethods.KIND, "contact_methods." + ContactMethods.KIND + " AS " + ContactMethods.KIND); 4496 map.putAll(peopleColumns); 4497 sPeopleWithEmailOrImProjectionMap = map; 4498 4499 // Groups projection map 4500 map = new HashMap<String, String>(); 4501 map.put(Groups._ID, Groups._ID); 4502 map.putAll(groupsColumns); 4503 map.putAll(syncColumns); 4504 sGroupsProjectionMap = map; 4505 4506 // Group Membership projection map 4507 map = new HashMap<String, String>(); 4508 map.put(GroupMembership._ID, "groupmembership._id AS " + GroupMembership._ID); 4509 map.put(GroupMembership.PERSON_ID, GroupMembership.PERSON_ID); 4510 map.put(GroupMembership.GROUP_ID, "groups._id AS " + GroupMembership.GROUP_ID); 4511 map.put(GroupMembership.GROUP_SYNC_ACCOUNT, GroupMembership.GROUP_SYNC_ACCOUNT); 4512 map.put(GroupMembership.GROUP_SYNC_ID, GroupMembership.GROUP_SYNC_ID); 4513 map.putAll(groupsColumns); 4514 sGroupMembershipProjectionMap = map; 4515 4516 // Use this when you need to force items to the top of a times_contacted list 4517 map = new HashMap<String, String>(sPeopleProjectionMap); 4518 map.put(People.TIMES_CONTACTED, Long.MAX_VALUE + " AS " + People.TIMES_CONTACTED); 4519 map.put("photo_data", "photos.data AS photo_data"); 4520 sStrequentStarredProjectionMap = map; 4521 4522 // Calls projection map 4523 map = new HashMap<String, String>(); 4524 map.put(Calls._ID, Calls._ID); 4525 map.put(Calls.NUMBER, Calls.NUMBER); 4526 map.put(Calls.DATE, Calls.DATE); 4527 map.put(Calls.DURATION, Calls.DURATION); 4528 map.put(Calls.TYPE, Calls.TYPE); 4529 map.put(Calls.NEW, Calls.NEW); 4530 map.put(Calls.CACHED_NAME, Calls.CACHED_NAME); 4531 map.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE); 4532 map.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL); 4533 sCallsProjectionMap = map; 4534 4535 // Phones projection map 4536 map = new HashMap<String, String>(); 4537 map.put(Phones._ID, "phones._id AS " + Phones._ID); 4538 map.putAll(phonesColumns); 4539 map.put(Phones.PERSON_ID, "phones.person AS " + Phones.PERSON_ID); 4540 map.put(Phones.ISPRIMARY, Phones.ISPRIMARY); 4541 map.putAll(peopleColumns); 4542 sPhonesProjectionMap = map; 4543 4544 // Phones with presence projection map 4545 map = new HashMap<String, String>(sPhonesProjectionMap); 4546 map.putAll(presenceColumns); 4547 sPhonesWithPresenceProjectionMap = map; 4548 4549 // Organizations projection map 4550 map = new HashMap<String, String>(); 4551 map.put(Organizations._ID, "organizations._id AS " + Organizations._ID); 4552 map.put(Organizations.LABEL, Organizations.LABEL); 4553 map.put(Organizations.TYPE, Organizations.TYPE); 4554 map.put(Organizations.PERSON_ID, Organizations.PERSON_ID); 4555 map.put(Organizations.COMPANY, Organizations.COMPANY); 4556 map.put(Organizations.TITLE, Organizations.TITLE); 4557 map.put(Organizations.ISPRIMARY, Organizations.ISPRIMARY); 4558 sOrganizationsProjectionMap = map; 4559 4560 // Extensions projection map 4561 map = new HashMap<String, String>(); 4562 map.put(Extensions._ID, Extensions._ID); 4563 map.put(Extensions.NAME, Extensions.NAME); 4564 map.put(Extensions.VALUE, Extensions.VALUE); 4565 map.put(Extensions.PERSON_ID, Extensions.PERSON_ID); 4566 sExtensionsProjectionMap = map; 4567 4568 // Contact methods projection map 4569 map = new HashMap<String, String>(); 4570 map.put(ContactMethods._ID, "contact_methods._id AS " + ContactMethods._ID); 4571 map.put(ContactMethods.KIND, ContactMethods.KIND); 4572 map.put(ContactMethods.TYPE, ContactMethods.TYPE); 4573 map.put(ContactMethods.LABEL, ContactMethods.LABEL); 4574 map.put(ContactMethods.DATA, ContactMethods.DATA); 4575 map.put(ContactMethods.AUX_DATA, ContactMethods.AUX_DATA); 4576 map.put(ContactMethods.PERSON_ID, "contact_methods.person AS " + ContactMethods.PERSON_ID); 4577 map.put(ContactMethods.ISPRIMARY, ContactMethods.ISPRIMARY); 4578 map.putAll(peopleColumns); 4579 sContactMethodsProjectionMap = map; 4580 4581 // Contact methods with presence projection map 4582 map = new HashMap<String, String>(sContactMethodsProjectionMap); 4583 map.putAll(presenceColumns); 4584 sContactMethodsWithPresenceProjectionMap = map; 4585 4586 // Email search projection map 4587 map = new HashMap<String, String>(); 4588 map.put(ContactMethods.NAME, ContactMethods.NAME); 4589 map.put(ContactMethods.DATA, ContactMethods.DATA); 4590 map.put(ContactMethods._ID, "contact_methods._id AS " + ContactMethods._ID); 4591 sEmailSearchProjectionMap = map; 4592 4593 // Presence projection map 4594 map = new HashMap<String, String>(); 4595 map.put(Presence._ID, "presence._id AS " + Presence._ID); 4596 map.putAll(presenceColumns); 4597 map.putAll(peopleColumns); 4598 sPresenceProjectionMap = map; 4599 4600 // Photos projection map 4601 map = new HashMap<String, String>(); 4602 map.put(Photos._ID, Photos._ID); 4603 map.put(Photos.LOCAL_VERSION, Photos.LOCAL_VERSION); 4604 map.put(Photos.EXISTS_ON_SERVER, Photos.EXISTS_ON_SERVER); 4605 map.put(Photos.SYNC_ERROR, Photos.SYNC_ERROR); 4606 map.put(Photos.PERSON_ID, Photos.PERSON_ID); 4607 map.put(Photos.DATA, Photos.DATA); 4608 map.put(Photos.DOWNLOAD_REQUIRED, "" 4609 + "(exists_on_server!=0 " 4610 + " AND sync_error IS NULL " 4611 + " AND (local_version IS NULL OR _sync_version != local_version)) " 4612 + "AS " + Photos.DOWNLOAD_REQUIRED); 4613 map.putAll(syncColumns); 4614 sPhotosProjectionMap = map; 4615 4616 // Live folder projection 4617 map = new HashMap<String, String>(); 4618 map.put(LiveFolders._ID, "people._id AS " + LiveFolders._ID); 4619 map.put(LiveFolders.NAME, DISPLAY_NAME_SQL + " AS " + LiveFolders.NAME); 4620 // TODO: Put contact photo back when we have a way to display a default icon 4621 // for contacts without a photo 4622 // map.put(LiveFolders.ICON_BITMAP, Photos.DATA + " AS " + LiveFolders.ICON_BITMAP); 4623 sLiveFoldersProjectionMap = map; 4624 4625 // Order by statements 4626 sPhonesKeyOrderBy = buildOrderBy(sPhonesTable, Phones.NUMBER); 4627 sContactMethodsKeyOrderBy = buildOrderBy(sContactMethodsTable, 4628 ContactMethods.DATA, ContactMethods.KIND); 4629 sOrganizationsKeyOrderBy = buildOrderBy(sOrganizationsTable, Organizations.COMPANY); 4630 sGroupmembershipKeyOrderBy = 4631 buildOrderBy(sGroupmembershipTable, GroupMembership.GROUP_SYNC_ACCOUNT); 4632 4633 sPhonesKeyColumns = new String[]{Phones.NUMBER}; 4634 sContactMethodsKeyColumns = new String[]{ContactMethods.DATA, ContactMethods.KIND}; 4635 sOrganizationsKeyColumns = new String[]{Organizations.COMPANY}; 4636 sGroupmembershipKeyColumns = new String[]{GroupMembership.GROUP_SYNC_ACCOUNT}; 4637 sExtensionsKeyColumns = new String[]{Extensions.NAME}; 4638 4639 String groupJoinByLocalId = "groups._id=groupmembership.group_id"; 4640 String groupJoinByServerId = "(" 4641 + "groups._sync_account=groupmembership.group_sync_account" 4642 + " AND " 4643 + "groups._sync_id=groupmembership.group_sync_id" 4644 + ")"; 4645 sGroupsJoinString = "(" + groupJoinByLocalId + " OR " + groupJoinByServerId + ")"; 4646 } 4647} 4648