MmsSmsDatabaseHelper.java revision 075b10540170f65f868d283950216d543cfe38a0
1/*
2 * Copyright (C) 2008 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
17package com.android.providers.telephony;
18
19import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND;
20import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
21import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND;
22import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_REC_IND;
23import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF;
24import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_SEND_REQ;
25
26import java.io.IOException;
27import java.io.InputStream;
28import java.io.FileInputStream;
29import java.io.File;
30import java.util.ArrayList;
31
32import com.google.android.mms.pdu.EncodedStringValue;
33
34import android.content.Context;
35import android.database.Cursor;
36import android.database.sqlite.SQLiteDatabase;
37import android.database.sqlite.SQLiteOpenHelper;
38import android.provider.BaseColumns;
39import android.provider.Telephony.Mms;
40import android.provider.Telephony.MmsSms;
41import android.provider.Telephony.Sms;
42import android.provider.Telephony.Threads;
43import android.provider.Telephony.Mms.Addr;
44import android.provider.Telephony.Mms.Part;
45import android.provider.Telephony.Mms.Rate;
46import android.provider.Telephony.MmsSms.PendingMessages;
47import android.util.Log;
48
49public class MmsSmsDatabaseHelper extends SQLiteOpenHelper {
50    private static final String TAG = "MmsSmsDatabaseHelper";
51
52    private static final String SMS_UPDATE_THREAD_READ_BODY =
53                        "  UPDATE threads SET read = " +
54                        "    CASE (SELECT COUNT(*)" +
55                        "          FROM sms" +
56                        "          WHERE " + Sms.READ + " = 0" +
57                        "            AND " + Sms.THREAD_ID + " = threads._id)" +
58                        "      WHEN 0 THEN 1" +
59                        "      ELSE 0" +
60                        "    END" +
61                        "  WHERE threads._id = new." + Sms.THREAD_ID + "; ";
62
63    private static final String UPDATE_THREAD_COUNT_ON_NEW =
64                        "  UPDATE threads SET message_count = " +
65                        "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
66                        "      ON threads._id = " + Sms.THREAD_ID +
67                        "      WHERE " + Sms.THREAD_ID + " = new.thread_id" +
68                        "        AND sms." + Sms.TYPE + " != 3) + " +
69                        "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
70                        "      ON threads._id = " + Mms.THREAD_ID +
71                        "      WHERE " + Mms.THREAD_ID + " = new.thread_id" +
72                        "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
73                        "        AND " + Mms.MESSAGE_BOX + " != 3) " +
74                        "  WHERE threads._id = new.thread_id; ";
75
76    private static final String UPDATE_THREAD_COUNT_ON_OLD =
77                        "  UPDATE threads SET message_count = " +
78                        "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
79                        "      ON threads._id = " + Sms.THREAD_ID +
80                        "      WHERE " + Sms.THREAD_ID + " = old.thread_id" +
81                        "        AND sms." + Sms.TYPE + " != 3) + " +
82                        "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
83                        "      ON threads._id = " + Mms.THREAD_ID +
84                        "      WHERE " + Mms.THREAD_ID + " = old.thread_id" +
85                        "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
86                        "        AND " + Mms.MESSAGE_BOX + " != 3) " +
87                        "  WHERE threads._id = old.thread_id; ";
88
89    private static final String SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE =
90                        "BEGIN" +
91                        "  UPDATE threads SET" +
92                        "    date = (strftime('%s','now') * 1000), " +
93                        "    snippet = new." + Sms.BODY + ", " +
94                        "    snippet_cs = 0" +
95                        "  WHERE threads._id = new." + Sms.THREAD_ID + "; " +
96                        UPDATE_THREAD_COUNT_ON_NEW +
97                        SMS_UPDATE_THREAD_READ_BODY +
98                        "END;";
99
100    private static final String PDU_UPDATE_THREAD_CONSTRAINTS =
101                        "  WHEN new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF +
102                        "    OR new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND +
103                        "    OR new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_SEND_REQ + " ";
104
105    private static final String PDU_UPDATE_THREAD_READ_BODY =
106                        "  UPDATE threads SET read = " +
107                        "    CASE (SELECT COUNT(*)" +
108                        "          FROM " + MmsProvider.TABLE_PDU +
109                        "          WHERE " + Mms.READ + " = 0" +
110                        "            AND " + Mms.THREAD_ID + " = threads._id)" +
111                        "      WHEN 0 THEN 1" +
112                        "      ELSE 0" +
113                        "    END" +
114                        "  WHERE threads._id = new." + Mms.THREAD_ID + "; ";
115
116    private static final String PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE =
117                        "BEGIN" +
118                        "  UPDATE threads SET" +
119                        "    date = (strftime('%s','now') * 1000), " +
120                        "    snippet = new." + Mms.SUBJECT + ", " +
121                        "    snippet_cs = new." + Mms.SUBJECT_CHARSET +
122                        "  WHERE threads._id = new." + Mms.THREAD_ID + "; " +
123                        UPDATE_THREAD_COUNT_ON_NEW +
124                        PDU_UPDATE_THREAD_READ_BODY +
125                        "END;";
126
127    private static final String UPDATE_THREAD_SNIPPET_SNIPPET_CS_ON_DELETE =
128                        "  UPDATE threads SET snippet = " +
129                        "   (SELECT snippet FROM" +
130                        "     (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
131                        "      UNION SELECT date, body AS snippet, thread_id FROM sms)" +
132                        "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
133                        "  WHERE threads._id = OLD.thread_id; " +
134                        "  UPDATE threads SET snippet_cs = " +
135                        "   (SELECT snippet_cs FROM" +
136                        "     (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
137                        "      UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
138                        "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
139                        "  WHERE threads._id = OLD.thread_id; ";
140
141
142    // When a part is inserted, if it is not text/plain or application/smil
143    // (which both can exist with text-only MMSes), then there is an attachment.
144    // Set has_attachment=1 in the threads table for the thread in question.
145    private static final String PART_UPDATE_THREADS_ON_INSERT_TRIGGER =
146                        "CREATE TRIGGER update_threads_on_insert_part " +
147                        " AFTER INSERT ON part " +
148                        " WHEN new.ct != 'text/plain' AND new.ct != 'application/smil' " +
149                        " BEGIN " +
150                        "  UPDATE threads SET has_attachment=1 WHERE _id IN " +
151                        "   (SELECT pdu.thread_id FROM part JOIN pdu ON pdu._id=part.mid " +
152                        "     WHERE part._id=new._id LIMIT 1); " +
153                        " END";
154
155    // When the 'mid' column in the part table is updated, we need to run the trigger to update
156    // the threads table's has_attachment column, if the part is an attachment.
157    private static final String PART_UPDATE_THREADS_ON_UPDATE_TRIGGER =
158                        "CREATE TRIGGER update_threads_on_update_part " +
159                        " AFTER UPDATE of " + Part.MSG_ID + " ON part " +
160                        " WHEN new.ct != 'text/plain' AND new.ct != 'application/smil' " +
161                        " BEGIN " +
162                        "  UPDATE threads SET has_attachment=1 WHERE _id IN " +
163                        "   (SELECT pdu.thread_id FROM part JOIN pdu ON pdu._id=part.mid " +
164                        "     WHERE part._id=new._id LIMIT 1); " +
165                        " END";
166
167
168    // When a part is deleted (with the same non-text/SMIL constraint as when
169    // we set has_attachment), update the threads table for all threads.
170    // Unfortunately we cannot update only the thread that the part was
171    // attached to, as it is possible that the part has been orphaned and
172    // the message it was attached to is already gone.
173    private static final String PART_UPDATE_THREADS_ON_DELETE_TRIGGER =
174                        "CREATE TRIGGER update_threads_on_delete_part " +
175                        " AFTER DELETE ON part " +
176                        " WHEN old.ct != 'text/plain' AND old.ct != 'application/smil' " +
177                        " BEGIN " +
178                        "  UPDATE threads SET has_attachment = " +
179                        "   CASE " +
180                        "    (SELECT COUNT(*) FROM part JOIN pdu ON pdu._id=part.mid " +
181                        "     WHERE pdu.thread_id = threads._id " +
182                        "     AND part.ct != 'text/plain' AND part.ct != 'application/smil')" +
183                        "   WHEN 0 THEN 0 " +
184                        "   ELSE 1 " +
185                        "   END; " +
186                        " END";
187
188    private static MmsSmsDatabaseHelper mInstance = null;
189
190    static final String DATABASE_NAME = "mmssms.db";
191    static final int DATABASE_VERSION = 46;
192
193    private MmsSmsDatabaseHelper(Context context) {
194        super(context, DATABASE_NAME, null, DATABASE_VERSION);
195    }
196
197    /**
198     * Return a singleton helper for the combined MMS and SMS
199     * database.
200     */
201    /* package */ static synchronized MmsSmsDatabaseHelper getInstance(Context context) {
202        if (mInstance == null) {
203            mInstance = new MmsSmsDatabaseHelper(context);
204        }
205        return mInstance;
206    }
207
208    public static void updateThread(SQLiteDatabase db, long thread_id) {
209        if (thread_id < 0) {
210            updateAllThreads(db, null, null);
211            return;
212        }
213
214        // Delete the row for this thread in the threads table if
215        // there are no more messages attached to it in either
216        // the sms or pdu tables.
217        int rows = db.delete("threads",
218                  "_id = ? AND _id NOT IN" +
219                  "          (SELECT thread_id FROM sms " +
220                  "           UNION SELECT thread_id FROM pdu)",
221                  new String[] { String.valueOf(thread_id) });
222        if (rows > 0) {
223            // If this deleted a row, we have no more work to do.
224            return;
225        }
226        // Update the message count in the threads table as the sum
227        // of all messages in both the sms and pdu tables.
228        db.execSQL(
229            "  UPDATE threads SET message_count = " +
230            "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
231            "      ON threads._id = " + Sms.THREAD_ID +
232            "      WHERE " + Sms.THREAD_ID + " = " + thread_id +
233            "        AND sms." + Sms.TYPE + " != 3) + " +
234            "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
235            "      ON threads._id = " + Mms.THREAD_ID +
236            "      WHERE " + Mms.THREAD_ID + " = " + thread_id +
237            "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
238            "        AND " + Mms.MESSAGE_BOX + " != 3) " +
239            "  WHERE threads._id = " + thread_id + ";");
240
241        // Update the date and the snippet (and its character set) in
242        // the threads table to be that of the most recent message in
243        // the thread.
244        db.execSQL(
245            "  UPDATE threads" +
246            "  SET" +
247            "  date =" +
248            "    (SELECT date FROM" +
249            "        (SELECT date * 1000 AS date, thread_id FROM pdu" +
250            "         UNION SELECT date, thread_id FROM sms)" +
251            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)," +
252            "  snippet =" +
253            "    (SELECT snippet FROM" +
254            "        (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
255            "         UNION SELECT date, body AS snippet, thread_id FROM sms)" +
256            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)," +
257            "  snippet_cs =" +
258            "    (SELECT snippet_cs FROM" +
259            "        (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
260            "         UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
261            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)" +
262            "  WHERE threads._id = " + thread_id + ";");
263
264        // Update the error column of the thread to indicate if there
265        // are any messages in it that have failed to send.
266        db.execSQL(
267            "UPDATE threads SET error =" +
268            "       (SELECT COUNT(*) FROM sms WHERE type=5" +
269            "        AND thread_id = " + thread_id + " LIMIT 1)" +
270            "   WHERE threads._id = " + thread_id + ";");
271    }
272
273    public static void updateAllThreads(SQLiteDatabase db, String where, String[] whereArgs) {
274        if (where == null) {
275            where = "";
276        } else {
277            where = "WHERE (" + where + ")";
278        }
279        String query = "SELECT _id FROM threads WHERE _id IN " +
280                       "(SELECT DISTINCT thread_id FROM sms " + where + ")";
281        Cursor c = db.rawQuery(query, whereArgs);
282        if (c != null) {
283            while (c.moveToNext()) {
284                updateThread(db, c.getInt(0));
285            }
286            c.close();
287        }
288        // remove orphaned threads
289        db.delete("threads",
290                "_id NOT IN (SELECT DISTINCT thread_id FROM sms " +
291                "UNION SELECT DISTINCT thread_id FROM pdu)", null);
292    }
293
294    public static int deleteOneSms(SQLiteDatabase db, int message_id) {
295        int thread_id = -1;
296        // Find the thread ID that the specified SMS belongs to.
297        Cursor c = db.query("sms", new String[] { "thread_id" },
298                            "_id=" + message_id, null, null, null, null);
299        if (c != null) {
300            if (c.moveToFirst()) {
301                thread_id = c.getInt(0);
302            }
303            c.close();
304        }
305
306        // Delete the specified message.
307        int rows = db.delete("sms", "_id=" + message_id, null);
308        if (thread_id > 0) {
309            // Update its thread.
310            updateThread(db, thread_id);
311        }
312        return rows;
313    }
314
315    @Override
316    public void onCreate(SQLiteDatabase db) {
317        createMmsTables(db);
318        createSmsTables(db);
319        createCommonTables(db);
320        createCommonTriggers(db);
321        createMmsTriggers(db);
322    }
323
324    private void createMmsTables(SQLiteDatabase db) {
325        // N.B.: Whenever the columns here are changed, the columns in
326        // {@ref MmsSmsProvider} must be changed to match.
327        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_PDU + " (" +
328                   Mms._ID + " INTEGER PRIMARY KEY," +
329                   Mms.THREAD_ID + " INTEGER," +
330                   Mms.DATE + " INTEGER," +
331                   Mms.MESSAGE_BOX + " INTEGER," +
332                   Mms.READ + " INTEGER DEFAULT 0," +
333                   Mms.MESSAGE_ID + " TEXT," +
334                   Mms.SUBJECT + " TEXT," +
335                   Mms.SUBJECT_CHARSET + " INTEGER," +
336                   Mms.CONTENT_TYPE + " TEXT," +
337                   Mms.CONTENT_LOCATION + " TEXT," +
338                   Mms.EXPIRY + " INTEGER," +
339                   Mms.MESSAGE_CLASS + " TEXT," +
340                   Mms.MESSAGE_TYPE + " INTEGER," +
341                   Mms.MMS_VERSION + " INTEGER," +
342                   Mms.MESSAGE_SIZE + " INTEGER," +
343                   Mms.PRIORITY + " INTEGER," +
344                   Mms.READ_REPORT + " INTEGER," +
345                   Mms.REPORT_ALLOWED + " INTEGER," +
346                   Mms.RESPONSE_STATUS + " INTEGER," +
347                   Mms.STATUS + " INTEGER," +
348                   Mms.TRANSACTION_ID + " TEXT," +
349                   Mms.RETRIEVE_STATUS + " INTEGER," +
350                   Mms.RETRIEVE_TEXT + " TEXT," +
351                   Mms.RETRIEVE_TEXT_CHARSET + " INTEGER," +
352                   Mms.READ_STATUS + " INTEGER," +
353                   Mms.CONTENT_CLASS + " INTEGER," +
354                   Mms.RESPONSE_TEXT + " TEXT," +
355                   Mms.DELIVERY_TIME + " INTEGER," +
356                   Mms.DELIVERY_REPORT + " INTEGER," +
357                   Mms.LOCKED + " INTEGER DEFAULT 0" +
358                   ");");
359
360        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_ADDR + " (" +
361                   Addr._ID + " INTEGER PRIMARY KEY," +
362                   Addr.MSG_ID + " INTEGER," +
363                   Addr.CONTACT_ID + " INTEGER," +
364                   Addr.ADDRESS + " TEXT," +
365                   Addr.TYPE + " INTEGER," +
366                   Addr.CHARSET + " INTEGER);");
367
368        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_PART + " (" +
369                   Part._ID + " INTEGER PRIMARY KEY," +
370                   Part.MSG_ID + " INTEGER," +
371                   Part.SEQ + " INTEGER DEFAULT 0," +
372                   Part.CONTENT_TYPE + " TEXT," +
373                   Part.NAME + " TEXT," +
374                   Part.CHARSET + " INTEGER," +
375                   Part.CONTENT_DISPOSITION + " TEXT," +
376                   Part.FILENAME + " TEXT," +
377                   Part.CONTENT_ID + " TEXT," +
378                   Part.CONTENT_LOCATION + " TEXT," +
379                   Part.CT_START + " INTEGER," +
380                   Part.CT_TYPE + " TEXT," +
381                   Part._DATA + " TEXT," +
382                   Part.TEXT + " TEXT);");
383
384        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_RATE + " (" +
385                   Rate.SENT_TIME + " INTEGER);");
386
387        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_DRM + " (" +
388                   BaseColumns._ID + " INTEGER PRIMARY KEY," +
389                   "_data TEXT);");
390    }
391
392    private void createMmsTriggers(SQLiteDatabase db) {
393        // Cleans up parts when a MM is deleted.
394        db.execSQL("CREATE TRIGGER part_cleanup DELETE ON " + MmsProvider.TABLE_PDU + " " +
395                   "BEGIN " +
396                   "  DELETE FROM " + MmsProvider.TABLE_PART +
397                   "  WHERE " + Part.MSG_ID + "=old._id;" +
398                   "END;");
399
400        // Cleans up address info when a MM is deleted.
401        db.execSQL("CREATE TRIGGER addr_cleanup DELETE ON " + MmsProvider.TABLE_PDU + " " +
402                   "BEGIN " +
403                   "  DELETE FROM " + MmsProvider.TABLE_ADDR +
404                   "  WHERE " + Addr.MSG_ID + "=old._id;" +
405                   "END;");
406
407        // Delete obsolete delivery-report, read-report while deleting their
408        // associated Send.req.
409        db.execSQL("CREATE TRIGGER cleanup_delivery_and_read_report " +
410                   "AFTER DELETE ON " + MmsProvider.TABLE_PDU + " " +
411                   "WHEN old." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_SEND_REQ + " " +
412                   "BEGIN " +
413                   "  DELETE FROM " + MmsProvider.TABLE_PDU +
414                   "  WHERE (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_DELIVERY_IND +
415                   "    OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_READ_ORIG_IND + ")" +
416                   "    AND " + Mms.MESSAGE_ID + "=old." + Mms.MESSAGE_ID + "; " +
417                   "END;");
418
419        // Update threads table to indicate whether attachments exist when
420        // parts are inserted or deleted.
421        db.execSQL(PART_UPDATE_THREADS_ON_INSERT_TRIGGER);
422        db.execSQL(PART_UPDATE_THREADS_ON_UPDATE_TRIGGER);
423        db.execSQL(PART_UPDATE_THREADS_ON_DELETE_TRIGGER);
424    }
425
426    private void createSmsTables(SQLiteDatabase db) {
427        // N.B.: Whenever the columns here are changed, the columns in
428        // {@ref MmsSmsProvider} must be changed to match.
429        db.execSQL("CREATE TABLE sms (" +
430                   "_id INTEGER PRIMARY KEY," +
431                   "thread_id INTEGER," +
432                   "address TEXT," +
433                   "person INTEGER," +
434                   "date INTEGER," +
435                   "protocol INTEGER," +
436                   "read INTEGER DEFAULT 0," +
437                   "status INTEGER DEFAULT -1," + // a TP-Status value
438                                                  // or -1 if it
439                                                  // status hasn't
440                                                  // been received
441                   "type INTEGER," +
442                   "reply_path_present INTEGER," +
443                   "subject TEXT," +
444                   "body TEXT," +
445                   "service_center TEXT," +
446                   "locked INTEGER DEFAULT 0" +
447                   ");");
448
449        /**
450         * This table is used by the SMS dispatcher to hold
451         * incomplete partial messages until all the parts arrive.
452         */
453        db.execSQL("CREATE TABLE raw (" +
454                   "_id INTEGER PRIMARY KEY," +
455                   "date INTEGER," +
456                   "reference_number INTEGER," + // one per full message
457                   "count INTEGER," + // the number of parts
458                   "sequence INTEGER," + // the part number of this message
459                   "destination_port INTEGER," +
460                   "address TEXT," +
461                   "pdu TEXT);"); // the raw PDU for this part
462
463        db.execSQL("CREATE TABLE attachments (" +
464                   "sms_id INTEGER," +
465                   "content_url TEXT," +
466                   "offset INTEGER);");
467
468        /**
469         * This table is used by the SMS dispatcher to hold pending
470         * delivery status report intents.
471         */
472        db.execSQL("CREATE TABLE sr_pending (" +
473                   "reference_number INTEGER," +
474                   "action TEXT," +
475                   "data TEXT);");
476    }
477
478    private void createCommonTables(SQLiteDatabase db) {
479        // TODO Ensure that each entry is removed when the last use of
480        // any address equivalent to its address is removed.
481
482        /**
483         * This table maps the first instance seen of any particular
484         * MMS/SMS address to an ID, which is then used as its
485         * canonical representation.  If the same address or an
486         * equivalent address (as determined by our Sqlite
487         * PHONE_NUMBERS_EQUAL extension) is seen later, this same ID
488         * will be used.
489         */
490        db.execSQL("CREATE TABLE canonical_addresses (" +
491                   "_id INTEGER PRIMARY KEY," +
492                   "address TEXT);");
493
494        /**
495         * This table maps the subject and an ordered set of recipient
496         * IDs, separated by spaces, to a unique thread ID.  The IDs
497         * come from the canonical_addresses table.  This works
498         * because messages are considered to be part of the same
499         * thread if they have the same subject (or a null subject)
500         * and the same set of recipients.
501         */
502        db.execSQL("CREATE TABLE threads (" +
503                   Threads._ID + " INTEGER PRIMARY KEY," +
504                   Threads.DATE + " INTEGER DEFAULT 0," +
505                   Threads.MESSAGE_COUNT + " INTEGER DEFAULT 0," +
506                   Threads.RECIPIENT_IDS + " TEXT," +
507                   Threads.SNIPPET + " TEXT," +
508                   Threads.SNIPPET_CHARSET + " INTEGER DEFAULT 0," +
509                   Threads.READ + " INTEGER DEFAULT 1," +
510                   Threads.TYPE + " INTEGER DEFAULT 0," +
511                   Threads.ERROR + " INTEGER DEFAULT 0," +
512                   Threads.HAS_ATTACHMENT + " INTEGER DEFAULT 0);");
513
514        /**
515         * This table stores the queue of messages to be sent/downloaded.
516         */
517        db.execSQL("CREATE TABLE " + MmsSmsProvider.TABLE_PENDING_MSG +" (" +
518                   PendingMessages._ID + " INTEGER PRIMARY KEY," +
519                   PendingMessages.PROTO_TYPE + " INTEGER," +
520                   PendingMessages.MSG_ID + " INTEGER," +
521                   PendingMessages.MSG_TYPE + " INTEGER," +
522                   PendingMessages.ERROR_TYPE + " INTEGER," +
523                   PendingMessages.ERROR_CODE + " INTEGER," +
524                   PendingMessages.RETRY_INDEX + " INTEGER NOT NULL DEFAULT 0," +
525                   PendingMessages.DUE_TIME + " INTEGER," +
526                   PendingMessages.LAST_TRY + " INTEGER);");
527
528    }
529
530    // TODO Check the query plans for these triggers.
531    private void createCommonTriggers(SQLiteDatabase db) {
532        // Updates threads table whenever a message is added to pdu.
533        db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON " +
534                   MmsProvider.TABLE_PDU + " " +
535                   PDU_UPDATE_THREAD_CONSTRAINTS +
536                   PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
537
538        // Updates threads table whenever a message is added to sms.
539        db.execSQL("CREATE TRIGGER sms_update_thread_on_insert AFTER INSERT ON sms " +
540                   SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
541
542        // Updates threads table whenever a message in pdu is updated.
543        db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
544                   "  UPDATE OF " + Mms.DATE + ", " + Mms.SUBJECT + ", " + Mms.MESSAGE_BOX +
545                   "  ON " + MmsProvider.TABLE_PDU + " " +
546                   PDU_UPDATE_THREAD_CONSTRAINTS +
547                   PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
548
549        // Updates threads table whenever a message in sms is updated.
550        db.execSQL("CREATE TRIGGER sms_update_thread_date_subject_on_update AFTER" +
551                   "  UPDATE OF " + Sms.DATE + ", " + Sms.BODY + ", " + Sms.TYPE +
552                   "  ON sms " +
553                   SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
554
555        // Updates threads table whenever a message in pdu is updated.
556        db.execSQL("CREATE TRIGGER pdu_update_thread_read_on_update AFTER" +
557                   "  UPDATE OF " + Mms.READ +
558                   "  ON " + MmsProvider.TABLE_PDU + " " +
559                   PDU_UPDATE_THREAD_CONSTRAINTS +
560                   "BEGIN " +
561                   PDU_UPDATE_THREAD_READ_BODY +
562                   "END;");
563
564        // Updates threads table whenever a message in sms is updated.
565        db.execSQL("CREATE TRIGGER sms_update_thread_read_on_update AFTER" +
566                   "  UPDATE OF " + Sms.READ +
567                   "  ON sms " +
568                   "BEGIN " +
569                   SMS_UPDATE_THREAD_READ_BODY +
570                   "END;");
571
572        // Update threads table whenever a message in pdu is deleted
573        db.execSQL("CREATE TRIGGER pdu_update_thread_on_delete " +
574                   "AFTER DELETE ON pdu " +
575                   "BEGIN " +
576                   "  UPDATE threads SET " +
577                   "     date = (strftime('%s','now') * 1000)" +
578                   "  WHERE threads._id = old." + Mms.THREAD_ID + "; " +
579                   UPDATE_THREAD_COUNT_ON_OLD +
580                   UPDATE_THREAD_SNIPPET_SNIPPET_CS_ON_DELETE +
581                   "END;");
582
583        // When the last message in a thread is deleted, these
584        // triggers ensure that the entry for its thread ID is removed
585        // from the threads table.
586        db.execSQL("CREATE TRIGGER delete_obsolete_threads_pdu " +
587                   "AFTER DELETE ON pdu " +
588                   "BEGIN " +
589                   "  DELETE FROM threads " +
590                   "  WHERE " +
591                   "    _id = old.thread_id " +
592                   "    AND _id NOT IN " +
593                   "    (SELECT thread_id FROM sms " +
594                   "     UNION SELECT thread_id from pdu); " +
595                   "END;");
596
597        db.execSQL("CREATE TRIGGER delete_obsolete_threads_when_update_pdu " +
598                   "AFTER UPDATE OF " + Mms.THREAD_ID + " ON pdu " +
599                   "WHEN old." + Mms.THREAD_ID + " != new." + Mms.THREAD_ID + " " +
600                   "BEGIN " +
601                   "  DELETE FROM threads " +
602                   "  WHERE " +
603                   "    _id = old.thread_id " +
604                   "    AND _id NOT IN " +
605                   "    (SELECT thread_id FROM sms " +
606                   "     UNION SELECT thread_id from pdu); " +
607                   "END;");
608        // Insert pending status for M-Notification.ind or M-ReadRec.ind
609        // when they are inserted into Inbox/Outbox.
610        db.execSQL("CREATE TRIGGER insert_mms_pending_on_insert " +
611                   "AFTER INSERT ON pdu " +
612                   "WHEN new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND +
613                   "  OR new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_READ_REC_IND + " " +
614                   "BEGIN " +
615                   "  INSERT INTO " + MmsSmsProvider.TABLE_PENDING_MSG +
616                   "    (" + PendingMessages.PROTO_TYPE + "," +
617                   "     " + PendingMessages.MSG_ID + "," +
618                   "     " + PendingMessages.MSG_TYPE + "," +
619                   "     " + PendingMessages.ERROR_TYPE + "," +
620                   "     " + PendingMessages.ERROR_CODE + "," +
621                   "     " + PendingMessages.RETRY_INDEX + "," +
622                   "     " + PendingMessages.DUE_TIME + ") " +
623                   "  VALUES " +
624                   "    (" + MmsSms.MMS_PROTO + "," +
625                   "      new." + BaseColumns._ID + "," +
626                   "      new." + Mms.MESSAGE_TYPE + ",0,0,0,0);" +
627                   "END;");
628
629        // Insert pending status for M-Send.req when it is moved into Outbox.
630        db.execSQL("CREATE TRIGGER insert_mms_pending_on_update " +
631                   "AFTER UPDATE ON pdu " +
632                   "WHEN new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_SEND_REQ +
633                   "  AND new." + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_OUTBOX +
634                   "  AND old." + Mms.MESSAGE_BOX + "!=" + Mms.MESSAGE_BOX_OUTBOX + " " +
635                   "BEGIN " +
636                   "  INSERT INTO " + MmsSmsProvider.TABLE_PENDING_MSG +
637                   "    (" + PendingMessages.PROTO_TYPE + "," +
638                   "     " + PendingMessages.MSG_ID + "," +
639                   "     " + PendingMessages.MSG_TYPE + "," +
640                   "     " + PendingMessages.ERROR_TYPE + "," +
641                   "     " + PendingMessages.ERROR_CODE + "," +
642                   "     " + PendingMessages.RETRY_INDEX + "," +
643                   "     " + PendingMessages.DUE_TIME + ") " +
644                   "  VALUES " +
645                   "    (" + MmsSms.MMS_PROTO + "," +
646                   "      new." + BaseColumns._ID + "," +
647                   "      new." + Mms.MESSAGE_TYPE + ",0,0,0,0);" +
648                   "END;");
649
650        // When a message is moved out of Outbox, delete its pending status.
651        db.execSQL("CREATE TRIGGER delete_mms_pending_on_update " +
652                   "AFTER UPDATE ON " + MmsProvider.TABLE_PDU + " " +
653                   "WHEN old." + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_OUTBOX +
654                   "  AND new." + Mms.MESSAGE_BOX + "!=" + Mms.MESSAGE_BOX_OUTBOX + " " +
655                   "BEGIN " +
656                   "  DELETE FROM " + MmsSmsProvider.TABLE_PENDING_MSG +
657                   "  WHERE " + PendingMessages.MSG_ID + "=new._id; " +
658                   "END;");
659
660        // Delete pending status for a message when it is deleted.
661        db.execSQL("CREATE TRIGGER delete_mms_pending_on_delete " +
662                   "AFTER DELETE ON " + MmsProvider.TABLE_PDU + " " +
663                   "BEGIN " +
664                   "  DELETE FROM " + MmsSmsProvider.TABLE_PENDING_MSG +
665                   "  WHERE " + PendingMessages.MSG_ID + "=old._id; " +
666                   "END;");
667
668        // TODO Add triggers for SMS retry-status management.
669
670        // Update the error flag of threads when the error type of
671        // a pending MM is updated.
672        db.execSQL("CREATE TRIGGER update_threads_error_on_update_mms " +
673                   "  AFTER UPDATE OF err_type ON pending_msgs " +
674                   "  WHEN (OLD.err_type < 10 AND NEW.err_type >= 10)" +
675                   "    OR (OLD.err_type >= 10 AND NEW.err_type < 10) " +
676                   "BEGIN" +
677                   "  UPDATE threads SET error = " +
678                   "    CASE" +
679                   "      WHEN NEW.err_type >= 10 THEN error + 1" +
680                   "      ELSE error - 1" +
681                   "    END " +
682                   "  WHERE _id =" +
683                   "   (SELECT DISTINCT thread_id" +
684                   "    FROM pdu" +
685                   "    WHERE _id = NEW.msg_id); " +
686                   "END;");
687
688        // Update the error flag of threads when delete pending message.
689        db.execSQL("CREATE TRIGGER update_threads_error_on_delete_mms " +
690                   "  BEFORE DELETE ON pdu" +
691                   "  WHEN OLD._id IN (SELECT DISTINCT msg_id" +
692                   "                   FROM pending_msgs" +
693                   "                   WHERE err_type >= 10) " +
694                   "BEGIN " +
695                   "  UPDATE threads SET error = error - 1" +
696                   "  WHERE _id = OLD.thread_id; " +
697                   "END;");
698
699        // Update the error flag of threads while moving an MM out of Outbox,
700        // which was failed to be sent permanently.
701        db.execSQL("CREATE TRIGGER update_threads_error_on_move_mms " +
702                   "  BEFORE UPDATE OF msg_box ON pdu " +
703                   "  WHEN (OLD.msg_box = 4 AND NEW.msg_box != 4) " +
704                   "  AND (OLD._id IN (SELECT DISTINCT msg_id" +
705                   "                   FROM pending_msgs" +
706                   "                   WHERE err_type >= 10)) " +
707                   "BEGIN " +
708                   "  UPDATE threads SET error = error - 1" +
709                   "  WHERE _id = OLD.thread_id; " +
710                   "END;");
711
712        // Update the error flag of threads after a text message was
713        // failed to send/receive.
714        db.execSQL("CREATE TRIGGER update_threads_error_on_update_sms " +
715                   "  AFTER UPDATE OF type ON sms" +
716                   "  WHEN (OLD.type != 5 AND NEW.type = 5)" +
717                   "    OR (OLD.type = 5 AND NEW.type != 5) " +
718                   "BEGIN " +
719                   "  UPDATE threads SET error = " +
720                   "    CASE" +
721                   "      WHEN NEW.type = 5 THEN error + 1" +
722                   "      ELSE error - 1" +
723                   "    END " +
724                   "  WHERE _id = NEW.thread_id; " +
725                   "END;");
726    }
727
728    @Override
729    public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
730        Log.w(TAG, "Upgrading database from version " + oldVersion
731                + " to " + currentVersion + ".");
732
733        switch (oldVersion) {
734        case 40:
735            if (currentVersion <= 40) {
736                return;
737            }
738
739            db.beginTransaction();
740            try {
741                upgradeDatabaseToVersion41(db);
742                db.setTransactionSuccessful();
743            } catch (Throwable ex) {
744                Log.e(TAG, ex.getMessage(), ex);
745                break;
746            } finally {
747                db.endTransaction();
748            }
749            // fall through
750        case 41:
751            if (currentVersion <= 41) {
752                return;
753            }
754
755            db.beginTransaction();
756            try {
757                upgradeDatabaseToVersion42(db);
758                db.setTransactionSuccessful();
759            } catch (Throwable ex) {
760                Log.e(TAG, ex.getMessage(), ex);
761                break;
762            } finally {
763                db.endTransaction();
764            }
765            // fall through
766        case 42:
767            if (currentVersion <= 42) {
768                return;
769            }
770
771            db.beginTransaction();
772            try {
773                upgradeDatabaseToVersion43(db);
774                db.setTransactionSuccessful();
775            } catch (Throwable ex) {
776                Log.e(TAG, ex.getMessage(), ex);
777                break;
778            } finally {
779                db.endTransaction();
780            }
781            // fall through
782        case 43:
783            if (currentVersion <= 43) {
784                return;
785            }
786
787            db.beginTransaction();
788            try {
789                upgradeDatabaseToVersion44(db);
790                db.setTransactionSuccessful();
791            } catch (Throwable ex) {
792                Log.e(TAG, ex.getMessage(), ex);
793                break;
794            } finally {
795                db.endTransaction();
796            }
797            // fall through
798        case 44:
799            if (currentVersion <= 44) {
800                return;
801            }
802
803            db.beginTransaction();
804            try {
805                upgradeDatabaseToVersion45(db);
806                db.setTransactionSuccessful();
807            } catch (Throwable ex) {
808                Log.e(TAG, ex.getMessage(), ex);
809                break;
810            } finally {
811                db.endTransaction();
812            }
813            // fall through
814        case 45:
815            if (currentVersion <= 45) {
816                return;
817            }
818            db.beginTransaction();
819            try {
820                upgradeDatabaseToVersion46(db);
821                db.setTransactionSuccessful();
822            } catch (Throwable ex) {
823                Log.e(TAG, ex.getMessage(), ex);
824                break;
825            } finally {
826                db.endTransaction();
827            }
828            return;
829        }
830
831
832        Log.e(TAG, "Destroying all old data.");
833        dropAll(db);
834        onCreate(db);
835    }
836
837    private void dropAll(SQLiteDatabase db) {
838        // Clean the database out in order to start over from scratch.
839        // We don't need to drop our triggers here because SQLite automatically
840        // drops a trigger when its attached database is dropped.
841        db.execSQL("DROP TABLE IF EXISTS canonical_addresses");
842        db.execSQL("DROP TABLE IF EXISTS threads");
843        db.execSQL("DROP TABLE IF EXISTS " + MmsSmsProvider.TABLE_PENDING_MSG);
844        db.execSQL("DROP TABLE IF EXISTS sms");
845        db.execSQL("DROP TABLE IF EXISTS raw");
846        db.execSQL("DROP TABLE IF EXISTS attachments");
847        db.execSQL("DROP TABLE IF EXISTS thread_ids");
848        db.execSQL("DROP TABLE IF EXISTS sr_pending");
849        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_PDU + ";");
850        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_ADDR + ";");
851        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_PART + ";");
852        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_RATE + ";");
853        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_DRM + ";");
854    }
855
856    private void upgradeDatabaseToVersion41(SQLiteDatabase db) {
857        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_move_mms");
858        db.execSQL("CREATE TRIGGER update_threads_error_on_move_mms " +
859                   "  BEFORE UPDATE OF msg_box ON pdu " +
860                   "  WHEN (OLD.msg_box = 4 AND NEW.msg_box != 4) " +
861                   "  AND (OLD._id IN (SELECT DISTINCT msg_id" +
862                   "                   FROM pending_msgs" +
863                   "                   WHERE err_type >= 10)) " +
864                   "BEGIN " +
865                   "  UPDATE threads SET error = error - 1" +
866                   "  WHERE _id = OLD.thread_id; " +
867                   "END;");
868    }
869
870    private void upgradeDatabaseToVersion42(SQLiteDatabase db) {
871        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_delete");
872        db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_sms");
873        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_delete_sms");
874    }
875
876    private void upgradeDatabaseToVersion43(SQLiteDatabase db) {
877        // Add 'has_attachment' column to threads table.
878        db.execSQL("ALTER TABLE threads ADD COLUMN has_attachment INTEGER DEFAULT 0");
879
880        // Set the values of that column correctly based on the current
881        // contents of the database.
882        db.execSQL("UPDATE threads SET has_attachment=1 WHERE _id IN " +
883                   "  (SELECT DISTINCT pdu.thread_id FROM part " +
884                   "   JOIN pdu ON pdu._id=part.mid " +
885                   "   WHERE part.ct != 'text/plain' AND part.ct != 'application/smil')");
886
887        // Add insert and delete triggers for keeping it up to date.
888        db.execSQL(PART_UPDATE_THREADS_ON_INSERT_TRIGGER);
889        db.execSQL(PART_UPDATE_THREADS_ON_DELETE_TRIGGER);
890    }
891
892    private void upgradeDatabaseToVersion44(SQLiteDatabase db) {
893        // Set the values of that column correctly based on the current
894        // contents of the database.
895        db.execSQL("UPDATE threads SET has_attachment=1 WHERE _id IN " +
896                   "  (SELECT DISTINCT pdu.thread_id FROM part " +
897                   "   JOIN pdu ON pdu._id=part.mid " +
898                   "   WHERE part.ct != 'text/plain' AND part.ct != 'application/smil')");
899
900        // add the update trigger for keeping the threads up to date.
901        db.execSQL(PART_UPDATE_THREADS_ON_UPDATE_TRIGGER);
902    }
903
904    private void upgradeDatabaseToVersion45(SQLiteDatabase db) {
905        // Add 'locked' column to sms table.
906        db.execSQL("ALTER TABLE sms ADD COLUMN locked INTEGER DEFAULT 0");
907
908        // Add 'locked' column to pdu table.
909        db.execSQL("ALTER TABLE pdu ADD COLUMN " + Mms.LOCKED + " INTEGER DEFAULT 0");
910    }
911
912    private void upgradeDatabaseToVersion46(SQLiteDatabase db) {
913        // add the "text" column for caching inline text (e.g. strings) instead of
914        // putting them in an external file
915        db.execSQL("ALTER TABLE part ADD COLUMN " + Part.TEXT + " TEXT");
916
917        Cursor textRows = db.query(
918                "part",
919                new String[] { Part._ID, Part._DATA, Part.TEXT},
920                "ct = 'text/plain' OR ct == 'application/smil'",
921                null,
922                null,
923                null,
924                null);
925        ArrayList<String> filesToDelete = new ArrayList<String>();
926        try {
927            if (textRows != null) {
928                int partIdColumn = textRows.getColumnIndex(Part._ID);
929                int partDataColumn = textRows.getColumnIndex(Part._DATA);
930                int partTextColumn = textRows.getColumnIndex(Part.TEXT);
931
932                // This code is imperfect in that we can't guarantee that all the
933                // backing files get deleted.  For example if the system aborts after
934                // the database is updated but before we complete the process of
935                // deleting files.
936                while (textRows.moveToNext()) {
937                    String path = textRows.getString(partDataColumn);
938                    if (path != null) {
939                        try {
940                            InputStream is = new FileInputStream(path);
941                            byte [] data = new byte[is.available()];
942                            is.read(data);
943                            EncodedStringValue v = new EncodedStringValue(data);
944                            textRows.updateString(partTextColumn, v.getString());
945                            textRows.updateToNull(partDataColumn);
946                            is.close();
947                            filesToDelete.add(path);
948                        } catch (IOException e) {
949                            // TODO Auto-generated catch block
950                            e.printStackTrace();
951                        }
952                    }
953                }
954            }
955        } finally {
956            textRows.commitUpdates();
957            for (String pathToDelete : filesToDelete) {
958                try {
959                    (new File(pathToDelete)).delete();
960                } catch (SecurityException ex) {
961                    Log.e(TAG, "unable to clean up old mms file for " + pathToDelete, ex);
962                }
963            }
964            if (textRows != null) {
965                textRows.close();
966            }
967        }
968    }
969}
970