+ Added missing androix libraries to LOCAL_STATIC_ANDROID_LIBRARIES.
+ Added keep class statements for androidx and messaging classes.
+ Deleted Assert.isEngBuild call (this package is the last user of that method).
+ Added import statements for androidx classes.
Test: manual - Tested the messaging UI. Ran the following tests on Pixel phone.
$ make -j 40
$ make messagingtests -j
$ find out -name "messaging*.apk" -print -exec ls -l {} \;
-rw-r----- 1 rtenneti primarygroup 733809 Jul 9 17:42 out/target/product/bonito/testcases/messagingtests/arm64/messagingtests.apk
-rw-r----- 1 rtenneti primarygroup 10799132 Jul 9 18:34 out/target/product/bonito/product/app/messaging/messaging.apk
$ adb install -r -d -t out/target/product/bonito/product/app/messaging/messaging.apk
$ adb install -r -d -t out/target/product/bonito/testcases/messagingtests/arm64/messagingtests.apk
$ adb shell am instrument -w com.android.messaging.test
com.android.messaging.datamodel.BindingTest:.......
com.android.messaging.datamodel.BitmapPoolTest:....
com.android.messaging.datamodel.ConversationListTest:.
com.android.messaging.datamodel.DataModelTest:...
com.android.messaging.datamodel.FrequentContactsCursorBuilderTest:...
com.android.messaging.datamodel.MemoryCacheManagerTest:.
com.android.messaging.datamodel.ParticipantRefreshTest:..........
com.android.messaging.datamodel.action.ActionServiceSystemTest:.....
com.android.messaging.datamodel.action.ActionServiceTest:..
com.android.messaging.datamodel.action.ActionTest:.....
com.android.messaging.datamodel.action.GetOrCreateConversationActionTest:.
com.android.messaging.datamodel.action.ReadWriteDraftMessageActionTest:......
com.android.messaging.datamodel.data.ConversationMessageDataTest:...
com.android.messaging.datamodel.data.ConversationParticipantsDataTest:.
com.android.messaging.datamodel.media.ImageRequestTest:..
com.android.messaging.datamodel.media.MediaResourceManagerTest:.....
com.android.messaging.ui.CustomHeaderViewPagerTest:.
com.android.messaging.ui.MultiAttachmentLayoutTest:...
com.android.messaging.ui.attachmentchooser.AttachmentChooserFragmentTest:.
com.android.messaging.ui.contact.ContactListItemViewTest:....
com.android.messaging.ui.contact.ContactPickerFragmentTest:......
com.android.messaging.ui.conversation.ComposeMessageViewTest:..
com.android.messaging.ui.conversation.ConversationActivityUiStateTest:....
com.android.messaging.ui.conversation.ConversationFragmentTest:..
com.android.messaging.ui.conversation.ConversationInputManagerTest:.....
com.android.messaging.ui.conversation.ConversationMessageViewTest:...
com.android.messaging.ui.conversationlist.ConversationListFragmentTest:...
com.android.messaging.ui.conversationlist.ConversationListItemViewTest:....
com.android.messaging.ui.mediapicker.AudioRecordViewTest:.
com.android.messaging.ui.mediapicker.CameraManagerTest:......
com.android.messaging.ui.mediapicker.GalleryGridItemViewTest:.....
com.android.messaging.ui.mediapicker.MediaPickerTest:..
com.android.messaging.util.BugleGservicesTest:.
com.android.messaging.util.YouTubeUtilTest:.
Test results for InstrumentationTestRunner=.........................................
.........................................
...............................
Time: 9.903
OK (113 tests)
Change-Id: If8fd8036791027739eb9bbb1d3849a46211a5137
823 lines
38 KiB
Java
823 lines
38 KiB
Java
/*
|
|
* 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.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);
|
|
}
|
|
}
|