blob: 2025e2cdcf16ca80ebf5f051cf28a10b7e76de14 [file] [log] [blame]
/*
* 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 */
private static final String MASTER_TABLE = "sqlite_master";
/** Column containing the name of the tables and views */
private static final String[] MASTER_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.isEngBuild();
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(MASTER_TABLE, MASTER_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(MASTER_TABLE, MASTER_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(MASTER_TABLE, MASTER_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(MASTER_TABLE, MASTER_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);
}
}