ContactsDatabaseHelper.java revision c039cfb78c40730483fd71178df63ada5826a315
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 = 415; 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.GROUP_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," + 863 Groups.SYNC1 + " TEXT, " + 864 Groups.SYNC2 + " TEXT, " + 865 Groups.SYNC3 + " TEXT, " + 866 Groups.SYNC4 + " TEXT " + 867 ");"); 868 869 db.execSQL("CREATE INDEX groups_source_id_index ON " + Tables.GROUPS + " (" + 870 Groups.SOURCE_ID + ", " + 871 Groups.ACCOUNT_TYPE + ", " + 872 Groups.ACCOUNT_NAME + 873 ");"); 874 875 db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.AGGREGATION_EXCEPTIONS + " (" + 876 AggregationExceptionColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 877 AggregationExceptions.TYPE + " INTEGER NOT NULL, " + 878 AggregationExceptions.RAW_CONTACT_ID1 879 + " INTEGER REFERENCES raw_contacts(_id), " + 880 AggregationExceptions.RAW_CONTACT_ID2 881 + " INTEGER REFERENCES raw_contacts(_id)" + 882 ");"); 883 884 db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index1 ON " + 885 Tables.AGGREGATION_EXCEPTIONS + " (" + 886 AggregationExceptions.RAW_CONTACT_ID1 + ", " + 887 AggregationExceptions.RAW_CONTACT_ID2 + 888 ");"); 889 890 db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index2 ON " + 891 Tables.AGGREGATION_EXCEPTIONS + " (" + 892 AggregationExceptions.RAW_CONTACT_ID2 + ", " + 893 AggregationExceptions.RAW_CONTACT_ID1 + 894 ");"); 895 896 db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.SETTINGS + " (" + 897 Settings.ACCOUNT_NAME + " STRING NOT NULL," + 898 Settings.ACCOUNT_TYPE + " STRING NOT NULL," + 899 Settings.UNGROUPED_VISIBLE + " INTEGER NOT NULL DEFAULT 0," + 900 Settings.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1, " + 901 "PRIMARY KEY (" + Settings.ACCOUNT_NAME + ", " + 902 Settings.ACCOUNT_TYPE + ") ON CONFLICT REPLACE" + 903 ");"); 904 905 db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" + 906 Contacts._ID + " INTEGER PRIMARY KEY" + 907 ");"); 908 909 db.execSQL("CREATE TABLE " + Tables.DEFAULT_DIRECTORY + " (" + 910 Contacts._ID + " INTEGER PRIMARY KEY" + 911 ");"); 912 913 // The table for recent calls is here so we can do table joins 914 // on people, phones, and calls all in one place. 915 db.execSQL("CREATE TABLE " + Tables.CALLS + " (" + 916 Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 917 Calls.NUMBER + " TEXT," + 918 Calls.DATE + " INTEGER," + 919 Calls.DURATION + " INTEGER," + 920 Calls.TYPE + " INTEGER," + 921 Calls.NEW + " INTEGER," + 922 Calls.CACHED_NAME + " TEXT," + 923 Calls.CACHED_NUMBER_TYPE + " INTEGER," + 924 Calls.CACHED_NUMBER_LABEL + " TEXT," + 925 Calls.COUNTRY_ISO + " TEXT" + ");"); 926 927 // Activities table 928 db.execSQL("CREATE TABLE " + Tables.ACTIVITIES + " (" + 929 Activities._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 930 ActivitiesColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," + 931 ActivitiesColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," + 932 Activities.RAW_ID + " TEXT," + 933 Activities.IN_REPLY_TO + " TEXT," + 934 Activities.AUTHOR_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," + 935 Activities.TARGET_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," + 936 Activities.PUBLISHED + " INTEGER NOT NULL," + 937 Activities.THREAD_PUBLISHED + " INTEGER NOT NULL," + 938 Activities.TITLE + " TEXT NOT NULL," + 939 Activities.SUMMARY + " TEXT," + 940 Activities.LINK + " TEXT, " + 941 Activities.THUMBNAIL + " BLOB" + 942 ");"); 943 944 db.execSQL("CREATE TABLE " + Tables.STATUS_UPDATES + " (" + 945 StatusUpdatesColumns.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," + 946 StatusUpdates.STATUS + " TEXT," + 947 StatusUpdates.STATUS_TIMESTAMP + " INTEGER," + 948 StatusUpdates.STATUS_RES_PACKAGE + " TEXT, " + 949 StatusUpdates.STATUS_LABEL + " INTEGER, " + 950 StatusUpdates.STATUS_ICON + " INTEGER" + 951 ");"); 952 953 db.execSQL("CREATE TABLE " + Tables.PROPERTIES + " (" + 954 PropertiesColumns.PROPERTY_KEY + " TEXT PRIMARY KEY, " + 955 PropertiesColumns.PROPERTY_VALUE + " TEXT " + 956 ");"); 957 958 db.execSQL("CREATE TABLE " + Tables.ACCOUNTS + " (" + 959 RawContacts.ACCOUNT_NAME + " TEXT, " + 960 RawContacts.ACCOUNT_TYPE + " TEXT " + 961 ");"); 962 963 // Allow contacts without any account to be created for now. Achieve that 964 // by inserting a fake account with both type and name as NULL. 965 // This "account" should be eliminated as soon as the first real writable account 966 // is added to the phone. 967 db.execSQL("INSERT INTO accounts VALUES(NULL, NULL)"); 968 969 createDirectoriesTable(db); 970 971 createContactsViews(db); 972 createGroupsView(db); 973 createContactsTriggers(db); 974 createContactsIndexes(db); 975 976 loadNicknameLookupTable(db); 977 978 // Add the legacy API support views, etc 979 LegacyApiSupport.createDatabase(db); 980 981 // This will create a sqlite_stat1 table that is used for query optimization 982 db.execSQL("ANALYZE;"); 983 984 updateSqliteStats(db); 985 986 // We need to close and reopen the database connection so that the stats are 987 // taken into account. Make a note of it and do the actual reopening in the 988 // getWritableDatabase method. 989 mReopenDatabase = true; 990 991 ContentResolver.requestSync(null /* all accounts */, 992 ContactsContract.AUTHORITY, new Bundle()); 993 } 994 995 private void createDirectoriesTable(SQLiteDatabase db) { 996 db.execSQL("CREATE TABLE " + Tables.DIRECTORIES + "(" + 997 Directory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 998 Directory.PACKAGE_NAME + " TEXT NOT NULL," + 999 Directory.DIRECTORY_AUTHORITY + " TEXT NOT NULL," + 1000 Directory.TYPE_RESOURCE_ID + " INTEGER," + 1001 DirectoryColumns.TYPE_RESOURCE_NAME + " TEXT," + 1002 Directory.ACCOUNT_TYPE + " TEXT," + 1003 Directory.ACCOUNT_NAME + " TEXT," + 1004 Directory.DISPLAY_NAME + " TEXT, " + 1005 Directory.EXPORT_SUPPORT + " INTEGER NOT NULL" + 1006 " DEFAULT " + Directory.EXPORT_SUPPORT_NONE + "," + 1007 Directory.SHORTCUT_SUPPORT + " INTEGER NOT NULL" + 1008 " DEFAULT " + Directory.SHORTCUT_SUPPORT_NONE + "," + 1009 Directory.PHOTO_SUPPORT + " INTEGER NOT NULL" + 1010 " DEFAULT " + Directory.PHOTO_SUPPORT_NONE + 1011 ");"); 1012 1013 // Trigger a full scan of directories in the system 1014 setProperty(db, ContactDirectoryManager.PROPERTY_DIRECTORY_SCAN_COMPLETE, "0"); 1015 } 1016 1017 private static void createContactsTriggers(SQLiteDatabase db) { 1018 1019 /* 1020 * Automatically delete Data rows when a raw contact is deleted. 1021 */ 1022 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_deleted;"); 1023 db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_deleted " 1024 + " BEFORE DELETE ON " + Tables.RAW_CONTACTS 1025 + " BEGIN " 1026 + " DELETE FROM " + Tables.DATA 1027 + " WHERE " + Data.RAW_CONTACT_ID 1028 + "=OLD." + RawContacts._ID + ";" 1029 + " DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS 1030 + " WHERE " + AggregationExceptions.RAW_CONTACT_ID1 1031 + "=OLD." + RawContacts._ID 1032 + " OR " + AggregationExceptions.RAW_CONTACT_ID2 1033 + "=OLD." + RawContacts._ID + ";" 1034 + " DELETE FROM " + Tables.VISIBLE_CONTACTS 1035 + " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID 1036 + " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS 1037 + " WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID 1038 + " )=1;" 1039 + " DELETE FROM " + Tables.DEFAULT_DIRECTORY 1040 + " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID 1041 + " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS 1042 + " WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID 1043 + " )=1;" 1044 + " DELETE FROM " + Tables.CONTACTS 1045 + " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID 1046 + " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS 1047 + " WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID 1048 + " )=1;" 1049 + " END"); 1050 1051 1052 db.execSQL("DROP TRIGGER IF EXISTS contacts_times_contacted;"); 1053 db.execSQL("DROP TRIGGER IF EXISTS raw_contacts_times_contacted;"); 1054 1055 /* 1056 * Triggers that update {@link RawContacts#VERSION} when the contact is 1057 * marked for deletion or any time a data row is inserted, updated or 1058 * deleted. 1059 */ 1060 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;"); 1061 db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted " 1062 + " AFTER UPDATE ON " + Tables.RAW_CONTACTS 1063 + " BEGIN " 1064 + " UPDATE " + Tables.RAW_CONTACTS 1065 + " SET " 1066 + RawContacts.VERSION + "=OLD." + RawContacts.VERSION + "+1 " 1067 + " WHERE " + RawContacts._ID + "=OLD." + RawContacts._ID 1068 + " AND NEW." + RawContacts.DELETED + "!= OLD." + RawContacts.DELETED + ";" 1069 + " END"); 1070 1071 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_updated;"); 1072 db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA 1073 + " BEGIN " 1074 + " UPDATE " + Tables.DATA 1075 + " SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 " 1076 + " WHERE " + Data._ID + "=OLD." + Data._ID + ";" 1077 + " UPDATE " + Tables.RAW_CONTACTS 1078 + " SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 " 1079 + " WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";" 1080 + " END"); 1081 1082 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_deleted;"); 1083 db.execSQL("CREATE TRIGGER " + Tables.DATA + "_deleted BEFORE DELETE ON " + Tables.DATA 1084 + " BEGIN " 1085 + " UPDATE " + Tables.RAW_CONTACTS 1086 + " SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 " 1087 + " WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";" 1088 + " DELETE FROM " + Tables.PHONE_LOOKUP 1089 + " WHERE " + PhoneLookupColumns.DATA_ID + "=OLD." + Data._ID + ";" 1090 + " DELETE FROM " + Tables.STATUS_UPDATES 1091 + " WHERE " + StatusUpdatesColumns.DATA_ID + "=OLD." + Data._ID + ";" 1092 + " DELETE FROM " + Tables.NAME_LOOKUP 1093 + " WHERE " + NameLookupColumns.DATA_ID + "=OLD." + Data._ID + ";" 1094 + " END"); 1095 1096 1097 db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_updated1;"); 1098 db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 " 1099 + " AFTER UPDATE ON " + Tables.GROUPS 1100 + " BEGIN " 1101 + " UPDATE " + Tables.GROUPS 1102 + " SET " 1103 + Groups.VERSION + "=OLD." + Groups.VERSION + "+1" 1104 + " WHERE " + Groups._ID + "=OLD." + Groups._ID + ";" 1105 + " END"); 1106 } 1107 1108 private static void createContactsIndexes(SQLiteDatabase db) { 1109 db.execSQL("DROP INDEX IF EXISTS name_lookup_index"); 1110 db.execSQL("CREATE INDEX name_lookup_index ON " + Tables.NAME_LOOKUP + " (" + 1111 NameLookupColumns.NORMALIZED_NAME + "," + 1112 NameLookupColumns.NAME_TYPE + ", " + 1113 NameLookupColumns.RAW_CONTACT_ID + ", " + 1114 NameLookupColumns.DATA_ID + 1115 ");"); 1116 1117 db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key1_index"); 1118 db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" + 1119 RawContacts.SORT_KEY_PRIMARY + 1120 ");"); 1121 1122 db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key2_index"); 1123 db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" + 1124 RawContacts.SORT_KEY_ALTERNATIVE + 1125 ");"); 1126 } 1127 1128 private static void createContactsViews(SQLiteDatabase db) { 1129 db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS_ALL + ";"); 1130 db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS_RESTRICTED + ";"); 1131 db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_ALL + ";"); 1132 db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_RESTRICTED + ";"); 1133 db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS_ALL + ";"); 1134 db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS_RESTRICTED + ";"); 1135 db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES + ";"); 1136 db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES_RESTRICTED + ";"); 1137 db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";"); 1138 db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES_RESTRICTED + ";"); 1139 1140 String dataColumns = 1141 Data.IS_PRIMARY + ", " 1142 + Data.IS_SUPER_PRIMARY + ", " 1143 + Data.DATA_VERSION + ", " 1144 + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + "," 1145 + MimetypesColumns.MIMETYPE + " AS " + Data.MIMETYPE + ", " 1146 + Data.IS_READ_ONLY + ", " 1147 + Data.DATA1 + ", " 1148 + Data.DATA2 + ", " 1149 + Data.DATA3 + ", " 1150 + Data.DATA4 + ", " 1151 + Data.DATA5 + ", " 1152 + Data.DATA6 + ", " 1153 + Data.DATA7 + ", " 1154 + Data.DATA8 + ", " 1155 + Data.DATA9 + ", " 1156 + Data.DATA10 + ", " 1157 + Data.DATA11 + ", " 1158 + Data.DATA12 + ", " 1159 + Data.DATA13 + ", " 1160 + Data.DATA14 + ", " 1161 + Data.DATA15 + ", " 1162 + Data.SYNC1 + ", " 1163 + Data.SYNC2 + ", " 1164 + Data.SYNC3 + ", " 1165 + Data.SYNC4; 1166 1167 String syncColumns = 1168 RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + "," 1169 + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + "," 1170 + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + "," 1171 + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS " + RawContacts.NAME_VERIFIED + "," 1172 + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + "," 1173 + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + "," 1174 + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + "," 1175 + RawContactsColumns.CONCRETE_SYNC2 + " AS " + RawContacts.SYNC2 + "," 1176 + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + "," 1177 + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4; 1178 1179 String baseContactColumns = 1180 Contacts.HAS_PHONE_NUMBER + ", " 1181 + Contacts.NAME_RAW_CONTACT_ID + ", " 1182 + Contacts.LOOKUP_KEY + ", " 1183 + Contacts.PHOTO_ID + ", " 1184 + Clauses.CONTACT_VISIBLE + " AS " + Contacts.IN_VISIBLE_GROUP + ", " 1185 + ContactsColumns.LAST_STATUS_UPDATE_ID; 1186 1187 String contactOptionColumns = 1188 ContactsColumns.CONCRETE_CUSTOM_RINGTONE 1189 + " AS " + RawContacts.CUSTOM_RINGTONE + "," 1190 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL 1191 + " AS " + RawContacts.SEND_TO_VOICEMAIL + "," 1192 + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED 1193 + " AS " + RawContacts.LAST_TIME_CONTACTED + "," 1194 + ContactsColumns.CONCRETE_TIMES_CONTACTED 1195 + " AS " + RawContacts.TIMES_CONTACTED + "," 1196 + ContactsColumns.CONCRETE_STARRED 1197 + " AS " + RawContacts.STARRED; 1198 1199 String contactNameColumns = 1200 "name_raw_contact." + RawContacts.DISPLAY_NAME_SOURCE 1201 + " AS " + Contacts.DISPLAY_NAME_SOURCE + ", " 1202 + "name_raw_contact." + RawContacts.DISPLAY_NAME_PRIMARY 1203 + " AS " + Contacts.DISPLAY_NAME_PRIMARY + ", " 1204 + "name_raw_contact." + RawContacts.DISPLAY_NAME_ALTERNATIVE 1205 + " AS " + Contacts.DISPLAY_NAME_ALTERNATIVE + ", " 1206 + "name_raw_contact." + RawContacts.PHONETIC_NAME 1207 + " AS " + Contacts.PHONETIC_NAME + ", " 1208 + "name_raw_contact." + RawContacts.PHONETIC_NAME_STYLE 1209 + " AS " + Contacts.PHONETIC_NAME_STYLE + ", " 1210 + "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY 1211 + " AS " + Contacts.SORT_KEY_PRIMARY + ", " 1212 + "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE 1213 + " AS " + Contacts.SORT_KEY_ALTERNATIVE; 1214 1215 String dataSelect = "SELECT " 1216 + DataColumns.CONCRETE_ID + " AS " + Data._ID + "," 1217 + Data.RAW_CONTACT_ID + ", " 1218 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", " 1219 + syncColumns + ", " 1220 + dataColumns + ", " 1221 + contactOptionColumns + ", " 1222 + contactNameColumns + ", " 1223 + baseContactColumns + ", " 1224 + buildPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID, 1225 Contacts.PHOTO_URI) + ", " 1226 + buildPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID, 1227 Contacts.PHOTO_THUMBNAIL_URI) + ", " 1228 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID 1229 + " FROM " + Tables.DATA 1230 + " JOIN " + Tables.MIMETYPES + " ON (" 1231 + DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")" 1232 + " JOIN " + Tables.RAW_CONTACTS + " ON (" 1233 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")" 1234 + " JOIN " + Tables.CONTACTS + " ON (" 1235 + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")" 1236 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON(" 1237 + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")" 1238 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON (" 1239 + DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")" 1240 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON (" 1241 + MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 1242 + "' AND " + GroupsColumns.CONCRETE_ID + "=" 1243 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")"; 1244 1245 db.execSQL("CREATE VIEW " + Views.DATA_ALL + " AS " + dataSelect); 1246 db.execSQL("CREATE VIEW " + Views.DATA_RESTRICTED + " AS " + dataSelect + " WHERE " 1247 + RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0"); 1248 1249 String rawContactOptionColumns = 1250 RawContacts.CUSTOM_RINGTONE + "," 1251 + RawContacts.SEND_TO_VOICEMAIL + "," 1252 + RawContacts.LAST_TIME_CONTACTED + "," 1253 + RawContacts.TIMES_CONTACTED + "," 1254 + RawContacts.STARRED; 1255 1256 String rawContactsSelect = "SELECT " 1257 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + "," 1258 + RawContacts.CONTACT_ID + ", " 1259 + RawContacts.AGGREGATION_MODE + ", " 1260 + RawContacts.RAW_CONTACT_IS_READ_ONLY + ", " 1261 + RawContacts.DELETED + ", " 1262 + RawContacts.DISPLAY_NAME_SOURCE + ", " 1263 + RawContacts.DISPLAY_NAME_PRIMARY + ", " 1264 + RawContacts.DISPLAY_NAME_ALTERNATIVE + ", " 1265 + RawContacts.PHONETIC_NAME + ", " 1266 + RawContacts.PHONETIC_NAME_STYLE + ", " 1267 + RawContacts.SORT_KEY_PRIMARY + ", " 1268 + RawContacts.SORT_KEY_ALTERNATIVE + ", " 1269 + rawContactOptionColumns + ", " 1270 + syncColumns 1271 + " FROM " + Tables.RAW_CONTACTS; 1272 1273 db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS_ALL + " AS " + rawContactsSelect); 1274 db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS_RESTRICTED + " AS " + rawContactsSelect 1275 + " WHERE " + RawContacts.IS_RESTRICTED + "=0"); 1276 1277 String contactsColumns = 1278 ContactsColumns.CONCRETE_CUSTOM_RINGTONE 1279 + " AS " + Contacts.CUSTOM_RINGTONE + ", " 1280 + contactNameColumns + ", " 1281 + baseContactColumns + ", " 1282 + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED 1283 + " AS " + Contacts.LAST_TIME_CONTACTED + ", " 1284 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL 1285 + " AS " + Contacts.SEND_TO_VOICEMAIL + ", " 1286 + ContactsColumns.CONCRETE_STARRED 1287 + " AS " + Contacts.STARRED + ", " 1288 + ContactsColumns.CONCRETE_TIMES_CONTACTED 1289 + " AS " + Contacts.TIMES_CONTACTED; 1290 1291 String contactsSelect = "SELECT " 1292 + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + "," 1293 + contactsColumns + ", " 1294 + buildPhotoUriAlias(ContactsColumns.CONCRETE_ID, Contacts.PHOTO_URI) + ", " 1295 + buildPhotoUriAlias(ContactsColumns.CONCRETE_ID, Contacts.PHOTO_THUMBNAIL_URI) 1296 + " FROM " + Tables.CONTACTS 1297 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON(" 1298 + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"; 1299 1300 db.execSQL("CREATE VIEW " + Views.CONTACTS_ALL + " AS " + contactsSelect); 1301 db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + contactsSelect 1302 + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0"); 1303 1304 String rawEntitiesSelect = "SELECT " 1305 + RawContacts.CONTACT_ID + ", " 1306 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + "," 1307 + dataColumns + ", " 1308 + syncColumns + ", " 1309 + Data.SYNC1 + ", " 1310 + Data.SYNC2 + ", " 1311 + Data.SYNC3 + ", " 1312 + Data.SYNC4 + ", " 1313 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ", " 1314 + DataColumns.CONCRETE_ID + " AS " + RawContacts.Entity.DATA_ID + "," 1315 + RawContactsColumns.CONCRETE_STARRED + " AS " + RawContacts.STARRED + "," 1316 + RawContactsColumns.CONCRETE_IS_RESTRICTED + " AS " 1317 + RawContacts.IS_RESTRICTED + "," 1318 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID 1319 + " FROM " + Tables.RAW_CONTACTS 1320 + " LEFT OUTER JOIN " + Tables.DATA + " ON (" 1321 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")" 1322 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON (" 1323 + DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")" 1324 + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON (" 1325 + DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")" 1326 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON (" 1327 + MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 1328 + "' AND " + GroupsColumns.CONCRETE_ID + "=" 1329 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")"; 1330 1331 db.execSQL("CREATE VIEW " + Views.RAW_ENTITIES + " AS " 1332 + rawEntitiesSelect); 1333 db.execSQL("CREATE VIEW " + Views.RAW_ENTITIES_RESTRICTED + " AS " 1334 + rawEntitiesSelect + " WHERE " + RawContacts.IS_RESTRICTED + "=0"); 1335 1336 String entitiesSelect = "SELECT " 1337 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + Contacts._ID + ", " 1338 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", " 1339 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + "," 1340 + RawContactsColumns.CONCRETE_IS_RESTRICTED 1341 + " AS " + RawContacts.IS_RESTRICTED + "," 1342 + dataColumns + ", " 1343 + syncColumns + ", " 1344 + contactsColumns + ", " 1345 + buildPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID, 1346 Contacts.PHOTO_URI) + ", " 1347 + buildPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID, 1348 Contacts.PHOTO_THUMBNAIL_URI) + ", " 1349 + Data.SYNC1 + ", " 1350 + Data.SYNC2 + ", " 1351 + Data.SYNC3 + ", " 1352 + Data.SYNC4 + ", " 1353 + RawContactsColumns.CONCRETE_ID + " AS " + Contacts.Entity.RAW_CONTACT_ID + ", " 1354 + DataColumns.CONCRETE_ID + " AS " + Contacts.Entity.DATA_ID + "," 1355 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID 1356 + " FROM " + Tables.RAW_CONTACTS 1357 + " JOIN " + Tables.CONTACTS + " ON (" 1358 + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")" 1359 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON(" 1360 + Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")" 1361 + " LEFT OUTER JOIN " + Tables.DATA + " ON (" 1362 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")" 1363 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON (" 1364 + DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")" 1365 + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON (" 1366 + DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")" 1367 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON (" 1368 + MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE 1369 + "' AND " + GroupsColumns.CONCRETE_ID + "=" 1370 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")"; 1371 1372 db.execSQL("CREATE VIEW " + Views.ENTITIES + " AS " 1373 + entitiesSelect); 1374 db.execSQL("CREATE VIEW " + Views.ENTITIES_RESTRICTED + " AS " 1375 + entitiesSelect + " WHERE " + RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0"); 1376 } 1377 1378 private static String buildPhotoUriAlias(String contactIdColumn, String alias) { 1379 return "(CASE WHEN " + Contacts.PHOTO_ID + " IS NULL" 1380 + " OR " + Contacts.PHOTO_ID + "=0" 1381 + " THEN NULL" 1382 + " ELSE " + "'" + Contacts.CONTENT_URI + "/'||" 1383 + contactIdColumn + "|| '/" + Photo.CONTENT_DIRECTORY + "'" 1384 + " END)" 1385 + " AS " + alias; 1386 } 1387 1388 private static void createGroupsView(SQLiteDatabase db) { 1389 db.execSQL("DROP VIEW IF EXISTS " + Views.GROUPS_ALL + ";"); 1390 String groupsColumns = 1391 Groups.ACCOUNT_NAME + "," 1392 + Groups.ACCOUNT_TYPE + "," 1393 + Groups.SOURCE_ID + "," 1394 + Groups.VERSION + "," 1395 + Groups.DIRTY + "," 1396 + Groups.TITLE + "," 1397 + Groups.TITLE_RES + "," 1398 + Groups.NOTES + "," 1399 + Groups.SYSTEM_ID + "," 1400 + Groups.DELETED + "," 1401 + Groups.GROUP_VISIBLE + "," 1402 + Groups.SHOULD_SYNC + "," 1403 + Groups.AUTO_ADD + "," 1404 + Groups.FAVORITES + "," 1405 + Groups.GROUP_IS_READ_ONLY + "," 1406 + Groups.SYNC1 + "," 1407 + Groups.SYNC2 + "," 1408 + Groups.SYNC3 + "," 1409 + Groups.SYNC4 + "," 1410 + PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE; 1411 1412 String groupsSelect = "SELECT " 1413 + GroupsColumns.CONCRETE_ID + " AS " + Groups._ID + "," 1414 + groupsColumns 1415 + " FROM " + Tables.GROUPS_JOIN_PACKAGES; 1416 1417 db.execSQL("CREATE VIEW " + Views.GROUPS_ALL + " AS " + groupsSelect); 1418 } 1419 1420 @Override 1421 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 1422 if (oldVersion < 99) { 1423 Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion 1424 + ", data will be lost!"); 1425 1426 db.execSQL("DROP TABLE IF EXISTS " + Tables.CONTACTS + ";"); 1427 db.execSQL("DROP TABLE IF EXISTS " + Tables.RAW_CONTACTS + ";"); 1428 db.execSQL("DROP TABLE IF EXISTS " + Tables.PACKAGES + ";"); 1429 db.execSQL("DROP TABLE IF EXISTS " + Tables.MIMETYPES + ";"); 1430 db.execSQL("DROP TABLE IF EXISTS " + Tables.DATA + ";"); 1431 db.execSQL("DROP TABLE IF EXISTS " + Tables.PHONE_LOOKUP + ";"); 1432 db.execSQL("DROP TABLE IF EXISTS " + Tables.NAME_LOOKUP + ";"); 1433 db.execSQL("DROP TABLE IF EXISTS " + Tables.NICKNAME_LOOKUP + ";"); 1434 db.execSQL("DROP TABLE IF EXISTS " + Tables.GROUPS + ";"); 1435 db.execSQL("DROP TABLE IF EXISTS " + Tables.ACTIVITIES + ";"); 1436 db.execSQL("DROP TABLE IF EXISTS " + Tables.CALLS + ";"); 1437 db.execSQL("DROP TABLE IF EXISTS " + Tables.SETTINGS + ";"); 1438 db.execSQL("DROP TABLE IF EXISTS " + Tables.STATUS_UPDATES + ";"); 1439 1440 // TODO: we should not be dropping agg_exceptions and contact_options. In case that 1441 // table's schema changes, we should try to preserve the data, because it was entered 1442 // by the user and has never been synched to the server. 1443 db.execSQL("DROP TABLE IF EXISTS " + Tables.AGGREGATION_EXCEPTIONS + ";"); 1444 1445 onCreate(db); 1446 return; 1447 } 1448 1449 Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion); 1450 1451 boolean upgradeViewsAndTriggers = false; 1452 boolean upgradeNameLookup = false; 1453 1454 if (oldVersion == 99) { 1455 upgradeViewsAndTriggers = true; 1456 oldVersion++; 1457 } 1458 1459 if (oldVersion == 100) { 1460 db.execSQL("CREATE INDEX IF NOT EXISTS mimetypes_mimetype_index ON " 1461 + Tables.MIMETYPES + " (" 1462 + MimetypesColumns.MIMETYPE + "," 1463 + MimetypesColumns._ID + ");"); 1464 updateIndexStats(db, Tables.MIMETYPES, 1465 "mimetypes_mimetype_index", "50 1 1"); 1466 1467 upgradeViewsAndTriggers = true; 1468 oldVersion++; 1469 } 1470 1471 if (oldVersion == 101) { 1472 upgradeViewsAndTriggers = true; 1473 oldVersion++; 1474 } 1475 1476 if (oldVersion == 102) { 1477 upgradeViewsAndTriggers = true; 1478 oldVersion++; 1479 } 1480 1481 if (oldVersion == 103) { 1482 upgradeViewsAndTriggers = true; 1483 oldVersion++; 1484 } 1485 1486 if (oldVersion == 104 || oldVersion == 201) { 1487 LegacyApiSupport.createSettingsTable(db); 1488 upgradeViewsAndTriggers = true; 1489 oldVersion++; 1490 } 1491 1492 if (oldVersion == 105) { 1493 upgradeToVersion202(db); 1494 upgradeNameLookup = true; 1495 oldVersion = 202; 1496 } 1497 1498 if (oldVersion == 202) { 1499 upgradeToVersion203(db); 1500 upgradeViewsAndTriggers = true; 1501 oldVersion++; 1502 } 1503 1504 if (oldVersion == 203) { 1505 upgradeViewsAndTriggers = true; 1506 oldVersion++; 1507 } 1508 1509 if (oldVersion == 204) { 1510 upgradeToVersion205(db); 1511 upgradeViewsAndTriggers = true; 1512 oldVersion++; 1513 } 1514 1515 if (oldVersion == 205) { 1516 upgrateToVersion206(db); 1517 upgradeViewsAndTriggers = true; 1518 oldVersion++; 1519 } 1520 1521 if (oldVersion == 206) { 1522 upgradeToVersion300(db); 1523 oldVersion = 300; 1524 } 1525 1526 if (oldVersion == 300) { 1527 upgradeViewsAndTriggers = true; 1528 oldVersion = 301; 1529 } 1530 1531 if (oldVersion == 301) { 1532 upgradeViewsAndTriggers = true; 1533 oldVersion = 302; 1534 } 1535 1536 if (oldVersion == 302) { 1537 upgradeEmailToVersion303(db); 1538 upgradeNicknameToVersion303(db); 1539 oldVersion = 303; 1540 } 1541 1542 if (oldVersion == 303) { 1543 upgradeToVersion304(db); 1544 oldVersion = 304; 1545 } 1546 1547 if (oldVersion == 304) { 1548 upgradeNameLookup = true; 1549 oldVersion = 305; 1550 } 1551 1552 if (oldVersion == 305) { 1553 upgradeToVersion306(db); 1554 oldVersion = 306; 1555 } 1556 1557 if (oldVersion == 306) { 1558 upgradeToVersion307(db); 1559 oldVersion = 307; 1560 } 1561 1562 if (oldVersion == 307) { 1563 upgradeToVersion308(db); 1564 oldVersion = 308; 1565 } 1566 1567 // Gingerbread upgrades 1568 if (oldVersion < 350) { 1569 upgradeViewsAndTriggers = true; 1570 oldVersion = 351; 1571 } 1572 1573 if (oldVersion == 351) { 1574 upgradeNameLookup = true; 1575 oldVersion = 352; 1576 } 1577 1578 if (oldVersion == 352) { 1579 upgradeToVersion353(db); 1580 oldVersion = 353; 1581 } 1582 1583 // Honeycomb upgrades 1584 if (oldVersion < 400) { 1585 upgradeViewsAndTriggers = true; 1586 upgradeToVersion400(db); 1587 oldVersion = 400; 1588 } 1589 1590 if (oldVersion == 400) { 1591 upgradeViewsAndTriggers = true; 1592 upgradeToVersion401(db); 1593 oldVersion = 401; 1594 } 1595 1596 if (oldVersion == 401) { 1597 upgradeToVersion402(db); 1598 oldVersion = 402; 1599 } 1600 1601 if (oldVersion == 402) { 1602 upgradeViewsAndTriggers = true; 1603 upgradeToVersion403(db); 1604 oldVersion = 403; 1605 } 1606 1607 if (oldVersion == 403) { 1608 upgradeViewsAndTriggers = true; 1609 oldVersion = 404; 1610 } 1611 1612 if (oldVersion == 404) { 1613 upgradeViewsAndTriggers = true; 1614 upgradeToVersion405(db); 1615 oldVersion = 405; 1616 } 1617 1618 if (oldVersion == 405) { 1619 upgradeViewsAndTriggers = true; 1620 upgradeToVersion406(db); 1621 oldVersion = 406; 1622 } 1623 1624 if (oldVersion == 406) { 1625 upgradeViewsAndTriggers = true; 1626 oldVersion = 407; 1627 } 1628 1629 if (oldVersion == 407) { 1630 // Obsolete 1631 oldVersion = 408; 1632 } 1633 1634 if (oldVersion == 408) { 1635 upgradeViewsAndTriggers = true; 1636 upgradeToVersion409(db); 1637 oldVersion = 409; 1638 } 1639 1640 if (oldVersion == 409) { 1641 upgradeViewsAndTriggers = true; 1642 oldVersion = 410; 1643 } 1644 1645 if (oldVersion == 410) { 1646 upgradeToVersion411(db); 1647 oldVersion = 411; 1648 } 1649 1650 if (oldVersion == 411) { 1651 // Same upgrade as 353, only on Honeycomb devices 1652 upgradeToVersion353(db); 1653 oldVersion = 412; 1654 } 1655 1656 if (oldVersion == 412) { 1657 upgradeToVersion413(db); 1658 oldVersion = 413; 1659 } 1660 1661 if (oldVersion == 413) { 1662 upgradeNameLookup = true; 1663 oldVersion = 414; 1664 } 1665 1666 if (oldVersion == 414) { 1667 upgradeToVersion415(db); 1668 upgradeViewsAndTriggers = true; 1669 oldVersion = 415; 1670 } 1671 1672 if (upgradeViewsAndTriggers) { 1673 createContactsViews(db); 1674 createGroupsView(db); 1675 createContactsTriggers(db); 1676 createContactsIndexes(db); 1677 LegacyApiSupport.createViews(db); 1678 updateSqliteStats(db); 1679 mReopenDatabase = true; 1680 } 1681 1682 if (upgradeNameLookup) { 1683 rebuildNameLookup(db); 1684 } 1685 1686 if (oldVersion != newVersion) { 1687 throw new IllegalStateException( 1688 "error upgrading the database to version " + newVersion); 1689 } 1690 } 1691 1692 private void upgradeToVersion202(SQLiteDatabase db) { 1693 db.execSQL( 1694 "ALTER TABLE " + Tables.PHONE_LOOKUP + 1695 " ADD " + PhoneLookupColumns.MIN_MATCH + " TEXT;"); 1696 1697 db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" + 1698 PhoneLookupColumns.MIN_MATCH + "," + 1699 PhoneLookupColumns.RAW_CONTACT_ID + "," + 1700 PhoneLookupColumns.DATA_ID + 1701 ");"); 1702 1703 updateIndexStats(db, Tables.PHONE_LOOKUP, 1704 "phone_lookup_min_match_index", "10000 2 2 1"); 1705 1706 SQLiteStatement update = db.compileStatement( 1707 "UPDATE " + Tables.PHONE_LOOKUP + 1708 " SET " + PhoneLookupColumns.MIN_MATCH + "=?" + 1709 " WHERE " + PhoneLookupColumns.DATA_ID + "=?"); 1710 1711 // Populate the new column 1712 Cursor c = db.query(Tables.PHONE_LOOKUP + " JOIN " + Tables.DATA + 1713 " ON (" + PhoneLookupColumns.DATA_ID + "=" + DataColumns.CONCRETE_ID + ")", 1714 new String[]{Data._ID, Phone.NUMBER}, null, null, null, null, null); 1715 try { 1716 while (c.moveToNext()) { 1717 long dataId = c.getLong(0); 1718 String number = c.getString(1); 1719 if (!TextUtils.isEmpty(number)) { 1720 update.bindString(1, PhoneNumberUtils.toCallerIDMinMatch(number)); 1721 update.bindLong(2, dataId); 1722 update.execute(); 1723 } 1724 } 1725 } finally { 1726 c.close(); 1727 } 1728 } 1729 1730 private void upgradeToVersion203(SQLiteDatabase db) { 1731 // Garbage-collect first. A bug in Eclair was sometimes leaving 1732 // raw_contacts in the database that no longer had contacts associated 1733 // with them. To avoid failures during this database upgrade, drop 1734 // the orphaned raw_contacts. 1735 db.execSQL( 1736 "DELETE FROM raw_contacts" + 1737 " WHERE contact_id NOT NULL" + 1738 " AND contact_id NOT IN (SELECT _id FROM contacts)"); 1739 1740 db.execSQL( 1741 "ALTER TABLE " + Tables.CONTACTS + 1742 " ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)"); 1743 db.execSQL( 1744 "ALTER TABLE " + Tables.RAW_CONTACTS + 1745 " ADD contact_in_visible_group INTEGER NOT NULL DEFAULT 0"); 1746 1747 // For each Contact, find the RawContact that contributed the display name 1748 db.execSQL( 1749 "UPDATE " + Tables.CONTACTS + 1750 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" + 1751 " SELECT " + RawContacts._ID + 1752 " FROM " + Tables.RAW_CONTACTS + 1753 " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + 1754 " AND " + RawContactsColumns.CONCRETE_DISPLAY_NAME + "=" + 1755 Tables.CONTACTS + "." + Contacts.DISPLAY_NAME + 1756 " ORDER BY " + RawContacts._ID + 1757 " LIMIT 1)" 1758 ); 1759 1760 db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" + 1761 Contacts.NAME_RAW_CONTACT_ID + 1762 ");"); 1763 1764 // If for some unknown reason we missed some names, let's make sure there are 1765 // no contacts without a name, picking a raw contact "at random". 1766 db.execSQL( 1767 "UPDATE " + Tables.CONTACTS + 1768 " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" + 1769 " SELECT " + RawContacts._ID + 1770 " FROM " + Tables.RAW_CONTACTS + 1771 " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + 1772 " ORDER BY " + RawContacts._ID + 1773 " LIMIT 1)" + 1774 " WHERE " + Contacts.NAME_RAW_CONTACT_ID + " IS NULL" 1775 ); 1776 1777 // Wipe out DISPLAY_NAME on the Contacts table as it is no longer in use. 1778 db.execSQL( 1779 "UPDATE " + Tables.CONTACTS + 1780 " SET " + Contacts.DISPLAY_NAME + "=NULL" 1781 ); 1782 1783 // Copy the IN_VISIBLE_GROUP flag down to all raw contacts to allow 1784 // indexing on (display_name, in_visible_group) 1785 db.execSQL( 1786 "UPDATE " + Tables.RAW_CONTACTS + 1787 " SET contact_in_visible_group=(" + 1788 "SELECT " + Contacts.IN_VISIBLE_GROUP + 1789 " FROM " + Tables.CONTACTS + 1790 " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" + 1791 " WHERE " + RawContacts.CONTACT_ID + " NOT NULL" 1792 ); 1793 1794 db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" + 1795 "contact_in_visible_group" + "," + 1796 RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" + 1797 ");"); 1798 1799 db.execSQL("DROP INDEX contacts_visible_index"); 1800 db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" + 1801 Contacts.IN_VISIBLE_GROUP + 1802 ");"); 1803 } 1804 1805 private void upgradeToVersion205(SQLiteDatabase db) { 1806 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1807 + " ADD " + RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT;"); 1808 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1809 + " ADD " + RawContacts.PHONETIC_NAME + " TEXT;"); 1810 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1811 + " ADD " + RawContacts.PHONETIC_NAME_STYLE + " INTEGER;"); 1812 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1813 + " ADD " + RawContacts.SORT_KEY_PRIMARY 1814 + " TEXT COLLATE " + ContactsProvider2.PHONEBOOK_COLLATOR_NAME + ";"); 1815 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 1816 + " ADD " + RawContacts.SORT_KEY_ALTERNATIVE 1817 + " TEXT COLLATE " + ContactsProvider2.PHONEBOOK_COLLATOR_NAME + ";"); 1818 1819 final Locale locale = Locale.getDefault(); 1820 1821 NameSplitter splitter = createNameSplitter(); 1822 1823 SQLiteStatement rawContactUpdate = db.compileStatement( 1824 "UPDATE " + Tables.RAW_CONTACTS + 1825 " SET " + 1826 RawContacts.DISPLAY_NAME_PRIMARY + "=?," + 1827 RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," + 1828 RawContacts.PHONETIC_NAME + "=?," + 1829 RawContacts.PHONETIC_NAME_STYLE + "=?," + 1830 RawContacts.SORT_KEY_PRIMARY + "=?," + 1831 RawContacts.SORT_KEY_ALTERNATIVE + "=?" + 1832 " WHERE " + RawContacts._ID + "=?"); 1833 1834 upgradeStructuredNamesToVersion205(db, rawContactUpdate, splitter); 1835 upgradeOrganizationsToVersion205(db, rawContactUpdate, splitter); 1836 1837 db.execSQL("DROP INDEX raw_contact_sort_key1_index"); 1838 db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" + 1839 "contact_in_visible_group" + "," + 1840 RawContacts.SORT_KEY_PRIMARY + 1841 ");"); 1842 1843 db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" + 1844 "contact_in_visible_group" + "," + 1845 RawContacts.SORT_KEY_ALTERNATIVE + 1846 ");"); 1847 } 1848 1849 private interface StructName205Query { 1850 String TABLE = Tables.DATA_JOIN_RAW_CONTACTS; 1851 1852 String COLUMNS[] = { 1853 DataColumns.CONCRETE_ID, 1854 Data.RAW_CONTACT_ID, 1855 RawContacts.DISPLAY_NAME_SOURCE, 1856 RawContacts.DISPLAY_NAME_PRIMARY, 1857 StructuredName.PREFIX, 1858 StructuredName.GIVEN_NAME, 1859 StructuredName.MIDDLE_NAME, 1860 StructuredName.FAMILY_NAME, 1861 StructuredName.SUFFIX, 1862 StructuredName.PHONETIC_FAMILY_NAME, 1863 StructuredName.PHONETIC_MIDDLE_NAME, 1864 StructuredName.PHONETIC_GIVEN_NAME, 1865 }; 1866 1867 int ID = 0; 1868 int RAW_CONTACT_ID = 1; 1869 int DISPLAY_NAME_SOURCE = 2; 1870 int DISPLAY_NAME = 3; 1871 int PREFIX = 4; 1872 int GIVEN_NAME = 5; 1873 int MIDDLE_NAME = 6; 1874 int FAMILY_NAME = 7; 1875 int SUFFIX = 8; 1876 int PHONETIC_FAMILY_NAME = 9; 1877 int PHONETIC_MIDDLE_NAME = 10; 1878 int PHONETIC_GIVEN_NAME = 11; 1879 } 1880 1881 private void upgradeStructuredNamesToVersion205(SQLiteDatabase db, 1882 SQLiteStatement rawContactUpdate, NameSplitter splitter) { 1883 1884 // Process structured names to detect the style of the full name and phonetic name 1885 1886 long mMimeType; 1887 try { 1888 mMimeType = DatabaseUtils.longForQuery(db, 1889 "SELECT " + MimetypesColumns._ID + 1890 " FROM " + Tables.MIMETYPES + 1891 " WHERE " + MimetypesColumns.MIMETYPE 1892 + "='" + StructuredName.CONTENT_ITEM_TYPE + "'", null); 1893 } catch (SQLiteDoneException e) { 1894 // No structured names in the database 1895 return; 1896 } 1897 1898 SQLiteStatement structuredNameUpdate = db.compileStatement( 1899 "UPDATE " + Tables.DATA + 1900 " SET " + 1901 StructuredName.FULL_NAME_STYLE + "=?," + 1902 StructuredName.DISPLAY_NAME + "=?," + 1903 StructuredName.PHONETIC_NAME_STYLE + "=?" + 1904 " WHERE " + Data._ID + "=?"); 1905 1906 NameSplitter.Name name = new NameSplitter.Name(); 1907 StringBuilder sb = new StringBuilder(); 1908 Cursor cursor = db.query(StructName205Query.TABLE, 1909 StructName205Query.COLUMNS, 1910 DataColumns.MIMETYPE_ID + "=" + mMimeType, null, null, null, null); 1911 try { 1912 while (cursor.moveToNext()) { 1913 long dataId = cursor.getLong(StructName205Query.ID); 1914 long rawContactId = cursor.getLong(StructName205Query.RAW_CONTACT_ID); 1915 int displayNameSource = cursor.getInt(StructName205Query.DISPLAY_NAME_SOURCE); 1916 String displayName = cursor.getString(StructName205Query.DISPLAY_NAME); 1917 1918 name.clear(); 1919 name.prefix = cursor.getString(StructName205Query.PREFIX); 1920 name.givenNames = cursor.getString(StructName205Query.GIVEN_NAME); 1921 name.middleName = cursor.getString(StructName205Query.MIDDLE_NAME); 1922 name.familyName = cursor.getString(StructName205Query.FAMILY_NAME); 1923 name.suffix = cursor.getString(StructName205Query.SUFFIX); 1924 name.phoneticFamilyName = cursor.getString(StructName205Query.PHONETIC_FAMILY_NAME); 1925 name.phoneticMiddleName = cursor.getString(StructName205Query.PHONETIC_MIDDLE_NAME); 1926 name.phoneticGivenName = cursor.getString(StructName205Query.PHONETIC_GIVEN_NAME); 1927 1928 upgradeNameToVersion205(dataId, rawContactId, displayNameSource, displayName, name, 1929 structuredNameUpdate, rawContactUpdate, splitter, sb); 1930 } 1931 } finally { 1932 cursor.close(); 1933 } 1934 } 1935 1936 private void upgradeNameToVersion205(long dataId, long rawContactId, int displayNameSource, 1937 String currentDisplayName, NameSplitter.Name name, 1938 SQLiteStatement structuredNameUpdate, SQLiteStatement rawContactUpdate, 1939 NameSplitter splitter, StringBuilder sb) { 1940 1941 splitter.guessNameStyle(name); 1942 int unadjustedFullNameStyle = name.fullNameStyle; 1943 name.fullNameStyle = splitter.getAdjustedFullNameStyle(name.fullNameStyle); 1944 String displayName = splitter.join(name, true); 1945 1946 // Don't update database with the adjusted fullNameStyle as it is locale 1947 // related 1948 structuredNameUpdate.bindLong(1, unadjustedFullNameStyle); 1949 DatabaseUtils.bindObjectToProgram(structuredNameUpdate, 2, displayName); 1950 structuredNameUpdate.bindLong(3, name.phoneticNameStyle); 1951 structuredNameUpdate.bindLong(4, dataId); 1952 structuredNameUpdate.execute(); 1953 1954 if (displayNameSource == DisplayNameSources.STRUCTURED_NAME) { 1955 String displayNameAlternative = splitter.join(name, false); 1956 String phoneticName = splitter.joinPhoneticName(name); 1957 String sortKey = null; 1958 String sortKeyAlternative = null; 1959 1960 if (phoneticName != null) { 1961 sortKey = sortKeyAlternative = phoneticName; 1962 } else if (name.fullNameStyle == FullNameStyle.CHINESE || 1963 name.fullNameStyle == FullNameStyle.CJK) { 1964 sortKey = sortKeyAlternative = ContactLocaleUtils.getIntance() 1965 .getSortKey(displayName, name.fullNameStyle); 1966 } 1967 1968 if (sortKey == null) { 1969 sortKey = displayName; 1970 sortKeyAlternative = displayNameAlternative; 1971 } 1972 1973 updateRawContact205(rawContactUpdate, rawContactId, displayName, 1974 displayNameAlternative, name.phoneticNameStyle, phoneticName, sortKey, 1975 sortKeyAlternative); 1976 } 1977 } 1978 1979 private interface Organization205Query { 1980 String TABLE = Tables.DATA_JOIN_RAW_CONTACTS; 1981 1982 String COLUMNS[] = { 1983 DataColumns.CONCRETE_ID, 1984 Data.RAW_CONTACT_ID, 1985 Organization.COMPANY, 1986 Organization.PHONETIC_NAME, 1987 }; 1988 1989 int ID = 0; 1990 int RAW_CONTACT_ID = 1; 1991 int COMPANY = 2; 1992 int PHONETIC_NAME = 3; 1993 } 1994 1995 private void upgradeOrganizationsToVersion205(SQLiteDatabase db, 1996 SQLiteStatement rawContactUpdate, NameSplitter splitter) { 1997 final long mimeType = lookupMimeTypeId(db, Organization.CONTENT_ITEM_TYPE); 1998 1999 SQLiteStatement organizationUpdate = db.compileStatement( 2000 "UPDATE " + Tables.DATA + 2001 " SET " + 2002 Organization.PHONETIC_NAME_STYLE + "=?" + 2003 " WHERE " + Data._ID + "=?"); 2004 2005 Cursor cursor = db.query(Organization205Query.TABLE, Organization205Query.COLUMNS, 2006 DataColumns.MIMETYPE_ID + "=" + mimeType + " AND " 2007 + RawContacts.DISPLAY_NAME_SOURCE + "=" + DisplayNameSources.ORGANIZATION, 2008 null, null, null, null); 2009 try { 2010 while (cursor.moveToNext()) { 2011 long dataId = cursor.getLong(Organization205Query.ID); 2012 long rawContactId = cursor.getLong(Organization205Query.RAW_CONTACT_ID); 2013 String company = cursor.getString(Organization205Query.COMPANY); 2014 String phoneticName = cursor.getString(Organization205Query.PHONETIC_NAME); 2015 2016 int phoneticNameStyle = splitter.guessPhoneticNameStyle(phoneticName); 2017 2018 organizationUpdate.bindLong(1, phoneticNameStyle); 2019 organizationUpdate.bindLong(2, dataId); 2020 organizationUpdate.execute(); 2021 2022 String sortKey = null; 2023 if (phoneticName == null && company != null) { 2024 int nameStyle = splitter.guessFullNameStyle(company); 2025 nameStyle = splitter.getAdjustedFullNameStyle(nameStyle); 2026 if (nameStyle == FullNameStyle.CHINESE || 2027 nameStyle == FullNameStyle.CJK ) { 2028 sortKey = ContactLocaleUtils.getIntance() 2029 .getSortKey(company, nameStyle); 2030 } 2031 } 2032 2033 if (sortKey == null) { 2034 sortKey = company; 2035 } 2036 2037 updateRawContact205(rawContactUpdate, rawContactId, company, 2038 company, phoneticNameStyle, phoneticName, sortKey, sortKey); 2039 } 2040 } finally { 2041 cursor.close(); 2042 } 2043 } 2044 2045 private void updateRawContact205(SQLiteStatement rawContactUpdate, long rawContactId, 2046 String displayName, String displayNameAlternative, int phoneticNameStyle, 2047 String phoneticName, String sortKeyPrimary, String sortKeyAlternative) { 2048 bindString(rawContactUpdate, 1, displayName); 2049 bindString(rawContactUpdate, 2, displayNameAlternative); 2050 bindString(rawContactUpdate, 3, phoneticName); 2051 rawContactUpdate.bindLong(4, phoneticNameStyle); 2052 bindString(rawContactUpdate, 5, sortKeyPrimary); 2053 bindString(rawContactUpdate, 6, sortKeyAlternative); 2054 rawContactUpdate.bindLong(7, rawContactId); 2055 rawContactUpdate.execute(); 2056 } 2057 2058 private void upgrateToVersion206(SQLiteDatabase db) { 2059 db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS 2060 + " ADD " + RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0;"); 2061 } 2062 2063 private interface Organization300Query { 2064 String TABLE = Tables.DATA; 2065 2066 String SELECTION = DataColumns.MIMETYPE_ID + "=?"; 2067 2068 String COLUMNS[] = { 2069 Organization._ID, 2070 Organization.RAW_CONTACT_ID, 2071 Organization.COMPANY, 2072 Organization.TITLE 2073 }; 2074 2075 int ID = 0; 2076 int RAW_CONTACT_ID = 1; 2077 int COMPANY = 2; 2078 int TITLE = 3; 2079 } 2080 2081 /** 2082 * Fix for the bug where name lookup records for organizations would get removed by 2083 * unrelated updates of the data rows. 2084 */ 2085 private void upgradeToVersion300(SQLiteDatabase db) { 2086 final long mimeType = lookupMimeTypeId(db, Organization.CONTENT_ITEM_TYPE); 2087 if (mimeType == -1) { 2088 return; 2089 } 2090 2091 ContentValues values = new ContentValues(); 2092 2093 // Find all data rows with the mime type "organization" 2094 Cursor cursor = db.query(Organization300Query.TABLE, Organization300Query.COLUMNS, 2095 Organization300Query.SELECTION, new String[] {String.valueOf(mimeType)}, 2096 null, null, null); 2097 try { 2098 while (cursor.moveToNext()) { 2099 long dataId = cursor.getLong(Organization300Query.ID); 2100 long rawContactId = cursor.getLong(Organization300Query.RAW_CONTACT_ID); 2101 String company = cursor.getString(Organization300Query.COMPANY); 2102 String title = cursor.getString(Organization300Query.TITLE); 2103 2104 // First delete name lookup if there is any (chances are there won't be) 2105 db.delete(Tables.NAME_LOOKUP, NameLookupColumns.DATA_ID + "=?", 2106 new String[]{String.valueOf(dataId)}); 2107 2108 // Now insert two name lookup records: one for company name, one for title 2109 values.put(NameLookupColumns.DATA_ID, dataId); 2110 values.put(NameLookupColumns.RAW_CONTACT_ID, rawContactId); 2111 values.put(NameLookupColumns.NAME_TYPE, NameLookupType.ORGANIZATION); 2112 2113 if (!TextUtils.isEmpty(company)) { 2114 values.put(NameLookupColumns.NORMALIZED_NAME, 2115 NameNormalizer.normalize(company)); 2116 db.insert(Tables.NAME_LOOKUP, null, values); 2117 } 2118 2119 if (!TextUtils.isEmpty(title)) { 2120 values.put(NameLookupColumns.NORMALIZED_NAME, 2121 NameNormalizer.normalize(title)); 2122 db.insert(Tables.NAME_LOOKUP, null, values); 2123 } 2124 } 2125 } finally { 2126 cursor.close(); 2127 } 2128 } 2129 2130 private static final class Upgrade303Query { 2131 public static final String TABLE = Tables.DATA; 2132 2133 public static final String SELECTION = 2134 DataColumns.MIMETYPE_ID + "=?" + 2135 " AND " + Data._ID + " NOT IN " + 2136 "(SELECT " + NameLookupColumns.DATA_ID + " FROM " + Tables.NAME_LOOKUP + ")" + 2137 " AND " + Data.DATA1 + " NOT NULL"; 2138 2139 public static final String COLUMNS[] = { 2140 Data._ID, 2141 Data.RAW_CONTACT_ID, 2142 Data.DATA1, 2143 }; 2144 2145 public static final int ID = 0; 2146 public static final int RAW_CONTACT_ID = 1; 2147 public static final int DATA1 = 2; 2148 } 2149 2150 /** 2151 * The {@link ContactsProvider2#update} method was deleting name lookup for new 2152 * emails during the sync. We need to restore the lost name lookup rows. 2153 */ 2154 private void upgradeEmailToVersion303(SQLiteDatabase db) { 2155 final long mimeTypeId = lookupMimeTypeId(db, Email.CONTENT_ITEM_TYPE); 2156 if (mimeTypeId == -1) { 2157 return; 2158 } 2159 2160 ContentValues values = new ContentValues(); 2161 2162 // Find all data rows with the mime type "email" that are missing name lookup 2163 Cursor cursor = db.query(Upgrade303Query.TABLE, Upgrade303Query.COLUMNS, 2164 Upgrade303Query.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2165 null, null, null); 2166 try { 2167 while (cursor.moveToNext()) { 2168 long dataId = cursor.getLong(Upgrade303Query.ID); 2169 long rawContactId = cursor.getLong(Upgrade303Query.RAW_CONTACT_ID); 2170 String value = cursor.getString(Upgrade303Query.DATA1); 2171 value = extractHandleFromEmailAddress(value); 2172 2173 if (value != null) { 2174 values.put(NameLookupColumns.DATA_ID, dataId); 2175 values.put(NameLookupColumns.RAW_CONTACT_ID, rawContactId); 2176 values.put(NameLookupColumns.NAME_TYPE, NameLookupType.EMAIL_BASED_NICKNAME); 2177 values.put(NameLookupColumns.NORMALIZED_NAME, NameNormalizer.normalize(value)); 2178 db.insert(Tables.NAME_LOOKUP, null, values); 2179 } 2180 } 2181 } finally { 2182 cursor.close(); 2183 } 2184 } 2185 2186 /** 2187 * The {@link ContactsProvider2#update} method was deleting name lookup for new 2188 * nicknames during the sync. We need to restore the lost name lookup rows. 2189 */ 2190 private void upgradeNicknameToVersion303(SQLiteDatabase db) { 2191 final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE); 2192 if (mimeTypeId == -1) { 2193 return; 2194 } 2195 2196 ContentValues values = new ContentValues(); 2197 2198 // Find all data rows with the mime type "nickname" that are missing name lookup 2199 Cursor cursor = db.query(Upgrade303Query.TABLE, Upgrade303Query.COLUMNS, 2200 Upgrade303Query.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2201 null, null, null); 2202 try { 2203 while (cursor.moveToNext()) { 2204 long dataId = cursor.getLong(Upgrade303Query.ID); 2205 long rawContactId = cursor.getLong(Upgrade303Query.RAW_CONTACT_ID); 2206 String value = cursor.getString(Upgrade303Query.DATA1); 2207 2208 values.put(NameLookupColumns.DATA_ID, dataId); 2209 values.put(NameLookupColumns.RAW_CONTACT_ID, rawContactId); 2210 values.put(NameLookupColumns.NAME_TYPE, NameLookupType.NICKNAME); 2211 values.put(NameLookupColumns.NORMALIZED_NAME, NameNormalizer.normalize(value)); 2212 db.insert(Tables.NAME_LOOKUP, null, values); 2213 } 2214 } finally { 2215 cursor.close(); 2216 } 2217 } 2218 2219 private void upgradeToVersion304(SQLiteDatabase db) { 2220 // Mimetype table requires an index on mime type 2221 db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS mime_type ON " + Tables.MIMETYPES + " (" + 2222 MimetypesColumns.MIMETYPE + 2223 ");"); 2224 } 2225 2226 private void upgradeToVersion306(SQLiteDatabase db) { 2227 // Fix invalid lookup that was used for Exchange contacts (it was not escaped) 2228 // It happened when a new contact was created AND synchronized 2229 final StringBuilder lookupKeyBuilder = new StringBuilder(); 2230 final SQLiteStatement updateStatement = db.compileStatement( 2231 "UPDATE contacts " + 2232 "SET lookup=? " + 2233 "WHERE _id=?"); 2234 final Cursor contactIdCursor = db.rawQuery( 2235 "SELECT DISTINCT contact_id " + 2236 "FROM raw_contacts " + 2237 "WHERE deleted=0 AND account_type='com.android.exchange'", 2238 null); 2239 try { 2240 while (contactIdCursor.moveToNext()) { 2241 final long contactId = contactIdCursor.getLong(0); 2242 lookupKeyBuilder.setLength(0); 2243 final Cursor c = db.rawQuery( 2244 "SELECT account_type, account_name, _id, sourceid, display_name " + 2245 "FROM raw_contacts " + 2246 "WHERE contact_id=? " + 2247 "ORDER BY _id", 2248 new String[] { String.valueOf(contactId) }); 2249 try { 2250 while (c.moveToNext()) { 2251 ContactLookupKey.appendToLookupKey(lookupKeyBuilder, 2252 c.getString(0), 2253 c.getString(1), 2254 c.getLong(2), 2255 c.getString(3), 2256 c.getString(4)); 2257 } 2258 } finally { 2259 c.close(); 2260 } 2261 2262 if (lookupKeyBuilder.length() == 0) { 2263 updateStatement.bindNull(1); 2264 } else { 2265 updateStatement.bindString(1, Uri.encode(lookupKeyBuilder.toString())); 2266 } 2267 updateStatement.bindLong(2, contactId); 2268 2269 updateStatement.execute(); 2270 } 2271 } finally { 2272 updateStatement.close(); 2273 contactIdCursor.close(); 2274 } 2275 } 2276 2277 private void upgradeToVersion307(SQLiteDatabase db) { 2278 db.execSQL("CREATE TABLE properties (" + 2279 "property_key TEXT PRIMARY_KEY, " + 2280 "property_value TEXT" + 2281 ");"); 2282 } 2283 2284 private void upgradeToVersion308(SQLiteDatabase db) { 2285 db.execSQL("CREATE TABLE accounts (" + 2286 "account_name TEXT, " + 2287 "account_type TEXT " + 2288 ");"); 2289 2290 db.execSQL("INSERT INTO accounts " + 2291 "SELECT DISTINCT account_name, account_type FROM raw_contacts"); 2292 } 2293 2294 private void upgradeToVersion400(SQLiteDatabase db) { 2295 db.execSQL("ALTER TABLE " + Tables.GROUPS 2296 + " ADD " + Groups.FAVORITES + " INTEGER NOT NULL DEFAULT 0;"); 2297 db.execSQL("ALTER TABLE " + Tables.GROUPS 2298 + " ADD " + Groups.AUTO_ADD + " INTEGER NOT NULL DEFAULT 0;"); 2299 } 2300 2301 private void upgradeToVersion353(SQLiteDatabase db) { 2302 db.execSQL("DELETE FROM contacts " + 2303 "WHERE NOT EXISTS (SELECT 1 FROM raw_contacts WHERE contact_id=contacts._id)"); 2304 } 2305 2306 private void rebuildNameLookup(SQLiteDatabase db) { 2307 db.execSQL("DROP INDEX IF EXISTS name_lookup_index"); 2308 insertNameLookup(db); 2309 createContactsIndexes(db); 2310 } 2311 2312 /** 2313 * Regenerates all locale-sensitive data: nickname_lookup, name_lookup and sort keys. 2314 */ 2315 public void setLocale(ContactsProvider2 provider, Locale locale) { 2316 Log.i(TAG, "Switching to locale " + locale); 2317 2318 long start = SystemClock.uptimeMillis(); 2319 SQLiteDatabase db = getWritableDatabase(); 2320 db.setLocale(locale); 2321 db.beginTransaction(); 2322 try { 2323 db.execSQL("DROP INDEX raw_contact_sort_key1_index"); 2324 db.execSQL("DROP INDEX raw_contact_sort_key2_index"); 2325 db.execSQL("DROP INDEX IF EXISTS name_lookup_index"); 2326 2327 loadNicknameLookupTable(db); 2328 insertNameLookup(db); 2329 rebuildSortKeys(db, provider); 2330 createContactsIndexes(db); 2331 db.setTransactionSuccessful(); 2332 } finally { 2333 db.endTransaction(); 2334 } 2335 2336 Log.i(TAG, "Locale change completed in " + (SystemClock.uptimeMillis() - start) + "ms"); 2337 } 2338 2339 /** 2340 * Regenerates sort keys for all contacts. 2341 */ 2342 private void rebuildSortKeys(SQLiteDatabase db, ContactsProvider2 provider) { 2343 Cursor cursor = db.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID}, 2344 null, null, null, null, null); 2345 try { 2346 while (cursor.moveToNext()) { 2347 long rawContactId = cursor.getLong(0); 2348 provider.updateRawContactDisplayName(db, rawContactId); 2349 } 2350 } finally { 2351 cursor.close(); 2352 } 2353 } 2354 2355 private void insertNameLookup(SQLiteDatabase db) { 2356 db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP); 2357 2358 SQLiteStatement nameLookupInsert = db.compileStatement( 2359 "INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "(" 2360 + NameLookupColumns.RAW_CONTACT_ID + "," 2361 + NameLookupColumns.DATA_ID + "," 2362 + NameLookupColumns.NAME_TYPE + "," 2363 + NameLookupColumns.NORMALIZED_NAME + 2364 ") VALUES (?,?,?,?)"); 2365 2366 try { 2367 insertStructuredNameLookup(db, nameLookupInsert); 2368 insertOrganizationLookup(db, nameLookupInsert); 2369 insertEmailLookup(db, nameLookupInsert); 2370 insertNicknameLookup(db, nameLookupInsert); 2371 } finally { 2372 nameLookupInsert.close(); 2373 } 2374 } 2375 2376 private static final class StructuredNameQuery { 2377 public static final String TABLE = Tables.DATA; 2378 2379 public static final String SELECTION = 2380 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL"; 2381 2382 public static final String COLUMNS[] = { 2383 StructuredName._ID, 2384 StructuredName.RAW_CONTACT_ID, 2385 StructuredName.DISPLAY_NAME, 2386 }; 2387 2388 public static final int ID = 0; 2389 public static final int RAW_CONTACT_ID = 1; 2390 public static final int DISPLAY_NAME = 2; 2391 } 2392 2393 private class StructuredNameLookupBuilder extends NameLookupBuilder { 2394 2395 private final SQLiteStatement mNameLookupInsert; 2396 private final CommonNicknameCache mCommonNicknameCache; 2397 2398 public StructuredNameLookupBuilder(NameSplitter splitter, 2399 CommonNicknameCache commonNicknameCache, SQLiteStatement nameLookupInsert) { 2400 super(splitter); 2401 this.mCommonNicknameCache = commonNicknameCache; 2402 this.mNameLookupInsert = nameLookupInsert; 2403 } 2404 2405 @Override 2406 protected void insertNameLookup(long rawContactId, long dataId, int lookupType, 2407 String name) { 2408 if (!TextUtils.isEmpty(name)) { 2409 ContactsDatabaseHelper.this.insertNormalizedNameLookup(mNameLookupInsert, 2410 rawContactId, dataId, lookupType, name); 2411 } 2412 } 2413 2414 @Override 2415 protected String[] getCommonNicknameClusters(String normalizedName) { 2416 return mCommonNicknameCache.getCommonNicknameClusters(normalizedName); 2417 } 2418 } 2419 2420 /** 2421 * Inserts name lookup rows for all structured names in the database. 2422 */ 2423 private void insertStructuredNameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) { 2424 NameSplitter nameSplitter = createNameSplitter(); 2425 NameLookupBuilder nameLookupBuilder = new StructuredNameLookupBuilder(nameSplitter, 2426 new CommonNicknameCache(db), nameLookupInsert); 2427 final long mimeTypeId = lookupMimeTypeId(db, StructuredName.CONTENT_ITEM_TYPE); 2428 Cursor cursor = db.query(StructuredNameQuery.TABLE, StructuredNameQuery.COLUMNS, 2429 StructuredNameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2430 null, null, null); 2431 try { 2432 while (cursor.moveToNext()) { 2433 long dataId = cursor.getLong(StructuredNameQuery.ID); 2434 long rawContactId = cursor.getLong(StructuredNameQuery.RAW_CONTACT_ID); 2435 String name = cursor.getString(StructuredNameQuery.DISPLAY_NAME); 2436 int fullNameStyle = nameSplitter.guessFullNameStyle(name); 2437 fullNameStyle = nameSplitter.getAdjustedFullNameStyle(fullNameStyle); 2438 nameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle); 2439 } 2440 } finally { 2441 cursor.close(); 2442 } 2443 } 2444 2445 private static final class OrganizationQuery { 2446 public static final String TABLE = Tables.DATA; 2447 2448 public static final String SELECTION = 2449 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL"; 2450 2451 public static final String COLUMNS[] = { 2452 Organization._ID, 2453 Organization.RAW_CONTACT_ID, 2454 Organization.COMPANY, 2455 Organization.TITLE, 2456 }; 2457 2458 public static final int ID = 0; 2459 public static final int RAW_CONTACT_ID = 1; 2460 public static final int COMPANY = 2; 2461 public static final int TITLE = 3; 2462 } 2463 2464 /** 2465 * Inserts name lookup rows for all organizations in the database. 2466 */ 2467 private void insertOrganizationLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) { 2468 final long mimeTypeId = lookupMimeTypeId(db, Organization.CONTENT_ITEM_TYPE); 2469 Cursor cursor = db.query(OrganizationQuery.TABLE, OrganizationQuery.COLUMNS, 2470 OrganizationQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2471 null, null, null); 2472 try { 2473 while (cursor.moveToNext()) { 2474 long dataId = cursor.getLong(OrganizationQuery.ID); 2475 long rawContactId = cursor.getLong(OrganizationQuery.RAW_CONTACT_ID); 2476 String organization = cursor.getString(OrganizationQuery.COMPANY); 2477 String title = cursor.getString(OrganizationQuery.TITLE); 2478 insertNameLookup(nameLookupInsert, rawContactId, dataId, 2479 NameLookupType.ORGANIZATION, organization); 2480 insertNameLookup(nameLookupInsert, rawContactId, dataId, 2481 NameLookupType.ORGANIZATION, title); 2482 } 2483 } finally { 2484 cursor.close(); 2485 } 2486 } 2487 2488 private static final class EmailQuery { 2489 public static final String TABLE = Tables.DATA; 2490 2491 public static final String SELECTION = 2492 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL"; 2493 2494 public static final String COLUMNS[] = { 2495 Email._ID, 2496 Email.RAW_CONTACT_ID, 2497 Email.ADDRESS, 2498 }; 2499 2500 public static final int ID = 0; 2501 public static final int RAW_CONTACT_ID = 1; 2502 public static final int ADDRESS = 2; 2503 } 2504 2505 /** 2506 * Inserts name lookup rows for all email addresses in the database. 2507 */ 2508 private void insertEmailLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) { 2509 final long mimeTypeId = lookupMimeTypeId(db, Email.CONTENT_ITEM_TYPE); 2510 Cursor cursor = db.query(EmailQuery.TABLE, EmailQuery.COLUMNS, 2511 EmailQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2512 null, null, null); 2513 try { 2514 while (cursor.moveToNext()) { 2515 long dataId = cursor.getLong(EmailQuery.ID); 2516 long rawContactId = cursor.getLong(EmailQuery.RAW_CONTACT_ID); 2517 String address = cursor.getString(EmailQuery.ADDRESS); 2518 address = extractHandleFromEmailAddress(address); 2519 insertNameLookup(nameLookupInsert, rawContactId, dataId, 2520 NameLookupType.EMAIL_BASED_NICKNAME, address); 2521 } 2522 } finally { 2523 cursor.close(); 2524 } 2525 } 2526 2527 private static final class NicknameQuery { 2528 public static final String TABLE = Tables.DATA; 2529 2530 public static final String SELECTION = 2531 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL"; 2532 2533 public static final String COLUMNS[] = { 2534 Nickname._ID, 2535 Nickname.RAW_CONTACT_ID, 2536 Nickname.NAME, 2537 }; 2538 2539 public static final int ID = 0; 2540 public static final int RAW_CONTACT_ID = 1; 2541 public static final int NAME = 2; 2542 } 2543 2544 /** 2545 * Inserts name lookup rows for all nicknames in the database. 2546 */ 2547 private void insertNicknameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) { 2548 final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE); 2549 Cursor cursor = db.query(NicknameQuery.TABLE, NicknameQuery.COLUMNS, 2550 NicknameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)}, 2551 null, null, null); 2552 try { 2553 while (cursor.moveToNext()) { 2554 long dataId = cursor.getLong(NicknameQuery.ID); 2555 long rawContactId = cursor.getLong(NicknameQuery.RAW_CONTACT_ID); 2556 String nickname = cursor.getString(NicknameQuery.NAME); 2557 insertNameLookup(nameLookupInsert, rawContactId, dataId, 2558 NameLookupType.NICKNAME, nickname); 2559 } 2560 } finally { 2561 cursor.close(); 2562 } 2563 } 2564 2565 /** 2566 * Inserts a record in the {@link Tables#NAME_LOOKUP} table. 2567 */ 2568 public void insertNameLookup(SQLiteStatement stmt, long rawContactId, long dataId, 2569 int lookupType, String name) { 2570 if (TextUtils.isEmpty(name)) { 2571 return; 2572 } 2573 2574 String normalized = NameNormalizer.normalize(name); 2575 if (TextUtils.isEmpty(normalized)) { 2576 return; 2577 } 2578 2579 insertNormalizedNameLookup(stmt, rawContactId, dataId, lookupType, normalized); 2580 } 2581 2582 private void insertNormalizedNameLookup(SQLiteStatement stmt, long rawContactId, long dataId, 2583 int lookupType, String normalizedName) { 2584 stmt.bindLong(1, rawContactId); 2585 stmt.bindLong(2, dataId); 2586 stmt.bindLong(3, lookupType); 2587 stmt.bindString(4, normalizedName); 2588 stmt.executeInsert(); 2589 } 2590 2591 /** 2592 * Changing the VISIBLE bit from a field on both RawContacts and Contacts to a separate table. 2593 */ 2594 private void upgradeToVersion401(SQLiteDatabase db) { 2595 db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" + 2596 Contacts._ID + " INTEGER PRIMARY KEY" + 2597 ");"); 2598 db.execSQL("INSERT INTO " + Tables.VISIBLE_CONTACTS + 2599 " SELECT " + Contacts._ID + 2600 " FROM " + Tables.CONTACTS + 2601 " WHERE " + Contacts.IN_VISIBLE_GROUP + "!=0"); 2602 db.execSQL("DROP INDEX contacts_visible_index"); 2603 } 2604 2605 /** 2606 * Introducing a new table: directories. 2607 */ 2608 private void upgradeToVersion402(SQLiteDatabase db) { 2609 createDirectoriesTable(db); 2610 } 2611 2612 private void upgradeToVersion403(SQLiteDatabase db) { 2613 db.execSQL("DROP TABLE IF EXISTS directories;"); 2614 createDirectoriesTable(db); 2615 2616 db.execSQL("ALTER TABLE raw_contacts" 2617 + " ADD raw_contact_is_read_only INTEGER NOT NULL DEFAULT 0;"); 2618 2619 db.execSQL("ALTER TABLE data" 2620 + " ADD is_read_only INTEGER NOT NULL DEFAULT 0;"); 2621 } 2622 2623 private void upgradeToVersion405(SQLiteDatabase db) { 2624 db.execSQL("DROP TABLE IF EXISTS phone_lookup;"); 2625 // Private phone numbers table used for lookup 2626 db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" + 2627 PhoneLookupColumns.DATA_ID 2628 + " INTEGER REFERENCES data(_id) NOT NULL," + 2629 PhoneLookupColumns.RAW_CONTACT_ID 2630 + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," + 2631 PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," + 2632 PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" + 2633 ");"); 2634 2635 db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" + 2636 PhoneLookupColumns.NORMALIZED_NUMBER + "," + 2637 PhoneLookupColumns.RAW_CONTACT_ID + "," + 2638 PhoneLookupColumns.DATA_ID + 2639 ");"); 2640 2641 db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" + 2642 PhoneLookupColumns.MIN_MATCH + "," + 2643 PhoneLookupColumns.RAW_CONTACT_ID + "," + 2644 PhoneLookupColumns.DATA_ID + 2645 ");"); 2646 2647 final long mimeTypeId = lookupMimeTypeId(db, Phone.CONTENT_ITEM_TYPE); 2648 if (mimeTypeId == -1) { 2649 return; 2650 } 2651 2652 String mCountryIso = getCountryIso(); 2653 Cursor cursor = db.rawQuery( 2654 "SELECT _id, " + Phone.RAW_CONTACT_ID + ", " + Phone.NUMBER + 2655 " FROM " + Tables.DATA + 2656 " WHERE " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId 2657 + " AND " + Phone.NUMBER + " NOT NULL", null); 2658 2659 ContentValues phoneValues = new ContentValues(); 2660 try { 2661 while (cursor.moveToNext()) { 2662 long dataID = cursor.getLong(0); 2663 long rawContactID = cursor.getLong(1); 2664 String number = cursor.getString(2); 2665 String numberE164 = PhoneNumberUtils.formatNumberToE164(number, mCountryIso); 2666 String normalizedNumber = PhoneNumberUtils.normalizeNumber(number); 2667 if (!TextUtils.isEmpty(normalizedNumber)) { 2668 phoneValues.clear(); 2669 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactID); 2670 phoneValues.put(PhoneLookupColumns.DATA_ID, dataID); 2671 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber); 2672 phoneValues.put(PhoneLookupColumns.MIN_MATCH, 2673 PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber)); 2674 db.insert(Tables.PHONE_LOOKUP, null, phoneValues); 2675 2676 if (numberE164 != null && !numberE164.equals(normalizedNumber)) { 2677 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, numberE164); 2678 phoneValues.put(PhoneLookupColumns.MIN_MATCH, 2679 PhoneNumberUtils.toCallerIDMinMatch(numberE164)); 2680 db.insert(Tables.PHONE_LOOKUP, null, phoneValues); 2681 } 2682 } 2683 } 2684 } finally { 2685 cursor.close(); 2686 } 2687 } 2688 2689 private void upgradeToVersion406(SQLiteDatabase db) { 2690 db.execSQL("ALTER TABLE calls ADD countryiso TEXT;"); 2691 } 2692 2693 private void upgradeToVersion409(SQLiteDatabase db) { 2694 db.execSQL("DROP TABLE IF EXISTS directories;"); 2695 createDirectoriesTable(db); 2696 } 2697 2698 /** 2699 * Adding DEFAULT_DIRECTORY table. 2700 */ 2701 private void upgradeToVersion411(SQLiteDatabase db) { 2702 db.execSQL("DROP TABLE IF EXISTS " + Tables.DEFAULT_DIRECTORY); 2703 db.execSQL("CREATE TABLE " + Tables.DEFAULT_DIRECTORY + " (" + 2704 Contacts._ID + " INTEGER PRIMARY KEY" + 2705 ");"); 2706 2707 // Process contacts without an account 2708 db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + 2709 " SELECT " + RawContacts.CONTACT_ID + 2710 " FROM " + Tables.RAW_CONTACTS + 2711 " WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " IS NULL " + 2712 " AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL "); 2713 2714 // Process accounts that don't have a default group (e.g. Exchange) 2715 db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + 2716 " SELECT " + RawContacts.CONTACT_ID + 2717 " FROM " + Tables.RAW_CONTACTS + 2718 " WHERE NOT EXISTS" + 2719 " (SELECT " + Groups._ID + 2720 " FROM " + Tables.GROUPS + 2721 " WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " = " 2722 + GroupsColumns.CONCRETE_ACCOUNT_NAME + 2723 " AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " = " 2724 + GroupsColumns.CONCRETE_ACCOUNT_TYPE + 2725 " AND " + Groups.AUTO_ADD + " != 0" + 2726 ")"); 2727 2728 long mimetype = lookupMimeTypeId(db, GroupMembership.CONTENT_ITEM_TYPE); 2729 2730 // Process accounts that do have a default group (e.g. Google) 2731 db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + 2732 " SELECT " + RawContacts.CONTACT_ID + 2733 " FROM " + Tables.RAW_CONTACTS + 2734 " JOIN " + Tables.DATA + 2735 " ON (" + RawContactsColumns.CONCRETE_ID + "=" + Data.RAW_CONTACT_ID + ")" + 2736 " WHERE " + DataColumns.MIMETYPE_ID + "=" + mimetype + 2737 " AND EXISTS" + 2738 " (SELECT " + Groups._ID + 2739 " FROM " + Tables.GROUPS + 2740 " WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " = " 2741 + GroupsColumns.CONCRETE_ACCOUNT_NAME + 2742 " AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " = " 2743 + GroupsColumns.CONCRETE_ACCOUNT_TYPE + 2744 " AND " + Groups.AUTO_ADD + " != 0" + 2745 ")"); 2746 } 2747 2748 private void upgradeToVersion413(SQLiteDatabase db) { 2749 db.execSQL( 2750 "ALTER TABLE " + Tables.DIRECTORIES + 2751 " ADD " + DirectoryColumns.TYPE_RESOURCE_NAME + " TEXT;"); 2752 } 2753 2754 private void upgradeToVersion415(SQLiteDatabase db) { 2755 db.execSQL( 2756 "ALTER TABLE " + Tables.GROUPS + 2757 " ADD " + Groups.GROUP_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0"); 2758 db.execSQL( 2759 "UPDATE " + Tables.GROUPS + 2760 " SET " + Groups.GROUP_IS_READ_ONLY + "=1" + 2761 " WHERE " + Groups.SYSTEM_ID + " NOT NULL"); 2762 } 2763 2764 public String extractHandleFromEmailAddress(String email) { 2765 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email); 2766 if (tokens.length == 0) { 2767 return null; 2768 } 2769 2770 String address = tokens[0].getAddress(); 2771 int at = address.indexOf('@'); 2772 if (at != -1) { 2773 return address.substring(0, at); 2774 } 2775 return null; 2776 } 2777 2778 public String extractAddressFromEmailAddress(String email) { 2779 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email); 2780 if (tokens.length == 0) { 2781 return null; 2782 } 2783 2784 return tokens[0].getAddress(); 2785 } 2786 2787 private long lookupMimeTypeId(SQLiteDatabase db, String mimeType) { 2788 try { 2789 return DatabaseUtils.longForQuery(db, 2790 "SELECT " + MimetypesColumns._ID + 2791 " FROM " + Tables.MIMETYPES + 2792 " WHERE " + MimetypesColumns.MIMETYPE 2793 + "='" + mimeType + "'", null); 2794 } catch (SQLiteDoneException e) { 2795 // No rows of this type in the database 2796 return -1; 2797 } 2798 } 2799 2800 private void bindString(SQLiteStatement stmt, int index, String value) { 2801 if (value == null) { 2802 stmt.bindNull(index); 2803 } else { 2804 stmt.bindString(index, value); 2805 } 2806 } 2807 2808 /** 2809 * Adds index stats into the SQLite database to force it to always use the lookup indexes. 2810 */ 2811 private void updateSqliteStats(SQLiteDatabase db) { 2812 2813 // Specific stats strings are based on an actual large database after running ANALYZE 2814 try { 2815 updateIndexStats(db, Tables.CONTACTS, 2816 "contacts_restricted_index", "10000 9000"); 2817 updateIndexStats(db, Tables.CONTACTS, 2818 "contacts_has_phone_index", "10000 500"); 2819 2820 updateIndexStats(db, Tables.RAW_CONTACTS, 2821 "raw_contacts_source_id_index", "10000 1 1 1"); 2822 updateIndexStats(db, Tables.RAW_CONTACTS, 2823 "raw_contacts_contact_id_index", "10000 2"); 2824 2825 updateIndexStats(db, Tables.NAME_LOOKUP, 2826 "name_lookup_raw_contact_id_index", "10000 3"); 2827 updateIndexStats(db, Tables.NAME_LOOKUP, 2828 "name_lookup_index", "10000 3 2 2 1"); 2829 updateIndexStats(db, Tables.NAME_LOOKUP, 2830 "sqlite_autoindex_name_lookup_1", "10000 3 2 1"); 2831 2832 updateIndexStats(db, Tables.PHONE_LOOKUP, 2833 "phone_lookup_index", "10000 2 2 1"); 2834 updateIndexStats(db, Tables.PHONE_LOOKUP, 2835 "phone_lookup_min_match_index", "10000 2 2 1"); 2836 2837 updateIndexStats(db, Tables.DATA, 2838 "data_mimetype_data1_index", "60000 5000 2"); 2839 updateIndexStats(db, Tables.DATA, 2840 "data_raw_contact_id", "60000 10"); 2841 2842 updateIndexStats(db, Tables.GROUPS, 2843 "groups_source_id_index", "50 1 1 1"); 2844 2845 updateIndexStats(db, Tables.NICKNAME_LOOKUP, 2846 "sqlite_autoindex_name_lookup_1", "500 2 1"); 2847 2848 } catch (SQLException e) { 2849 Log.e(TAG, "Could not update index stats", e); 2850 } 2851 } 2852 2853 /** 2854 * Stores statistics for a given index. 2855 * 2856 * @param stats has the following structure: the first index is the expected size of 2857 * the table. The following integer(s) are the expected number of records selected with the 2858 * index. There should be one integer per indexed column. 2859 */ 2860 private void updateIndexStats(SQLiteDatabase db, String table, String index, 2861 String stats) { 2862 db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl='" + table + "' AND idx='" + index + "';"); 2863 db.execSQL("INSERT INTO sqlite_stat1 (tbl,idx,stat)" 2864 + " VALUES ('" + table + "','" + index + "','" + stats + "');"); 2865 } 2866 2867 @Override 2868 public synchronized SQLiteDatabase getWritableDatabase() { 2869 SQLiteDatabase db = super.getWritableDatabase(); 2870 if (mReopenDatabase) { 2871 mReopenDatabase = false; 2872 close(); 2873 db = super.getWritableDatabase(); 2874 } 2875 return db; 2876 } 2877 2878 /** 2879 * Wipes all data except mime type and package lookup tables. 2880 */ 2881 public void wipeData() { 2882 SQLiteDatabase db = getWritableDatabase(); 2883 2884 db.execSQL("DELETE FROM " + Tables.ACCOUNTS + ";"); 2885 db.execSQL("INSERT INTO " + Tables.ACCOUNTS + " VALUES(NULL, NULL)"); 2886 2887 db.execSQL("DELETE FROM " + Tables.CONTACTS + ";"); 2888 db.execSQL("DELETE FROM " + Tables.RAW_CONTACTS + ";"); 2889 db.execSQL("DELETE FROM " + Tables.DATA + ";"); 2890 db.execSQL("DELETE FROM " + Tables.PHONE_LOOKUP + ";"); 2891 db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + ";"); 2892 db.execSQL("DELETE FROM " + Tables.GROUPS + ";"); 2893 db.execSQL("DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS + ";"); 2894 db.execSQL("DELETE FROM " + Tables.SETTINGS + ";"); 2895 db.execSQL("DELETE FROM " + Tables.ACTIVITIES + ";"); 2896 db.execSQL("DELETE FROM " + Tables.CALLS + ";"); 2897 db.execSQL("DELETE FROM " + Tables.DIRECTORIES + ";"); 2898 2899 // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP 2900 } 2901 2902 public NameSplitter createNameSplitter() { 2903 return new NameSplitter( 2904 mContext.getString(com.android.internal.R.string.common_name_prefixes), 2905 mContext.getString(com.android.internal.R.string.common_last_name_prefixes), 2906 mContext.getString(com.android.internal.R.string.common_name_suffixes), 2907 mContext.getString(com.android.internal.R.string.common_name_conjunctions), 2908 Locale.getDefault()); 2909 } 2910 2911 /** 2912 * Return the {@link ApplicationInfo#uid} for the given package name. 2913 */ 2914 public static int getUidForPackageName(PackageManager pm, String packageName) { 2915 try { 2916 ApplicationInfo clientInfo = pm.getApplicationInfo(packageName, 0 /* no flags */); 2917 return clientInfo.uid; 2918 } catch (NameNotFoundException e) { 2919 throw new RuntimeException(e); 2920 } 2921 } 2922 2923 /** 2924 * Perform an internal string-to-integer lookup using the compiled 2925 * {@link SQLiteStatement} provided, using the in-memory cache to speed up 2926 * lookups. If a mapping isn't found in cache or database, it will be 2927 * created. All new, uncached answers are added to the cache automatically. 2928 * 2929 * @param query Compiled statement used to query for the mapping. 2930 * @param insert Compiled statement used to insert a new mapping when no 2931 * existing one is found in cache or from query. 2932 * @param value Value to find mapping for. 2933 * @param cache In-memory cache of previous answers. 2934 * @return An unique integer mapping for the given value. 2935 */ 2936 private long getCachedId(SQLiteStatement query, SQLiteStatement insert, 2937 String value, HashMap<String, Long> cache) { 2938 // Try an in-memory cache lookup 2939 if (cache.containsKey(value)) { 2940 return cache.get(value); 2941 } 2942 2943 long id = -1; 2944 try { 2945 // Try searching database for mapping 2946 DatabaseUtils.bindObjectToProgram(query, 1, value); 2947 id = query.simpleQueryForLong(); 2948 } catch (SQLiteDoneException e) { 2949 // Nothing found, so try inserting new mapping 2950 DatabaseUtils.bindObjectToProgram(insert, 1, value); 2951 id = insert.executeInsert(); 2952 } 2953 2954 if (id != -1) { 2955 // Cache and return the new answer 2956 cache.put(value, id); 2957 return id; 2958 } else { 2959 // Otherwise throw if no mapping found or created 2960 throw new IllegalStateException("Couldn't find or create internal " 2961 + "lookup table entry for value " + value); 2962 } 2963 } 2964 2965 /** 2966 * Convert a package name into an integer, using {@link Tables#PACKAGES} for 2967 * lookups and possible allocation of new IDs as needed. 2968 */ 2969 public long getPackageId(String packageName) { 2970 // Make sure compiled statements are ready by opening database 2971 getReadableDatabase(); 2972 return getCachedId(mPackageQuery, mPackageInsert, packageName, mPackageCache); 2973 } 2974 2975 /** 2976 * Convert a mimetype into an integer, using {@link Tables#MIMETYPES} for 2977 * lookups and possible allocation of new IDs as needed. 2978 */ 2979 public long getMimeTypeId(String mimetype) { 2980 // Make sure compiled statements are ready by opening database 2981 getReadableDatabase(); 2982 return getMimeTypeIdNoDbCheck(mimetype); 2983 } 2984 2985 private long getMimeTypeIdNoDbCheck(String mimetype) { 2986 return getCachedId(mMimetypeQuery, mMimetypeInsert, mimetype, mMimetypeCache); 2987 } 2988 2989 /** 2990 * Find the mimetype for the given {@link Data#_ID}. 2991 */ 2992 public String getDataMimeType(long dataId) { 2993 // Make sure compiled statements are ready by opening database 2994 getReadableDatabase(); 2995 try { 2996 // Try database query to find mimetype 2997 DatabaseUtils.bindObjectToProgram(mDataMimetypeQuery, 1, dataId); 2998 String mimetype = mDataMimetypeQuery.simpleQueryForString(); 2999 return mimetype; 3000 } catch (SQLiteDoneException e) { 3001 // No valid mapping found, so return null 3002 return null; 3003 } 3004 } 3005 3006 /** 3007 * Find the mime-type for the given {@link Activities#_ID}. 3008 */ 3009 public String getActivityMimeType(long activityId) { 3010 // Make sure compiled statements are ready by opening database 3011 getReadableDatabase(); 3012 try { 3013 // Try database query to find mimetype 3014 DatabaseUtils.bindObjectToProgram(mActivitiesMimetypeQuery, 1, activityId); 3015 String mimetype = mActivitiesMimetypeQuery.simpleQueryForString(); 3016 return mimetype; 3017 } catch (SQLiteDoneException e) { 3018 // No valid mapping found, so return null 3019 return null; 3020 } 3021 } 3022 3023 /** 3024 * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts. 3025 */ 3026 public void updateAllVisible() { 3027 updateCustomContactVisibility(getWritableDatabase(), ""); 3028 } 3029 3030 /** 3031 * Update {@link Contacts#IN_VISIBLE_GROUP} and 3032 * {@link Tables#DEFAULT_DIRECTORY} for a specific contact. 3033 */ 3034 public void updateContactVisible(long contactId) { 3035 SQLiteDatabase db = getWritableDatabase(); 3036 updateCustomContactVisibility(getWritableDatabase(), 3037 " AND " + Contacts._ID + "=" + contactId); 3038 3039 String contactIdAsString = String.valueOf(contactId); 3040 long mimetype = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 3041 3042 // The contact will be included in the default directory if contains 3043 // a raw contact that is in any group or in an account that 3044 // does not have any AUTO_ADD groups. 3045 long visibleRawContact = DatabaseUtils.longForQuery(db, 3046 "SELECT EXISTS (" + 3047 "SELECT " + RawContacts.CONTACT_ID + 3048 " FROM " + Tables.RAW_CONTACTS + 3049 " JOIN " + Tables.DATA + 3050 " ON (" + RawContactsColumns.CONCRETE_ID + "=" 3051 + Data.RAW_CONTACT_ID + ")" + 3052 " WHERE " + RawContacts.CONTACT_ID + "=?" + 3053 " AND " + DataColumns.MIMETYPE_ID + "=?" + 3054 ") OR EXISTS (" + 3055 "SELECT " + RawContacts._ID + 3056 " FROM " + Tables.RAW_CONTACTS + 3057 " WHERE " + RawContacts.CONTACT_ID + "=?" + 3058 " AND NOT EXISTS" + 3059 " (SELECT " + Groups._ID + 3060 " FROM " + Tables.GROUPS + 3061 " WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " = " 3062 + GroupsColumns.CONCRETE_ACCOUNT_NAME + 3063 " AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " = " 3064 + GroupsColumns.CONCRETE_ACCOUNT_TYPE + 3065 " AND " + Groups.AUTO_ADD + " != 0" + 3066 ")" + 3067 ") OR EXISTS (" + 3068 "SELECT " + RawContacts._ID + 3069 " FROM " + Tables.RAW_CONTACTS + 3070 " WHERE " + RawContacts.CONTACT_ID + "=?" + 3071 " AND " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " IS NULL " + 3072 " AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL" + 3073 ")", 3074 new String[] { 3075 contactIdAsString, 3076 String.valueOf(mimetype), 3077 contactIdAsString, 3078 contactIdAsString 3079 }); 3080 3081 if (visibleRawContact != 0) { 3082 db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + " VALUES(?)", 3083 new String[] { contactIdAsString }); 3084 } else { 3085 db.execSQL("DELETE FROM " + Tables.DEFAULT_DIRECTORY + " WHERE " + Contacts._ID + "=?", 3086 new String[] { contactIdAsString }); 3087 } 3088 } 3089 3090 private void updateCustomContactVisibility(SQLiteDatabase db, String selection) { 3091 final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE); 3092 String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)}; 3093 3094 // First delete what needs to be deleted, then insert what needs to be added. 3095 // Since flash writes are very expensive, this approach is much better than 3096 // delete-all-insert-all. 3097 db.execSQL("DELETE FROM " + Tables.VISIBLE_CONTACTS + 3098 " WHERE " + "_id NOT IN" + 3099 "(SELECT " + Contacts._ID + 3100 " FROM " + Tables.CONTACTS + 3101 " WHERE (" + Clauses.CONTACT_IS_VISIBLE + ")=1) " + selection, 3102 selectionArgs); 3103 3104 db.execSQL("INSERT INTO " + Tables.VISIBLE_CONTACTS + 3105 " SELECT " + Contacts._ID + 3106 " FROM " + Tables.CONTACTS + 3107 " WHERE " + Contacts._ID + 3108 " NOT IN " + Tables.VISIBLE_CONTACTS + 3109 " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1 " + selection, 3110 selectionArgs); 3111 } 3112 3113 /** 3114 * Returns contact ID for the given contact or zero if it is NULL. 3115 */ 3116 public long getContactId(long rawContactId) { 3117 getReadableDatabase(); 3118 try { 3119 DatabaseUtils.bindObjectToProgram(mContactIdQuery, 1, rawContactId); 3120 return mContactIdQuery.simpleQueryForLong(); 3121 } catch (SQLiteDoneException e) { 3122 // No valid mapping found, so return 0 3123 return 0; 3124 } 3125 } 3126 3127 public int getAggregationMode(long rawContactId) { 3128 getReadableDatabase(); 3129 try { 3130 DatabaseUtils.bindObjectToProgram(mAggregationModeQuery, 1, rawContactId); 3131 return (int)mAggregationModeQuery.simpleQueryForLong(); 3132 } catch (SQLiteDoneException e) { 3133 // No valid row found, so return "disabled" 3134 return RawContacts.AGGREGATION_MODE_DISABLED; 3135 } 3136 } 3137 3138 public void buildPhoneLookupAndContactQuery( 3139 SQLiteQueryBuilder qb, String normalizedNumber, String numberE164) { 3140 String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber); 3141 StringBuilder sb = new StringBuilder(); 3142 appendPhoneLookupTables(sb, minMatch, true); 3143 qb.setTables(sb.toString()); 3144 3145 sb = new StringBuilder(); 3146 appendPhoneLookupSelection(sb, normalizedNumber, numberE164); 3147 qb.appendWhere(sb.toString()); 3148 } 3149 3150 public String buildPhoneLookupAsNestedQuery(String number) { 3151 StringBuilder sb = new StringBuilder(); 3152 final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number); 3153 sb.append("(SELECT DISTINCT raw_contact_id" + " FROM "); 3154 appendPhoneLookupTables(sb, minMatch, false); 3155 sb.append(" WHERE "); 3156 appendPhoneLookupSelection(sb, number, null); 3157 sb.append(")"); 3158 return sb.toString(); 3159 } 3160 3161 private void appendPhoneLookupTables(StringBuilder sb, final String minMatch, 3162 boolean joinContacts) { 3163 sb.append(Tables.RAW_CONTACTS); 3164 if (joinContacts) { 3165 sb.append(" JOIN " + getContactView() + " contacts_view" 3166 + " ON (contacts_view._id = raw_contacts.contact_id)"); 3167 } 3168 sb.append(", (SELECT data_id, normalized_number, length(normalized_number) as len " 3169 + " FROM phone_lookup " + " WHERE (" + Tables.PHONE_LOOKUP + "." 3170 + PhoneLookupColumns.MIN_MATCH + " = '"); 3171 sb.append(minMatch); 3172 sb.append("')) AS lookup, " + Tables.DATA); 3173 } 3174 3175 private void appendPhoneLookupSelection(StringBuilder sb, String number, String numberE164) { 3176 sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id"); 3177 boolean hasNumberE164 = !TextUtils.isEmpty(numberE164); 3178 boolean hasNumber = !TextUtils.isEmpty(number); 3179 if (hasNumberE164 || hasNumber) { 3180 sb.append(" AND ( "); 3181 if (hasNumberE164) { 3182 sb.append(" lookup.normalized_number = "); 3183 DatabaseUtils.appendEscapedSQLString(sb, numberE164); 3184 } 3185 if (hasNumberE164 && hasNumber) { 3186 sb.append(" OR "); 3187 } 3188 if (hasNumber) { 3189 int numberLen = number.length(); 3190 sb.append(" lookup.len <= "); 3191 sb.append(numberLen); 3192 sb.append(" AND substr("); 3193 DatabaseUtils.appendEscapedSQLString(sb, number); 3194 sb.append(','); 3195 sb.append(numberLen); 3196 sb.append(" - lookup.len + 1) = lookup.normalized_number"); 3197 } 3198 sb.append(')'); 3199 } 3200 } 3201 3202 public String getUseStrictPhoneNumberComparisonParameter() { 3203 return mUseStrictPhoneNumberComparison ? "1" : "0"; 3204 } 3205 3206 /** 3207 * Loads common nickname mappings into the database. 3208 */ 3209 private void loadNicknameLookupTable(SQLiteDatabase db) { 3210 db.execSQL("DELETE FROM " + Tables.NICKNAME_LOOKUP); 3211 3212 String[] strings = mContext.getResources().getStringArray( 3213 com.android.internal.R.array.common_nicknames); 3214 if (strings == null || strings.length == 0) { 3215 return; 3216 } 3217 3218 SQLiteStatement nicknameLookupInsert = db.compileStatement("INSERT INTO " 3219 + Tables.NICKNAME_LOOKUP + "(" + NicknameLookupColumns.NAME + "," 3220 + NicknameLookupColumns.CLUSTER + ") VALUES (?,?)"); 3221 3222 try { 3223 for (int clusterId = 0; clusterId < strings.length; clusterId++) { 3224 String[] names = strings[clusterId].split(","); 3225 for (int j = 0; j < names.length; j++) { 3226 String name = NameNormalizer.normalize(names[j]); 3227 try { 3228 DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 1, name); 3229 DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 2, 3230 String.valueOf(clusterId)); 3231 nicknameLookupInsert.executeInsert(); 3232 } catch (SQLiteException e) { 3233 3234 // Print the exception and keep going - this is not a fatal error 3235 Log.e(TAG, "Cannot insert nickname: " + names[j], e); 3236 } 3237 } 3238 } 3239 } finally { 3240 nicknameLookupInsert.close(); 3241 } 3242 } 3243 3244 public static void copyStringValue(ContentValues toValues, String toKey, 3245 ContentValues fromValues, String fromKey) { 3246 if (fromValues.containsKey(fromKey)) { 3247 toValues.put(toKey, fromValues.getAsString(fromKey)); 3248 } 3249 } 3250 3251 public static void copyLongValue(ContentValues toValues, String toKey, 3252 ContentValues fromValues, String fromKey) { 3253 if (fromValues.containsKey(fromKey)) { 3254 long longValue; 3255 Object value = fromValues.get(fromKey); 3256 if (value instanceof Boolean) { 3257 if ((Boolean)value) { 3258 longValue = 1; 3259 } else { 3260 longValue = 0; 3261 } 3262 } else if (value instanceof String) { 3263 longValue = Long.parseLong((String)value); 3264 } else { 3265 longValue = ((Number)value).longValue(); 3266 } 3267 toValues.put(toKey, longValue); 3268 } 3269 } 3270 3271 public SyncStateContentProviderHelper getSyncState() { 3272 return mSyncState; 3273 } 3274 3275 /** 3276 * Delete the aggregate contact if it has no constituent raw contacts other 3277 * than the supplied one. 3278 */ 3279 public void removeContactIfSingleton(long rawContactId) { 3280 SQLiteDatabase db = getWritableDatabase(); 3281 3282 // Obtain contact ID from the supplied raw contact ID 3283 String contactIdFromRawContactId = "(SELECT " + RawContacts.CONTACT_ID + " FROM " 3284 + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=" + rawContactId + ")"; 3285 3286 // Find other raw contacts in the same aggregate contact 3287 String otherRawContacts = "(SELECT contacts1." + RawContacts._ID + " FROM " 3288 + Tables.RAW_CONTACTS + " contacts1 JOIN " + Tables.RAW_CONTACTS + " contacts2 ON (" 3289 + "contacts1." + RawContacts.CONTACT_ID + "=contacts2." + RawContacts.CONTACT_ID 3290 + ") WHERE contacts1." + RawContacts._ID + "!=" + rawContactId + "" 3291 + " AND contacts2." + RawContacts._ID + "=" + rawContactId + ")"; 3292 3293 db.execSQL("DELETE FROM " + Tables.CONTACTS 3294 + " WHERE " + Contacts._ID + "=" + contactIdFromRawContactId 3295 + " AND NOT EXISTS " + otherRawContacts + ";"); 3296 } 3297 3298 /** 3299 * Returns the value from the {@link Tables#PROPERTIES} table. 3300 */ 3301 public String getProperty(String key, String defaultValue) { 3302 Cursor cursor = getReadableDatabase().query(Tables.PROPERTIES, 3303 new String[]{PropertiesColumns.PROPERTY_VALUE}, 3304 PropertiesColumns.PROPERTY_KEY + "=?", 3305 new String[]{key}, null, null, null); 3306 String value = null; 3307 try { 3308 if (cursor.moveToFirst()) { 3309 value = cursor.getString(0); 3310 } 3311 } finally { 3312 cursor.close(); 3313 } 3314 3315 return value != null ? value : defaultValue; 3316 } 3317 3318 /** 3319 * Stores a key-value pair in the {@link Tables#PROPERTIES} table. 3320 */ 3321 public void setProperty(String key, String value) { 3322 setProperty(getWritableDatabase(), key, value); 3323 } 3324 3325 private void setProperty(SQLiteDatabase db, String key, String value) { 3326 ContentValues values = new ContentValues(); 3327 values.put(PropertiesColumns.PROPERTY_KEY, key); 3328 values.put(PropertiesColumns.PROPERTY_VALUE, value); 3329 db.replace(Tables.PROPERTIES, null, values); 3330 } 3331 3332 /** 3333 * Check if {@link Binder#getCallingUid()} should be allowed access to 3334 * {@link RawContacts#IS_RESTRICTED} data. 3335 */ 3336 boolean hasAccessToRestrictedData() { 3337 final PackageManager pm = mContext.getPackageManager(); 3338 int caller = Binder.getCallingUid(); 3339 if (caller == 0) return true; // root can do anything 3340 final String[] callerPackages = pm.getPackagesForUid(caller); 3341 3342 // Has restricted access if caller matches any packages 3343 for (String callerPackage : callerPackages) { 3344 if (hasAccessToRestrictedData(callerPackage)) { 3345 return true; 3346 } 3347 } 3348 return false; 3349 } 3350 3351 /** 3352 * Check if requestingPackage should be allowed access to 3353 * {@link RawContacts#IS_RESTRICTED} data. 3354 */ 3355 boolean hasAccessToRestrictedData(String requestingPackage) { 3356 if (mUnrestrictedPackages != null) { 3357 for (String allowedPackage : mUnrestrictedPackages) { 3358 if (allowedPackage.equals(requestingPackage)) { 3359 return true; 3360 } 3361 } 3362 } 3363 return false; 3364 } 3365 3366 public String getDataView() { 3367 return getDataView(false); 3368 } 3369 3370 public String getDataView(boolean requireRestrictedView) { 3371 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 3372 Views.DATA_ALL : Views.DATA_RESTRICTED; 3373 } 3374 3375 public String getRawContactView() { 3376 return getRawContactView(false); 3377 } 3378 3379 public String getRawContactView(boolean requireRestrictedView) { 3380 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 3381 Views.RAW_CONTACTS_ALL : Views.RAW_CONTACTS_RESTRICTED; 3382 } 3383 3384 public String getContactView() { 3385 return getContactView(false); 3386 } 3387 3388 public String getContactView(boolean requireRestrictedView) { 3389 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 3390 Views.CONTACTS_ALL : Views.CONTACTS_RESTRICTED; 3391 } 3392 3393 public String getGroupView() { 3394 return Views.GROUPS_ALL; 3395 } 3396 3397 public String getRawEntitiesView() { 3398 return getRawEntitiesView(false); 3399 } 3400 3401 public String getRawEntitiesView(boolean requireRestrictedView) { 3402 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 3403 Views.RAW_ENTITIES : Views.RAW_ENTITIES_RESTRICTED; 3404 } 3405 3406 public String getEntitiesView() { 3407 return getEntitiesView(false); 3408 } 3409 3410 public String getEntitiesView(boolean requireRestrictedView) { 3411 return (hasAccessToRestrictedData() && !requireRestrictedView) ? 3412 Views.ENTITIES : Views.ENTITIES_RESTRICTED; 3413 } 3414 3415 /** 3416 * Test if any of the columns appear in the given projection. 3417 */ 3418 public boolean isInProjection(String[] projection, String... columns) { 3419 if (projection == null) { 3420 return true; 3421 } 3422 3423 // Optimized for a single-column test 3424 if (columns.length == 1) { 3425 String column = columns[0]; 3426 for (String test : projection) { 3427 if (column.equals(test)) { 3428 return true; 3429 } 3430 } 3431 } else { 3432 for (String test : projection) { 3433 for (String column : columns) { 3434 if (column.equals(test)) { 3435 return true; 3436 } 3437 } 3438 } 3439 } 3440 return false; 3441 } 3442 3443 /** 3444 * Returns a detailed exception message for the supplied URI. It includes the calling 3445 * user and calling package(s). 3446 */ 3447 public String exceptionMessage(Uri uri) { 3448 return exceptionMessage(null, uri); 3449 } 3450 3451 /** 3452 * Returns a detailed exception message for the supplied URI. It includes the calling 3453 * user and calling package(s). 3454 */ 3455 public String exceptionMessage(String message, Uri uri) { 3456 StringBuilder sb = new StringBuilder(); 3457 if (message != null) { 3458 sb.append(message).append("; "); 3459 } 3460 sb.append("URI: ").append(uri); 3461 final PackageManager pm = mContext.getPackageManager(); 3462 int callingUid = Binder.getCallingUid(); 3463 sb.append(", calling user: "); 3464 String userName = pm.getNameForUid(callingUid); 3465 if (userName != null) { 3466 sb.append(userName); 3467 } else { 3468 sb.append(callingUid); 3469 } 3470 3471 final String[] callerPackages = pm.getPackagesForUid(callingUid); 3472 if (callerPackages != null && callerPackages.length > 0) { 3473 if (callerPackages.length == 1) { 3474 sb.append(", calling package:"); 3475 sb.append(callerPackages[0]); 3476 } else { 3477 sb.append(", calling package is one of: ["); 3478 for (int i = 0; i < callerPackages.length; i++) { 3479 if (i != 0) { 3480 sb.append(", "); 3481 } 3482 sb.append(callerPackages[i]); 3483 } 3484 sb.append("]"); 3485 } 3486 } 3487 3488 return sb.toString(); 3489 } 3490 3491 protected String getCountryIso() { 3492 CountryDetector detector = 3493 (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR); 3494 return detector.detectCountry().getCountryIso(); 3495 } 3496} 3497