ContactsDatabaseHelper.java revision b2e27298ae54ec2215eadf98ecc100aedba98d1a
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.providers.contacts; 18 19import com.android.internal.content.SyncStateContentProviderHelper; 20 21import android.content.ContentResolver; 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.pm.ApplicationInfo; 25import android.content.pm.PackageManager; 26import android.content.pm.PackageManager.NameNotFoundException; 27import android.content.res.Resources; 28import android.database.Cursor; 29import android.database.DatabaseUtils; 30import android.database.SQLException; 31import android.database.sqlite.SQLiteDatabase; 32import android.database.sqlite.SQLiteDoneException; 33import android.database.sqlite.SQLiteException; 34import android.database.sqlite.SQLiteOpenHelper; 35import android.database.sqlite.SQLiteQueryBuilder; 36import android.database.sqlite.SQLiteStatement; 37import android.net.Uri; 38import android.os.Binder; 39import android.os.Bundle; 40import android.provider.BaseColumns; 41import android.provider.ContactsContract; 42import android.provider.CallLog.Calls; 43import android.provider.ContactsContract.AggregationExceptions; 44import android.provider.ContactsContract.Contacts; 45import android.provider.ContactsContract.Data; 46import android.provider.ContactsContract.DisplayNameSources; 47import android.provider.ContactsContract.FullNameStyle; 48import android.provider.ContactsContract.Groups; 49import android.provider.ContactsContract.RawContacts; 50import android.provider.ContactsContract.Settings; 51import android.provider.ContactsContract.StatusUpdates; 52import android.provider.ContactsContract.CommonDataKinds.Email; 53import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 54import android.provider.ContactsContract.CommonDataKinds.Nickname; 55import android.provider.ContactsContract.CommonDataKinds.Organization; 56import android.provider.ContactsContract.CommonDataKinds.Phone; 57import android.provider.ContactsContract.CommonDataKinds.StructuredName; 58import android.provider.SocialContract.Activities; 59import android.telephony.PhoneNumberUtils; 60import android.text.TextUtils; 61import android.text.util.Rfc822Token; 62import android.text.util.Rfc822Tokenizer; 63import android.util.Log; 64 65import java.util.HashMap; 66import java.util.Locale; 67 68/** 69 * Database helper for contacts. Designed as a singleton to make sure that all 70 * {@link android.content.ContentProvider} users get the same reference. 71 * Provides handy methods for maintaining package and mime-type lookup tables. 72 */ 73/* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper { 74 private static final String TAG = "ContactsDatabaseHelper"; 75 76 private static final int DATABASE_VERSION = 307; 77 78 private static final String DATABASE_NAME = "contacts2.db"; 79 private static final String DATABASE_PRESENCE = "presence_db"; 80 81 public interface Tables { 82 public static final String CONTACTS = "contacts"; 83 public static final String RAW_CONTACTS = "raw_contacts"; 84 public static final String PACKAGES = "packages"; 85 public static final String MIMETYPES = "mimetypes"; 86 public static final String PHONE_LOOKUP = "phone_lookup"; 87 public static final String NAME_LOOKUP = "name_lookup"; 88 public static final String AGGREGATION_EXCEPTIONS = "agg_exceptions"; 89 public static final String SETTINGS = "settings"; 90 public static final String DATA = "data"; 91 public static final String GROUPS = "groups"; 92 public static final String PRESENCE = "presence"; 93 public static final String AGGREGATED_PRESENCE = "agg_presence"; 94 public static final String NICKNAME_LOOKUP = "nickname_lookup"; 95 public static final String CALLS = "calls"; 96 public static final String CONTACT_ENTITIES = "contact_entities_view"; 97 public static final String CONTACT_ENTITIES_RESTRICTED = "contact_entities_view_restricted"; 98 public static final String STATUS_UPDATES = "status_updates"; 99 public static final String PROPERTIES = "properties"; 100 101 public static final String DATA_JOIN_MIMETYPES = "data " 102 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)"; 103 104 public static final String DATA_JOIN_RAW_CONTACTS = "data " 105 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"; 106 107 public static final String DATA_JOIN_MIMETYPE_RAW_CONTACTS = "data " 108 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) " 109 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"; 110 111 // NOTE: This requires late binding of GroupMembership MIME-type 112 public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = "raw_contacts " 113 + "LEFT OUTER JOIN settings ON (" 114 + "raw_contacts.account_name = settings.account_name AND " 115 + "raw_contacts.account_type = settings.account_type) " 116 + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND " 117 + "data.raw_contact_id = raw_contacts._id) " 118 + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID 119 + ")"; 120 121 // NOTE: This requires late binding of GroupMembership MIME-type 122 public static final String SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS = "settings " 123 + "LEFT OUTER JOIN raw_contacts ON (" 124 + "raw_contacts.account_name = settings.account_name AND " 125 + "raw_contacts.account_type = settings.account_type) " 126 + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND " 127 + "data.raw_contact_id = raw_contacts._id) " 128 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; 129 130 public static final String DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS = "data " 131 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) " 132 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) " 133 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; 134 135 public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS = "data " 136 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) " 137 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) " 138 + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) " 139 + "LEFT OUTER JOIN groups " 140 + " ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' " 141 + " AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "; 142 143 public static final String GROUPS_JOIN_PACKAGES = "groups " 144 + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id)"; 145 146 147 public static final String ACTIVITIES = "activities"; 148 149 public static final String ACTIVITIES_JOIN_MIMETYPES = "activities " 150 + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id)"; 151 152 public static final String ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS = 153 "activities " 154 + "LEFT OUTER JOIN packages ON (activities.package_id = packages._id) " 155 + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id) " 156 + "LEFT OUTER JOIN raw_contacts ON (activities.author_contact_id = " + 157 "raw_contacts._id) " 158 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)"; 159 160 public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup " 161 + "INNER JOIN raw_contacts ON (name_lookup.raw_contact_id = raw_contacts._id)"; 162 } 163 164 public interface Views { 165 public static final String DATA_ALL = "view_data"; 166 public static final String DATA_RESTRICTED = "view_data_restricted"; 167 168 public static final String RAW_CONTACTS_ALL = "view_raw_contacts"; 169 public static final String RAW_CONTACTS_RESTRICTED = "view_raw_contacts_restricted"; 170 171 public static final String CONTACTS_ALL = "view_contacts"; 172 public static final String CONTACTS_RESTRICTED = "view_contacts_restricted"; 173 174 public static final String GROUPS_ALL = "view_groups"; 175 } 176 177 public interface Clauses { 178 final String MIMETYPE_IS_GROUP_MEMBERSHIP = MimetypesColumns.CONCRETE_MIMETYPE + "='" 179 + GroupMembership.CONTENT_ITEM_TYPE + "'"; 180 181 final String BELONGS_TO_GROUP = DataColumns.CONCRETE_GROUP_ID + "=" 182 + GroupsColumns.CONCRETE_ID; 183 184 final String HAVING_NO_GROUPS = "COUNT(" + DataColumns.CONCRETE_GROUP_ID + ") == 0"; 185 186 final String GROUP_BY_ACCOUNT_CONTACT_ID = SettingsColumns.CONCRETE_ACCOUNT_NAME + "," 187 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "," + RawContacts.CONTACT_ID; 188 189 final String RAW_CONTACT_IS_LOCAL = RawContactsColumns.CONCRETE_ACCOUNT_NAME 190 + " IS NULL AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL"; 191 192 final String ZERO_GROUP_MEMBERSHIPS = "COUNT(" + GroupsColumns.CONCRETE_ID + ")=0"; 193 194 final String OUTER_RAW_CONTACTS = "outer_raw_contacts"; 195 final String OUTER_RAW_CONTACTS_ID = OUTER_RAW_CONTACTS + "." + RawContacts._ID; 196 197 final String CONTACT_IS_VISIBLE = 198 "SELECT " + 199 "MAX((SELECT (CASE WHEN " + 200 "(CASE" + 201 " WHEN " + RAW_CONTACT_IS_LOCAL + 202 " THEN 1 " + 203 " WHEN " + ZERO_GROUP_MEMBERSHIPS + 204 " THEN " + Settings.UNGROUPED_VISIBLE + 205 " ELSE MAX(" + Groups.GROUP_VISIBLE + ")" + 206 "END)=1 THEN 1 ELSE 0 END)" + 207 " FROM " + Tables.RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS + 208 " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + OUTER_RAW_CONTACTS_ID + "))" + 209 " FROM " + Tables.RAW_CONTACTS + " AS " + OUTER_RAW_CONTACTS + 210 " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + 211 " GROUP BY " + RawContacts.CONTACT_ID; 212 213 final String GROUP_HAS_ACCOUNT_AND_SOURCE_ID = Groups.SOURCE_ID + "=? AND " 214 + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?"; 215 } 216 217 public interface ContactsColumns { 218 /** 219 * This flag is set for a contact if it has only one constituent raw contact and 220 * it is restricted. 221 */ 222 public static final String SINGLE_IS_RESTRICTED = "single_is_restricted"; 223 224 public static final String LAST_STATUS_UPDATE_ID = "status_update_id"; 225 226 public static final String CONCRETE_ID = Tables.CONTACTS + "." + BaseColumns._ID; 227 228 public static final String CONCRETE_TIMES_CONTACTED = Tables.CONTACTS + "." 229 + Contacts.TIMES_CONTACTED; 230 public static final String CONCRETE_LAST_TIME_CONTACTED = Tables.CONTACTS + "." 231 + Contacts.LAST_TIME_CONTACTED; 232 public static final String CONCRETE_STARRED = Tables.CONTACTS + "." + Contacts.STARRED; 233 public static final String CONCRETE_CUSTOM_RINGTONE = Tables.CONTACTS + "." 234 + Contacts.CUSTOM_RINGTONE; 235 public static final String CONCRETE_SEND_TO_VOICEMAIL = Tables.CONTACTS + "." 236 + Contacts.SEND_TO_VOICEMAIL; 237 public static final String CONCRETE_LOOKUP_KEY = Tables.CONTACTS + "." 238 + Contacts.LOOKUP_KEY; 239 } 240 241 public interface RawContactsColumns { 242 public static final String CONCRETE_ID = 243 Tables.RAW_CONTACTS + "." + BaseColumns._ID; 244 public static final String CONCRETE_ACCOUNT_NAME = 245 Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_NAME; 246 public static final String CONCRETE_ACCOUNT_TYPE = 247 Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE; 248 public static final String CONCRETE_SOURCE_ID = 249 Tables.RAW_CONTACTS + "." + RawContacts.SOURCE_ID; 250 public static final String CONCRETE_VERSION = 251 Tables.RAW_CONTACTS + "." + RawContacts.VERSION; 252 public static final String CONCRETE_DIRTY = 253 Tables.RAW_CONTACTS + "." + RawContacts.DIRTY; 254 public static final String CONCRETE_DELETED = 255 Tables.RAW_CONTACTS + "." + RawContacts.DELETED; 256 public static final String CONCRETE_SYNC1 = 257 Tables.RAW_CONTACTS + "." + RawContacts.SYNC1; 258 public static final String CONCRETE_SYNC2 = 259 Tables.RAW_CONTACTS + "." + RawContacts.SYNC2; 260 public static final String CONCRETE_SYNC3 = 261 Tables.RAW_CONTACTS + "." + RawContacts.SYNC3; 262 public static final String CONCRETE_SYNC4 = 263 Tables.RAW_CONTACTS + "." + RawContacts.SYNC4; 264 public static final String CONCRETE_STARRED = 265 Tables.RAW_CONTACTS + "." + RawContacts.STARRED; 266 public static final String CONCRETE_IS_RESTRICTED = 267 Tables.RAW_CONTACTS + "." + RawContacts.IS_RESTRICTED; 268 269 public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY; 270 public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE; 271 public static final String AGGREGATION_NEEDED = "aggregation_needed"; 272 public static final String CONTACT_IN_VISIBLE_GROUP = "contact_in_visible_group"; 273 274 public static final String CONCRETE_DISPLAY_NAME = 275 Tables.RAW_CONTACTS + "." + DISPLAY_NAME; 276 public static final String CONCRETE_CONTACT_ID = 277 Tables.RAW_CONTACTS + "." + RawContacts.CONTACT_ID; 278 public static final String CONCRETE_NAME_VERIFIED = 279 Tables.RAW_CONTACTS + "." + RawContacts.NAME_VERIFIED; 280 } 281 282 public interface DataColumns { 283 public static final String PACKAGE_ID = "package_id"; 284 public static final String MIMETYPE_ID = "mimetype_id"; 285 286 public static final String CONCRETE_ID = Tables.DATA + "." + BaseColumns._ID; 287 public static final String CONCRETE_MIMETYPE_ID = Tables.DATA + "." + MIMETYPE_ID; 288 public static final String CONCRETE_RAW_CONTACT_ID = Tables.DATA + "." 289 + Data.RAW_CONTACT_ID; 290 public static final String CONCRETE_GROUP_ID = Tables.DATA + "." 291 + GroupMembership.GROUP_ROW_ID; 292 293 public static final String CONCRETE_DATA1 = Tables.DATA + "." + Data.DATA1; 294 public static final String CONCRETE_DATA2 = Tables.DATA + "." + Data.DATA2; 295 public static final String CONCRETE_DATA3 = Tables.DATA + "." + Data.DATA3; 296 public static final String CONCRETE_DATA4 = Tables.DATA + "." + Data.DATA4; 297 public static final String CONCRETE_DATA5 = Tables.DATA + "." + Data.DATA5; 298 public static final String CONCRETE_DATA6 = Tables.DATA + "." + Data.DATA6; 299 public static final String CONCRETE_DATA7 = Tables.DATA + "." + Data.DATA7; 300 public static final String CONCRETE_DATA8 = Tables.DATA + "." + Data.DATA8; 301 public static final String CONCRETE_DATA9 = Tables.DATA + "." + Data.DATA9; 302 public static final String CONCRETE_DATA10 = Tables.DATA + "." + Data.DATA10; 303 public static final String CONCRETE_DATA11 = Tables.DATA + "." + Data.DATA11; 304 public static final String CONCRETE_DATA12 = Tables.DATA + "." + Data.DATA12; 305 public static final String CONCRETE_DATA13 = Tables.DATA + "." + Data.DATA13; 306 public static final String CONCRETE_DATA14 = Tables.DATA + "." + Data.DATA14; 307 public static final String CONCRETE_DATA15 = Tables.DATA + "." + Data.DATA15; 308 public static final String CONCRETE_IS_PRIMARY = Tables.DATA + "." + Data.IS_PRIMARY; 309 public static final String CONCRETE_PACKAGE_ID = Tables.DATA + "." + PACKAGE_ID; 310 } 311 312 // Used only for legacy API support 313 public interface ExtensionsColumns { 314 public static final String NAME = Data.DATA1; 315 public static final String VALUE = Data.DATA2; 316 } 317 318 public interface GroupMembershipColumns { 319 public static final String RAW_CONTACT_ID = Data.RAW_CONTACT_ID; 320 public static final String GROUP_ROW_ID = GroupMembership.GROUP_ROW_ID; 321 } 322 323 public interface PhoneColumns { 324 public static final String NORMALIZED_NUMBER = Data.DATA4; 325 public static final String CONCRETE_NORMALIZED_NUMBER = DataColumns.CONCRETE_DATA4; 326 } 327 328 public interface GroupsColumns { 329 public static final String PACKAGE_ID = "package_id"; 330 331 public static final String CONCRETE_ID = Tables.GROUPS + "." + BaseColumns._ID; 332 public static final String CONCRETE_SOURCE_ID = Tables.GROUPS + "." + Groups.SOURCE_ID; 333 public static final String CONCRETE_ACCOUNT_NAME = Tables.GROUPS + "." + Groups.ACCOUNT_NAME; 334 public static final String CONCRETE_ACCOUNT_TYPE = Tables.GROUPS + "." + Groups.ACCOUNT_TYPE; 335 } 336 337 public interface ActivitiesColumns { 338 public static final String PACKAGE_ID = "package_id"; 339 public static final String MIMETYPE_ID = "mimetype_id"; 340 } 341 342 public interface PhoneLookupColumns { 343 public static final String _ID = BaseColumns._ID; 344 public static final String DATA_ID = "data_id"; 345 public static final String RAW_CONTACT_ID = "raw_contact_id"; 346 public static final String NORMALIZED_NUMBER = "normalized_number"; 347 public static final String MIN_MATCH = "min_match"; 348 } 349 350 public interface NameLookupColumns { 351 public static final String RAW_CONTACT_ID = "raw_contact_id"; 352 public static final String DATA_ID = "data_id"; 353 public static final String NORMALIZED_NAME = "normalized_name"; 354 public static final String NAME_TYPE = "name_type"; 355 } 356 357 public final static class NameLookupType { 358 public static final int NAME_EXACT = 0; 359 public static final int NAME_VARIANT = 1; 360 public static final int NAME_COLLATION_KEY = 2; 361 public static final int NICKNAME = 3; 362 public static final int EMAIL_BASED_NICKNAME = 4; 363 public static final int ORGANIZATION = 5; 364 public static final int NAME_SHORTHAND = 6; 365 public static final int NAME_CONSONANTS = 7; 366 367 // This is the highest name lookup type code plus one 368 public static final int TYPE_COUNT = 8; 369 370 public static boolean isBasedOnStructuredName(int nameLookupType) { 371 return nameLookupType == NameLookupType.NAME_EXACT 372 || nameLookupType == NameLookupType.NAME_VARIANT 373 || nameLookupType == NameLookupType.NAME_COLLATION_KEY; 374 } 375 } 376 377 public interface PackagesColumns { 378 public static final String _ID = BaseColumns._ID; 379 public static final String PACKAGE = "package"; 380 381 public static final String CONCRETE_ID = Tables.PACKAGES + "." + _ID; 382 } 383 384 public interface MimetypesColumns { 385 public static final String _ID = BaseColumns._ID; 386 public static final String MIMETYPE = "mimetype"; 387 388 public static final String CONCRETE_ID = Tables.MIMETYPES + "." + BaseColumns._ID; 389 public static final String CONCRETE_MIMETYPE = Tables.MIMETYPES + "." + MIMETYPE; 390 } 391 392 public interface AggregationExceptionColumns { 393 public static final String _ID = BaseColumns._ID; 394 } 395 396 public interface NicknameLookupColumns { 397 public static final String NAME = "name"; 398 public static final String CLUSTER = "cluster"; 399 } 400 401 public interface SettingsColumns { 402 public static final String CONCRETE_ACCOUNT_NAME = Tables.SETTINGS + "." 403 + Settings.ACCOUNT_NAME; 404 public static final String CONCRETE_ACCOUNT_TYPE = Tables.SETTINGS + "." 405 + Settings.ACCOUNT_TYPE; 406 } 407 408 public interface PresenceColumns { 409 String RAW_CONTACT_ID = "presence_raw_contact_id"; 410 String CONTACT_ID = "presence_contact_id"; 411 } 412 413 public interface AggregatedPresenceColumns { 414 String CONTACT_ID = "presence_contact_id"; 415 416 String CONCRETE_CONTACT_ID = Tables.AGGREGATED_PRESENCE + "." + CONTACT_ID; 417 } 418 419 public interface StatusUpdatesColumns { 420 String DATA_ID = "status_update_data_id"; 421 422 String CONCRETE_DATA_ID = Tables.STATUS_UPDATES + "." + DATA_ID; 423 424 String CONCRETE_PRESENCE = Tables.STATUS_UPDATES + "." + StatusUpdates.PRESENCE; 425 String CONCRETE_STATUS = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS; 426 String CONCRETE_STATUS_TIMESTAMP = Tables.STATUS_UPDATES + "." 427 + StatusUpdates.STATUS_TIMESTAMP; 428 String CONCRETE_STATUS_RES_PACKAGE = Tables.STATUS_UPDATES + "." 429 + StatusUpdates.STATUS_RES_PACKAGE; 430 String CONCRETE_STATUS_LABEL = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS_LABEL; 431 String CONCRETE_STATUS_ICON = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS_ICON; 432 } 433 434 public interface ContactsStatusUpdatesColumns { 435 String ALIAS = "contacts_" + Tables.STATUS_UPDATES; 436 437 String CONCRETE_DATA_ID = ALIAS + "." + StatusUpdatesColumns.DATA_ID; 438 439 String CONCRETE_PRESENCE = ALIAS + "." + StatusUpdates.PRESENCE; 440 String CONCRETE_STATUS = ALIAS + "." + StatusUpdates.STATUS; 441 String CONCRETE_STATUS_TIMESTAMP = ALIAS + "." + StatusUpdates.STATUS_TIMESTAMP; 442 String CONCRETE_STATUS_RES_PACKAGE = ALIAS + "." + StatusUpdates.STATUS_RES_PACKAGE; 443 String CONCRETE_STATUS_LABEL = ALIAS + "." + StatusUpdates.STATUS_LABEL; 444 String CONCRETE_STATUS_ICON = ALIAS + "." + StatusUpdates.STATUS_ICON; 445 } 446 447 public interface PropertiesColumns { 448 String PROPERTY_KEY = "property_key"; 449 String PROPERTY_VALUE = "property_value"; 450 } 451 452 /** In-memory cache of previously found MIME-type mappings */ 453 private final HashMap<String, Long> mMimetypeCache = new HashMap<String, Long>(); 454 /** In-memory cache of previously found package name mappings */ 455 private final HashMap<String, Long> mPackageCache = new HashMap<String, Long>(); 456 457 458 /** Compiled statements for querying and inserting mappings */ 459 private SQLiteStatement mMimetypeQuery; 460 private SQLiteStatement mPackageQuery; 461 private SQLiteStatement mContactIdQuery; 462 private SQLiteStatement mAggregationModeQuery; 463 private SQLiteStatement mMimetypeInsert; 464 private SQLiteStatement mPackageInsert; 465 private SQLiteStatement mDataMimetypeQuery; 466 private SQLiteStatement mActivitiesMimetypeQuery; 467 468 private final Context mContext; 469 private final SyncStateContentProviderHelper mSyncState; 470 471 472 /** Compiled statements for updating {@link Contacts#IN_VISIBLE_GROUP}. */ 473 private SQLiteStatement mVisibleSpecificUpdate; 474 private SQLiteStatement mVisibleUpdateRawContacts; 475 private SQLiteStatement mVisibleSpecificUpdateRawContacts; 476 477 private boolean mReopenDatabase = false; 478 479 private static ContactsDatabaseHelper sSingleton = null; 480 481 private boolean mUseStrictPhoneNumberComparison; 482 483 /** 484 * List of package names with access to {@link RawContacts#IS_RESTRICTED} data. 485 */ 486 private String[] mUnrestrictedPackages; 487 488 public static synchronized ContactsDatabaseHelper getInstance(Context context) { 489 if (sSingleton == null) { 490 sSingleton = new ContactsDatabaseHelper(context); 491 } 492 return sSingleton; 493 } 494 495 /** 496 * Private constructor, callers except unit tests should obtain an instance through 497 * {@link #getInstance(android.content.Context)} instead. 498 */ 499 ContactsDatabaseHelper(Context context) { 500 super(context, DATABASE_NAME, null, DATABASE_VERSION); 501 if (false) Log.i(TAG, "Creating OpenHelper"); 502 Resources resources = context.getResources(); 503 504 mContext = context; 505 mSyncState = new SyncStateContentProviderHelper(); 506 mUseStrictPhoneNumberComparison = 507 resources.getBoolean( 508 com.android.internal.R.bool.config_use_strict_phone_number_comparation); 509 int resourceId = resources.getIdentifier("unrestricted_packages", "array", 510 context.getPackageName()); 511 if (resourceId != 0) { 512 mUnrestrictedPackages = resources.getStringArray(resourceId); 513 } else { 514 mUnrestrictedPackages = new String[0]; 515 } 516 } 517 518 @Override 519 public void onOpen(SQLiteDatabase db) { 520 mSyncState.onDatabaseOpened(db); 521 522 // Create compiled statements for package and mimetype lookups 523 mMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns._ID + " FROM " 524 + Tables.MIMETYPES + " WHERE " + MimetypesColumns.MIMETYPE + "=?"); 525 mPackageQuery = db.compileStatement("SELECT " + PackagesColumns._ID + " FROM " 526 + Tables.PACKAGES + " WHERE " + PackagesColumns.PACKAGE + "=?"); 527 mContactIdQuery = db.compileStatement("SELECT " + RawContacts.CONTACT_ID + " FROM " 528 + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?"); 529 mAggregationModeQuery = db.compileStatement("SELECT " + RawContacts.AGGREGATION_MODE 530 + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?"); 531 mMimetypeInsert = db.compileStatement("INSERT INTO " + Tables.MIMETYPES + "(" 532 + MimetypesColumns.MIMETYPE + ") VALUES (?)"); 533 mPackageInsert = db.compileStatement("INSERT INTO " + Tables.PACKAGES + "(" 534 + PackagesColumns.PACKAGE + ") VALUES (?)"); 535 536 mDataMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns.MIMETYPE + " FROM " 537 + Tables.DATA_JOIN_MIMETYPES + " WHERE " + Tables.DATA + "." + Data._ID + "=?"); 538 mActivitiesMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns.MIMETYPE 539 + " FROM " + Tables.ACTIVITIES_JOIN_MIMETYPES + " WHERE " + Tables.ACTIVITIES + "." 540 + Activities._ID + "=?"); 541 542 // Change visibility of a specific contact 543 mVisibleSpecificUpdate = db.compileStatement( 544 "UPDATE " + Tables.CONTACTS + 545 " SET " + Contacts.IN_VISIBLE_GROUP + "=(" + Clauses.CONTACT_IS_VISIBLE + ")" + 546 " WHERE " + ContactsColumns.CONCRETE_ID + "=?"); 547 548 // Return visibility of the aggregate contact joined with the raw contact 549 String contactVisibility = 550 "SELECT " + Contacts.IN_VISIBLE_GROUP + 551 " FROM " + Tables.CONTACTS + 552 " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID; 553 554 // Set visibility of raw contacts to the visibility of corresponding aggregate contacts 555 mVisibleUpdateRawContacts = db.compileStatement( 556 "UPDATE " + Tables.RAW_CONTACTS + 557 " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" 558 + contactVisibility + ")" + 559 " WHERE " + RawContacts.DELETED + "=0" + 560 " AND " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "!=(" 561 + contactVisibility + ")=1"); 562 563 // Set visibility of a raw contact to the visibility of corresponding aggregate contact 564 mVisibleSpecificUpdateRawContacts = db.compileStatement( 565 "UPDATE " + Tables.RAW_CONTACTS + 566 " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" 567 + contactVisibility + ")" + 568 " WHERE " + RawContacts.DELETED + "=0 AND " + RawContacts.CONTACT_ID + "=?"); 569 570 db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";"); 571 db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+ 572 StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," + 573 StatusUpdates.PROTOCOL + " INTEGER NOT NULL," + 574 StatusUpdates.CUSTOM_PROTOCOL + " TEXT," + 575 StatusUpdates.IM_HANDLE + " TEXT," + 576 StatusUpdates.IM_ACCOUNT + " TEXT," + 577 PresenceColumns.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," + 578 PresenceColumns.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," + 579 StatusUpdates.PRESENCE + " INTEGER," + 580 "UNIQUE(" + StatusUpdates.PROTOCOL + ", " + StatusUpdates.CUSTOM_PROTOCOL 581 + ", " + StatusUpdates.IM_HANDLE + ", " + StatusUpdates.IM_ACCOUNT + ")" + 582 ");"); 583 584 db.execSQL("CREATE INDEX IF NOT EXISTS " + DATABASE_PRESENCE + ".presenceIndex" + " ON " 585 + Tables.PRESENCE + " (" + PresenceColumns.RAW_CONTACT_ID + ");"); 586 587 db.execSQL("CREATE TABLE IF NOT EXISTS " 588 + DATABASE_PRESENCE + "." + Tables.AGGREGATED_PRESENCE + " ("+ 589 AggregatedPresenceColumns.CONTACT_ID 590 + " INTEGER PRIMARY KEY REFERENCES contacts(_id)," + 591 StatusUpdates.PRESENCE_STATUS + " INTEGER" + 592 ");"); 593 594 595 db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_deleted" 596 + " BEFORE DELETE ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE 597 + " BEGIN " 598 + " DELETE FROM " + Tables.AGGREGATED_PRESENCE 599 + " WHERE " + AggregatedPresenceColumns.CONTACT_ID + " = " + 600 "(SELECT " + PresenceColumns.CONTACT_ID + 601 " FROM " + Tables.PRESENCE + 602 " WHERE " + PresenceColumns.RAW_CONTACT_ID 603 + "=OLD." + PresenceColumns.RAW_CONTACT_ID + 604 " AND NOT EXISTS" + 605 "(SELECT " + PresenceColumns.RAW_CONTACT_ID + 606 " FROM " + Tables.PRESENCE + 607 " WHERE " + PresenceColumns.CONTACT_ID 608 + "=OLD." + PresenceColumns.CONTACT_ID + 609 " AND " + PresenceColumns.RAW_CONTACT_ID 610 + "!=OLD." + PresenceColumns.RAW_CONTACT_ID + "));" 611 + " END"); 612 613 String replaceAggregatePresenceSql = 614 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "(" 615 + AggregatedPresenceColumns.CONTACT_ID + ", " 616 + StatusUpdates.PRESENCE_STATUS + ")" + 617 " SELECT " + PresenceColumns.CONTACT_ID + "," 618 + "MAX(" + StatusUpdates.PRESENCE_STATUS + ")" + 619 " FROM " + Tables.PRESENCE + 620 " WHERE " + PresenceColumns.CONTACT_ID 621 + "=NEW." + PresenceColumns.CONTACT_ID + ";"; 622 623 db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_inserted" 624 + " AFTER INSERT ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE 625 + " BEGIN " 626 + replaceAggregatePresenceSql 627 + " END"); 628 629 db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_updated" 630 + " AFTER UPDATE ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE 631 + " BEGIN " 632 + replaceAggregatePresenceSql 633 + " END"); 634 } 635 636 @Override 637 public void onCreate(SQLiteDatabase db) { 638 Log.i(TAG, "Bootstrapping database"); 639 640 mSyncState.createDatabase(db); 641 642 // One row per group of contacts corresponding to the same person 643 db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" + 644 BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 645 Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," + 646 Contacts.PHOTO_ID + " INTEGER REFERENCES data(_id)," + 647 Contacts.CUSTOM_RINGTONE + " TEXT," + 648 Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," + 649 Contacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," + 650 Contacts.LAST_TIME_CONTACTED + " INTEGER," + 651 Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," + 652 Contacts.IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 1," + 653 Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," + 654 Contacts.LOOKUP_KEY + " TEXT," + 655 ContactsColumns.LAST_STATUS_UPDATE_ID + " INTEGER REFERENCES data(_id)," + 656 ContactsColumns.SINGLE_IS_RESTRICTED + " INTEGER NOT NULL DEFAULT 0" + 657 ");"); 658 659 db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" + 660 Contacts.IN_VISIBLE_GROUP + 661 ");"); 662 663 db.execSQL("CREATE INDEX contacts_has_phone_index ON " + Tables.CONTACTS + " (" + 664 Contacts.HAS_PHONE_NUMBER + 665 ");"); 666 667 db.execSQL("CREATE INDEX contacts_restricted_index ON " + Tables.CONTACTS + " (" + 668 ContactsColumns.SINGLE_IS_RESTRICTED + 669 ");"); 670 671 db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" + 672 Contacts.NAME_RAW_CONTACT_ID + 673 ");"); 674 675 // Contacts table 676 db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" + 677 RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 678 RawContacts.IS_RESTRICTED + " INTEGER DEFAULT 0," + 679 RawContacts.ACCOUNT_NAME + " STRING DEFAULT NULL, " + 680 RawContacts.ACCOUNT_TYPE + " STRING DEFAULT NULL, " + 681 RawContacts.SOURCE_ID + " TEXT," + 682 RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," + 683 RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," + 684 RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," + 685 RawContacts.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," + 686 RawContacts.AGGREGATION_MODE + " INTEGER NOT NULL DEFAULT " + 687 RawContacts.AGGREGATION_MODE_DEFAULT + "," + 688 RawContactsColumns.AGGREGATION_NEEDED + " INTEGER NOT NULL DEFAULT 1," + 689 RawContacts.CUSTOM_RINGTONE + " TEXT," + 690 RawContacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," + 691 RawContacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," + 692 RawContacts.LAST_TIME_CONTACTED + " INTEGER," + 693 RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," + 694 RawContacts.DISPLAY_NAME_PRIMARY + " TEXT," + 695 RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT," + 696 RawContacts.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " + 697 DisplayNameSources.UNDEFINED + "," + 698 RawContacts.PHONETIC_NAME + " TEXT," + 699 RawContacts.PHONETIC_NAME_STYLE + " TEXT," + 700 RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE LOCALIZED," + 701 RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE LOCALIZED," + 702 RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0," + 703 RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 0," + 704 RawContacts.SYNC1 + " TEXT, " + 705 RawContacts.SYNC2 + " TEXT, " + 706 RawContacts.SYNC3 + " TEXT, " + 707 RawContacts.SYNC4 + " TEXT " + 708 ");"); 709 710 db.execSQL("CREATE INDEX raw_contacts_contact_id_index ON " + Tables.RAW_CONTACTS + " (" + 711 RawContacts.CONTACT_ID + 712 ");"); 713 714 db.execSQL("CREATE INDEX raw_contacts_source_id_index ON " + Tables.RAW_CONTACTS + " (" + 715 RawContacts.SOURCE_ID + ", " + 716 RawContacts.ACCOUNT_TYPE + ", " + 717 RawContacts.ACCOUNT_NAME + 718 ");"); 719 720 // TODO readd the index and investigate a controlled use of it 721// db.execSQL("CREATE INDEX raw_contacts_agg_index ON " + Tables.RAW_CONTACTS + " (" + 722// RawContactsColumns.AGGREGATION_NEEDED + 723// ");"); 724 725 // Package name mapping table 726 db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" + 727 PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 728 PackagesColumns.PACKAGE + " TEXT NOT NULL" + 729 ");"); 730 731 // Mimetype mapping table 732 db.execSQL("CREATE TABLE " + Tables.MIMETYPES + " (" + 733 MimetypesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 734 MimetypesColumns.MIMETYPE + " TEXT NOT NULL" + 735 ");"); 736 737 // Mimetype table requires an index on mime type 738 db.execSQL("CREATE UNIQUE INDEX mime_type ON " + Tables.MIMETYPES + " (" + 739 MimetypesColumns.MIMETYPE + 740 ");"); 741 742 // Public generic data table 743 db.execSQL("CREATE TABLE " + Tables.DATA + " (" + 744 Data._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 745 DataColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," + 746 DataColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," + 747 Data.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," + 748 Data.IS_PRIMARY + " INTEGER NOT NULL DEFAULT 0," + 749 Data.IS_SUPER_PRIMARY + " INTEGER NOT NULL DEFAULT 0," + 750 Data.DATA_VERSION + " INTEGER NOT NULL DEFAULT 0," + 751 Data.DATA1 + " TEXT," + 752 Data.DATA2 + " TEXT," + 753 Data.DATA3 + " TEXT," + 754 Data.DATA4 + " TEXT," + 755 Data.DATA5 + " TEXT," + 756 Data.DATA6 + " TEXT," + 757 Data.DATA7 + " TEXT," + 758 Data.DATA8 + " TEXT," + 759 Data.DATA9 + " TEXT," + 760 Data.DATA10 + " TEXT," + 761 Data.DATA11 + " TEXT," + 762 Data.DATA12 + " TEXT," + 763 Data.DATA13 + " TEXT," + 764 Data.DATA14 + " TEXT," + 765 Data.DATA15 + " TEXT," + 766 Data.SYNC1 + " TEXT, " + 767 Data.SYNC2 + " TEXT, " + 768 Data.SYNC3 + " TEXT, " + 769 Data.SYNC4 + " TEXT " + 770 ");"); 771 772 db.execSQL("CREATE INDEX data_raw_contact_id ON " + Tables.DATA + " (" + 773 Data.RAW_CONTACT_ID + 774 ");"); 775 776 /** 777 * For email lookup and similar queries. 778 */ 779 db.execSQL("CREATE INDEX data_mimetype_data1_index ON " + Tables.DATA + " (" + 780 DataColumns.MIMETYPE_ID + "," + 781 Data.DATA1 + 782 ");"); 783 784 // Private phone numbers table used for lookup 785 db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" + 786 PhoneLookupColumns.DATA_ID 787 + " INTEGER PRIMARY KEY REFERENCES data(_id) NOT NULL," + 788 PhoneLookupColumns.RAW_CONTACT_ID 789 + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," + 790 PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," + 791 PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" + 792 ");"); 793 794 db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" + 795 PhoneLookupColumns.NORMALIZED_NUMBER + "," + 796 PhoneLookupColumns.RAW_CONTACT_ID + "," + 797 PhoneLookupColumns.DATA_ID + 798 ");"); 799 800 db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" + 801 PhoneLookupColumns.MIN_MATCH + "," + 802 PhoneLookupColumns.RAW_CONTACT_ID + "," + 803 PhoneLookupColumns.DATA_ID + 804 ");"); 805 806 // Private name/nickname table used for lookup 807 db.execSQL("CREATE TABLE " + Tables.NAME_LOOKUP + " (" + 808 NameLookupColumns.DATA_ID 809 + " INTEGER REFERENCES data(_id) NOT NULL," + 810 NameLookupColumns.RAW_CONTACT_ID 811 + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," + 812 NameLookupColumns.NORMALIZED_NAME + " TEXT NOT NULL," + 813 NameLookupColumns.NAME_TYPE + " INTEGER NOT NULL," + 814 "PRIMARY KEY (" 815 + NameLookupColumns.DATA_ID + ", " 816 + NameLookupColumns.NORMALIZED_NAME + ", " 817 + NameLookupColumns.NAME_TYPE + ")" + 818 ");"); 819 820 db.execSQL("CREATE INDEX name_lookup_raw_contact_id_index ON " + Tables.NAME_LOOKUP + " (" + 821 NameLookupColumns.RAW_CONTACT_ID + 822 ");"); 823 824 db.execSQL("CREATE TABLE " + Tables.NICKNAME_LOOKUP + " (" + 825 NicknameLookupColumns.NAME + " TEXT," + 826 NicknameLookupColumns.CLUSTER + " TEXT" + 827 ");"); 828 829 db.execSQL("CREATE UNIQUE INDEX nickname_lookup_index ON " + Tables.NICKNAME_LOOKUP + " (" + 830 NicknameLookupColumns.NAME + ", " + 831 NicknameLookupColumns.CLUSTER + 832 ");"); 833 834 // Groups table 835 db.execSQL("CREATE TABLE " + Tables.GROUPS + " (" + 836 Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 837 GroupsColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," + 838 Groups.ACCOUNT_NAME + " STRING DEFAULT NULL, " + 839 Groups.ACCOUNT_TYPE + " STRING DEFAULT NULL, " + 840 Groups.SOURCE_ID + " TEXT," + 841 Groups.VERSION + " INTEGER NOT NULL DEFAULT 1," + 842 Groups.DIRTY + " INTEGER NOT NULL DEFAULT 0," + 843 Groups.TITLE + " TEXT," + 844 Groups.TITLE_RES + " INTEGER," + 845 Groups.NOTES + " TEXT," + 846 Groups.SYSTEM_ID + " TEXT," + 847 Groups.DELETED + " INTEGER NOT NULL DEFAULT 0," + 848 Groups.GROUP_VISIBLE + " INTEGER NOT NULL DEFAULT 0," + 849 Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1," + 850 Groups.SYNC1 + " TEXT, " + 851 Groups.SYNC2 + " TEXT, " + 852 Groups.SYNC3 + " TEXT, " + 853 Groups.SYNC4 + " TEXT " + 854 ");"); 855 856 db.execSQL("CREATE INDEX groups_source_id_index ON " + Tables.GROUPS + " (" + 857 Groups.SOURCE_ID + ", " + 858 Groups.ACCOUNT_TYPE + ", " + 859 Groups.ACCOUNT_NAME + 860 ");"); 861 862 db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.AGGREGATION_EXCEPTIONS + " (" + 863 AggregationExceptionColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 864 AggregationExceptions.TYPE + " INTEGER NOT NULL, " + 865 AggregationExceptions.RAW_CONTACT_ID1 866 + " INTEGER REFERENCES raw_contacts(_id), " + 867 AggregationExceptions.RAW_CONTACT_ID2 868 + " INTEGER REFERENCES raw_contacts(_id)" + 869 ");"); 870 871 db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index1 ON " + 872 Tables.AGGREGATION_EXCEPTIONS + " (" + 873 AggregationExceptions.RAW_CONTACT_ID1 + ", " + 874 AggregationExceptions.RAW_CONTACT_ID2 + 875 ");"); 876 877 db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index2 ON " + 878 Tables.AGGREGATION_EXCEPTIONS + " (" + 879 AggregationExceptions.RAW_CONTACT_ID2 + ", " + 880 AggregationExceptions.RAW_CONTACT_ID1 + 881 ");"); 882 883 db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.SETTINGS + " (" + 884 Settings.ACCOUNT_NAME + " STRING NOT NULL," + 885 Settings.ACCOUNT_TYPE + " STRING NOT NULL," + 886 Settings.UNGROUPED_VISIBLE + " INTEGER NOT NULL DEFAULT 0," + 887 Settings.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1, " + 888 "PRIMARY KEY (" + Settings.ACCOUNT_NAME + ", " + 889 Settings.ACCOUNT_TYPE + ") ON CONFLICT REPLACE" + 890 ");"); 891 892 // The table for recent calls is here so we can do table joins 893 // on people, phones, and calls all in one place. 894 db.execSQL("CREATE TABLE " + Tables.CALLS + " (" + 895 Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 896 Calls.NUMBER + " TEXT," + 897 Calls.DATE + " INTEGER," + 898 Calls.DURATION + " INTEGER," + 899 Calls.TYPE + " INTEGER," + 900 Calls.NEW + " INTEGER," + 901 Calls.CACHED_NAME + " TEXT," + 902 Calls.CACHED_NUMBER_TYPE + " INTEGER," + 903 Calls.CACHED_NUMBER_LABEL + " TEXT" + 904 ");"); 905 906 // Activities table 907 db.execSQL("CREATE TABLE " + Tables.ACTIVITIES + " (" + 908 Activities._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 909 ActivitiesColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," + 910 ActivitiesColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," + 911 Activities.RAW_ID + " TEXT," + 912 Activities.IN_REPLY_TO + " TEXT," + 913 Activities.AUTHOR_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," + 914 Activities.TARGET_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," + 915 Activities.PUBLISHED + " INTEGER NOT NULL," + 916 Activities.THREAD_PUBLISHED + " INTEGER NOT NULL," + 917 Activities.TITLE + " TEXT NOT NULL," + 918 Activities.SUMMARY + " TEXT," + 919 Activities.LINK + " TEXT, " + 920 Activities.THUMBNAIL + " BLOB" + 921 ");"); 922 923 db.execSQL("CREATE TABLE " + Tables.STATUS_UPDATES + " (" + 924 StatusUpdatesColumns.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," + 925 StatusUpdates.STATUS + " TEXT," + 926 StatusUpdates.STATUS_TIMESTAMP + " INTEGER," + 927 StatusUpdates.STATUS_RES_PACKAGE + " TEXT, " + 928 StatusUpdates.STATUS_LABEL + " INTEGER, " + 929 StatusUpdates.STATUS_ICON + " INTEGER" + 930 ");"); 931 932 db.execSQL("CREATE TABLE " + Tables.PROPERTIES + " (" + 933 PropertiesColumns.PROPERTY_KEY + " TEXT PRIMARY KEY, " + 934 PropertiesColumns.PROPERTY_VALUE + " TEXT " + 935 ");"); 936 937 createContactsViews(db); 938 createGroupsView(db); 939 createContactEntitiesView(db); 940 createContactsTriggers(db); 941 createContactsIndexes(db); 942 943 loadNicknameLookupTable(db); 944 945 // Add the legacy API support views, etc 946 LegacyApiSupport.createDatabase(db); 947 948 // This will create a sqlite_stat1 table that is used for query optimization 949 db.execSQL("ANALYZE;"); 950 951 updateSqliteStats(db); 952 953 // We need to close and reopen the database connection so that the stats are 954 // taken into account. Make a note of it and do the actual reopening in the 955 // getWritableDatabase method. 956 mReopenDatabase = true; 957 958 ContentResolver.requestSync(null /* all accounts */, 959 ContactsContract.AUTHORITY, new Bundle()); 960 } 961 962 private static void createContactsTriggers(SQLiteDatabase db) { 963 964 /* 965 * Automatically delete Data rows when a raw contact is deleted. 966 */ 967 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_deleted;"); 968 db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_deleted " 969 + " BEFORE DELETE ON " + Tables.RAW_CONTACTS 970 + " BEGIN " 971 + " DELETE FROM " + Tables.DATA 972 + " WHERE " + Data.RAW_CONTACT_ID 973 + "=OLD." + RawContacts._ID + ";" 974 + " DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS 975 + " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 976 + "=OLD." + RawContacts._ID 977 + " OR " + AggregationExceptions.RAW_CONTACT_ID2 978 + "=OLD." + RawContacts._ID + ";" 979 + " DELETE FROM " + Tables.CONTACTS 980 + " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID 981 + " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS 982 + " WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID 983 + " )=1;" 984 + " END"); 985 986 987 db.execSQL("DROP TRIGGER IF EXISTS contacts_times_contacted;"); 988 db.execSQL("DROP TRIGGER IF EXISTS raw_contacts_times_contacted;"); 989 990 /* 991 * Triggers that update {@link RawContacts#VERSION} when the contact is 992 * marked for deletion or any time a data row is inserted, updated or 993 * deleted. 994 */ 995 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;"); 996 db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted " 997 + " AFTER UPDATE ON " + Tables.RAW_CONTACTS 998 + " BEGIN " 999 + " UPDATE " + Tables.RAW_CONTACTS 1000 + " SET " 1001 + RawContacts.VERSION + "=OLD." + RawContacts.VERSION + "+1 " 1002 + " WHERE " + RawContacts._ID + "=OLD." + RawContacts._ID 1003 + " AND NEW." + RawContacts.DELETED + "!= OLD." + RawContacts.DELETED + ";" 1004 + " END"); 1005 1006 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_updated;"); 1007 db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA 1008 + " BEGIN " 1009 + " UPDATE " + Tables.DATA 1010 + " SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 " 1011 + " WHERE " + Data._ID + "=OLD." + Data._ID + ";" 1012 + " UPDATE " + Tables.RAW_CONTACTS 1013 + " SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 " 1014 + " WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";" 1015 + " END"); 1016 1017 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_deleted;"); 1018 db.execSQL("CREATE TRIGGER " + Tables.DATA + "_deleted BEFORE DELETE ON " + Tables.DATA 1019 + " BEGIN " 1020 + " UPDATE " + Tables.RAW_CONTACTS 1021 + " SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 " 1022 + " WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";" 1023 + " DELETE FROM " + Tables.PHONE_LOOKUP 1024 + " WHERE " + PhoneLookupColumns.DATA_ID + "=OLD." + Data._ID + ";" 1025 + " DELETE FROM " + Tables.STATUS_UPDATES 1026 + " WHERE " + StatusUpdatesColumns.DATA_ID + "=OLD." + Data._ID + ";" 1027 + " DELETE FROM " + Tables.NAME_LOOKUP 1028 + " WHERE " + NameLookupColumns.DATA_ID + "=OLD." + Data._ID + ";" 1029 + " END"); 1030 1031 1032 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_updated1;"); 1033 db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 " 1034 + " AFTER UPDATE ON " + Tables.GROUPS 1035 + " BEGIN " 1036 + " UPDATE " + Tables.GROUPS 1037 + " SET " 1038 + Groups.VERSION + "=OLD." + Groups.VERSION + "+1" 1039 + " WHERE " + Groups._ID + "=OLD." + Groups._ID + ";" 1040 + " END"); 1041 } 1042 1043 private static void createContactsIndexes(SQLiteDatabase db) { 1044 db.execSQL("DROP INDEX IF EXISTS name_lookup_index"); 1045 db.execSQL("CREATE INDEX name_lookup_index ON " + Tables.NAME_LOOKUP + " (" + 1046 NameLookupColumns.NORMALIZED_NAME + "," + 1047 NameLookupColumns.NAME_TYPE + ", " + 1048 NameLookupColumns.RAW_CONTACT_ID + ", " + 1049 NameLookupColumns.DATA_ID + 1050 ");"); 1051 1052 db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key1_index"); 1053 db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" + 1054 RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," + 1055 RawContacts.SORT_KEY_PRIMARY + 1056 ");"); 1057 1058 db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key2_index"); 1059 db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" + 1060 RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," + 1061 RawContacts.SORT_KEY_ALTERNATIVE + 1062 ");"); 1063 } 1064 1065 private static void createContactsViews(SQLiteDatabase db) { 1066 db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS_ALL + ";"); 1067 db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS_RESTRICTED + ";"); 1068 db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_ALL + ";"); 1069 db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_RESTRICTED + ";"); 1070 db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS_ALL + ";"); 1071 db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS_RESTRICTED + ";"); 1072 1073 String dataColumns = 1074 Data.IS_PRIMARY + ", " 1075 + Data.IS_SUPER_PRIMARY + ", " 1076 + Data.DATA_VERSION + ", " 1077 + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + "," 1078 + MimetypesColumns.MIMETYPE + " AS " + Data.MIMETYPE + ", " 1079 + Data.DATA1 + ", " 1080 + Data.DATA2 + ", " 1081 + Data.DATA3 + ", " 1082 + Data.DATA4 + ", " 1083 + Data.DATA5 + ", " 1084 + Data.DATA6 + ", " 1085 + Data.DATA7 + ", " 1086 + Data.DATA8 + ", " 1087 + Data.DATA9 + ", " 1088 + Data.DATA10 + ", " 1089 + Data.DATA11 + ", " 1090 + Data.DATA12 + ", " 1091 + Data.DATA13 + ", " 1092 + Data.DATA14 + ", " 1093 + Data.DATA15 + ", " 1094 + Data.SYNC1 + ", " 1095 + Data.SYNC2 + ", " 1096 + Data.SYNC3 + ", " 1097 + Data.SYNC4; 1098 1099 String syncColumns = 1100 RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + "," 1101 + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + "," 1102 + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + "," 1103 + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS " + RawContacts.NAME_VERIFIED + "," 1104 + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + "," 1105 + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + "," 1106 + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + "," 1107 + RawContactsColumns.CONCRETE_SYNC2 + " AS " + RawContacts.SYNC2 + "," 1108 + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + "," 1109 + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4; 1110 1111 String contactOptionColumns = 1112 ContactsColumns.CONCRETE_CUSTOM_RINGTONE 1113 + " AS " + RawContacts.CUSTOM_RINGTONE + "," 1114 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL 1115 + " AS " + RawContacts.SEND_TO_VOICEMAIL + "," 1116 + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED 1117 + " AS " + RawContacts.LAST_TIME_CONTACTED + "," 1118 + ContactsColumns.CONCRETE_TIMES_CONTACTED 1119 + " AS " + RawContacts.TIMES_CONTACTED + "," 1120 + ContactsColumns.CONCRETE_STARRED 1121 + " AS " + RawContacts.STARRED; 1122 1123 String contactNameColumns = 1124 "name_raw_contact." + RawContacts.DISPLAY_NAME_SOURCE 1125 + " AS " + Contacts.DISPLAY_NAME_SOURCE + ", " 1126 + "name_raw_contact." + RawContacts.DISPLAY_NAME_PRIMARY 1127 + " AS " + Contacts.DISPLAY_NAME_PRIMARY + ", " 1128 + "name_raw_contact." + RawContacts.DISPLAY_NAME_ALTERNATIVE 1129 + " AS " + Contacts.DISPLAY_NAME_ALTERNATIVE + ", " 1130 + "name_raw_contact." + RawContacts.PHONETIC_NAME 1131 + " AS " + Contacts.PHONETIC_NAME + ", " 1132 + "name_raw_contact." + RawContacts.PHONETIC_NAME_STYLE 1133 + " AS " + Contacts.PHONETIC_NAME_STYLE + ", " 1134 + "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY 1135 + " AS " + Contacts.SORT_KEY_PRIMARY + ", " 1136 + "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE 1137 + " AS " + Contacts.SORT_KEY_ALTERNATIVE + ", " 1138 + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP 1139 + " AS " + Contacts.IN_VISIBLE_GROUP; 1140 1141 String dataSelect = "SELECT " 1142 + DataColumns.CONCRETE_ID + " AS " + Data._ID + "," 1143 + Data.RAW_CONTACT_ID + ", " 1144 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", " 1145 + syncColumns + ", " 1146 + dataColumns + ", " 1147 + contactOptionColumns + ", " 1148 + contactNameColumns + ", " 1149 + Contacts.LOOKUP_KEY + ", " 1150 + Contacts.PHOTO_ID + ", " 1151 + ContactsColumns.LAST_STATUS_UPDATE_ID + ", " 1152 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID 1153 + " FROM " + Tables.DATA 1154 + " JOIN " + Tables.MIMETYPES + " ON (" 1155 + DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")" 1156 + " JOIN " + Tables.RAW_CONTACTS + " ON (" 1157 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")" 1158 + " JOIN " + Tables.CONTACTS + " ON (" 1159 + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")" 1160 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON(" 1161 + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")" 1162 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON (" 1163 + DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")" 1164 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON (" 1165 + MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 1166 + "' AND " + GroupsColumns.CONCRETE_ID + "=" 1167 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")"; 1168 1169 db.execSQL("CREATE VIEW " + Views.DATA_ALL + " AS " + dataSelect); 1170 db.execSQL("CREATE VIEW " + Views.DATA_RESTRICTED + " AS " + dataSelect + " WHERE " 1171 + RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0"); 1172 1173 String rawContactOptionColumns = 1174 RawContacts.CUSTOM_RINGTONE + "," 1175 + RawContacts.SEND_TO_VOICEMAIL + "," 1176 + RawContacts.LAST_TIME_CONTACTED + "," 1177 + RawContacts.TIMES_CONTACTED + "," 1178 + RawContacts.STARRED; 1179 1180 String rawContactsSelect = "SELECT " 1181 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + "," 1182 + RawContacts.CONTACT_ID + ", " 1183 + RawContacts.AGGREGATION_MODE + ", " 1184 + RawContacts.DELETED + ", " 1185 + RawContacts.DISPLAY_NAME_SOURCE + ", " 1186 + RawContacts.DISPLAY_NAME_PRIMARY + ", " 1187 + RawContacts.DISPLAY_NAME_ALTERNATIVE + ", " 1188 + RawContacts.PHONETIC_NAME + ", " 1189 + RawContacts.PHONETIC_NAME_STYLE + ", " 1190 + RawContacts.SORT_KEY_PRIMARY + ", " 1191 + RawContacts.SORT_KEY_ALTERNATIVE + ", " 1192 + rawContactOptionColumns + ", " 1193 + syncColumns 1194 + " FROM " + Tables.RAW_CONTACTS; 1195 1196 db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS_ALL + " AS " + rawContactsSelect); 1197 db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS_RESTRICTED + " AS " + rawContactsSelect 1198 + " WHERE " + RawContacts.IS_RESTRICTED + "=0"); 1199 1200 String contactsColumns = 1201 ContactsColumns.CONCRETE_CUSTOM_RINGTONE 1202 + " AS " + Contacts.CUSTOM_RINGTONE + ", " 1203 + contactNameColumns + ", " 1204 + Contacts.HAS_PHONE_NUMBER + ", " 1205 + Contacts.LOOKUP_KEY + ", " 1206 + Contacts.PHOTO_ID + ", " 1207 + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED 1208 + " AS " + Contacts.LAST_TIME_CONTACTED + ", " 1209 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL 1210 + " AS " + Contacts.SEND_TO_VOICEMAIL + ", " 1211 + ContactsColumns.CONCRETE_STARRED 1212 + " AS " + Contacts.STARRED + ", " 1213 + ContactsColumns.CONCRETE_TIMES_CONTACTED 1214 + " AS " + Contacts.TIMES_CONTACTED + ", " 1215 + ContactsColumns.LAST_STATUS_UPDATE_ID; 1216 1217 String contactsSelect = "SELECT " 1218 + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + "," 1219 + contactsColumns 1220 + " FROM " + Tables.CONTACTS 1221 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON(" 1222 + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"; 1223 1224 db.execSQL("CREATE VIEW " + Views.CONTACTS_ALL + " AS " + contactsSelect); 1225 db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + contactsSelect 1226 + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0"); 1227 } 1228 1229 private static void createGroupsView(SQLiteDatabase db) { 1230 db.execSQL("DROP VIEW IF EXISTS " + Views.GROUPS_ALL + ";"); 1231 String groupsColumns = 1232 Groups.ACCOUNT_NAME + "," 1233 + Groups.ACCOUNT_TYPE + "," 1234 + Groups.SOURCE_ID + "," 1235 + Groups.VERSION + "," 1236 + Groups.DIRTY + "," 1237 + Groups.TITLE + "," 1238 + Groups.TITLE_RES + "," 1239 + Groups.NOTES + "," 1240 + Groups.SYSTEM_ID + "," 1241 + Groups.DELETED + "," 1242 + Groups.GROUP_VISIBLE + "," 1243 + Groups.SHOULD_SYNC + "," 1244 + Groups.SYNC1 + "," 1245 + Groups.SYNC2 + "," 1246 + Groups.SYNC3 + "," 1247 + Groups.SYNC4 + "," 1248 + PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE; 1249 1250 String groupsSelect = "SELECT " 1251 + GroupsColumns.CONCRETE_ID + " AS " + Groups._ID + "," 1252 + groupsColumns 1253 + " FROM " + Tables.GROUPS_JOIN_PACKAGES; 1254 1255 db.execSQL("CREATE VIEW " + Views.GROUPS_ALL + " AS " + groupsSelect); 1256 } 1257 1258 private static void createContactEntitiesView(SQLiteDatabase db) { 1259 db.execSQL("DROP VIEW IF EXISTS " + Tables.CONTACT_ENTITIES + ";"); 1260 db.execSQL("DROP VIEW IF EXISTS " + Tables.CONTACT_ENTITIES_RESTRICTED + ";"); 1261 1262 String contactEntitiesSelect = "SELECT " 1263 + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + "," 1264 + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + "," 1265 + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + "," 1266 + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + "," 1267 + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + "," 1268 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + "," 1269 + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS " + RawContacts.NAME_VERIFIED + "," 1270 + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + "," 1271 + RawContacts.CONTACT_ID + ", " 1272 + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ", " 1273 + RawContactsColumns.CONCRETE_SYNC2 + " AS " + RawContacts.SYNC2 + ", " 1274 + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + ", " 1275 + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4 + ", " 1276 + Data.MIMETYPE + ", " 1277 + Data.DATA1 + ", " 1278 + Data.DATA2 + ", " 1279 + Data.DATA3 + ", " 1280 + Data.DATA4 + ", " 1281 + Data.DATA5 + ", " 1282 + Data.DATA6 + ", " 1283 + Data.DATA7 + ", " 1284 + Data.DATA8 + ", " 1285 + Data.DATA9 + ", " 1286 + Data.DATA10 + ", " 1287 + Data.DATA11 + ", " 1288 + Data.DATA12 + ", " 1289 + Data.DATA13 + ", " 1290 + Data.DATA14 + ", " 1291 + Data.DATA15 + ", " 1292 + Data.SYNC1 + ", " 1293 + Data.SYNC2 + ", " 1294 + Data.SYNC3 + ", " 1295 + Data.SYNC4 + ", " 1296 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ", " 1297 + Data.IS_PRIMARY + ", " 1298 + Data.IS_SUPER_PRIMARY + ", " 1299 + Data.DATA_VERSION + ", " 1300 + DataColumns.CONCRETE_ID + " AS " + RawContacts.Entity.DATA_ID + "," 1301 + RawContactsColumns.CONCRETE_STARRED + " AS " + RawContacts.STARRED + "," 1302 + RawContactsColumns.CONCRETE_IS_RESTRICTED + " AS " 1303 + RawContacts.IS_RESTRICTED + "," 1304 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID 1305 + " FROM " + Tables.RAW_CONTACTS 1306 + " LEFT OUTER JOIN " + Tables.DATA + " ON (" 1307 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")" 1308 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON (" 1309 + DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")" 1310 + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON (" 1311 + DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")" 1312 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON (" 1313 + MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 1314 + "' AND " + GroupsColumns.CONCRETE_ID + "=" 1315 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")"; 1316 1317 db.execSQL("CREATE VIEW " + Tables.CONTACT_ENTITIES + " AS " 1318 + contactEntitiesSelect); 1319 db.execSQL("CREATE VIEW " + Tables.CONTACT_ENTITIES_RESTRICTED + " AS " 1320 + contactEntitiesSelect + " WHERE " + RawContacts.IS_RESTRICTED + "=0"); 1321 } 1322 1323 @Override 1324 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 1325 if (oldVersion < 99) { 1326 Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion 1327 + ", data will be lost!"); 1328 1329 db.execSQL("DROP TABLE IF EXISTS " + Tables.CONTACTS + ";"); 1330 db.execSQL("DROP TABLE IF EXISTS " + Tables.RAW_CONTACTS + ";"); 1331 db.execSQL("DROP TABLE IF EXISTS " + Tables.PACKAGES + ";"); 1332 db.execSQL("DROP TABLE IF EXISTS " + Tables.MIMETYPES + ";"); 1333 db.execSQL("DROP TABLE IF EXISTS " + Tables.DATA + ";"); 1334 db.execSQL("DROP TABLE IF EXISTS " + Tables.PHONE_LOOKUP + ";"); 1335 db.execSQL("DROP TABLE IF EXISTS " + Tables.NAME_LOOKUP + ";"); 1336 db.execSQL("DROP TABLE IF EXISTS " + Tables.NICKNAME_LOOKUP + ";"); 1337 db.execSQL("DROP TABLE IF EXISTS " + Tables.GROUPS + ";"); 1338 db.execSQL("DROP TABLE IF EXISTS " + Tables.ACTIVITIES + ";"); 1339 db.execSQL("DROP TABLE IF EXISTS " + Tables.CALLS + ";"); 1340 db.execSQL("DROP TABLE IF EXISTS " + Tables.SETTINGS + ";"); 1341 db.execSQL("DROP TABLE IF EXISTS " + Tables.STATUS_UPDATES + ";"); 1342 1343 // TODO: we should not be dropping agg_exceptions and contact_options. In case that 1344 // table's schema changes, we should try to preserve the data, because it was entered 1345 // by the user and has never been synched to the server. 1346 db.execSQL("DROP TABLE IF EXISTS " + Tables.AGGREGATION_EXCEPTIONS + ";"); 1347 1348 onCreate(db); 1349 return; 1350 } 1351 1352 Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion); 1353 1354 boolean upgradeViewsAndTriggers = false; 1355 boolean upgradeNameLookup = false; 1356 1357 if (oldVersion == 99) { 1358 upgradeViewsAndTriggers = true; 1359 oldVersion++; 1360 } 1361 1362 if (oldVersion == 100) { 1363 db.execSQL("CREATE INDEX IF NOT EXISTS mimetypes_mimetype_index ON " 1364 + Tables.MIMETYPES + " (" 1365 + MimetypesColumns.MIMETYPE + "," 1366 + MimetypesColumns._ID + ");"); 1367 updateIndexStats(db, Tables.MIMETYPES, 1368 "mimetypes_mimetype_index", "50 1 1"); 1369 1370 upgradeViewsAndTriggers = true; 1371 oldVersion++; 1372 } 1373 1374 if (oldVersion == 101) { 1375 upgradeViewsAndTriggers = true; 1376 oldVersion++; 1377 } 1378 1379 if (oldVersion == 102) { 1380 upgradeViewsAndTriggers = true; 1381 oldVersion++; 1382 } 1383 1384 if (oldVersion == 103) { 1385 upgradeViewsAndTriggers = true; 1386 oldVersion++; 1387 } 1388 1389 if (oldVersion == 104 || oldVersion == 201) { 1390 LegacyApiSupport.createSettingsTable(db); 1391 upgradeViewsAndTriggers = true; 1392 oldVersion++; 1393 } 1394 1395 if (oldVersion == 105) { 1396 upgradeToVersion202(db); 1397 upgradeNameLookup = true; 1398 oldVersion = 202; 1399 } 1400 1401 if (oldVersion == 202) { 1402 upgradeToVersion203(db); 1403 upgradeViewsAndTriggers = true; 1404 oldVersion++; 1405 } 1406 1407 if (oldVersion == 203) { 1408 upgradeViewsAndTriggers = true; 1409 oldVersion++; 1410 } 1411 1412 if (oldVersion == 204) { 1413 upgradeToVersion205(db); 1414 upgradeViewsAndTriggers = true; 1415 oldVersion++; 1416 } 1417 1418 if (oldVersion == 205) { 1419 upgrateToVersion206(db); 1420 upgradeViewsAndTriggers = true; 1421 oldVersion++; 1422 } 1423 1424 if (oldVersion == 206) { 1425 upgradeToVersion300(db); 1426 oldVersion = 300; 1427 } 1428 1429 if (oldVersion == 300) { 1430 upgradeViewsAndTriggers = true; 1431 oldVersion = 301; 1432 } 1433 1434 if (oldVersion == 301) { 1435 upgradeViewsAndTriggers = true; 1436 oldVersion = 302; 1437 } 1438 1439 if (oldVersion == 302) { 1440 upgradeEmailToVersion303(db); 1441 upgradeNicknameToVersion303(db); 1442 oldVersion = 303; 1443 } 1444 1445 if (oldVersion == 303) { 1446 upgradeToVersion304(db); 1447 oldVersion = 304; 1448 } 1449 1450 if (oldVersion == 304) { 1451 upgradeNameLookup = true; 1452 oldVersion = 305; 1453 } 1454 1455 if (oldVersion == 305) { 1456 upgradeToVersion306(db); 1457 oldVersion = 306; 1458 } 1459 1460 if (oldVersion == 306) { 1461 upgradeToVersion307(db); 1462 oldVersion = 307; 1463 } 1464 1465 if (upgradeViewsAndTriggers) { 1466 createContactsViews(db); 1467 createGroupsView(db); 1468 createContactEntitiesView(db); 1469 createContactsTriggers(db); 1470 createContactsIndexes(db); 1471 LegacyApiSupport.createViews(db); 1472 updateSqliteStats(db); 1473 mReopenDatabase = true; 1474 } 1475 1476 if (upgradeNameLookup) { 1477 rebuildNameLookup(db); 1478 } 1479 1480 if (oldVersion != newVersion) { 1481 throw new IllegalStateException( 1482 "error upgrading the database to version " + newVersion); 1483 } 1484 } 1485 1486 private void upgradeToVersion202(SQLiteDatabase db) { 1487 db.execSQL( 1488 "ALTER TABLE " + Tables.PHONE_LOOKUP + 1489 " ADD " + PhoneLookupColumns.MIN_MATCH + " TEXT;"); 1490 1491 db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" + 1492 PhoneLookupColumns.MIN_MATCH + "," + 1493 PhoneLookupColumns.RAW_CONTACT_ID + "," + 1494 PhoneLookupColumns.DATA_ID + 1495 ");"); 1496 1497 updateIndexStats(db, Tables.PHONE_LOOKUP, 1498 "phone_lookup_min_match_index", "10000 2 2 1"); 1499 1500 SQLiteStatement update = db.compileStatement( 1501 "UPDATE " + Tables.PHONE_LOOKUP + 1502 " SET " + PhoneLookupColumns.MIN_MATCH + "=?" + 1503 " WHERE " + PhoneLookupColumns.DATA_ID + "=?"); 1504 1505 // Populate the new column 1506 Cursor c = db.query(Tables.PHONE_LOOKUP + " JOIN " + Tables.DATA + 1507 " ON (" + PhoneLookupColumns.DATA_ID + "=" + DataColumns.CONCRETE_ID + ")", 1508 new String[]{Data._ID, Phone.NUMBER}, null, null, null, null, null); 1509 try { 1510 while (c.moveToNext()) { 1511 long dataId = c.getLong(0); 1512 String number = c.getString(1); 1513 if (!TextUtils.isEmpty(number)) { 1514 update.bindString(1, PhoneNumberUtils.toCallerIDMinMatch(number)); 1515 update.bindLong(2, dataId); 1516 update.execute(); 1517 } 1518 } 1519 } finally { 1520 c.close(); 1521 } 1522 } 1523 1524 private void upgradeToVersion203(SQLiteDatabase db) { 1525 db.execSQL( 1526 "ALTER TABLE " + Tables.CONTACTS + 1527 " ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)"); 1528 db.execSQL( 1529 "ALTER TABLE " + Tables.RAW_CONTACTS + 1530 " ADD " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP 1531 + " INTEGER NOT NULL DEFAULT 0"); 1532 1533 // For each Contact, find the RawContact that contributed the display name 1534 db.execSQL( 1535 "UPDATE " + Tables.CONTACTS + 1536 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" + 1537 " SELECT " + RawContacts._ID + 1538 " FROM " + Tables.RAW_CONTACTS + 1539 " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + 1540 " AND " + RawContactsColumns.CONCRETE_DISPLAY_NAME + "=" + 1541 Tables.CONTACTS + "." + Contacts.DISPLAY_NAME + 1542 " ORDER BY " + RawContacts._ID + 1543 " LIMIT 1)" 1544 ); 1545 1546 db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" + 1547 Contacts.NAME_RAW_CONTACT_ID + 1548 ");"); 1549 1550 // If for some unknown reason we missed some names, let's make sure there are 1551 // no contacts without a name, picking a raw contact "at random". 1552 db.execSQL( 1553 "UPDATE " + Tables.CONTACTS + 1554 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" + 1555 " SELECT " + RawContacts._ID + 1556 " FROM " + Tables.RAW_CONTACTS + 1557 " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + 1558 " ORDER BY " + RawContacts._ID + 1559 " LIMIT 1)" + 1560 " WHERE " + Contacts.NAME_RAW_CONTACT_ID + " IS NULL" 1561 ); 1562 1563 // Wipe out DISPLAY_NAME on the Contacts table as it is no longer in use. 1564 db.execSQL( 1565 "UPDATE " + Tables.CONTACTS + 1566 " SET " + Contacts.DISPLAY_NAME + "=NULL" 1567 ); 1568 1569 // Copy the IN_VISIBLE_GROUP flag down to all raw contacts to allow 1570 // indexing on (display_name, in_visible_group) 1571 db.execSQL( 1572 "UPDATE " + Tables.RAW_CONTACTS + 1573 " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" + 1574 "SELECT " + Contacts.IN_VISIBLE_GROUP + 1575 " FROM " + Tables.CONTACTS + 1576 " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" + 1577 " WHERE " + RawContacts.CONTACT_ID + " NOT NULL" 1578 ); 1579 1580 db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" + 1581 RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," + 1582 RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" + 1583 ");"); 1584 1585 db.execSQL("DROP INDEX contacts_visible_index"); 1586 db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" + 1587 Contacts.IN_VISIBLE_GROUP + 1588 ");"); 1589 } 1590 1591 private void upgradeToVersion205(SQLiteDatabase db) { 1592 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1593 + " ADD " + RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT;"); 1594 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1595 + " ADD " + RawContacts.PHONETIC_NAME + " TEXT;"); 1596 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1597 + " ADD " + RawContacts.PHONETIC_NAME_STYLE + " INTEGER;"); 1598 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1599 + " ADD " + RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE LOCALIZED;"); 1600 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1601 + " ADD " + RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE LOCALIZED;"); 1602 1603 final Locale locale = Locale.getDefault(); 1604 1605 NameSplitter splitter = createNameSplitter(); 1606 1607 SQLiteStatement rawContactUpdate = db.compileStatement( 1608 "UPDATE " + Tables.RAW_CONTACTS + 1609 " SET " + 1610 RawContacts.DISPLAY_NAME_PRIMARY + "=?," + 1611 RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," + 1612 RawContacts.PHONETIC_NAME + "=?," + 1613 RawContacts.PHONETIC_NAME_STYLE + "=?," + 1614 RawContacts.SORT_KEY_PRIMARY + "=?," + 1615 RawContacts.SORT_KEY_ALTERNATIVE + "=?" + 1616 " WHERE " + RawContacts._ID + "=?"); 1617 1618 upgradeStructuredNamesToVersion205(db, rawContactUpdate, splitter); 1619 upgradeOrganizationsToVersion205(db, rawContactUpdate, splitter); 1620 1621 db.execSQL("DROP INDEX raw_contact_sort_key1_index"); 1622 db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" + 1623 RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," + 1624 RawContacts.SORT_KEY_PRIMARY + 1625 ");"); 1626 1627 db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" + 1628 RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," + 1629 RawContacts.SORT_KEY_ALTERNATIVE + 1630 ");"); 1631 } 1632 1633 private interface StructName205Query { 1634 String TABLE = Tables.DATA_JOIN_RAW_CONTACTS; 1635 1636 String COLUMNS[] = { 1637 DataColumns.CONCRETE_ID, 1638 Data.RAW_CONTACT_ID, 1639 RawContacts.DISPLAY_NAME_SOURCE, 1640 RawContacts.DISPLAY_NAME_PRIMARY, 1641 StructuredName.PREFIX, 1642 StructuredName.GIVEN_NAME, 1643 StructuredName.MIDDLE_NAME, 1644 StructuredName.FAMILY_NAME, 1645 StructuredName.SUFFIX, 1646 StructuredName.PHONETIC_FAMILY_NAME, 1647 StructuredName.PHONETIC_MIDDLE_NAME, 1648 StructuredName.PHONETIC_GIVEN_NAME, 1649 }; 1650 1651 int ID = 0; 1652 int RAW_CONTACT_ID = 1; 1653 int DISPLAY_NAME_SOURCE = 2; 1654 int DISPLAY_NAME = 3; 1655 int PREFIX = 4; 1656 int GIVEN_NAME = 5; 1657 int MIDDLE_NAME = 6; 1658 int FAMILY_NAME = 7; 1659 int SUFFIX = 8; 1660 int PHONETIC_FAMILY_NAME = 9; 1661 int PHONETIC_MIDDLE_NAME = 10; 1662 int PHONETIC_GIVEN_NAME = 11; 1663 } 1664 1665 private void upgradeStructuredNamesToVersion205(SQLiteDatabase db, 1666 SQLiteStatement rawContactUpdate, NameSplitter splitter) { 1667 1668 // Process structured names to detect the style of the full name and phonetic name 1669 1670 long mMimeType; 1671 try { 1672 mMimeType = DatabaseUtils.longForQuery(db, 1673 "SELECT " + MimetypesColumns._ID + 1674 " FROM " + Tables.MIMETYPES + 1675 " WHERE " + MimetypesColumns.MIMETYPE 1676 + "='" + StructuredName.CONTENT_ITEM_TYPE + "'", null); 1677 } catch (SQLiteDoneException e) { 1678 // No structured names in the database 1679 return; 1680 } 1681 1682 SQLiteStatement structuredNameUpdate = db.compileStatement( 1683 "UPDATE " + Tables.DATA + 1684 " SET " + 1685 StructuredName.FULL_NAME_STYLE + "=?," + 1686 StructuredName.DISPLAY_NAME + "=?," + 1687 StructuredName.PHONETIC_NAME_STYLE + "=?" + 1688 " WHERE " + Data._ID + "=?"); 1689 1690 NameSplitter.Name name = new NameSplitter.Name(); 1691 StringBuilder sb = new StringBuilder(); 1692 Cursor cursor = db.query(StructName205Query.TABLE, 1693 StructName205Query.COLUMNS, 1694 DataColumns.MIMETYPE_ID + "=" + mMimeType, null, null, null, null); 1695 try { 1696 while (cursor.moveToNext()) { 1697 long dataId = cursor.getLong(StructName205Query.ID); 1698 long rawContactId = cursor.getLong(StructName205Query.RAW_CONTACT_ID); 1699 int displayNameSource = cursor.getInt(StructName205Query.DISPLAY_NAME_SOURCE); 1700 String displayName = cursor.getString(StructName205Query.DISPLAY_NAME); 1701 1702 name.clear(); 1703 name.prefix = cursor.getString(StructName205Query.PREFIX); 1704 name.givenNames = cursor.getString(StructName205Query.GIVEN_NAME); 1705 name.middleName = cursor.getString(StructName205Query.MIDDLE_NAME); 1706 name.familyName = cursor.getString(StructName205Query.FAMILY_NAME); 1707 name.suffix = cursor.getString(StructName205Query.SUFFIX); 1708 name.phoneticFamilyName = cursor.getString(StructName205Query.PHONETIC_FAMILY_NAME); 1709 name.phoneticMiddleName = cursor.getString(StructName205Query.PHONETIC_MIDDLE_NAME); 1710 name.phoneticGivenName = cursor.getString(StructName205Query.PHONETIC_GIVEN_NAME); 1711 1712 upgradeNameToVersion205(dataId, rawContactId, displayNameSource, displayName, name, 1713 structuredNameUpdate, rawContactUpdate, splitter, sb); 1714 } 1715 } finally { 1716 cursor.close(); 1717 } 1718 } 1719 1720 private void upgradeNameToVersion205(long dataId, long rawContactId, int displayNameSource, 1721 String currentDisplayName, NameSplitter.Name name, 1722 SQLiteStatement structuredNameUpdate, SQLiteStatement rawContactUpdate, 1723 NameSplitter splitter, StringBuilder sb) { 1724 1725 splitter.guessNameStyle(name); 1726 name.fullNameStyle = splitter.getAdjustedFullNameStyle(name.fullNameStyle); 1727 String displayName = splitter.join(name, true); 1728 1729 structuredNameUpdate.bindLong(1, name.fullNameStyle); 1730 DatabaseUtils.bindObjectToProgram(structuredNameUpdate, 2, displayName); 1731 structuredNameUpdate.bindLong(3, name.phoneticNameStyle); 1732 structuredNameUpdate.bindLong(4, dataId); 1733 structuredNameUpdate.execute(); 1734 1735 if (displayNameSource == DisplayNameSources.STRUCTURED_NAME) { 1736 String displayNameAlternative = splitter.join(name, false); 1737 String phoneticName = splitter.joinPhoneticName(name); 1738 String sortKey = null; 1739 String sortKeyAlternative = null; 1740 1741 if (phoneticName != null) { 1742 sortKey = sortKeyAlternative = phoneticName; 1743 } else if (name.fullNameStyle == FullNameStyle.CHINESE) { 1744 sortKey = sortKeyAlternative = splitter.convertHanziToPinyin(displayName); 1745 } 1746 1747 if (sortKey == null) { 1748 sortKey = displayName; 1749 sortKeyAlternative = displayNameAlternative; 1750 } 1751 1752 updateRawContact205(rawContactUpdate, rawContactId, displayName, 1753 displayNameAlternative, name.phoneticNameStyle, phoneticName, sortKey, 1754 sortKeyAlternative); 1755 } 1756 } 1757 1758 private interface Organization205Query { 1759 String TABLE = Tables.DATA_JOIN_RAW_CONTACTS; 1760 1761 String COLUMNS[] = { 1762 DataColumns.CONCRETE_ID, 1763 Data.RAW_CONTACT_ID, 1764 Organization.COMPANY, 1765 Organization.PHONETIC_NAME, 1766 }; 1767 1768 int ID = 0; 1769 int RAW_CONTACT_ID = 1; 1770 int COMPANY = 2; 1771 int PHONETIC_NAME = 3; 1772 } 1773 1774 private void upgradeOrganizationsToVersion205(SQLiteDatabase db, 1775 SQLiteStatement rawContactUpdate, NameSplitter splitter) { 1776 final long mimeType = lookupMimeTypeId(db, Organization.CONTENT_ITEM_TYPE); 1777 1778 SQLiteStatement organizationUpdate = db.compileStatement( 1779 "UPDATE " + Tables.DATA + 1780 " SET " + 1781 Organization.PHONETIC_NAME_STYLE + "=?" + 1782 " WHERE " + Data._ID + "=?"); 1783 1784 Cursor cursor = db.query(Organization205Query.TABLE, Organization205Query.COLUMNS, 1785 DataColumns.MIMETYPE_ID + "=" + mimeType + " AND " 1786 + RawContacts.DISPLAY_NAME_SOURCE + "=" + DisplayNameSources.ORGANIZATION, 1787 null, null, null, null); 1788 try { 1789 while (cursor.moveToNext()) { 1790 long dataId = cursor.getLong(Organization205Query.ID); 1791 long rawContactId = cursor.getLong(Organization205Query.RAW_CONTACT_ID); 1792 String company = cursor.getString(Organization205Query.COMPANY); 1793 String phoneticName = cursor.getString(Organization205Query.PHONETIC_NAME); 1794 1795 int phoneticNameStyle = splitter.guessPhoneticNameStyle(phoneticName); 1796 1797 organizationUpdate.bindLong(1, phoneticNameStyle); 1798 organizationUpdate.bindLong(2, dataId); 1799 organizationUpdate.execute(); 1800 1801 String sortKey = null; 1802 if (phoneticName == null && company != null) { 1803 int nameStyle = splitter.guessFullNameStyle(company); 1804 nameStyle = splitter.getAdjustedFullNameStyle(nameStyle); 1805 if (nameStyle == FullNameStyle.CHINESE) { 1806 sortKey = splitter.convertHanziToPinyin(company); 1807 } 1808 } 1809 1810 if (sortKey == null) { 1811 sortKey = company; 1812 } 1813 1814 updateRawContact205(rawContactUpdate, rawContactId, company, 1815 company, phoneticNameStyle, phoneticName, sortKey, sortKey); 1816 } 1817 } finally { 1818 cursor.close(); 1819 } 1820 } 1821 1822 private void updateRawContact205(SQLiteStatement rawContactUpdate, long rawContactId, 1823 String displayName, String displayNameAlternative, int phoneticNameStyle, 1824 String phoneticName, String sortKeyPrimary, String sortKeyAlternative) { 1825 bindString(rawContactUpdate, 1, displayName); 1826 bindString(rawContactUpdate, 2, displayNameAlternative); 1827 bindString(rawContactUpdate, 3, phoneticName); 1828 rawContactUpdate.bindLong(4, phoneticNameStyle); 1829 bindString(rawContactUpdate, 5, sortKeyPrimary); 1830 bindString(rawContactUpdate, 6, sortKeyAlternative); 1831 rawContactUpdate.bindLong(7, rawContactId); 1832 rawContactUpdate.execute(); 1833 } 1834 1835 private void upgrateToVersion206(SQLiteDatabase db) { 1836 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1837 + " ADD " + RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0;"); 1838 } 1839 1840 private interface Organization300Query { 1841 String TABLE = Tables.DATA; 1842 1843 String SELECTION = DataColumns.MIMETYPE_ID + "=?"; 1844 1845 String COLUMNS[] = { 1846 Organization._ID, 1847 Organization.RAW_CONTACT_ID, 1848 Organization.COMPANY, 1849 Organization.TITLE 1850 }; 1851 1852 int ID = 0; 1853 int RAW_CONTACT_ID = 1; 1854 int COMPANY = 2; 1855 int TITLE = 3; 1856 } 1857 1858 /** 1859 * Fix for the bug where name lookup records for organizations would get removed by 1860 * unrelated updates of the data rows. 1861 */ 1862 private void upgradeToVersion300(SQLiteDatabase db) { 1863 final long mimeType = lookupMimeTypeId(db, Organization.CONTENT_ITEM_TYPE); 1864 if (mimeType == -1) { 1865 return; 1866 } 1867 1868 ContentValues values = new ContentValues(); 1869 1870 // Find all data rows with the mime type "organization" 1871 Cursor cursor = db.query(Organization300Query.TABLE, Organization300Query.COLUMNS, 1872 Organization300Query.SELECTION, new String[] {String.valueOf(mimeType)}, 1873 null, null, null); 1874 try { 1875 while (cursor.moveToNext()) { 1876 long dataId = cursor.getLong(Organization300Query.ID); 1877 long rawContactId = cursor.getLong(Organization300Query.RAW_CONTACT_ID); 1878 String company = cursor.getString(Organization300Query.COMPANY); 1879 String title = cursor.getString(Organization300Query.TITLE); 1880 1881 // First delete name lookup if there is any (chances are there won't be) 1882 db.delete(Tables.NAME_LOOKUP, NameLookupColumns.DATA_ID + "=?", 1883 new String[]{String.valueOf(dataId)}); 1884 1885 // Now insert two name lookup records: one for company name, one for title 1886 values.put(NameLookupColumns.DATA_ID, dataId); 1887 values.put(NameLookupColumns.RAW_CONTACT_ID, rawContactId); 1888 values.put(NameLookupColumns.NAME_TYPE, NameLookupType.ORGANIZATION); 1889 1890 if (!TextUtils.isEmpty(company)) { 1891 values.put(NameLookupColumns.NORMALIZED_NAME, 1892 NameNormalizer.normalize(company)); 1893 db.insert(Tables.NAME_LOOKUP, null, values); 1894 } 1895 1896 if (!TextUtils.isEmpty(title)) { 1897 values.put(NameLookupColumns.NORMALIZED_NAME, 1898 NameNormalizer.normalize(title)); 1899 db.insert(Tables.NAME_LOOKUP, null, values); 1900 } 1901 } 1902 } finally { 1903 cursor.close(); 1904 } 1905 } 1906 1907 private static final class Upgrade303Query { 1908 public static final String TABLE = Tables.DATA; 1909 1910 public static final String SELECTION = 1911 DataColumns.MIMETYPE_ID + "=?" + 1912 " AND " + Data._ID + " NOT IN " + 1913 "(SELECT " + NameLookupColumns.DATA_ID + " FROM " + Tables.NAME_LOOKUP + ")" + 1914 " AND " + Data.DATA1 + " NOT NULL"; 1915 1916 public static final String COLUMNS[] = { 1917 Data._ID, 1918 Data.RAW_CONTACT_ID, 1919 Data.DATA1, 1920 }; 1921 1922 public static final int ID = 0; 1923 public static final int RAW_CONTACT_ID = 1; 1924 public static final int DATA1 = 2; 1925 } 1926 1927 /** 1928 * The {@link ContactsProvider2#update} method was deleting name lookup for new 1929 * emails during the sync. We need to restore the lost name lookup rows. 1930 */ 1931 private void upgradeEmailToVersion303(SQLiteDatabase db) { 1932 final long mimeTypeId = lookupMimeTypeId(db, Email.CONTENT_ITEM_TYPE); 1933 if (mimeTypeId == -1) { 1934 return; 1935 } 1936 1937 ContentValues values = new ContentValues(); 1938 1939 // Find all data rows with the mime type "email" that are missing name lookup 1940 Cursor cursor = db.query(Upgrade303Query.TABLE, Upgrade303Query.COLUMNS, 1941 Upgrade303Query.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 1942 null, null, null); 1943 try { 1944 while (cursor.moveToNext()) { 1945 long dataId = cursor.getLong(Upgrade303Query.ID); 1946 long rawContactId = cursor.getLong(Upgrade303Query.RAW_CONTACT_ID); 1947 String value = cursor.getString(Upgrade303Query.DATA1); 1948 value = extractHandleFromEmailAddress(value); 1949 1950 if (value != null) { 1951 values.put(NameLookupColumns.DATA_ID, dataId); 1952 values.put(NameLookupColumns.RAW_CONTACT_ID, rawContactId); 1953 values.put(NameLookupColumns.NAME_TYPE, NameLookupType.EMAIL_BASED_NICKNAME); 1954 values.put(NameLookupColumns.NORMALIZED_NAME, NameNormalizer.normalize(value)); 1955 db.insert(Tables.NAME_LOOKUP, null, values); 1956 } 1957 } 1958 } finally { 1959 cursor.close(); 1960 } 1961 } 1962 1963 /** 1964 * The {@link ContactsProvider2#update} method was deleting name lookup for new 1965 * nicknames during the sync. We need to restore the lost name lookup rows. 1966 */ 1967 private void upgradeNicknameToVersion303(SQLiteDatabase db) { 1968 final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE); 1969 if (mimeTypeId == -1) { 1970 return; 1971 } 1972 1973 ContentValues values = new ContentValues(); 1974 1975 // Find all data rows with the mime type "nickname" that are missing name lookup 1976 Cursor cursor = db.query(Upgrade303Query.TABLE, Upgrade303Query.COLUMNS, 1977 Upgrade303Query.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 1978 null, null, null); 1979 try { 1980 while (cursor.moveToNext()) { 1981 long dataId = cursor.getLong(Upgrade303Query.ID); 1982 long rawContactId = cursor.getLong(Upgrade303Query.RAW_CONTACT_ID); 1983 String value = cursor.getString(Upgrade303Query.DATA1); 1984 1985 values.put(NameLookupColumns.DATA_ID, dataId); 1986 values.put(NameLookupColumns.RAW_CONTACT_ID, rawContactId); 1987 values.put(NameLookupColumns.NAME_TYPE, NameLookupType.NICKNAME); 1988 values.put(NameLookupColumns.NORMALIZED_NAME, NameNormalizer.normalize(value)); 1989 db.insert(Tables.NAME_LOOKUP, null, values); 1990 } 1991 } finally { 1992 cursor.close(); 1993 } 1994 } 1995 1996 private void upgradeToVersion304(SQLiteDatabase db) { 1997 // Mimetype table requires an index on mime type 1998 db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS mime_type ON " + Tables.MIMETYPES + " (" + 1999 MimetypesColumns.MIMETYPE + 2000 ");"); 2001 } 2002 2003 private void upgradeToVersion306(SQLiteDatabase db) { 2004 // Fix invalid lookup that was used for Exchange contacts (it was not escaped) 2005 // It happened when a new contact was created AND synchronized 2006 final StringBuilder lookupKeyBuilder = new StringBuilder(); 2007 final SQLiteStatement updateStatement = db.compileStatement( 2008 "UPDATE contacts " + 2009 "SET lookup=? " + 2010 "WHERE _id=?"); 2011 final Cursor contactIdCursor = db.rawQuery( 2012 "SELECT DISTINCT contact_id " + 2013 "FROM raw_contacts " + 2014 "WHERE deleted=0 AND account_type='com.android.exchange'", 2015 null); 2016 try { 2017 while (contactIdCursor.moveToNext()) { 2018 final long contactId = contactIdCursor.getLong(0); 2019 lookupKeyBuilder.setLength(0); 2020 final Cursor c = db.rawQuery( 2021 "SELECT account_type, account_name, _id, sourceid, display_name " + 2022 "FROM raw_contacts " + 2023 "WHERE contact_id=? " + 2024 "ORDER BY _id", 2025 new String[] { String.valueOf(contactId) }); 2026 try { 2027 while (c.moveToNext()) { 2028 ContactLookupKey.appendToLookupKey(lookupKeyBuilder, 2029 c.getString(0), 2030 c.getString(1), 2031 c.getLong(2), 2032 c.getString(3), 2033 c.getString(4)); 2034 } 2035 } finally { 2036 c.close(); 2037 } 2038 2039 if (lookupKeyBuilder.length() == 0) { 2040 updateStatement.bindNull(1); 2041 } else { 2042 updateStatement.bindString(1, Uri.encode(lookupKeyBuilder.toString())); 2043 } 2044 updateStatement.bindLong(2, contactId); 2045 2046 updateStatement.execute(); 2047 } 2048 } finally { 2049 updateStatement.close(); 2050 contactIdCursor.close(); 2051 } 2052 } 2053 2054 private void upgradeToVersion307(SQLiteDatabase db) { 2055 db.execSQL("CREATE TABLE properties (" + 2056 "property_key TEXT PRIMARY_KEY, " + 2057 "property_value TEXT" + 2058 ");"); 2059 } 2060 2061 private void rebuildNameLookup(SQLiteDatabase db) { 2062 db.execSQL("DROP INDEX IF EXISTS name_lookup_index"); 2063 insertNameLookup(db); 2064 createContactsIndexes(db); 2065 } 2066 2067 /** 2068 * Regenerates all locale-sensitive data: nickname_lookup, name_lookup and sort keys. 2069 */ 2070 public void setLocale(ContactsProvider2 provider, Locale locale) { 2071 Log.i(TAG, "Switching to locale " + locale); 2072 2073 long start = System.currentTimeMillis(); 2074 SQLiteDatabase db = getWritableDatabase(); 2075 db.setLocale(locale); 2076 db.beginTransaction(); 2077 try { 2078 db.execSQL("DROP INDEX raw_contact_sort_key1_index"); 2079 db.execSQL("DROP INDEX raw_contact_sort_key2_index"); 2080 db.execSQL("DROP INDEX IF EXISTS name_lookup_index"); 2081 2082 loadNicknameLookupTable(db); 2083 insertNameLookup(db); 2084 rebuildSortKeys(db, provider); 2085 createContactsIndexes(db); 2086 db.setTransactionSuccessful(); 2087 } finally { 2088 db.endTransaction(); 2089 } 2090 2091 Log.i(TAG, "Locale change completed in " + (System.currentTimeMillis() - start) + "ms"); 2092 } 2093 2094 /** 2095 * Regenerates sort keys for all contacts. 2096 */ 2097 private void rebuildSortKeys(SQLiteDatabase db, ContactsProvider2 provider) { 2098 Cursor cursor = db.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2099 null, null, null, null, null); 2100 try { 2101 while (cursor.moveToNext()) { 2102 long rawContactId = cursor.getLong(0); 2103 provider.updateRawContactDisplayName(db, rawContactId); 2104 } 2105 } finally { 2106 cursor.close(); 2107 } 2108 } 2109 2110 private void insertNameLookup(SQLiteDatabase db) { 2111 db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP); 2112 2113 SQLiteStatement nameLookupInsert = db.compileStatement( 2114 "INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "(" 2115 + NameLookupColumns.RAW_CONTACT_ID + "," 2116 + NameLookupColumns.DATA_ID + "," 2117 + NameLookupColumns.NAME_TYPE + "," 2118 + NameLookupColumns.NORMALIZED_NAME + 2119 ") VALUES (?,?,?,?)"); 2120 2121 try { 2122 insertStructuredNameLookup(db, nameLookupInsert); 2123 insertOrganizationLookup(db, nameLookupInsert); 2124 insertEmailLookup(db, nameLookupInsert); 2125 insertNicknameLookup(db, nameLookupInsert); 2126 } finally { 2127 nameLookupInsert.close(); 2128 } 2129 } 2130 2131 private static final class StructuredNameQuery { 2132 public static final String TABLE = Tables.DATA; 2133 2134 public static final String SELECTION = 2135 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL"; 2136 2137 public static final String COLUMNS[] = { 2138 StructuredName._ID, 2139 StructuredName.RAW_CONTACT_ID, 2140 StructuredName.DISPLAY_NAME, 2141 }; 2142 2143 public static final int ID = 0; 2144 public static final int RAW_CONTACT_ID = 1; 2145 public static final int DISPLAY_NAME = 2; 2146 } 2147 2148 private class StructuredNameLookupBuilder extends NameLookupBuilder { 2149 2150 private final SQLiteStatement mNameLookupInsert; 2151 private final CommonNicknameCache mCommonNicknameCache; 2152 2153 public StructuredNameLookupBuilder(NameSplitter splitter, 2154 CommonNicknameCache commonNicknameCache, SQLiteStatement nameLookupInsert) { 2155 super(splitter); 2156 this.mCommonNicknameCache = commonNicknameCache; 2157 this.mNameLookupInsert = nameLookupInsert; 2158 } 2159 2160 @Override 2161 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 2162 String name) { 2163 if (!TextUtils.isEmpty(name)) { 2164 ContactsDatabaseHelper.this.insertNormalizedNameLookup(mNameLookupInsert, 2165 rawContactId, dataId, lookupType, name); 2166 } 2167 } 2168 2169 @Override 2170 protected String[] getCommonNicknameClusters(String normalizedName) { 2171 return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); 2172 } 2173 } 2174 2175 /** 2176 * Inserts name lookup rows for all structured names in the database. 2177 */ 2178 private void insertStructuredNameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) { 2179 NameSplitter nameSplitter = createNameSplitter(); 2180 NameLookupBuilder nameLookupBuilder = new StructuredNameLookupBuilder(nameSplitter, 2181 new CommonNicknameCache(db), nameLookupInsert); 2182 final long mimeTypeId = lookupMimeTypeId(db, StructuredName.CONTENT_ITEM_TYPE); 2183 Cursor cursor = db.query(StructuredNameQuery.TABLE, StructuredNameQuery.COLUMNS, 2184 StructuredNameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2185 null, null, null); 2186 try { 2187 while (cursor.moveToNext()) { 2188 long dataId = cursor.getLong(StructuredNameQuery.ID); 2189 long rawContactId = cursor.getLong(StructuredNameQuery.RAW_CONTACT_ID); 2190 String name = cursor.getString(StructuredNameQuery.DISPLAY_NAME); 2191 int fullNameStyle = nameSplitter.guessFullNameStyle(name); 2192 fullNameStyle = nameSplitter.getAdjustedFullNameStyle(fullNameStyle); 2193 nameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle); 2194 } 2195 } finally { 2196 cursor.close(); 2197 } 2198 } 2199 2200 private static final class OrganizationQuery { 2201 public static final String TABLE = Tables.DATA; 2202 2203 public static final String SELECTION = 2204 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL"; 2205 2206 public static final String COLUMNS[] = { 2207 Organization._ID, 2208 Organization.RAW_CONTACT_ID, 2209 Organization.COMPANY, 2210 Organization.TITLE, 2211 }; 2212 2213 public static final int ID = 0; 2214 public static final int RAW_CONTACT_ID = 1; 2215 public static final int COMPANY = 2; 2216 public static final int TITLE = 3; 2217 } 2218 2219 /** 2220 * Inserts name lookup rows for all organizations in the database. 2221 */ 2222 private void insertOrganizationLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) { 2223 final long mimeTypeId = lookupMimeTypeId(db, Organization.CONTENT_ITEM_TYPE); 2224 Cursor cursor = db.query(OrganizationQuery.TABLE, OrganizationQuery.COLUMNS, 2225 OrganizationQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2226 null, null, null); 2227 try { 2228 while (cursor.moveToNext()) { 2229 long dataId = cursor.getLong(OrganizationQuery.ID); 2230 long rawContactId = cursor.getLong(OrganizationQuery.RAW_CONTACT_ID); 2231 String organization = cursor.getString(OrganizationQuery.COMPANY); 2232 String title = cursor.getString(OrganizationQuery.TITLE); 2233 insertNameLookup(nameLookupInsert, rawContactId, dataId, 2234 NameLookupType.ORGANIZATION, organization); 2235 insertNameLookup(nameLookupInsert, rawContactId, dataId, 2236 NameLookupType.ORGANIZATION, title); 2237 } 2238 } finally { 2239 cursor.close(); 2240 } 2241 } 2242 2243 private static final class EmailQuery { 2244 public static final String TABLE = Tables.DATA; 2245 2246 public static final String SELECTION = 2247 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL"; 2248 2249 public static final String COLUMNS[] = { 2250 Email._ID, 2251 Email.RAW_CONTACT_ID, 2252 Email.ADDRESS, 2253 }; 2254 2255 public static final int ID = 0; 2256 public static final int RAW_CONTACT_ID = 1; 2257 public static final int ADDRESS = 2; 2258 } 2259 2260 /** 2261 * Inserts name lookup rows for all email addresses in the database. 2262 */ 2263 private void insertEmailLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) { 2264 final long mimeTypeId = lookupMimeTypeId(db, Email.CONTENT_ITEM_TYPE); 2265 Cursor cursor = db.query(EmailQuery.TABLE, EmailQuery.COLUMNS, 2266 EmailQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2267 null, null, null); 2268 try { 2269 while (cursor.moveToNext()) { 2270 long dataId = cursor.getLong(EmailQuery.ID); 2271 long rawContactId = cursor.getLong(EmailQuery.RAW_CONTACT_ID); 2272 String address = cursor.getString(EmailQuery.ADDRESS); 2273 address = extractHandleFromEmailAddress(address); 2274 insertNameLookup(nameLookupInsert, rawContactId, dataId, 2275 NameLookupType.EMAIL_BASED_NICKNAME, address); 2276 } 2277 } finally { 2278 cursor.close(); 2279 } 2280 } 2281 2282 private static final class NicknameQuery { 2283 public static final String TABLE = Tables.DATA; 2284 2285 public static final String SELECTION = 2286 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL"; 2287 2288 public static final String COLUMNS[] = { 2289 Nickname._ID, 2290 Nickname.RAW_CONTACT_ID, 2291 Nickname.NAME, 2292 }; 2293 2294 public static final int ID = 0; 2295 public static final int RAW_CONTACT_ID = 1; 2296 public static final int NAME = 2; 2297 } 2298 2299 /** 2300 * Inserts name lookup rows for all nicknames in the database. 2301 */ 2302 private void insertNicknameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) { 2303 final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE); 2304 Cursor cursor = db.query(NicknameQuery.TABLE, NicknameQuery.COLUMNS, 2305 NicknameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2306 null, null, null); 2307 try { 2308 while (cursor.moveToNext()) { 2309 long dataId = cursor.getLong(NicknameQuery.ID); 2310 long rawContactId = cursor.getLong(NicknameQuery.RAW_CONTACT_ID); 2311 String nickname = cursor.getString(NicknameQuery.NAME); 2312 insertNameLookup(nameLookupInsert, rawContactId, dataId, 2313 NameLookupType.NICKNAME, nickname); 2314 } 2315 } finally { 2316 cursor.close(); 2317 } 2318 } 2319 2320 /** 2321 * Inserts a record in the {@link Tables#NAME_LOOKUP} table. 2322 */ 2323 public void insertNameLookup(SQLiteStatement stmt, long rawContactId, long dataId, 2324 int lookupType, String name) { 2325 if (TextUtils.isEmpty(name)) { 2326 return; 2327 } 2328 2329 String normalized = NameNormalizer.normalize(name); 2330 if (TextUtils.isEmpty(normalized)) { 2331 return; 2332 } 2333 2334 insertNormalizedNameLookup(stmt, rawContactId, dataId, lookupType, normalized); 2335 } 2336 2337 private void insertNormalizedNameLookup(SQLiteStatement stmt, long rawContactId, long dataId, 2338 int lookupType, String normalizedName) { 2339 stmt.bindLong(1, rawContactId); 2340 stmt.bindLong(2, dataId); 2341 stmt.bindLong(3, lookupType); 2342 stmt.bindString(4, normalizedName); 2343 stmt.executeInsert(); 2344 } 2345 2346 public String extractHandleFromEmailAddress(String email) { 2347 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email); 2348 if (tokens.length == 0) { 2349 return null; 2350 } 2351 2352 String address = tokens[0].getAddress(); 2353 int at = address.indexOf('@'); 2354 if (at != -1) { 2355 return address.substring(0, at); 2356 } 2357 return null; 2358 } 2359 2360 public String extractAddressFromEmailAddress(String email) { 2361 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email); 2362 if (tokens.length == 0) { 2363 return null; 2364 } 2365 2366 return tokens[0].getAddress(); 2367 } 2368 2369 private long lookupMimeTypeId(SQLiteDatabase db, String mimeType) { 2370 try { 2371 return DatabaseUtils.longForQuery(db, 2372 "SELECT " + MimetypesColumns._ID + 2373 " FROM " + Tables.MIMETYPES + 2374 " WHERE " + MimetypesColumns.MIMETYPE 2375 + "='" + mimeType + "'", null); 2376 } catch (SQLiteDoneException e) { 2377 // No rows of this type in the database 2378 return -1; 2379 } 2380 } 2381 2382 private void bindString(SQLiteStatement stmt, int index, String value) { 2383 if (value == null) { 2384 stmt.bindNull(index); 2385 } else { 2386 stmt.bindString(index, value); 2387 } 2388 } 2389 2390 /** 2391 * Adds index stats into the SQLite database to force it to always use the lookup indexes. 2392 */ 2393 private void updateSqliteStats(SQLiteDatabase db) { 2394 2395 // Specific stats strings are based on an actual large database after running ANALYZE 2396 try { 2397 updateIndexStats(db, Tables.CONTACTS, 2398 "contacts_restricted_index", "10000 9000"); 2399 updateIndexStats(db, Tables.CONTACTS, 2400 "contacts_has_phone_index", "10000 500"); 2401 updateIndexStats(db, Tables.CONTACTS, 2402 "contacts_visible_index", "10000 500 1"); 2403 2404 updateIndexStats(db, Tables.RAW_CONTACTS, 2405 "raw_contacts_source_id_index", "10000 1 1 1"); 2406 updateIndexStats(db, Tables.RAW_CONTACTS, 2407 "raw_contacts_contact_id_index", "10000 2"); 2408 2409 updateIndexStats(db, Tables.NAME_LOOKUP, 2410 "name_lookup_raw_contact_id_index", "10000 3"); 2411 updateIndexStats(db, Tables.NAME_LOOKUP, 2412 "name_lookup_index", "10000 3 2 2 1"); 2413 updateIndexStats(db, Tables.NAME_LOOKUP, 2414 "sqlite_autoindex_name_lookup_1", "10000 3 2 1"); 2415 2416 updateIndexStats(db, Tables.PHONE_LOOKUP, 2417 "phone_lookup_index", "10000 2 2 1"); 2418 updateIndexStats(db, Tables.PHONE_LOOKUP, 2419 "phone_lookup_min_match_index", "10000 2 2 1"); 2420 2421 updateIndexStats(db, Tables.DATA, 2422 "data_mimetype_data1_index", "60000 5000 2"); 2423 updateIndexStats(db, Tables.DATA, 2424 "data_raw_contact_id", "60000 10"); 2425 2426 updateIndexStats(db, Tables.GROUPS, 2427 "groups_source_id_index", "50 1 1 1"); 2428 2429 updateIndexStats(db, Tables.NICKNAME_LOOKUP, 2430 "sqlite_autoindex_name_lookup_1", "500 2 1"); 2431 2432 } catch (SQLException e) { 2433 Log.e(TAG, "Could not update index stats", e); 2434 } 2435 } 2436 2437 /** 2438 * Stores statistics for a given index. 2439 * 2440 * @param stats has the following structure: the first index is the expected size of 2441 * the table. The following integer(s) are the expected number of records selected with the 2442 * index. There should be one integer per indexed column. 2443 */ 2444 private void updateIndexStats(SQLiteDatabase db, String table, String index, 2445 String stats) { 2446 db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl='" + table + "' AND idx='" + index + "';"); 2447 db.execSQL("INSERT INTO sqlite_stat1 (tbl,idx,stat)" 2448 + " VALUES ('" + table + "','" + index + "','" + stats + "');"); 2449 } 2450 2451 @Override 2452 public synchronized SQLiteDatabase getWritableDatabase() { 2453 SQLiteDatabase db = super.getWritableDatabase(); 2454 if (mReopenDatabase) { 2455 mReopenDatabase = false; 2456 close(); 2457 db = super.getWritableDatabase(); 2458 } 2459 return db; 2460 } 2461 2462 /** 2463 * Wipes all data except mime type and package lookup tables. 2464 */ 2465 public void wipeData() { 2466 SQLiteDatabase db = getWritableDatabase(); 2467 2468 db.execSQL("DELETE FROM " + Tables.CONTACTS + ";"); 2469 db.execSQL("DELETE FROM " + Tables.RAW_CONTACTS + ";"); 2470 db.execSQL("DELETE FROM " + Tables.DATA + ";"); 2471 db.execSQL("DELETE FROM " + Tables.PHONE_LOOKUP + ";"); 2472 db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + ";"); 2473 db.execSQL("DELETE FROM " + Tables.GROUPS + ";"); 2474 db.execSQL("DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS + ";"); 2475 db.execSQL("DELETE FROM " + Tables.SETTINGS + ";"); 2476 db.execSQL("DELETE FROM " + Tables.ACTIVITIES + ";"); 2477 db.execSQL("DELETE FROM " + Tables.CALLS + ";"); 2478 2479 // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP 2480 } 2481 2482 public NameSplitter createNameSplitter() { 2483 return new NameSplitter( 2484 mContext.getString(com.android.internal.R.string.common_name_prefixes), 2485 mContext.getString(com.android.internal.R.string.common_last_name_prefixes), 2486 mContext.getString(com.android.internal.R.string.common_name_suffixes), 2487 mContext.getString(com.android.internal.R.string.common_name_conjunctions), 2488 Locale.getDefault()); 2489 } 2490 2491 /** 2492 * Return the {@link ApplicationInfo#uid} for the given package name. 2493 */ 2494 public static int getUidForPackageName(PackageManager pm, String packageName) { 2495 try { 2496 ApplicationInfo clientInfo = pm.getApplicationInfo(packageName, 0 /* no flags */); 2497 return clientInfo.uid; 2498 } catch (NameNotFoundException e) { 2499 throw new RuntimeException(e); 2500 } 2501 } 2502 2503 /** 2504 * Perform an internal string-to-integer lookup using the compiled 2505 * {@link SQLiteStatement} provided, using the in-memory cache to speed up 2506 * lookups. If a mapping isn't found in cache or database, it will be 2507 * created. All new, uncached answers are added to the cache automatically. 2508 * 2509 * @param query Compiled statement used to query for the mapping. 2510 * @param insert Compiled statement used to insert a new mapping when no 2511 * existing one is found in cache or from query. 2512 * @param value Value to find mapping for. 2513 * @param cache In-memory cache of previous answers. 2514 * @return An unique integer mapping for the given value. 2515 */ 2516 private long getCachedId(SQLiteStatement query, SQLiteStatement insert, 2517 String value, HashMap<String, Long> cache) { 2518 // Try an in-memory cache lookup 2519 if (cache.containsKey(value)) { 2520 return cache.get(value); 2521 } 2522 2523 long id = -1; 2524 try { 2525 // Try searching database for mapping 2526 DatabaseUtils.bindObjectToProgram(query, 1, value); 2527 id = query.simpleQueryForLong(); 2528 } catch (SQLiteDoneException e) { 2529 // Nothing found, so try inserting new mapping 2530 DatabaseUtils.bindObjectToProgram(insert, 1, value); 2531 id = insert.executeInsert(); 2532 } 2533 2534 if (id != -1) { 2535 // Cache and return the new answer 2536 cache.put(value, id); 2537 return id; 2538 } else { 2539 // Otherwise throw if no mapping found or created 2540 throw new IllegalStateException("Couldn't find or create internal " 2541 + "lookup table entry for value " + value); 2542 } 2543 } 2544 2545 /** 2546 * Convert a package name into an integer, using {@link Tables#PACKAGES} for 2547 * lookups and possible allocation of new IDs as needed. 2548 */ 2549 public long getPackageId(String packageName) { 2550 // Make sure compiled statements are ready by opening database 2551 getReadableDatabase(); 2552 return getCachedId(mPackageQuery, mPackageInsert, packageName, mPackageCache); 2553 } 2554 2555 /** 2556 * Convert a mimetype into an integer, using {@link Tables#MIMETYPES} for 2557 * lookups and possible allocation of new IDs as needed. 2558 */ 2559 public long getMimeTypeId(String mimetype) { 2560 // Make sure compiled statements are ready by opening database 2561 getReadableDatabase(); 2562 return getMimeTypeIdNoDbCheck(mimetype); 2563 } 2564 2565 private long getMimeTypeIdNoDbCheck(String mimetype) { 2566 return getCachedId(mMimetypeQuery, mMimetypeInsert, mimetype, mMimetypeCache); 2567 } 2568 2569 /** 2570 * Find the mimetype for the given {@link Data#_ID}. 2571 */ 2572 public String getDataMimeType(long dataId) { 2573 // Make sure compiled statements are ready by opening database 2574 getReadableDatabase(); 2575 try { 2576 // Try database query to find mimetype 2577 DatabaseUtils.bindObjectToProgram(mDataMimetypeQuery, 1, dataId); 2578 String mimetype = mDataMimetypeQuery.simpleQueryForString(); 2579 return mimetype; 2580 } catch (SQLiteDoneException e) { 2581 // No valid mapping found, so return null 2582 return null; 2583 } 2584 } 2585 2586 /** 2587 * Find the mime-type for the given {@link Activities#_ID}. 2588 */ 2589 public String getActivityMimeType(long activityId) { 2590 // Make sure compiled statements are ready by opening database 2591 getReadableDatabase(); 2592 try { 2593 // Try database query to find mimetype 2594 DatabaseUtils.bindObjectToProgram(mActivitiesMimetypeQuery, 1, activityId); 2595 String mimetype = mActivitiesMimetypeQuery.simpleQueryForString(); 2596 return mimetype; 2597 } catch (SQLiteDoneException e) { 2598 // No valid mapping found, so return null 2599 return null; 2600 } 2601 } 2602 2603 /** 2604 * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts. 2605 */ 2606 public void updateAllVisible() { 2607 SQLiteDatabase db = getWritableDatabase(); 2608 final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 2609 String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)}; 2610 2611 // There are a couple questions that can be asked regarding the 2612 // following two update statements: 2613 // 2614 // Q: Why do we run these two queries separately? They seem like they could be combined. 2615 // A: This is a result of painstaking experimentation. Turns out that the most 2616 // important optimization is to make sure we never update a value to its current value. 2617 // Changing 0 to 0 is unexpectedly expensive - SQLite actually writes the unchanged 2618 // rows back to disk. The other consideration is that the CONTACT_IS_VISIBLE condition 2619 // is very complex and executing it twice in the same statement ("if contact_visible != 2620 // CONTACT_IS_VISIBLE change it to CONTACT_IS_VISIBLE") is more expensive than running 2621 // two update statements. 2622 // 2623 // Q: How come we are using db.update instead of compiled statements? 2624 // A: This is a limitation of the compiled statement API. It does not return the 2625 // number of rows changed. As you will see later in this method we really need 2626 // to know how many rows have been changed. 2627 2628 // First update contacts that are currently marked as invisible, but need to be visible 2629 ContentValues values = new ContentValues(); 2630 values.put(Contacts.IN_VISIBLE_GROUP, 1); 2631 int countMadeVisible = db.update(Tables.CONTACTS, values, 2632 Contacts.IN_VISIBLE_GROUP + "=0" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1", 2633 selectionArgs); 2634 2635 // Next update contacts that are currently marked as visible, but need to be invisible 2636 values.put(Contacts.IN_VISIBLE_GROUP, 0); 2637 int countMadeInvisible = db.update(Tables.CONTACTS, values, 2638 Contacts.IN_VISIBLE_GROUP + "=1" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=0", 2639 selectionArgs); 2640 2641 if (countMadeVisible != 0 || countMadeInvisible != 0) { 2642 // TODO break out the fields (contact_in_visible_group, sort_key, sort_key_alt) into 2643 // a separate table. 2644 // Rationale: The following statement will take a very long time on 2645 // a large database even though we are only changing one field from 0 to 1 or from 2646 // 1 to 0. The reason for the slowness is that SQLite will need to write the whole 2647 // page even when only one bit on it changes. Changing the visibility of a 2648 // significant number of contacts will likely read and write almost the entire 2649 // raw_contacts table. So, the solution is to break out into a separate table 2650 // the changing field along with the sort keys used for index-based sorting. 2651 // That table will occupy a smaller number of pages, so rewriting it would 2652 // not be as expensive. 2653 mVisibleUpdateRawContacts.execute(); 2654 } 2655 } 2656 2657 /** 2658 * Update {@link Contacts#IN_VISIBLE_GROUP} for a specific contact. 2659 */ 2660 public void updateContactVisible(long contactId) { 2661 final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 2662 mVisibleSpecificUpdate.bindLong(1, groupMembershipMimetypeId); 2663 mVisibleSpecificUpdate.bindLong(2, contactId); 2664 mVisibleSpecificUpdate.execute(); 2665 2666 mVisibleSpecificUpdateRawContacts.bindLong(1, contactId); 2667 mVisibleSpecificUpdateRawContacts.execute(); 2668 } 2669 2670 /** 2671 * Returns contact ID for the given contact or zero if it is NULL. 2672 */ 2673 public long getContactId(long rawContactId) { 2674 getReadableDatabase(); 2675 try { 2676 DatabaseUtils.bindObjectToProgram(mContactIdQuery, 1, rawContactId); 2677 return mContactIdQuery.simpleQueryForLong(); 2678 } catch (SQLiteDoneException e) { 2679 // No valid mapping found, so return 0 2680 return 0; 2681 } 2682 } 2683 2684 public int getAggregationMode(long rawContactId) { 2685 getReadableDatabase(); 2686 try { 2687 DatabaseUtils.bindObjectToProgram(mAggregationModeQuery, 1, rawContactId); 2688 return (int)mAggregationModeQuery.simpleQueryForLong(); 2689 } catch (SQLiteDoneException e) { 2690 // No valid row found, so return "disabled" 2691 return RawContacts.AGGREGATION_MODE_DISABLED; 2692 } 2693 } 2694 2695 public void buildPhoneLookupAndRawContactQuery(SQLiteQueryBuilder qb, String number) { 2696 String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number); 2697 qb.setTables(Tables.DATA_JOIN_RAW_CONTACTS + 2698 " JOIN " + Tables.PHONE_LOOKUP 2699 + " ON(" + DataColumns.CONCRETE_ID + "=" + PhoneLookupColumns.DATA_ID + ")"); 2700 2701 StringBuilder sb = new StringBuilder(); 2702 sb.append(PhoneLookupColumns.MIN_MATCH + "='"); 2703 sb.append(minMatch); 2704 sb.append("' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", "); 2705 DatabaseUtils.appendEscapedSQLString(sb, number); 2706 sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)"); 2707 2708 qb.appendWhere(sb.toString()); 2709 } 2710 2711 public void buildPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number) { 2712 String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number); 2713 StringBuilder sb = new StringBuilder(); 2714 appendPhoneLookupTables(sb, minMatch, true); 2715 qb.setTables(sb.toString()); 2716 2717 sb = new StringBuilder(); 2718 appendPhoneLookupSelection(sb, number); 2719 qb.appendWhere(sb.toString()); 2720 } 2721 2722 public String buildPhoneLookupAsNestedQuery(String number) { 2723 StringBuilder sb = new StringBuilder(); 2724 final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number); 2725 sb.append("(SELECT DISTINCT raw_contact_id" + " FROM "); 2726 appendPhoneLookupTables(sb, minMatch, false); 2727 sb.append(" WHERE "); 2728 appendPhoneLookupSelection(sb, number); 2729 sb.append(")"); 2730 return sb.toString(); 2731 } 2732 2733 private void appendPhoneLookupTables(StringBuilder sb, final String minMatch, 2734 boolean joinContacts) { 2735 sb.append(Tables.RAW_CONTACTS); 2736 if (joinContacts) { 2737 sb.append(" JOIN " + getContactView() + " contacts_view" 2738 + " ON (contacts_view._id = raw_contacts.contact_id)"); 2739 } 2740 sb.append(", (SELECT data_id FROM phone_lookup " 2741 + "WHERE (" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.MIN_MATCH + " = '"); 2742 sb.append(minMatch); 2743 sb.append("')) AS lookup, " + Tables.DATA); 2744 } 2745 2746 private void appendPhoneLookupSelection(StringBuilder sb, String number) { 2747 sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id" 2748 + " AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", "); 2749 DatabaseUtils.appendEscapedSQLString(sb, number); 2750 sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)"); 2751 } 2752 2753 public String getUseStrictPhoneNumberComparisonParameter() { 2754 return mUseStrictPhoneNumberComparison ? "1" : "0"; 2755 } 2756 2757 /** 2758 * Loads common nickname mappings into the database. 2759 */ 2760 private void loadNicknameLookupTable(SQLiteDatabase db) { 2761 db.execSQL("DELETE FROM " + Tables.NICKNAME_LOOKUP); 2762 2763 String[] strings = mContext.getResources().getStringArray( 2764 com.android.internal.R.array.common_nicknames); 2765 if (strings == null || strings.length == 0) { 2766 return; 2767 } 2768 2769 SQLiteStatement nicknameLookupInsert = db.compileStatement("INSERT INTO " 2770 + Tables.NICKNAME_LOOKUP + "(" + NicknameLookupColumns.NAME + "," 2771 + NicknameLookupColumns.CLUSTER + ") VALUES (?,?)"); 2772 2773 try { 2774 for (int clusterId = 0; clusterId < strings.length; clusterId++) { 2775 String[] names = strings[clusterId].split(","); 2776 for (int j = 0; j < names.length; j++) { 2777 String name = NameNormalizer.normalize(names[j]); 2778 try { 2779 DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 1, name); 2780 DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 2, 2781 String.valueOf(clusterId)); 2782 nicknameLookupInsert.executeInsert(); 2783 } catch (SQLiteException e) { 2784 2785 // Print the exception and keep going - this is not a fatal error 2786 Log.e(TAG, "Cannot insert nickname: " + names[j], e); 2787 } 2788 } 2789 } 2790 } finally { 2791 nicknameLookupInsert.close(); 2792 } 2793 } 2794 2795 public static void copyStringValue(ContentValues toValues, String toKey, 2796 ContentValues fromValues, String fromKey) { 2797 if (fromValues.containsKey(fromKey)) { 2798 toValues.put(toKey, fromValues.getAsString(fromKey)); 2799 } 2800 } 2801 2802 public static void copyLongValue(ContentValues toValues, String toKey, 2803 ContentValues fromValues, String fromKey) { 2804 if (fromValues.containsKey(fromKey)) { 2805 long longValue; 2806 Object value = fromValues.get(fromKey); 2807 if (value instanceof Boolean) { 2808 if ((Boolean)value) { 2809 longValue = 1; 2810 } else { 2811 longValue = 0; 2812 } 2813 } else if (value instanceof String) { 2814 longValue = Long.parseLong((String)value); 2815 } else { 2816 longValue = ((Number)value).longValue(); 2817 } 2818 toValues.put(toKey, longValue); 2819 } 2820 } 2821 2822 public SyncStateContentProviderHelper getSyncState() { 2823 return mSyncState; 2824 } 2825 2826 /** 2827 * Delete the aggregate contact if it has no constituent raw contacts other 2828 * than the supplied one. 2829 */ 2830 public void removeContactIfSingleton(long rawContactId) { 2831 SQLiteDatabase db = getWritableDatabase(); 2832 2833 // Obtain contact ID from the supplied raw contact ID 2834 String contactIdFromRawContactId = "(SELECT " + RawContacts.CONTACT_ID + " FROM " 2835 + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=" + rawContactId + ")"; 2836 2837 // Find other raw contacts in the same aggregate contact 2838 String otherRawContacts = "(SELECT contacts1." + RawContacts._ID + " FROM " 2839 + Tables.RAW_CONTACTS + " contacts1 JOIN " + Tables.RAW_CONTACTS + " contacts2 ON (" 2840 + "contacts1." + RawContacts.CONTACT_ID + "=contacts2." + RawContacts.CONTACT_ID 2841 + ") WHERE contacts1." + RawContacts._ID + "!=" + rawContactId + "" 2842 + " AND contacts2." + RawContacts._ID + "=" + rawContactId + ")"; 2843 2844 db.execSQL("DELETE FROM " + Tables.CONTACTS 2845 + " WHERE " + Contacts._ID + "=" + contactIdFromRawContactId 2846 + " AND NOT EXISTS " + otherRawContacts + ";"); 2847 } 2848 2849 /** 2850 * Returns the value from the {@link Tables#PROPERTIES} table. 2851 */ 2852 public String getProperty(String key, String defaultValue) { 2853 Cursor cursor = getReadableDatabase().query(Tables.PROPERTIES, 2854 new String[]{PropertiesColumns.PROPERTY_VALUE}, 2855 PropertiesColumns.PROPERTY_KEY + "=?", 2856 new String[]{key}, null, null, null); 2857 String value = null; 2858 try { 2859 if (cursor.moveToFirst()) { 2860 value = cursor.getString(0); 2861 } 2862 } finally { 2863 cursor.close(); 2864 } 2865 2866 return value != null ? value : defaultValue; 2867 } 2868 2869 /** 2870 * Stores a key-value pair in the {@link Tables#PROPERTIES} table. 2871 */ 2872 public void setProperty(String key, String value) { 2873 ContentValues values = new ContentValues(); 2874 values.put(PropertiesColumns.PROPERTY_KEY, key); 2875 values.put(PropertiesColumns.PROPERTY_VALUE, value); 2876 getWritableDatabase().replace(Tables.PROPERTIES, null, values); 2877 } 2878 2879 /** 2880 * Check if {@link Binder#getCallingUid()} should be allowed access to 2881 * {@link RawContacts#IS_RESTRICTED} data. 2882 */ 2883 boolean hasAccessToRestrictedData() { 2884 final PackageManager pm = mContext.getPackageManager(); 2885 final String[] callerPackages = pm.getPackagesForUid(Binder.getCallingUid()); 2886 2887 // Has restricted access if caller matches any packages 2888 for (String callerPackage : callerPackages) { 2889 if (hasAccessToRestrictedData(callerPackage)) { 2890 return true; 2891 } 2892 } 2893 return false; 2894 } 2895 2896 /** 2897 * Check if requestingPackage should be allowed access to 2898 * {@link RawContacts#IS_RESTRICTED} data. 2899 */ 2900 boolean hasAccessToRestrictedData(String requestingPackage) { 2901 if (mUnrestrictedPackages != null) { 2902 for (String allowedPackage : mUnrestrictedPackages) { 2903 if (allowedPackage.equals(requestingPackage)) { 2904 return true; 2905 } 2906 } 2907 } 2908 return false; 2909 } 2910 2911 public String getDataView() { 2912 return getDataView(false); 2913 } 2914 2915 public String getDataView(boolean requireRestrictedView) { 2916 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 2917 Views.DATA_ALL : Views.DATA_RESTRICTED; 2918 } 2919 2920 public String getRawContactView() { 2921 return getRawContactView(false); 2922 } 2923 2924 public String getRawContactView(boolean requireRestrictedView) { 2925 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 2926 Views.RAW_CONTACTS_ALL : Views.RAW_CONTACTS_RESTRICTED; 2927 } 2928 2929 public String getContactView() { 2930 return getContactView(false); 2931 } 2932 2933 public String getContactView(boolean requireRestrictedView) { 2934 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 2935 Views.CONTACTS_ALL : Views.CONTACTS_RESTRICTED; 2936 } 2937 2938 public String getGroupView() { 2939 return Views.GROUPS_ALL; 2940 } 2941 2942 public String getContactEntitiesView() { 2943 return getContactEntitiesView(false); 2944 } 2945 2946 public String getContactEntitiesView(boolean requireRestrictedView) { 2947 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 2948 Tables.CONTACT_ENTITIES : Tables.CONTACT_ENTITIES_RESTRICTED; 2949 } 2950 2951 /** 2952 * Test if any of the columns appear in the given projection. 2953 */ 2954 public boolean isInProjection(String[] projection, String... columns) { 2955 if (projection == null) { 2956 return true; 2957 } 2958 2959 // Optimized for a single-column test 2960 if (columns.length == 1) { 2961 String column = columns[0]; 2962 for (String test : projection) { 2963 if (column.equals(test)) { 2964 return true; 2965 } 2966 } 2967 } else { 2968 for (String test : projection) { 2969 for (String column : columns) { 2970 if (column.equals(test)) { 2971 return true; 2972 } 2973 } 2974 } 2975 } 2976 return false; 2977 } 2978 2979 /** 2980 * Returns a detailed exception message for the supplied URI. It includes the calling 2981 * user and calling package(s). 2982 */ 2983 public String exceptionMessage(Uri uri) { 2984 return exceptionMessage(null, uri); 2985 } 2986 2987 /** 2988 * Returns a detailed exception message for the supplied URI. It includes the calling 2989 * user and calling package(s). 2990 */ 2991 public String exceptionMessage(String message, Uri uri) { 2992 StringBuilder sb = new StringBuilder(); 2993 if (message != null) { 2994 sb.append(message).append("; "); 2995 } 2996 sb.append("URI: ").append(uri); 2997 final PackageManager pm = mContext.getPackageManager(); 2998 int callingUid = Binder.getCallingUid(); 2999 sb.append(", calling user: "); 3000 String userName = pm.getNameForUid(callingUid); 3001 if (userName != null) { 3002 sb.append(userName); 3003 } else { 3004 sb.append(callingUid); 3005 } 3006 3007 final String[] callerPackages = pm.getPackagesForUid(callingUid); 3008 if (callerPackages != null && callerPackages.length > 0) { 3009 if (callerPackages.length == 1) { 3010 sb.append(", calling package:"); 3011 sb.append(callerPackages[0]); 3012 } else { 3013 sb.append(", calling package is one of: ["); 3014 for (int i = 0; i < callerPackages.length; i++) { 3015 if (i != 0) { 3016 sb.append(", "); 3017 } 3018 sb.append(callerPackages[i]); 3019 } 3020 sb.append("]"); 3021 } 3022 } 3023 3024 return sb.toString(); 3025 } 3026} 3027