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