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