1 /* 2 * Copyright (C) 2015 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 17 package com.android.messaging.datamodel; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.database.SQLException; 22 import android.database.sqlite.SQLiteDatabase; 23 import android.database.sqlite.SQLiteOpenHelper; 24 import android.provider.BaseColumns; 25 26 import com.android.messaging.BugleApplication; 27 import com.android.messaging.R; 28 import com.android.messaging.datamodel.data.ConversationListItemData; 29 import com.android.messaging.datamodel.data.MessageData; 30 import com.android.messaging.datamodel.data.ParticipantData; 31 import com.android.messaging.util.Assert; 32 import com.android.messaging.util.Assert.DoesNotRunOnMainThread; 33 import com.android.messaging.util.LogUtil; 34 import com.google.common.annotations.VisibleForTesting; 35 36 /** 37 * TODO: Open Issues: 38 * - Should we be storing the draft messages in the regular messages table or should we have a 39 * separate table for drafts to keep the normal messages query as simple as possible? 40 */ 41 42 /** 43 * Allows access to the SQL database. This is package private. 44 */ 45 public class DatabaseHelper extends SQLiteOpenHelper { 46 public static final String DATABASE_NAME = "bugle_db"; 47 getDatabaseVersion(final Context context)48 private static final int getDatabaseVersion(final Context context) { 49 return Integer.parseInt(context.getResources().getString(R.string.database_version)); 50 } 51 52 /** Table containing names of all other tables and views */ 53 private static final String MASTER_TABLE = "sqlite_master"; 54 /** Column containing the name of the tables and views */ 55 private static final String[] MASTER_COLUMNS = new String[] { "name", }; 56 57 // Table names 58 public static final String CONVERSATIONS_TABLE = "conversations"; 59 public static final String MESSAGES_TABLE = "messages"; 60 public static final String PARTS_TABLE = "parts"; 61 public static final String PARTICIPANTS_TABLE = "participants"; 62 public static final String CONVERSATION_PARTICIPANTS_TABLE = "conversation_participants"; 63 64 // Views 65 static final String DRAFT_PARTS_VIEW = "draft_parts_view"; 66 67 // Conversations table schema 68 public static class ConversationColumns implements BaseColumns { 69 /* SMS/MMS Thread ID from the system provider */ 70 public static final String SMS_THREAD_ID = "sms_thread_id"; 71 72 /* Display name for the conversation */ 73 public static final String NAME = "name"; 74 75 /* Latest Message ID for the read status to display in conversation list */ 76 public static final String LATEST_MESSAGE_ID = "latest_message_id"; 77 78 /* Latest text snippet for display in conversation list */ 79 public static final String SNIPPET_TEXT = "snippet_text"; 80 81 /* Latest text subject for display in conversation list, empty string if none exists */ 82 public static final String SUBJECT_TEXT = "subject_text"; 83 84 /* Preview Uri */ 85 public static final String PREVIEW_URI = "preview_uri"; 86 87 /* The preview uri's content type */ 88 public static final String PREVIEW_CONTENT_TYPE = "preview_content_type"; 89 90 /* If we should display the current draft snippet/preview pair or snippet/preview pair */ 91 public static final String SHOW_DRAFT = "show_draft"; 92 93 /* Latest draft text subject for display in conversation list, empty string if none exists*/ 94 public static final String DRAFT_SUBJECT_TEXT = "draft_subject_text"; 95 96 /* Latest draft text snippet for display, empty string if none exists */ 97 public static final String DRAFT_SNIPPET_TEXT = "draft_snippet_text"; 98 99 /* Draft Preview Uri, empty string if none exists */ 100 public static final String DRAFT_PREVIEW_URI = "draft_preview_uri"; 101 102 /* The preview uri's content type */ 103 public static final String DRAFT_PREVIEW_CONTENT_TYPE = "draft_preview_content_type"; 104 105 /* If this conversation is archived */ 106 public static final String ARCHIVE_STATUS = "archive_status"; 107 108 /* Timestamp for sorting purposes */ 109 public static final String SORT_TIMESTAMP = "sort_timestamp"; 110 111 /* Last read message timestamp */ 112 public static final String LAST_READ_TIMESTAMP = "last_read_timestamp"; 113 114 /* Avatar for the conversation. Could be for group of individual */ 115 public static final String ICON = "icon"; 116 117 /* Participant contact ID if this conversation has a single participant. -1 otherwise */ 118 public static final String PARTICIPANT_CONTACT_ID = "participant_contact_id"; 119 120 /* Participant lookup key if this conversation has a single participant. null otherwise */ 121 public static final String PARTICIPANT_LOOKUP_KEY = "participant_lookup_key"; 122 123 /* 124 * Participant's normalized destination if this conversation has a single participant. 125 * null otherwise. 126 */ 127 public static final String OTHER_PARTICIPANT_NORMALIZED_DESTINATION = 128 "participant_normalized_destination"; 129 130 /* Default self participant for the conversation */ 131 public static final String CURRENT_SELF_ID = "current_self_id"; 132 133 /* Participant count not including self (so will be 1 for 1:1 or bigger for group) */ 134 public static final String PARTICIPANT_COUNT = "participant_count"; 135 136 /* Should notifications be enabled for this conversation? */ 137 public static final String NOTIFICATION_ENABLED = "notification_enabled"; 138 139 /* Notification sound used for the conversation */ 140 public static final String NOTIFICATION_SOUND_URI = "notification_sound_uri"; 141 142 /* Should vibrations be enabled for the conversation's notification? */ 143 public static final String NOTIFICATION_VIBRATION = "notification_vibration"; 144 145 /* Conversation recipients include email address */ 146 public static final String INCLUDE_EMAIL_ADDRESS = "include_email_addr"; 147 148 // Record the last received sms's service center info if it indicates that the reply path 149 // is present (TP-Reply-Path), so that we could use it for the subsequent message to send. 150 // Refer to TS 23.040 D.6 and SmsMessageSender.java in Android Messaging app. 151 public static final String SMS_SERVICE_CENTER = "sms_service_center"; 152 153 // A conversation is enterprise if one of the participant is a enterprise contact. 154 public static final String IS_ENTERPRISE = "IS_ENTERPRISE"; 155 } 156 157 // Conversation table SQL 158 private static final String CREATE_CONVERSATIONS_TABLE_SQL = 159 "CREATE TABLE " + CONVERSATIONS_TABLE + "(" 160 + ConversationColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " 161 // TODO : Int? Required not default? 162 + ConversationColumns.SMS_THREAD_ID + " INT DEFAULT(0), " 163 + ConversationColumns.NAME + " TEXT, " 164 + ConversationColumns.LATEST_MESSAGE_ID + " INT, " 165 + ConversationColumns.SNIPPET_TEXT + " TEXT, " 166 + ConversationColumns.SUBJECT_TEXT + " TEXT, " 167 + ConversationColumns.PREVIEW_URI + " TEXT, " 168 + ConversationColumns.PREVIEW_CONTENT_TYPE + " TEXT, " 169 + ConversationColumns.SHOW_DRAFT + " INT DEFAULT(0), " 170 + ConversationColumns.DRAFT_SNIPPET_TEXT + " TEXT, " 171 + ConversationColumns.DRAFT_SUBJECT_TEXT + " TEXT, " 172 + ConversationColumns.DRAFT_PREVIEW_URI + " TEXT, " 173 + ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE + " TEXT, " 174 + ConversationColumns.ARCHIVE_STATUS + " INT DEFAULT(0), " 175 + ConversationColumns.SORT_TIMESTAMP + " INT DEFAULT(0), " 176 + ConversationColumns.LAST_READ_TIMESTAMP + " INT DEFAULT(0), " 177 + ConversationColumns.ICON + " TEXT, " 178 + ConversationColumns.PARTICIPANT_CONTACT_ID + " INT DEFAULT ( " 179 + ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED + "), " 180 + ConversationColumns.PARTICIPANT_LOOKUP_KEY + " TEXT, " 181 + ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + " TEXT, " 182 + ConversationColumns.CURRENT_SELF_ID + " TEXT, " 183 + ConversationColumns.PARTICIPANT_COUNT + " INT DEFAULT(0), " 184 + ConversationColumns.NOTIFICATION_ENABLED + " INT DEFAULT(1), " 185 + ConversationColumns.NOTIFICATION_SOUND_URI + " TEXT, " 186 + ConversationColumns.NOTIFICATION_VIBRATION + " INT DEFAULT(1), " 187 + ConversationColumns.INCLUDE_EMAIL_ADDRESS + " INT DEFAULT(0), " 188 + ConversationColumns.SMS_SERVICE_CENTER + " TEXT ," 189 + ConversationColumns.IS_ENTERPRISE + " INT DEFAULT(0)" 190 + ");"; 191 192 private static final String CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL = 193 "CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SMS_THREAD_ID 194 + " ON " + CONVERSATIONS_TABLE 195 + "(" + ConversationColumns.SMS_THREAD_ID + ")"; 196 197 private static final String CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL = 198 "CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.ARCHIVE_STATUS 199 + " ON " + CONVERSATIONS_TABLE 200 + "(" + ConversationColumns.ARCHIVE_STATUS + ")"; 201 202 private static final String CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL = 203 "CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SORT_TIMESTAMP 204 + " ON " + CONVERSATIONS_TABLE 205 + "(" + ConversationColumns.SORT_TIMESTAMP + ")"; 206 207 // Messages table schema 208 public static class MessageColumns implements BaseColumns { 209 /* conversation id that this message belongs to */ 210 public static final String CONVERSATION_ID = "conversation_id"; 211 212 /* participant which send this message */ 213 public static final String SENDER_PARTICIPANT_ID = "sender_id"; 214 215 /* This is bugle's internal status for the message */ 216 public static final String STATUS = "message_status"; 217 218 /* Type of message: SMS, MMS or MMS notification */ 219 public static final String PROTOCOL = "message_protocol"; 220 221 /* This is the time that the sender sent the message */ 222 public static final String SENT_TIMESTAMP = "sent_timestamp"; 223 224 /* Time that we received the message on this device */ 225 public static final String RECEIVED_TIMESTAMP = "received_timestamp"; 226 227 /* When the message has been seen by a user in a notification */ 228 public static final String SEEN = "seen"; 229 230 /* When the message has been read by a user */ 231 public static final String READ = "read"; 232 233 /* participant representing the sim which processed this message */ 234 public static final String SELF_PARTICIPANT_ID = "self_id"; 235 236 /* 237 * Time when a retry is initiated. This is used to compute the retry window 238 * when we retry sending/downloading a message. 239 */ 240 public static final String RETRY_START_TIMESTAMP = "retry_start_timestamp"; 241 242 // Columns which map to the SMS provider 243 244 /* Message ID from the platform provider */ 245 public static final String SMS_MESSAGE_URI = "sms_message_uri"; 246 247 /* The message priority for MMS message */ 248 public static final String SMS_PRIORITY = "sms_priority"; 249 250 /* The message size for MMS message */ 251 public static final String SMS_MESSAGE_SIZE = "sms_message_size"; 252 253 /* The subject for MMS message */ 254 public static final String MMS_SUBJECT = "mms_subject"; 255 256 /* Transaction id for MMS notificaiton */ 257 public static final String MMS_TRANSACTION_ID = "mms_transaction_id"; 258 259 /* Content location for MMS notificaiton */ 260 public static final String MMS_CONTENT_LOCATION = "mms_content_location"; 261 262 /* The expiry time (ms) for MMS message */ 263 public static final String MMS_EXPIRY = "mms_expiry"; 264 265 /* The detailed status (RESPONSE_STATUS or RETRIEVE_STATUS) for MMS message */ 266 public static final String RAW_TELEPHONY_STATUS = "raw_status"; 267 } 268 269 // Messages table SQL 270 private static final String CREATE_MESSAGES_TABLE_SQL = 271 "CREATE TABLE " + MESSAGES_TABLE + " (" 272 + MessageColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " 273 + MessageColumns.CONVERSATION_ID + " INT, " 274 + MessageColumns.SENDER_PARTICIPANT_ID + " INT, " 275 + MessageColumns.SENT_TIMESTAMP + " INT DEFAULT(0), " 276 + MessageColumns.RECEIVED_TIMESTAMP + " INT DEFAULT(0), " 277 + MessageColumns.PROTOCOL + " INT DEFAULT(0), " 278 + MessageColumns.STATUS + " INT DEFAULT(0), " 279 + MessageColumns.SEEN + " INT DEFAULT(0), " 280 + MessageColumns.READ + " INT DEFAULT(0), " 281 + MessageColumns.SMS_MESSAGE_URI + " TEXT, " 282 + MessageColumns.SMS_PRIORITY + " INT DEFAULT(0), " 283 + MessageColumns.SMS_MESSAGE_SIZE + " INT DEFAULT(0), " 284 + MessageColumns.MMS_SUBJECT + " TEXT, " 285 + MessageColumns.MMS_TRANSACTION_ID + " TEXT, " 286 + MessageColumns.MMS_CONTENT_LOCATION + " TEXT, " 287 + MessageColumns.MMS_EXPIRY + " INT DEFAULT(0), " 288 + MessageColumns.RAW_TELEPHONY_STATUS + " INT DEFAULT(0), " 289 + MessageColumns.SELF_PARTICIPANT_ID + " INT, " 290 + MessageColumns.RETRY_START_TIMESTAMP + " INT DEFAULT(0), " 291 + "FOREIGN KEY (" + MessageColumns.CONVERSATION_ID + ") REFERENCES " 292 + CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ") ON DELETE CASCADE " 293 + "FOREIGN KEY (" + MessageColumns.SENDER_PARTICIPANT_ID + ") REFERENCES " 294 + PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + ") ON DELETE SET NULL " 295 + "FOREIGN KEY (" + MessageColumns.SELF_PARTICIPANT_ID + ") REFERENCES " 296 + PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + ") ON DELETE SET NULL " 297 + ");"; 298 299 // Primary sort index for messages table : by conversation id, status, received timestamp. 300 private static final String MESSAGES_TABLE_SORT_INDEX_SQL = 301 "CREATE INDEX index_" + MESSAGES_TABLE + "_sort ON " + MESSAGES_TABLE + "(" 302 + MessageColumns.CONVERSATION_ID + ", " 303 + MessageColumns.STATUS + ", " 304 + MessageColumns.RECEIVED_TIMESTAMP + ")"; 305 306 private static final String MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL = 307 "CREATE INDEX index_" + MESSAGES_TABLE + "_status_seen ON " + MESSAGES_TABLE + "(" 308 + MessageColumns.STATUS + ", " 309 + MessageColumns.SEEN + ")"; 310 311 // Parts table schema 312 // A part may contain text or a media url, but not both. 313 public static class PartColumns implements BaseColumns { 314 /* message id that this part belongs to */ 315 public static final String MESSAGE_ID = "message_id"; 316 317 /* conversation id that this part belongs to */ 318 public static final String CONVERSATION_ID = "conversation_id"; 319 320 /* text for this part */ 321 public static final String TEXT = "text"; 322 323 /* content uri for this part */ 324 public static final String CONTENT_URI = "uri"; 325 326 /* content type for this part */ 327 public static final String CONTENT_TYPE = "content_type"; 328 329 /* cached width for this part (for layout while loading) */ 330 public static final String WIDTH = "width"; 331 332 /* cached height for this part (for layout while loading) */ 333 public static final String HEIGHT = "height"; 334 335 /* de-normalized copy of timestamp from the messages table. This is populated 336 * via an insert trigger on the parts table. 337 */ 338 public static final String TIMESTAMP = "timestamp"; 339 } 340 341 // Message part table SQL 342 private static final String CREATE_PARTS_TABLE_SQL = 343 "CREATE TABLE " + PARTS_TABLE + "(" 344 + PartColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 345 + PartColumns.MESSAGE_ID + " INT," 346 + PartColumns.TEXT + " TEXT," 347 + PartColumns.CONTENT_URI + " TEXT," 348 + PartColumns.CONTENT_TYPE + " TEXT," 349 + PartColumns.WIDTH + " INT DEFAULT(" 350 + MessagingContentProvider.UNSPECIFIED_SIZE + ")," 351 + PartColumns.HEIGHT + " INT DEFAULT(" 352 + MessagingContentProvider.UNSPECIFIED_SIZE + ")," 353 + PartColumns.TIMESTAMP + " INT, " 354 + PartColumns.CONVERSATION_ID + " INT NOT NULL," 355 + "FOREIGN KEY (" + PartColumns.MESSAGE_ID + ") REFERENCES " 356 + MESSAGES_TABLE + "(" + MessageColumns._ID + ") ON DELETE CASCADE " 357 + "FOREIGN KEY (" + PartColumns.CONVERSATION_ID + ") REFERENCES " 358 + CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ") ON DELETE CASCADE " 359 + ");"; 360 361 public static final String CREATE_PARTS_TRIGGER_SQL = 362 "CREATE TRIGGER " + PARTS_TABLE + "_TRIGGER" + " AFTER INSERT ON " + PARTS_TABLE 363 + " FOR EACH ROW " 364 + " BEGIN UPDATE " + PARTS_TABLE 365 + " SET " + PartColumns.TIMESTAMP + "=" 366 + " (SELECT received_timestamp FROM " + MESSAGES_TABLE + " WHERE " + MESSAGES_TABLE 367 + "." + MessageColumns._ID + "=" + "NEW." + PartColumns.MESSAGE_ID + ")" 368 + " WHERE " + PARTS_TABLE + "." + PartColumns._ID + "=" + "NEW." + PartColumns._ID 369 + "; END"; 370 371 public static final String CREATE_MESSAGES_TRIGGER_SQL = 372 "CREATE TRIGGER " + MESSAGES_TABLE + "_TRIGGER" + " AFTER UPDATE OF " 373 + MessageColumns.RECEIVED_TIMESTAMP + " ON " + MESSAGES_TABLE 374 + " FOR EACH ROW BEGIN UPDATE " + PARTS_TABLE + " SET " + PartColumns.TIMESTAMP 375 + " = NEW." + MessageColumns.RECEIVED_TIMESTAMP + " WHERE " + PARTS_TABLE + "." 376 + PartColumns.MESSAGE_ID + " = NEW." + MessageColumns._ID 377 + "; END;"; 378 379 // Primary sort index for parts table : by message_id 380 private static final String PARTS_TABLE_MESSAGE_INDEX_SQL = 381 "CREATE INDEX index_" + PARTS_TABLE + "_message_id ON " + PARTS_TABLE + "(" 382 + PartColumns.MESSAGE_ID + ")"; 383 384 // Participants table schema 385 public static class ParticipantColumns implements BaseColumns { 386 /* The subscription id for the sim associated with this self participant. 387 * Introduced in L. For earlier versions will always be default_sub_id (-1). 388 * For multi sim devices (or cases where the sim was changed) single device 389 * may have several different sub_id values */ 390 public static final String SUB_ID = "sub_id"; 391 392 /* The slot of the active SIM (inserted in the device) for this self-participant. If the 393 * self-participant doesn't correspond to any active SIM, this will be 394 * {@link android.telephony.SubscriptionManager#INVALID_SLOT_ID}. 395 * The column is ignored for all non-self participants. 396 */ 397 public static final String SIM_SLOT_ID = "sim_slot_id"; 398 399 /* The phone number stored in a standard E164 format if possible. This is unique for a 400 * given participant. We can't handle multiple participants with the same phone number 401 * since we don't know which of them a message comes from. This can also be an email 402 * address, in which case this is the same as the displayed address */ 403 public static final String NORMALIZED_DESTINATION = "normalized_destination"; 404 405 /* The phone number as originally supplied and used for dialing. Not necessarily in E164 406 * format or unique */ 407 public static final String SEND_DESTINATION = "send_destination"; 408 409 /* The user-friendly formatting of the phone number according to the region setting of 410 * the device when the row was added. */ 411 public static final String DISPLAY_DESTINATION = "display_destination"; 412 413 /* A string with this participant's full name or a pretty printed phone number */ 414 public static final String FULL_NAME = "full_name"; 415 416 /* A string with just this participant's first name */ 417 public static final String FIRST_NAME = "first_name"; 418 419 /* A local URI to an asset for the icon for this participant */ 420 public static final String PROFILE_PHOTO_URI = "profile_photo_uri"; 421 422 /* Contact id for matching local contact for this participant */ 423 public static final String CONTACT_ID = "contact_id"; 424 425 /* String that contains hints on how to find contact information in a contact lookup */ 426 public static final String LOOKUP_KEY = "lookup_key"; 427 428 /* If this participant is blocked */ 429 public static final String BLOCKED = "blocked"; 430 431 /* The color of the subscription (FOR SELF PARTICIPANTS ONLY) */ 432 public static final String SUBSCRIPTION_COLOR = "subscription_color"; 433 434 /* The name of the subscription (FOR SELF PARTICIPANTS ONLY) */ 435 public static final String SUBSCRIPTION_NAME = "subscription_name"; 436 437 /* The exact destination stored in Contacts for this participant */ 438 public static final String CONTACT_DESTINATION = "contact_destination"; 439 } 440 441 // Participants table SQL 442 private static final String CREATE_PARTICIPANTS_TABLE_SQL = 443 "CREATE TABLE " + PARTICIPANTS_TABLE + "(" 444 + ParticipantColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 445 + ParticipantColumns.SUB_ID + " INT DEFAULT(" 446 + ParticipantData.OTHER_THAN_SELF_SUB_ID + ")," 447 + ParticipantColumns.SIM_SLOT_ID + " INT DEFAULT(" 448 + ParticipantData.INVALID_SLOT_ID + ")," 449 + ParticipantColumns.NORMALIZED_DESTINATION + " TEXT," 450 + ParticipantColumns.SEND_DESTINATION + " TEXT," 451 + ParticipantColumns.DISPLAY_DESTINATION + " TEXT," 452 + ParticipantColumns.FULL_NAME + " TEXT," 453 + ParticipantColumns.FIRST_NAME + " TEXT," 454 + ParticipantColumns.PROFILE_PHOTO_URI + " TEXT, " 455 + ParticipantColumns.CONTACT_ID + " INT DEFAULT( " 456 + ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED + "), " 457 + ParticipantColumns.LOOKUP_KEY + " STRING, " 458 + ParticipantColumns.BLOCKED + " INT DEFAULT(0), " 459 + ParticipantColumns.SUBSCRIPTION_NAME + " TEXT, " 460 + ParticipantColumns.SUBSCRIPTION_COLOR + " INT DEFAULT(0), " 461 + ParticipantColumns.CONTACT_DESTINATION + " TEXT, " 462 + "UNIQUE (" + ParticipantColumns.NORMALIZED_DESTINATION + ", " 463 + ParticipantColumns.SUB_ID + ") ON CONFLICT FAIL" + ");"; 464 465 private static final String CREATE_SELF_PARTICIPANT_SQL = 466 "INSERT INTO " + PARTICIPANTS_TABLE 467 + " ( " + ParticipantColumns.SUB_ID + " ) VALUES ( %s )"; 468 getCreateSelfParticipantSql(int subId)469 static String getCreateSelfParticipantSql(int subId) { 470 return String.format(CREATE_SELF_PARTICIPANT_SQL, subId); 471 } 472 473 // Conversation Participants table schema - contains a list of participants excluding the user 474 // in a given conversation. 475 public static class ConversationParticipantsColumns implements BaseColumns { 476 /* participant id of someone in this conversation */ 477 public static final String PARTICIPANT_ID = "participant_id"; 478 479 /* conversation id that this participant belongs to */ 480 public static final String CONVERSATION_ID = "conversation_id"; 481 } 482 483 // Conversation Participants table SQL 484 private static final String CREATE_CONVERSATION_PARTICIPANTS_TABLE_SQL = 485 "CREATE TABLE " + CONVERSATION_PARTICIPANTS_TABLE + "(" 486 + ConversationParticipantsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 487 + ConversationParticipantsColumns.CONVERSATION_ID + " INT," 488 + ConversationParticipantsColumns.PARTICIPANT_ID + " INT," 489 + "UNIQUE (" + ConversationParticipantsColumns.CONVERSATION_ID + "," 490 + ConversationParticipantsColumns.PARTICIPANT_ID + ") ON CONFLICT FAIL, " 491 + "FOREIGN KEY (" + ConversationParticipantsColumns.CONVERSATION_ID + ") " 492 + "REFERENCES " + CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ")" 493 + " ON DELETE CASCADE " 494 + "FOREIGN KEY (" + ConversationParticipantsColumns.PARTICIPANT_ID + ")" 495 + " REFERENCES " + PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + "));"; 496 497 // Primary access pattern for conversation participants is to look them up for a specific 498 // conversation. 499 private static final String CONVERSATION_PARTICIPANTS_TABLE_CONVERSATION_ID_INDEX_SQL = 500 "CREATE INDEX index_" + CONVERSATION_PARTICIPANTS_TABLE + "_" 501 + ConversationParticipantsColumns.CONVERSATION_ID 502 + " ON " + CONVERSATION_PARTICIPANTS_TABLE 503 + "(" + ConversationParticipantsColumns.CONVERSATION_ID + ")"; 504 505 // View for getting parts which are for draft messages. 506 static final String DRAFT_PARTS_VIEW_SQL = "CREATE VIEW " + 507 DRAFT_PARTS_VIEW + " AS SELECT " 508 + PARTS_TABLE + '.' + PartColumns._ID 509 + " as " + PartColumns._ID + ", " 510 + PARTS_TABLE + '.' + PartColumns.MESSAGE_ID 511 + " as " + PartColumns.MESSAGE_ID + ", " 512 + PARTS_TABLE + '.' + PartColumns.TEXT 513 + " as " + PartColumns.TEXT + ", " 514 + PARTS_TABLE + '.' + PartColumns.CONTENT_URI 515 + " as " + PartColumns.CONTENT_URI + ", " 516 + PARTS_TABLE + '.' + PartColumns.CONTENT_TYPE 517 + " as " + PartColumns.CONTENT_TYPE + ", " 518 + PARTS_TABLE + '.' + PartColumns.WIDTH 519 + " as " + PartColumns.WIDTH + ", " 520 + PARTS_TABLE + '.' + PartColumns.HEIGHT 521 + " as " + PartColumns.HEIGHT + ", " 522 + MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID 523 + " as " + MessageColumns.CONVERSATION_ID + " " 524 + " FROM " + MESSAGES_TABLE + " LEFT JOIN " + PARTS_TABLE + " ON (" 525 + MESSAGES_TABLE + "." + MessageColumns._ID 526 + "=" + PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ")" 527 // Exclude draft messages from main view 528 + " WHERE " + MESSAGES_TABLE + "." + MessageColumns.STATUS 529 + " = " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT; 530 531 // List of all our SQL tables 532 private static final String[] CREATE_TABLE_SQLS = new String[] { 533 CREATE_CONVERSATIONS_TABLE_SQL, 534 CREATE_MESSAGES_TABLE_SQL, 535 CREATE_PARTS_TABLE_SQL, 536 CREATE_PARTICIPANTS_TABLE_SQL, 537 CREATE_CONVERSATION_PARTICIPANTS_TABLE_SQL, 538 }; 539 540 // List of all our indices 541 private static final String[] CREATE_INDEX_SQLS = new String[] { 542 CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL, 543 CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL, 544 CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL, 545 MESSAGES_TABLE_SORT_INDEX_SQL, 546 MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL, 547 PARTS_TABLE_MESSAGE_INDEX_SQL, 548 CONVERSATION_PARTICIPANTS_TABLE_CONVERSATION_ID_INDEX_SQL, 549 }; 550 551 // List of all our SQL triggers 552 private static final String[] CREATE_TRIGGER_SQLS = new String[] { 553 CREATE_PARTS_TRIGGER_SQL, 554 CREATE_MESSAGES_TRIGGER_SQL, 555 }; 556 557 // List of all our views 558 private static final String[] CREATE_VIEW_SQLS = new String[] { 559 ConversationListItemData.getConversationListViewSql(), 560 ConversationImagePartsView.getCreateSql(), 561 DRAFT_PARTS_VIEW_SQL, 562 }; 563 564 private static final Object sLock = new Object(); 565 private final Context mApplicationContext; 566 private static DatabaseHelper sHelperInstance; // Protected by sLock. 567 568 private final Object mDatabaseWrapperLock = new Object(); 569 private DatabaseWrapper mDatabaseWrapper; // Protected by mDatabaseWrapperLock. 570 private final DatabaseUpgradeHelper mUpgradeHelper = new DatabaseUpgradeHelper(); 571 572 /** 573 * Get a (singleton) instance of {@link DatabaseHelper}, creating one if there isn't one yet. 574 * This is the only public method for getting a new instance of the class. 575 * @param context Should be the application context (or something that will live for the 576 * lifetime of the application). 577 * @return The current (or a new) DatabaseHelper instance. 578 */ getInstance(final Context context)579 public static DatabaseHelper getInstance(final Context context) { 580 synchronized (sLock) { 581 if (sHelperInstance == null) { 582 sHelperInstance = new DatabaseHelper(context); 583 } 584 return sHelperInstance; 585 } 586 } 587 588 /** 589 * Private constructor, used from {@link #getInstance()}. 590 * @param context Should be the application context (or something that will live for the 591 * lifetime of the application). 592 */ DatabaseHelper(final Context context)593 private DatabaseHelper(final Context context) { 594 super(context, DATABASE_NAME, null, getDatabaseVersion(context), null); 595 mApplicationContext = context; 596 } 597 598 /** 599 * Test method that always instantiates a new DatabaseHelper instance. This should 600 * be used ONLY by the tests and never by the real application. 601 * @param context Test context. 602 * @return Brand new DatabaseHelper instance. 603 */ 604 @VisibleForTesting getNewInstanceForTest(final Context context)605 static DatabaseHelper getNewInstanceForTest(final Context context) { 606 Assert.isEngBuild(); 607 Assert.isTrue(BugleApplication.isRunningTests()); 608 return new DatabaseHelper(context); 609 } 610 611 /** 612 * Get the (singleton) instance of @{link DatabaseWrapper}. 613 * <p>The database is always opened as a writeable database. 614 * @return The current (or a new) DatabaseWrapper instance. 615 */ 616 @DoesNotRunOnMainThread getDatabase()617 DatabaseWrapper getDatabase() { 618 // We prevent the main UI thread from accessing the database here since we have to allow 619 // public access to this class to enable sub-packages to access data. 620 Assert.isNotMainThread(); 621 622 synchronized (mDatabaseWrapperLock) { 623 if (mDatabaseWrapper == null) { 624 mDatabaseWrapper = new DatabaseWrapper(mApplicationContext, getWritableDatabase()); 625 } 626 return mDatabaseWrapper; 627 } 628 } 629 630 @Override onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion)631 public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { 632 mUpgradeHelper.onDowngrade(db, oldVersion, newVersion); 633 } 634 635 /** 636 * Drops and recreates all tables. 637 */ rebuildTables(final SQLiteDatabase db)638 public static void rebuildTables(final SQLiteDatabase db) { 639 // Drop tables first, then views, and indices. 640 dropAllTables(db); 641 dropAllViews(db); 642 dropAllIndexes(db); 643 dropAllTriggers(db); 644 645 // Recreate the whole database. 646 createDatabase(db); 647 } 648 649 /** 650 * Drop and rebuild a given view. 651 */ rebuildView(final SQLiteDatabase db, final String viewName, final String createViewSql)652 static void rebuildView(final SQLiteDatabase db, final String viewName, 653 final String createViewSql) { 654 dropView(db, viewName, true /* throwOnFailure */); 655 db.execSQL(createViewSql); 656 } 657 dropView(final SQLiteDatabase db, final String viewName, final boolean throwOnFailure)658 private static void dropView(final SQLiteDatabase db, final String viewName, 659 final boolean throwOnFailure) { 660 final String dropPrefix = "DROP VIEW IF EXISTS "; 661 try { 662 db.execSQL(dropPrefix + viewName); 663 } catch (final SQLException ex) { 664 if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { 665 LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop view " + viewName + " " 666 + ex); 667 } 668 669 if (throwOnFailure) { 670 throw ex; 671 } 672 } 673 } 674 rebuildAllViews(final DatabaseWrapper db)675 public static void rebuildAllViews(final DatabaseWrapper db) { 676 for (final String sql : DatabaseHelper.CREATE_VIEW_SQLS) { 677 db.execSQL(sql); 678 } 679 } 680 681 /** 682 * Drops all user-defined tables from the given database. 683 */ dropAllTables(final SQLiteDatabase db)684 private static void dropAllTables(final SQLiteDatabase db) { 685 final Cursor tableCursor = 686 db.query(MASTER_TABLE, MASTER_COLUMNS, "type='table'", null, null, null, null); 687 if (tableCursor != null) { 688 try { 689 final String dropPrefix = "DROP TABLE IF EXISTS "; 690 while (tableCursor.moveToNext()) { 691 final String tableName = tableCursor.getString(0); 692 693 // Skip special tables 694 if (tableName.startsWith("android_") || tableName.startsWith("sqlite_")) { 695 continue; 696 } 697 try { 698 db.execSQL(dropPrefix + tableName); 699 } catch (final SQLException ex) { 700 if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { 701 LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop table " + tableName + " " 702 + ex); 703 } 704 } 705 } 706 } finally { 707 tableCursor.close(); 708 } 709 } 710 } 711 712 /** 713 * Drops all user-defined triggers from the given database. 714 */ dropAllTriggers(final SQLiteDatabase db)715 private static void dropAllTriggers(final SQLiteDatabase db) { 716 final Cursor triggerCursor = 717 db.query(MASTER_TABLE, MASTER_COLUMNS, "type='trigger'", null, null, null, null); 718 if (triggerCursor != null) { 719 try { 720 final String dropPrefix = "DROP TRIGGER IF EXISTS "; 721 while (triggerCursor.moveToNext()) { 722 final String triggerName = triggerCursor.getString(0); 723 724 // Skip special tables 725 if (triggerName.startsWith("android_") || triggerName.startsWith("sqlite_")) { 726 continue; 727 } 728 try { 729 db.execSQL(dropPrefix + triggerName); 730 } catch (final SQLException ex) { 731 if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { 732 LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop trigger " + triggerName + 733 " " + ex); 734 } 735 } 736 } 737 } finally { 738 triggerCursor.close(); 739 } 740 } 741 } 742 743 /** 744 * Drops all user-defined views from the given database. 745 */ dropAllViews(final SQLiteDatabase db)746 public static void dropAllViews(final SQLiteDatabase db) { 747 final Cursor viewCursor = 748 db.query(MASTER_TABLE, MASTER_COLUMNS, "type='view'", null, null, null, null); 749 if (viewCursor != null) { 750 try { 751 while (viewCursor.moveToNext()) { 752 final String viewName = viewCursor.getString(0); 753 dropView(db, viewName, false /* throwOnFailure */); 754 } 755 } finally { 756 viewCursor.close(); 757 } 758 } 759 } 760 761 /** 762 * Drops all user-defined views from the given database. 763 */ dropAllIndexes(final SQLiteDatabase db)764 private static void dropAllIndexes(final SQLiteDatabase db) { 765 final Cursor indexCursor = 766 db.query(MASTER_TABLE, MASTER_COLUMNS, "type='index'", null, null, null, null); 767 if (indexCursor != null) { 768 try { 769 final String dropPrefix = "DROP INDEX IF EXISTS "; 770 while (indexCursor.moveToNext()) { 771 final String indexName = indexCursor.getString(0); 772 try { 773 db.execSQL(dropPrefix + indexName); 774 } catch (final SQLException ex) { 775 if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { 776 LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop index " + indexName + " " 777 + ex); 778 } 779 } 780 } 781 } finally { 782 indexCursor.close(); 783 } 784 } 785 } 786 createDatabase(final SQLiteDatabase db)787 private static void createDatabase(final SQLiteDatabase db) { 788 for (final String sql : CREATE_TABLE_SQLS) { 789 db.execSQL(sql); 790 } 791 792 for (final String sql : CREATE_INDEX_SQLS) { 793 db.execSQL(sql); 794 } 795 796 for (final String sql : CREATE_VIEW_SQLS) { 797 db.execSQL(sql); 798 } 799 800 for (final String sql : CREATE_TRIGGER_SQLS) { 801 db.execSQL(sql); 802 } 803 804 // Enable foreign key constraints 805 db.execSQL("PRAGMA foreign_keys=ON;"); 806 807 // Add the default self participant. The default self will be assigned a proper slot id 808 // during participant refresh. 809 db.execSQL(getCreateSelfParticipantSql(ParticipantData.DEFAULT_SELF_SUB_ID)); 810 811 DataModel.get().onCreateTables(db); 812 } 813 814 @Override onCreate(SQLiteDatabase db)815 public void onCreate(SQLiteDatabase db) { 816 createDatabase(db); 817 } 818 819 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)820 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 821 mUpgradeHelper.doOnUpgrade(db, oldVersion, newVersion); 822 } 823 } 824