Home | History | Annotate | Download | only in datamodel
      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 
     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 
    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      */
    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      */
    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
    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
    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
    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      */
    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      */
    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 
    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 
    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      */
    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      */
    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      */
    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      */
    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 
    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
    815     public void onCreate(SQLiteDatabase db) {
    816         createDatabase(db);
    817     }
    818 
    819     @Override
    820     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    821         mUpgradeHelper.doOnUpgrade(db, oldVersion, newVersion);
    822     }
    823 }
    824