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