| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.messaging.datamodel; |
| |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.SQLException; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteOpenHelper; |
| import android.provider.BaseColumns; |
| |
| import com.android.messaging.BugleApplication; |
| import com.android.messaging.R; |
| import com.android.messaging.datamodel.data.ConversationListItemData; |
| import com.android.messaging.datamodel.data.MessageData; |
| import com.android.messaging.datamodel.data.ParticipantData; |
| import com.android.messaging.util.Assert; |
| import com.android.messaging.util.Assert.DoesNotRunOnMainThread; |
| import com.android.messaging.util.LogUtil; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| /** |
| * TODO: Open Issues: |
| * - Should we be storing the draft messages in the regular messages table or should we have a |
| * separate table for drafts to keep the normal messages query as simple as possible? |
| */ |
| |
| /** |
| * Allows access to the SQL database. This is package private. |
| */ |
| public class DatabaseHelper extends SQLiteOpenHelper { |
| public static final String DATABASE_NAME = "bugle_db"; |
| |
| private static final int getDatabaseVersion(final Context context) { |
| return Integer.parseInt(context.getResources().getString(R.string.database_version)); |
| } |
| |
| /** |
| * Table containing names of all other tables and views. |
| * TODO(rtenneti): Fix the following special SQLLite table name when SQLLite changes. |
| */ |
| private static final String PRIMARY_TABLE = "sqlite_master"; |
| /** Column containing the name of the tables and views */ |
| private static final String[] PRIMARY_COLUMNS = new String[] { "name", }; |
| |
| // Table names |
| public static final String CONVERSATIONS_TABLE = "conversations"; |
| public static final String MESSAGES_TABLE = "messages"; |
| public static final String PARTS_TABLE = "parts"; |
| public static final String PARTICIPANTS_TABLE = "participants"; |
| public static final String CONVERSATION_PARTICIPANTS_TABLE = "conversation_participants"; |
| |
| // Views |
| static final String DRAFT_PARTS_VIEW = "draft_parts_view"; |
| |
| // Conversations table schema |
| public static class ConversationColumns implements BaseColumns { |
| /* SMS/MMS Thread ID from the system provider */ |
| public static final String SMS_THREAD_ID = "sms_thread_id"; |
| |
| /* Display name for the conversation */ |
| public static final String NAME = "name"; |
| |
| /* Latest Message ID for the read status to display in conversation list */ |
| public static final String LATEST_MESSAGE_ID = "latest_message_id"; |
| |
| /* Latest text snippet for display in conversation list */ |
| public static final String SNIPPET_TEXT = "snippet_text"; |
| |
| /* Latest text subject for display in conversation list, empty string if none exists */ |
| public static final String SUBJECT_TEXT = "subject_text"; |
| |
| /* Preview Uri */ |
| public static final String PREVIEW_URI = "preview_uri"; |
| |
| /* The preview uri's content type */ |
| public static final String PREVIEW_CONTENT_TYPE = "preview_content_type"; |
| |
| /* If we should display the current draft snippet/preview pair or snippet/preview pair */ |
| public static final String SHOW_DRAFT = "show_draft"; |
| |
| /* Latest draft text subject for display in conversation list, empty string if none exists*/ |
| public static final String DRAFT_SUBJECT_TEXT = "draft_subject_text"; |
| |
| /* Latest draft text snippet for display, empty string if none exists */ |
| public static final String DRAFT_SNIPPET_TEXT = "draft_snippet_text"; |
| |
| /* Draft Preview Uri, empty string if none exists */ |
| public static final String DRAFT_PREVIEW_URI = "draft_preview_uri"; |
| |
| /* The preview uri's content type */ |
| public static final String DRAFT_PREVIEW_CONTENT_TYPE = "draft_preview_content_type"; |
| |
| /* If this conversation is archived */ |
| public static final String ARCHIVE_STATUS = "archive_status"; |
| |
| /* Timestamp for sorting purposes */ |
| public static final String SORT_TIMESTAMP = "sort_timestamp"; |
| |
| /* Last read message timestamp */ |
| public static final String LAST_READ_TIMESTAMP = "last_read_timestamp"; |
| |
| /* Avatar for the conversation. Could be for group of individual */ |
| public static final String ICON = "icon"; |
| |
| /* Participant contact ID if this conversation has a single participant. -1 otherwise */ |
| public static final String PARTICIPANT_CONTACT_ID = "participant_contact_id"; |
| |
| /* Participant lookup key if this conversation has a single participant. null otherwise */ |
| public static final String PARTICIPANT_LOOKUP_KEY = "participant_lookup_key"; |
| |
| /* |
| * Participant's normalized destination if this conversation has a single participant. |
| * null otherwise. |
| */ |
| public static final String OTHER_PARTICIPANT_NORMALIZED_DESTINATION = |
| "participant_normalized_destination"; |
| |
| /* Default self participant for the conversation */ |
| public static final String CURRENT_SELF_ID = "current_self_id"; |
| |
| /* Participant count not including self (so will be 1 for 1:1 or bigger for group) */ |
| public static final String PARTICIPANT_COUNT = "participant_count"; |
| |
| /* Should notifications be enabled for this conversation? */ |
| public static final String NOTIFICATION_ENABLED = "notification_enabled"; |
| |
| /* Notification sound used for the conversation */ |
| public static final String NOTIFICATION_SOUND_URI = "notification_sound_uri"; |
| |
| /* Should vibrations be enabled for the conversation's notification? */ |
| public static final String NOTIFICATION_VIBRATION = "notification_vibration"; |
| |
| /* Conversation recipients include email address */ |
| public static final String INCLUDE_EMAIL_ADDRESS = "include_email_addr"; |
| |
| // Record the last received sms's service center info if it indicates that the reply path |
| // is present (TP-Reply-Path), so that we could use it for the subsequent message to send. |
| // Refer to TS 23.040 D.6 and SmsMessageSender.java in Android Messaging app. |
| public static final String SMS_SERVICE_CENTER = "sms_service_center"; |
| |
| // A conversation is enterprise if one of the participant is a enterprise contact. |
| public static final String IS_ENTERPRISE = "IS_ENTERPRISE"; |
| } |
| |
| // Conversation table SQL |
| private static final String CREATE_CONVERSATIONS_TABLE_SQL = |
| "CREATE TABLE " + CONVERSATIONS_TABLE + "(" |
| + ConversationColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " |
| // TODO : Int? Required not default? |
| + ConversationColumns.SMS_THREAD_ID + " INT DEFAULT(0), " |
| + ConversationColumns.NAME + " TEXT, " |
| + ConversationColumns.LATEST_MESSAGE_ID + " INT, " |
| + ConversationColumns.SNIPPET_TEXT + " TEXT, " |
| + ConversationColumns.SUBJECT_TEXT + " TEXT, " |
| + ConversationColumns.PREVIEW_URI + " TEXT, " |
| + ConversationColumns.PREVIEW_CONTENT_TYPE + " TEXT, " |
| + ConversationColumns.SHOW_DRAFT + " INT DEFAULT(0), " |
| + ConversationColumns.DRAFT_SNIPPET_TEXT + " TEXT, " |
| + ConversationColumns.DRAFT_SUBJECT_TEXT + " TEXT, " |
| + ConversationColumns.DRAFT_PREVIEW_URI + " TEXT, " |
| + ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE + " TEXT, " |
| + ConversationColumns.ARCHIVE_STATUS + " INT DEFAULT(0), " |
| + ConversationColumns.SORT_TIMESTAMP + " INT DEFAULT(0), " |
| + ConversationColumns.LAST_READ_TIMESTAMP + " INT DEFAULT(0), " |
| + ConversationColumns.ICON + " TEXT, " |
| + ConversationColumns.PARTICIPANT_CONTACT_ID + " INT DEFAULT ( " |
| + ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED + "), " |
| + ConversationColumns.PARTICIPANT_LOOKUP_KEY + " TEXT, " |
| + ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + " TEXT, " |
| + ConversationColumns.CURRENT_SELF_ID + " TEXT, " |
| + ConversationColumns.PARTICIPANT_COUNT + " INT DEFAULT(0), " |
| + ConversationColumns.NOTIFICATION_ENABLED + " INT DEFAULT(1), " |
| + ConversationColumns.NOTIFICATION_SOUND_URI + " TEXT, " |
| + ConversationColumns.NOTIFICATION_VIBRATION + " INT DEFAULT(1), " |
| + ConversationColumns.INCLUDE_EMAIL_ADDRESS + " INT DEFAULT(0), " |
| + ConversationColumns.SMS_SERVICE_CENTER + " TEXT ," |
| + ConversationColumns.IS_ENTERPRISE + " INT DEFAULT(0)" |
| + ");"; |
| |
| private static final String CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL = |
| "CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SMS_THREAD_ID |
| + " ON " + CONVERSATIONS_TABLE |
| + "(" + ConversationColumns.SMS_THREAD_ID + ")"; |
| |
| private static final String CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL = |
| "CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.ARCHIVE_STATUS |
| + " ON " + CONVERSATIONS_TABLE |
| + "(" + ConversationColumns.ARCHIVE_STATUS + ")"; |
| |
| private static final String CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL = |
| "CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SORT_TIMESTAMP |
| + " ON " + CONVERSATIONS_TABLE |
| + "(" + ConversationColumns.SORT_TIMESTAMP + ")"; |
| |
| // Messages table schema |
| public static class MessageColumns implements BaseColumns { |
| /* conversation id that this message belongs to */ |
| public static final String CONVERSATION_ID = "conversation_id"; |
| |
| /* participant which send this message */ |
| public static final String SENDER_PARTICIPANT_ID = "sender_id"; |
| |
| /* This is bugle's internal status for the message */ |
| public static final String STATUS = "message_status"; |
| |
| /* Type of message: SMS, MMS or MMS notification */ |
| public static final String PROTOCOL = "message_protocol"; |
| |
| /* This is the time that the sender sent the message */ |
| public static final String SENT_TIMESTAMP = "sent_timestamp"; |
| |
| /* Time that we received the message on this device */ |
| public static final String RECEIVED_TIMESTAMP = "received_timestamp"; |
| |
| /* When the message has been seen by a user in a notification */ |
| public static final String SEEN = "seen"; |
| |
| /* When the message has been read by a user */ |
| public static final String READ = "read"; |
| |
| /* participant representing the sim which processed this message */ |
| public static final String SELF_PARTICIPANT_ID = "self_id"; |
| |
| /* |
| * Time when a retry is initiated. This is used to compute the retry window |
| * when we retry sending/downloading a message. |
| */ |
| public static final String RETRY_START_TIMESTAMP = "retry_start_timestamp"; |
| |
| // Columns which map to the SMS provider |
| |
| /* Message ID from the platform provider */ |
| public static final String SMS_MESSAGE_URI = "sms_message_uri"; |
| |
| /* The message priority for MMS message */ |
| public static final String SMS_PRIORITY = "sms_priority"; |
| |
| /* The message size for MMS message */ |
| public static final String SMS_MESSAGE_SIZE = "sms_message_size"; |
| |
| /* The subject for MMS message */ |
| public static final String MMS_SUBJECT = "mms_subject"; |
| |
| /* Transaction id for MMS notificaiton */ |
| public static final String MMS_TRANSACTION_ID = "mms_transaction_id"; |
| |
| /* Content location for MMS notificaiton */ |
| public static final String MMS_CONTENT_LOCATION = "mms_content_location"; |
| |
| /* The expiry time (ms) for MMS message */ |
| public static final String MMS_EXPIRY = "mms_expiry"; |
| |
| /* The detailed status (RESPONSE_STATUS or RETRIEVE_STATUS) for MMS message */ |
| public static final String RAW_TELEPHONY_STATUS = "raw_status"; |
| } |
| |
| // Messages table SQL |
| private static final String CREATE_MESSAGES_TABLE_SQL = |
| "CREATE TABLE " + MESSAGES_TABLE + " (" |
| + MessageColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " |
| + MessageColumns.CONVERSATION_ID + " INT, " |
| + MessageColumns.SENDER_PARTICIPANT_ID + " INT, " |
| + MessageColumns.SENT_TIMESTAMP + " INT DEFAULT(0), " |
| + MessageColumns.RECEIVED_TIMESTAMP + " INT DEFAULT(0), " |
| + MessageColumns.PROTOCOL + " INT DEFAULT(0), " |
| + MessageColumns.STATUS + " INT DEFAULT(0), " |
| + MessageColumns.SEEN + " INT DEFAULT(0), " |
| + MessageColumns.READ + " INT DEFAULT(0), " |
| + MessageColumns.SMS_MESSAGE_URI + " TEXT, " |
| + MessageColumns.SMS_PRIORITY + " INT DEFAULT(0), " |
| + MessageColumns.SMS_MESSAGE_SIZE + " INT DEFAULT(0), " |
| + MessageColumns.MMS_SUBJECT + " TEXT, " |
| + MessageColumns.MMS_TRANSACTION_ID + " TEXT, " |
| + MessageColumns.MMS_CONTENT_LOCATION + " TEXT, " |
| + MessageColumns.MMS_EXPIRY + " INT DEFAULT(0), " |
| + MessageColumns.RAW_TELEPHONY_STATUS + " INT DEFAULT(0), " |
| + MessageColumns.SELF_PARTICIPANT_ID + " INT, " |
| + MessageColumns.RETRY_START_TIMESTAMP + " INT DEFAULT(0), " |
| + "FOREIGN KEY (" + MessageColumns.CONVERSATION_ID + ") REFERENCES " |
| + CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ") ON DELETE CASCADE " |
| + "FOREIGN KEY (" + MessageColumns.SENDER_PARTICIPANT_ID + ") REFERENCES " |
| + PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + ") ON DELETE SET NULL " |
| + "FOREIGN KEY (" + MessageColumns.SELF_PARTICIPANT_ID + ") REFERENCES " |
| + PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + ") ON DELETE SET NULL " |
| + ");"; |
| |
| // Primary sort index for messages table : by conversation id, status, received timestamp. |
| private static final String MESSAGES_TABLE_SORT_INDEX_SQL = |
| "CREATE INDEX index_" + MESSAGES_TABLE + "_sort ON " + MESSAGES_TABLE + "(" |
| + MessageColumns.CONVERSATION_ID + ", " |
| + MessageColumns.STATUS + ", " |
| + MessageColumns.RECEIVED_TIMESTAMP + ")"; |
| |
| private static final String MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL = |
| "CREATE INDEX index_" + MESSAGES_TABLE + "_status_seen ON " + MESSAGES_TABLE + "(" |
| + MessageColumns.STATUS + ", " |
| + MessageColumns.SEEN + ")"; |
| |
| // Parts table schema |
| // A part may contain text or a media url, but not both. |
| public static class PartColumns implements BaseColumns { |
| /* message id that this part belongs to */ |
| public static final String MESSAGE_ID = "message_id"; |
| |
| /* conversation id that this part belongs to */ |
| public static final String CONVERSATION_ID = "conversation_id"; |
| |
| /* text for this part */ |
| public static final String TEXT = "text"; |
| |
| /* content uri for this part */ |
| public static final String CONTENT_URI = "uri"; |
| |
| /* content type for this part */ |
| public static final String CONTENT_TYPE = "content_type"; |
| |
| /* cached width for this part (for layout while loading) */ |
| public static final String WIDTH = "width"; |
| |
| /* cached height for this part (for layout while loading) */ |
| public static final String HEIGHT = "height"; |
| |
| /* de-normalized copy of timestamp from the messages table. This is populated |
| * via an insert trigger on the parts table. |
| */ |
| public static final String TIMESTAMP = "timestamp"; |
| } |
| |
| // Message part table SQL |
| private static final String CREATE_PARTS_TABLE_SQL = |
| "CREATE TABLE " + PARTS_TABLE + "(" |
| + PartColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," |
| + PartColumns.MESSAGE_ID + " INT," |
| + PartColumns.TEXT + " TEXT," |
| + PartColumns.CONTENT_URI + " TEXT," |
| + PartColumns.CONTENT_TYPE + " TEXT," |
| + PartColumns.WIDTH + " INT DEFAULT(" |
| + MessagingContentProvider.UNSPECIFIED_SIZE + ")," |
| + PartColumns.HEIGHT + " INT DEFAULT(" |
| + MessagingContentProvider.UNSPECIFIED_SIZE + ")," |
| + PartColumns.TIMESTAMP + " INT, " |
| + PartColumns.CONVERSATION_ID + " INT NOT NULL," |
| + "FOREIGN KEY (" + PartColumns.MESSAGE_ID + ") REFERENCES " |
| + MESSAGES_TABLE + "(" + MessageColumns._ID + ") ON DELETE CASCADE " |
| + "FOREIGN KEY (" + PartColumns.CONVERSATION_ID + ") REFERENCES " |
| + CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ") ON DELETE CASCADE " |
| + ");"; |
| |
| public static final String CREATE_PARTS_TRIGGER_SQL = |
| "CREATE TRIGGER " + PARTS_TABLE + "_TRIGGER" + " AFTER INSERT ON " + PARTS_TABLE |
| + " FOR EACH ROW " |
| + " BEGIN UPDATE " + PARTS_TABLE |
| + " SET " + PartColumns.TIMESTAMP + "=" |
| + " (SELECT received_timestamp FROM " + MESSAGES_TABLE + " WHERE " + MESSAGES_TABLE |
| + "." + MessageColumns._ID + "=" + "NEW." + PartColumns.MESSAGE_ID + ")" |
| + " WHERE " + PARTS_TABLE + "." + PartColumns._ID + "=" + "NEW." + PartColumns._ID |
| + "; END"; |
| |
| public static final String CREATE_MESSAGES_TRIGGER_SQL = |
| "CREATE TRIGGER " + MESSAGES_TABLE + "_TRIGGER" + " AFTER UPDATE OF " |
| + MessageColumns.RECEIVED_TIMESTAMP + " ON " + MESSAGES_TABLE |
| + " FOR EACH ROW BEGIN UPDATE " + PARTS_TABLE + " SET " + PartColumns.TIMESTAMP |
| + " = NEW." + MessageColumns.RECEIVED_TIMESTAMP + " WHERE " + PARTS_TABLE + "." |
| + PartColumns.MESSAGE_ID + " = NEW." + MessageColumns._ID |
| + "; END;"; |
| |
| // Primary sort index for parts table : by message_id |
| private static final String PARTS_TABLE_MESSAGE_INDEX_SQL = |
| "CREATE INDEX index_" + PARTS_TABLE + "_message_id ON " + PARTS_TABLE + "(" |
| + PartColumns.MESSAGE_ID + ")"; |
| |
| // Participants table schema |
| public static class ParticipantColumns implements BaseColumns { |
| /* The subscription id for the sim associated with this self participant. |
| * Introduced in L. For earlier versions will always be default_sub_id (-1). |
| * For multi sim devices (or cases where the sim was changed) single device |
| * may have several different sub_id values */ |
| public static final String SUB_ID = "sub_id"; |
| |
| /* The slot of the active SIM (inserted in the device) for this self-participant. If the |
| * self-participant doesn't correspond to any active SIM, this will be |
| * {@link android.telephony.SubscriptionManager#INVALID_SLOT_ID}. |
| * The column is ignored for all non-self participants. |
| */ |
| public static final String SIM_SLOT_ID = "sim_slot_id"; |
| |
| /* The phone number stored in a standard E164 format if possible. This is unique for a |
| * given participant. We can't handle multiple participants with the same phone number |
| * since we don't know which of them a message comes from. This can also be an email |
| * address, in which case this is the same as the displayed address */ |
| public static final String NORMALIZED_DESTINATION = "normalized_destination"; |
| |
| /* The phone number as originally supplied and used for dialing. Not necessarily in E164 |
| * format or unique */ |
| public static final String SEND_DESTINATION = "send_destination"; |
| |
| /* The user-friendly formatting of the phone number according to the region setting of |
| * the device when the row was added. */ |
| public static final String DISPLAY_DESTINATION = "display_destination"; |
| |
| /* A string with this participant's full name or a pretty printed phone number */ |
| public static final String FULL_NAME = "full_name"; |
| |
| /* A string with just this participant's first name */ |
| public static final String FIRST_NAME = "first_name"; |
| |
| /* A local URI to an asset for the icon for this participant */ |
| public static final String PROFILE_PHOTO_URI = "profile_photo_uri"; |
| |
| /* Contact id for matching local contact for this participant */ |
| public static final String CONTACT_ID = "contact_id"; |
| |
| /* String that contains hints on how to find contact information in a contact lookup */ |
| public static final String LOOKUP_KEY = "lookup_key"; |
| |
| /* If this participant is blocked */ |
| public static final String BLOCKED = "blocked"; |
| |
| /* The color of the subscription (FOR SELF PARTICIPANTS ONLY) */ |
| public static final String SUBSCRIPTION_COLOR = "subscription_color"; |
| |
| /* The name of the subscription (FOR SELF PARTICIPANTS ONLY) */ |
| public static final String SUBSCRIPTION_NAME = "subscription_name"; |
| |
| /* The exact destination stored in Contacts for this participant */ |
| public static final String CONTACT_DESTINATION = "contact_destination"; |
| } |
| |
| // Participants table SQL |
| private static final String CREATE_PARTICIPANTS_TABLE_SQL = |
| "CREATE TABLE " + PARTICIPANTS_TABLE + "(" |
| + ParticipantColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," |
| + ParticipantColumns.SUB_ID + " INT DEFAULT(" |
| + ParticipantData.OTHER_THAN_SELF_SUB_ID + ")," |
| + ParticipantColumns.SIM_SLOT_ID + " INT DEFAULT(" |
| + ParticipantData.INVALID_SLOT_ID + ")," |
| + ParticipantColumns.NORMALIZED_DESTINATION + " TEXT," |
| + ParticipantColumns.SEND_DESTINATION + " TEXT," |
| + ParticipantColumns.DISPLAY_DESTINATION + " TEXT," |
| + ParticipantColumns.FULL_NAME + " TEXT," |
| + ParticipantColumns.FIRST_NAME + " TEXT," |
| + ParticipantColumns.PROFILE_PHOTO_URI + " TEXT, " |
| + ParticipantColumns.CONTACT_ID + " INT DEFAULT( " |
| + ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED + "), " |
| + ParticipantColumns.LOOKUP_KEY + " STRING, " |
| + ParticipantColumns.BLOCKED + " INT DEFAULT(0), " |
| + ParticipantColumns.SUBSCRIPTION_NAME + " TEXT, " |
| + ParticipantColumns.SUBSCRIPTION_COLOR + " INT DEFAULT(0), " |
| + ParticipantColumns.CONTACT_DESTINATION + " TEXT, " |
| + "UNIQUE (" + ParticipantColumns.NORMALIZED_DESTINATION + ", " |
| + ParticipantColumns.SUB_ID + ") ON CONFLICT FAIL" + ");"; |
| |
| private static final String CREATE_SELF_PARTICIPANT_SQL = |
| "INSERT INTO " + PARTICIPANTS_TABLE |
| + " ( " + ParticipantColumns.SUB_ID + " ) VALUES ( %s )"; |
| |
| static String getCreateSelfParticipantSql(int subId) { |
| return String.format(CREATE_SELF_PARTICIPANT_SQL, subId); |
| } |
| |
| // Conversation Participants table schema - contains a list of participants excluding the user |
| // in a given conversation. |
| public static class ConversationParticipantsColumns implements BaseColumns { |
| /* participant id of someone in this conversation */ |
| public static final String PARTICIPANT_ID = "participant_id"; |
| |
| /* conversation id that this participant belongs to */ |
| public static final String CONVERSATION_ID = "conversation_id"; |
| } |
| |
| // Conversation Participants table SQL |
| private static final String CREATE_CONVERSATION_PARTICIPANTS_TABLE_SQL = |
| "CREATE TABLE " + CONVERSATION_PARTICIPANTS_TABLE + "(" |
| + ConversationParticipantsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," |
| + ConversationParticipantsColumns.CONVERSATION_ID + " INT," |
| + ConversationParticipantsColumns.PARTICIPANT_ID + " INT," |
| + "UNIQUE (" + ConversationParticipantsColumns.CONVERSATION_ID + "," |
| + ConversationParticipantsColumns.PARTICIPANT_ID + ") ON CONFLICT FAIL, " |
| + "FOREIGN KEY (" + ConversationParticipantsColumns.CONVERSATION_ID + ") " |
| + "REFERENCES " + CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ")" |
| + " ON DELETE CASCADE " |
| + "FOREIGN KEY (" + ConversationParticipantsColumns.PARTICIPANT_ID + ")" |
| + " REFERENCES " + PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + "));"; |
| |
| // Primary access pattern for conversation participants is to look them up for a specific |
| // conversation. |
| private static final String CONVERSATION_PARTICIPANTS_TABLE_CONVERSATION_ID_INDEX_SQL = |
| "CREATE INDEX index_" + CONVERSATION_PARTICIPANTS_TABLE + "_" |
| + ConversationParticipantsColumns.CONVERSATION_ID |
| + " ON " + CONVERSATION_PARTICIPANTS_TABLE |
| + "(" + ConversationParticipantsColumns.CONVERSATION_ID + ")"; |
| |
| // View for getting parts which are for draft messages. |
| static final String DRAFT_PARTS_VIEW_SQL = "CREATE VIEW " + |
| DRAFT_PARTS_VIEW + " AS SELECT " |
| + PARTS_TABLE + '.' + PartColumns._ID |
| + " as " + PartColumns._ID + ", " |
| + PARTS_TABLE + '.' + PartColumns.MESSAGE_ID |
| + " as " + PartColumns.MESSAGE_ID + ", " |
| + PARTS_TABLE + '.' + PartColumns.TEXT |
| + " as " + PartColumns.TEXT + ", " |
| + PARTS_TABLE + '.' + PartColumns.CONTENT_URI |
| + " as " + PartColumns.CONTENT_URI + ", " |
| + PARTS_TABLE + '.' + PartColumns.CONTENT_TYPE |
| + " as " + PartColumns.CONTENT_TYPE + ", " |
| + PARTS_TABLE + '.' + PartColumns.WIDTH |
| + " as " + PartColumns.WIDTH + ", " |
| + PARTS_TABLE + '.' + PartColumns.HEIGHT |
| + " as " + PartColumns.HEIGHT + ", " |
| + MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID |
| + " as " + MessageColumns.CONVERSATION_ID + " " |
| + " FROM " + MESSAGES_TABLE + " LEFT JOIN " + PARTS_TABLE + " ON (" |
| + MESSAGES_TABLE + "." + MessageColumns._ID |
| + "=" + PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ")" |
| // Exclude draft messages from main view |
| + " WHERE " + MESSAGES_TABLE + "." + MessageColumns.STATUS |
| + " = " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT; |
| |
| // List of all our SQL tables |
| private static final String[] CREATE_TABLE_SQLS = new String[] { |
| CREATE_CONVERSATIONS_TABLE_SQL, |
| CREATE_MESSAGES_TABLE_SQL, |
| CREATE_PARTS_TABLE_SQL, |
| CREATE_PARTICIPANTS_TABLE_SQL, |
| CREATE_CONVERSATION_PARTICIPANTS_TABLE_SQL, |
| }; |
| |
| // List of all our indices |
| private static final String[] CREATE_INDEX_SQLS = new String[] { |
| CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL, |
| CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL, |
| CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL, |
| MESSAGES_TABLE_SORT_INDEX_SQL, |
| MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL, |
| PARTS_TABLE_MESSAGE_INDEX_SQL, |
| CONVERSATION_PARTICIPANTS_TABLE_CONVERSATION_ID_INDEX_SQL, |
| }; |
| |
| // List of all our SQL triggers |
| private static final String[] CREATE_TRIGGER_SQLS = new String[] { |
| CREATE_PARTS_TRIGGER_SQL, |
| CREATE_MESSAGES_TRIGGER_SQL, |
| }; |
| |
| // List of all our views |
| private static final String[] CREATE_VIEW_SQLS = new String[] { |
| ConversationListItemData.getConversationListViewSql(), |
| ConversationImagePartsView.getCreateSql(), |
| DRAFT_PARTS_VIEW_SQL, |
| }; |
| |
| private static final Object sLock = new Object(); |
| private final Context mApplicationContext; |
| private static DatabaseHelper sHelperInstance; // Protected by sLock. |
| |
| private final Object mDatabaseWrapperLock = new Object(); |
| private DatabaseWrapper mDatabaseWrapper; // Protected by mDatabaseWrapperLock. |
| private final DatabaseUpgradeHelper mUpgradeHelper = new DatabaseUpgradeHelper(); |
| |
| /** |
| * Get a (singleton) instance of {@link DatabaseHelper}, creating one if there isn't one yet. |
| * This is the only public method for getting a new instance of the class. |
| * @param context Should be the application context (or something that will live for the |
| * lifetime of the application). |
| * @return The current (or a new) DatabaseHelper instance. |
| */ |
| public static DatabaseHelper getInstance(final Context context) { |
| synchronized (sLock) { |
| if (sHelperInstance == null) { |
| sHelperInstance = new DatabaseHelper(context); |
| } |
| return sHelperInstance; |
| } |
| } |
| |
| /** |
| * Private constructor, used from {@link #getInstance()}. |
| * @param context Should be the application context (or something that will live for the |
| * lifetime of the application). |
| */ |
| private DatabaseHelper(final Context context) { |
| super(context, DATABASE_NAME, null, getDatabaseVersion(context), null); |
| mApplicationContext = context; |
| } |
| |
| /** |
| * Test method that always instantiates a new DatabaseHelper instance. This should |
| * be used ONLY by the tests and never by the real application. |
| * @param context Test context. |
| * @return Brand new DatabaseHelper instance. |
| */ |
| @VisibleForTesting |
| static DatabaseHelper getNewInstanceForTest(final Context context) { |
| Assert.isTrue(BugleApplication.isRunningTests()); |
| return new DatabaseHelper(context); |
| } |
| |
| /** |
| * Get the (singleton) instance of @{link DatabaseWrapper}. |
| * <p>The database is always opened as a writeable database. |
| * @return The current (or a new) DatabaseWrapper instance. |
| */ |
| @DoesNotRunOnMainThread |
| DatabaseWrapper getDatabase() { |
| // We prevent the main UI thread from accessing the database here since we have to allow |
| // public access to this class to enable sub-packages to access data. |
| Assert.isNotMainThread(); |
| |
| synchronized (mDatabaseWrapperLock) { |
| if (mDatabaseWrapper == null) { |
| mDatabaseWrapper = new DatabaseWrapper(mApplicationContext, getWritableDatabase()); |
| } |
| return mDatabaseWrapper; |
| } |
| } |
| |
| @Override |
| public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { |
| mUpgradeHelper.onDowngrade(db, oldVersion, newVersion); |
| } |
| |
| /** |
| * Drops and recreates all tables. |
| */ |
| public static void rebuildTables(final SQLiteDatabase db) { |
| // Drop tables first, then views, and indices. |
| dropAllTables(db); |
| dropAllViews(db); |
| dropAllIndexes(db); |
| dropAllTriggers(db); |
| |
| // Recreate the whole database. |
| createDatabase(db); |
| } |
| |
| /** |
| * Drop and rebuild a given view. |
| */ |
| static void rebuildView(final SQLiteDatabase db, final String viewName, |
| final String createViewSql) { |
| dropView(db, viewName, true /* throwOnFailure */); |
| db.execSQL(createViewSql); |
| } |
| |
| private static void dropView(final SQLiteDatabase db, final String viewName, |
| final boolean throwOnFailure) { |
| final String dropPrefix = "DROP VIEW IF EXISTS "; |
| try { |
| db.execSQL(dropPrefix + viewName); |
| } catch (final SQLException ex) { |
| if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { |
| LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop view " + viewName + " " |
| + ex); |
| } |
| |
| if (throwOnFailure) { |
| throw ex; |
| } |
| } |
| } |
| |
| public static void rebuildAllViews(final DatabaseWrapper db) { |
| for (final String sql : DatabaseHelper.CREATE_VIEW_SQLS) { |
| db.execSQL(sql); |
| } |
| } |
| |
| /** |
| * Drops all user-defined tables from the given database. |
| */ |
| private static void dropAllTables(final SQLiteDatabase db) { |
| final Cursor tableCursor = |
| db.query(PRIMARY_TABLE, PRIMARY_COLUMNS, "type='table'", null, null, null, null); |
| if (tableCursor != null) { |
| try { |
| final String dropPrefix = "DROP TABLE IF EXISTS "; |
| while (tableCursor.moveToNext()) { |
| final String tableName = tableCursor.getString(0); |
| |
| // Skip special tables |
| if (tableName.startsWith("android_") || tableName.startsWith("sqlite_")) { |
| continue; |
| } |
| try { |
| db.execSQL(dropPrefix + tableName); |
| } catch (final SQLException ex) { |
| if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { |
| LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop table " + tableName + " " |
| + ex); |
| } |
| } |
| } |
| } finally { |
| tableCursor.close(); |
| } |
| } |
| } |
| |
| /** |
| * Drops all user-defined triggers from the given database. |
| */ |
| private static void dropAllTriggers(final SQLiteDatabase db) { |
| final Cursor triggerCursor = |
| db.query(PRIMARY_TABLE, PRIMARY_COLUMNS, "type='trigger'", null, null, null, null); |
| if (triggerCursor != null) { |
| try { |
| final String dropPrefix = "DROP TRIGGER IF EXISTS "; |
| while (triggerCursor.moveToNext()) { |
| final String triggerName = triggerCursor.getString(0); |
| |
| // Skip special tables |
| if (triggerName.startsWith("android_") || triggerName.startsWith("sqlite_")) { |
| continue; |
| } |
| try { |
| db.execSQL(dropPrefix + triggerName); |
| } catch (final SQLException ex) { |
| if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { |
| LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop trigger " + triggerName + |
| " " + ex); |
| } |
| } |
| } |
| } finally { |
| triggerCursor.close(); |
| } |
| } |
| } |
| |
| /** |
| * Drops all user-defined views from the given database. |
| */ |
| public static void dropAllViews(final SQLiteDatabase db) { |
| final Cursor viewCursor = |
| db.query(PRIMARY_TABLE, PRIMARY_COLUMNS, "type='view'", null, null, null, null); |
| if (viewCursor != null) { |
| try { |
| while (viewCursor.moveToNext()) { |
| final String viewName = viewCursor.getString(0); |
| dropView(db, viewName, false /* throwOnFailure */); |
| } |
| } finally { |
| viewCursor.close(); |
| } |
| } |
| } |
| |
| /** |
| * Drops all user-defined views from the given database. |
| */ |
| private static void dropAllIndexes(final SQLiteDatabase db) { |
| final Cursor indexCursor = |
| db.query(PRIMARY_TABLE, PRIMARY_COLUMNS, "type='index'", null, null, null, null); |
| if (indexCursor != null) { |
| try { |
| final String dropPrefix = "DROP INDEX IF EXISTS "; |
| while (indexCursor.moveToNext()) { |
| final String indexName = indexCursor.getString(0); |
| try { |
| db.execSQL(dropPrefix + indexName); |
| } catch (final SQLException ex) { |
| if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) { |
| LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop index " + indexName + " " |
| + ex); |
| } |
| } |
| } |
| } finally { |
| indexCursor.close(); |
| } |
| } |
| } |
| |
| private static void createDatabase(final SQLiteDatabase db) { |
| for (final String sql : CREATE_TABLE_SQLS) { |
| db.execSQL(sql); |
| } |
| |
| for (final String sql : CREATE_INDEX_SQLS) { |
| db.execSQL(sql); |
| } |
| |
| for (final String sql : CREATE_VIEW_SQLS) { |
| db.execSQL(sql); |
| } |
| |
| for (final String sql : CREATE_TRIGGER_SQLS) { |
| db.execSQL(sql); |
| } |
| |
| // Enable foreign key constraints |
| db.execSQL("PRAGMA foreign_keys=ON;"); |
| |
| // Add the default self participant. The default self will be assigned a proper slot id |
| // during participant refresh. |
| db.execSQL(getCreateSelfParticipantSql(ParticipantData.DEFAULT_SELF_SUB_ID)); |
| |
| DataModel.get().onCreateTables(db); |
| } |
| |
| @Override |
| public void onCreate(SQLiteDatabase db) { |
| createDatabase(db); |
| } |
| |
| @Override |
| public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
| mUpgradeHelper.doOnUpgrade(db, oldVersion, newVersion); |
| } |
| } |