MmsSmsDatabaseHelper.java revision f88d1d6733158144e9e0c87f29b446068edf0507
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 java.io.IOException;
20import java.io.InputStream;
21import java.io.FileInputStream;
22import java.io.File;
23import java.util.ArrayList;
24import java.util.HashSet;
25import java.util.Iterator;
26
27import android.content.BroadcastReceiver;
28import android.content.ContentValues;
29import android.content.Context;
30import android.content.Intent;
31import android.content.IntentFilter;
32import android.database.Cursor;
33import android.database.sqlite.SQLiteDatabase;
34import android.database.sqlite.SQLiteOpenHelper;
35import android.provider.BaseColumns;
36import android.provider.Telephony;
37import android.provider.Telephony.Mms;
38import android.provider.Telephony.MmsSms;
39import android.provider.Telephony.Sms;
40import android.provider.Telephony.Threads;
41import android.provider.Telephony.Mms.Addr;
42import android.provider.Telephony.Mms.Part;
43import android.provider.Telephony.Mms.Rate;
44import android.provider.Telephony.MmsSms.PendingMessages;
45import android.util.Log;
46
47import com.google.android.mms.pdu.EncodedStringValue;
48import com.google.android.mms.pdu.PduHeaders;
49
50public class MmsSmsDatabaseHelper extends SQLiteOpenHelper {
51    private static final String TAG = "MmsSmsDatabaseHelper";
52
53    private static final String SMS_UPDATE_THREAD_READ_BODY =
54                        "  UPDATE threads SET read = " +
55                        "    CASE (SELECT COUNT(*)" +
56                        "          FROM sms" +
57                        "          WHERE " + Sms.READ + " = 0" +
58                        "            AND " + Sms.THREAD_ID + " = threads._id)" +
59                        "      WHEN 0 THEN 1" +
60                        "      ELSE 0" +
61                        "    END" +
62                        "  WHERE threads._id = new." + Sms.THREAD_ID + "; ";
63
64    private static final String UPDATE_THREAD_COUNT_ON_NEW =
65                        "  UPDATE threads SET message_count = " +
66                        "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
67                        "      ON threads._id = " + Sms.THREAD_ID +
68                        "      WHERE " + Sms.THREAD_ID + " = new.thread_id" +
69                        "        AND sms." + Sms.TYPE + " != 3) + " +
70                        "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
71                        "      ON threads._id = " + Mms.THREAD_ID +
72                        "      WHERE " + Mms.THREAD_ID + " = new.thread_id" +
73                        "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
74                        "        AND " + Mms.MESSAGE_BOX + " != 3) " +
75                        "  WHERE threads._id = new.thread_id; ";
76
77    private static final String UPDATE_THREAD_COUNT_ON_OLD =
78                        "  UPDATE threads SET message_count = " +
79                        "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
80                        "      ON threads._id = " + Sms.THREAD_ID +
81                        "      WHERE " + Sms.THREAD_ID + " = old.thread_id" +
82                        "        AND sms." + Sms.TYPE + " != 3) + " +
83                        "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
84                        "      ON threads._id = " + Mms.THREAD_ID +
85                        "      WHERE " + Mms.THREAD_ID + " = old.thread_id" +
86                        "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
87                        "        AND " + Mms.MESSAGE_BOX + " != 3) " +
88                        "  WHERE threads._id = old.thread_id; ";
89
90    private static final String SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE =
91                        "BEGIN" +
92                        "  UPDATE threads SET" +
93                        "    date = (strftime('%s','now') * 1000), " +
94                        "    snippet = new." + Sms.BODY + ", " +
95                        "    snippet_cs = 0" +
96                        "  WHERE threads._id = new." + Sms.THREAD_ID + "; " +
97                        UPDATE_THREAD_COUNT_ON_NEW +
98                        SMS_UPDATE_THREAD_READ_BODY +
99                        "END;";
100
101    private static final String PDU_UPDATE_THREAD_CONSTRAINTS =
102                        "  WHEN new." + Mms.MESSAGE_TYPE + "=" +
103                        PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF +
104                        "    OR new." + Mms.MESSAGE_TYPE + "=" +
105                        PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND +
106                        "    OR new." + Mms.MESSAGE_TYPE + "=" +
107                        PduHeaders.MESSAGE_TYPE_SEND_REQ + " ";
108
109    // When looking in the pdu table for unread messages, only count messages that
110    // are displayed to the user. The constants are defined in PduHeaders and could be used
111    // here, but the string "(m_type=132 OR m_type=130 OR m_type=128)" is used throughout this
112    // file and so it is used here to be consistent.
113    //     m_type=128   = MESSAGE_TYPE_SEND_REQ
114    //     m_type=130   = MESSAGE_TYPE_NOTIFICATION_IND
115    //     m_type=132   = MESSAGE_TYPE_RETRIEVE_CONF
116    private static final String PDU_UPDATE_THREAD_READ_BODY =
117                        "  UPDATE threads SET read = " +
118                        "    CASE (SELECT COUNT(*)" +
119                        "          FROM " + MmsProvider.TABLE_PDU +
120                        "          WHERE " + Mms.READ + " = 0" +
121                        "            AND " + Mms.THREAD_ID + " = threads._id " +
122                        "            AND (m_type=132 OR m_type=130 OR m_type=128)) " +
123                        "      WHEN 0 THEN 1" +
124                        "      ELSE 0" +
125                        "    END" +
126                        "  WHERE threads._id = new." + Mms.THREAD_ID + "; ";
127
128    private static final String PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE =
129                        "BEGIN" +
130                        "  UPDATE threads SET" +
131                        "    date = (strftime('%s','now') * 1000), " +
132                        "    snippet = new." + Mms.SUBJECT + ", " +
133                        "    snippet_cs = new." + Mms.SUBJECT_CHARSET +
134                        "  WHERE threads._id = new." + Mms.THREAD_ID + "; " +
135                        UPDATE_THREAD_COUNT_ON_NEW +
136                        PDU_UPDATE_THREAD_READ_BODY +
137                        "END;";
138
139    private static final String UPDATE_THREAD_SNIPPET_SNIPPET_CS_ON_DELETE =
140                        "  UPDATE threads SET snippet = " +
141                        "   (SELECT snippet FROM" +
142                        "     (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
143                        "      UNION SELECT date, body AS snippet, thread_id FROM sms)" +
144                        "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
145                        "  WHERE threads._id = OLD.thread_id; " +
146                        "  UPDATE threads SET snippet_cs = " +
147                        "   (SELECT snippet_cs FROM" +
148                        "     (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
149                        "      UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
150                        "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
151                        "  WHERE threads._id = OLD.thread_id; ";
152
153
154    // When a part is inserted, if it is not text/plain or application/smil
155    // (which both can exist with text-only MMSes), then there is an attachment.
156    // Set has_attachment=1 in the threads table for the thread in question.
157    private static final String PART_UPDATE_THREADS_ON_INSERT_TRIGGER =
158                        "CREATE TRIGGER update_threads_on_insert_part " +
159                        " AFTER INSERT 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    // When the 'mid' column in the part table is updated, we need to run the trigger to update
168    // the threads table's has_attachment column, if the part is an attachment.
169    private static final String PART_UPDATE_THREADS_ON_UPDATE_TRIGGER =
170                        "CREATE TRIGGER update_threads_on_update_part " +
171                        " AFTER UPDATE of " + Part.MSG_ID + " ON part " +
172                        " WHEN new.ct != 'text/plain' AND new.ct != 'application/smil' " +
173                        " BEGIN " +
174                        "  UPDATE threads SET has_attachment=1 WHERE _id IN " +
175                        "   (SELECT pdu.thread_id FROM part JOIN pdu ON pdu._id=part.mid " +
176                        "     WHERE part._id=new._id LIMIT 1); " +
177                        " END";
178
179
180    // When a part is deleted (with the same non-text/SMIL constraint as when
181    // we set has_attachment), update the threads table for all threads.
182    // Unfortunately we cannot update only the thread that the part was
183    // attached to, as it is possible that the part has been orphaned and
184    // the message it was attached to is already gone.
185    private static final String PART_UPDATE_THREADS_ON_DELETE_TRIGGER =
186                        "CREATE TRIGGER update_threads_on_delete_part " +
187                        " AFTER DELETE ON part " +
188                        " WHEN old.ct != 'text/plain' AND old.ct != 'application/smil' " +
189                        " BEGIN " +
190                        "  UPDATE threads SET has_attachment = " +
191                        "   CASE " +
192                        "    (SELECT COUNT(*) FROM part JOIN pdu " +
193                        "     WHERE pdu.thread_id = threads._id " +
194                        "     AND part.ct != 'text/plain' AND part.ct != 'application/smil' " +
195                        "     AND part.mid = pdu._id)" +
196                        "   WHEN 0 THEN 0 " +
197                        "   ELSE 1 " +
198                        "   END; " +
199                        " END";
200
201    // When the 'thread_id' column in the pdu table is updated, we need to run the trigger to update
202    // the threads table's has_attachment column, if the message has an attachment in 'part' table
203    private static final String PDU_UPDATE_THREADS_ON_UPDATE_TRIGGER =
204                        "CREATE TRIGGER update_threads_on_update_pdu " +
205                        " AFTER UPDATE of thread_id ON pdu " +
206                        " BEGIN " +
207                        "  UPDATE threads SET has_attachment=1 WHERE _id IN " +
208                        "   (SELECT pdu.thread_id FROM part JOIN pdu " +
209                        "     WHERE part.ct != 'text/plain' AND part.ct != 'application/smil' " +
210                        "     AND part.mid = pdu._id);" +
211                        " END";
212
213    private static MmsSmsDatabaseHelper sInstance = null;
214    private static boolean sTriedAutoIncrement = false;
215    private static boolean sFakeLowStorageTest = false;     // for testing only
216
217    static final String DATABASE_NAME = "mmssms.db";
218    static final int DATABASE_VERSION = 56;
219    private final Context mContext;
220    private LowStorageMonitor mLowStorageMonitor;
221
222
223    private MmsSmsDatabaseHelper(Context context) {
224        super(context, DATABASE_NAME, null, DATABASE_VERSION);
225
226        mContext = context;
227    }
228
229    /**
230     * Return a singleton helper for the combined MMS and SMS
231     * database.
232     */
233    /* package */ static synchronized MmsSmsDatabaseHelper getInstance(Context context) {
234        if (sInstance == null) {
235            sInstance = new MmsSmsDatabaseHelper(context);
236        }
237        return sInstance;
238    }
239
240    /**
241     * Look through all the recipientIds referenced by the threads and then delete any
242     * unreferenced rows from the canonical_addresses table.
243     */
244    private static void removeUnferencedCanonicalAddresses(SQLiteDatabase db) {
245        Cursor c = db.query("threads", new String[] { "recipient_ids" },
246                null, null, null, null, null);
247        if (c != null) {
248            try {
249                if (c.getCount() == 0) {
250                    // no threads, delete all addresses
251                    int rows = db.delete("canonical_addresses", null, null);
252                } else {
253                    // Find all the referenced recipient_ids from the threads. recipientIds is
254                    // a space-separated list of recipient ids: "1 14 21"
255                    HashSet<Integer> recipientIds = new HashSet<Integer>();
256                    while (c.moveToNext()) {
257                        String[] recips = c.getString(0).split(" ");
258                        for (String recip : recips) {
259                            try {
260                                int recipientId = Integer.parseInt(recip);
261                                recipientIds.add(recipientId);
262                            } catch (Exception e) {
263                            }
264                        }
265                    }
266                    // Now build a selection string of all the unique recipient ids
267                    StringBuilder sb = new StringBuilder();
268                    Iterator<Integer> iter = recipientIds.iterator();
269                    while (iter.hasNext()) {
270                        sb.append("_id != " + iter.next());
271                        if (iter.hasNext()) {
272                            sb.append(" AND ");
273                        }
274                    }
275                    if (sb.length() > 0) {
276                        int rows = db.delete("canonical_addresses", sb.toString(), null);
277                    }
278                }
279            } finally {
280                c.close();
281            }
282        }
283    }
284
285    public static void updateThread(SQLiteDatabase db, long thread_id) {
286        if (thread_id < 0) {
287            updateAllThreads(db, null, null);
288            return;
289        }
290
291        // Delete the row for this thread in the threads table if
292        // there are no more messages attached to it in either
293        // the sms or pdu tables.
294        int rows = db.delete("threads",
295                  "_id = ? AND _id NOT IN" +
296                  "          (SELECT thread_id FROM sms " +
297                  "           UNION SELECT thread_id FROM pdu)",
298                  new String[] { String.valueOf(thread_id) });
299        if (rows > 0) {
300            // If this deleted a row, let's remove orphaned canonical_addresses and get outta here
301            removeUnferencedCanonicalAddresses(db);
302            return;
303        }
304        // Update the message count in the threads table as the sum
305        // of all messages in both the sms and pdu tables.
306        db.execSQL(
307            "  UPDATE threads SET message_count = " +
308            "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
309            "      ON threads._id = " + Sms.THREAD_ID +
310            "      WHERE " + Sms.THREAD_ID + " = " + thread_id +
311            "        AND sms." + Sms.TYPE + " != 3) + " +
312            "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
313            "      ON threads._id = " + Mms.THREAD_ID +
314            "      WHERE " + Mms.THREAD_ID + " = " + thread_id +
315            "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
316            "        AND " + Mms.MESSAGE_BOX + " != 3) " +
317            "  WHERE threads._id = " + thread_id + ";");
318
319        // Update the date and the snippet (and its character set) in
320        // the threads table to be that of the most recent message in
321        // the thread.
322        db.execSQL(
323            "  UPDATE threads" +
324            "  SET" +
325            "  date =" +
326            "    (SELECT date FROM" +
327            "        (SELECT date * 1000 AS date, thread_id FROM pdu" +
328            "         UNION SELECT date, thread_id FROM sms)" +
329            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)," +
330            "  snippet =" +
331            "    (SELECT snippet FROM" +
332            "        (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
333            "         UNION SELECT date, body AS snippet, thread_id FROM sms)" +
334            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)," +
335            "  snippet_cs =" +
336            "    (SELECT snippet_cs FROM" +
337            "        (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
338            "         UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
339            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)" +
340            "  WHERE threads._id = " + thread_id + ";");
341
342        // Update the error column of the thread to indicate if there
343        // are any messages in it that have failed to send.
344        // First check to see if there are any messages with errors in this thread.
345        String query = "SELECT thread_id FROM sms WHERE type=" +
346            Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED +
347            " AND thread_id = " + thread_id +
348                                " LIMIT 1";
349        int setError = 0;
350        Cursor c = db.rawQuery(query, null);
351        if (c != null) {
352            try {
353                setError = c.getCount();    // Because of the LIMIT 1, count will be 1 or 0.
354            } finally {
355                c.close();
356            }
357        }
358        // What's the current state of the error flag in the threads table?
359        String errorQuery = "SELECT error FROM threads WHERE _id = " + thread_id;
360        c = db.rawQuery(errorQuery, null);
361        if (c != null) {
362            try {
363                if (c.moveToNext()) {
364                    int curError = c.getInt(0);
365                    if (curError != setError) {
366                        // The current thread error column differs, update it.
367                        db.execSQL("UPDATE threads SET error=" + setError +
368                                " WHERE _id = " + thread_id);
369                    }
370                }
371            } finally {
372                c.close();
373            }
374        }
375    }
376
377    public static void updateAllThreads(SQLiteDatabase db, String where, String[] whereArgs) {
378        if (where == null) {
379            where = "";
380        } else {
381            where = "WHERE (" + where + ")";
382        }
383        String query = "SELECT _id FROM threads WHERE _id IN " +
384                       "(SELECT DISTINCT thread_id FROM sms " + where + ")";
385        Cursor c = db.rawQuery(query, whereArgs);
386        if (c != null) {
387            try {
388                while (c.moveToNext()) {
389                    updateThread(db, c.getInt(0));
390                }
391            } finally {
392                c.close();
393            }
394        }
395        // TODO: there are several db operations in this function. Lets wrap them in a
396        // transaction to make it faster.
397        // remove orphaned threads
398        db.delete("threads",
399                "_id NOT IN (SELECT DISTINCT thread_id FROM sms where thread_id NOT NULL " +
400                "UNION SELECT DISTINCT thread_id FROM pdu where thread_id NOT NULL)", null);
401
402        // remove orphaned canonical_addresses
403        removeUnferencedCanonicalAddresses(db);
404    }
405
406    public static int deleteOneSms(SQLiteDatabase db, int message_id) {
407        int thread_id = -1;
408        // Find the thread ID that the specified SMS belongs to.
409        Cursor c = db.query("sms", new String[] { "thread_id" },
410                            "_id=" + message_id, null, null, null, null);
411        if (c != null) {
412            if (c.moveToFirst()) {
413                thread_id = c.getInt(0);
414            }
415            c.close();
416        }
417
418        // Delete the specified message.
419        int rows = db.delete("sms", "_id=" + message_id, null);
420        if (thread_id > 0) {
421            // Update its thread.
422            updateThread(db, thread_id);
423        }
424        return rows;
425    }
426
427    @Override
428    public void onCreate(SQLiteDatabase db) {
429        createMmsTables(db);
430        createSmsTables(db);
431        createCommonTables(db);
432        createCommonTriggers(db);
433        createMmsTriggers(db);
434        createWordsTables(db);
435        createIndices(db);
436    }
437
438    // When upgrading the database we need to populate the words
439    // table with the rows out of sms and part.
440    private void populateWordsTable(SQLiteDatabase db) {
441        final String TABLE_WORDS = "words";
442        {
443            Cursor smsRows = db.query(
444                    "sms",
445                    new String[] { Sms._ID, Sms.BODY },
446                    null,
447                    null,
448                    null,
449                    null,
450                    null);
451            try {
452                if (smsRows != null) {
453                    smsRows.moveToPosition(-1);
454                    ContentValues cv = new ContentValues();
455                    while (smsRows.moveToNext()) {
456                        cv.clear();
457
458                        long id = smsRows.getLong(0);        // 0 for Sms._ID
459                        String body = smsRows.getString(1);  // 1 for Sms.BODY
460
461                        cv.put(Telephony.MmsSms.WordsTable.ID, id);
462                        cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, body);
463                        cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, id);
464                        cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 1);
465                        db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv);
466                    }
467                }
468            } finally {
469                if (smsRows != null) {
470                    smsRows.close();
471                }
472            }
473        }
474
475        {
476            Cursor mmsRows = db.query(
477                    "part",
478                    new String[] { Part._ID, Part.TEXT },
479                    "ct = 'text/plain'",
480                    null,
481                    null,
482                    null,
483                    null);
484            try {
485                if (mmsRows != null) {
486                    mmsRows.moveToPosition(-1);
487                    ContentValues cv = new ContentValues();
488                    while (mmsRows.moveToNext()) {
489                        cv.clear();
490
491                        long id = mmsRows.getLong(0);         // 0 for Part._ID
492                        String body = mmsRows.getString(1);   // 1 for Part.TEXT
493
494                        cv.put(Telephony.MmsSms.WordsTable.ID, id);
495                        cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, body);
496                        cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, id);
497                        cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 1);
498                        db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv);
499                    }
500                }
501            } finally {
502                if (mmsRows != null) {
503                    mmsRows.close();
504                }
505            }
506        }
507    }
508
509    private void createWordsTables(SQLiteDatabase db) {
510        try {
511            db.execSQL("CREATE VIRTUAL TABLE words USING FTS3 (_id INTEGER PRIMARY KEY, index_text TEXT, source_id INTEGER, table_to_use INTEGER);");
512
513            // monitor the sms table
514            // NOTE don't handle inserts using a trigger because it has an unwanted
515            // side effect:  the value returned for the last row ends up being the
516            // id of one of the trigger insert not the original row insert.
517            // Handle inserts manually in the provider.
518            db.execSQL("CREATE TRIGGER sms_words_update AFTER UPDATE ON sms BEGIN UPDATE words " +
519                    " SET index_text = NEW.body WHERE (source_id=NEW._id AND table_to_use=1); " +
520                    " END;");
521            db.execSQL("CREATE TRIGGER sms_words_delete AFTER DELETE ON sms BEGIN DELETE FROM " +
522                    "  words WHERE source_id = OLD._id AND table_to_use = 1; END;");
523
524            // monitor the mms table
525            db.execSQL("CREATE TRIGGER mms_words_update AFTER UPDATE ON part BEGIN UPDATE words " +
526                    " SET index_text = NEW.text WHERE (source_id=NEW._id AND table_to_use=2); " +
527                    " END;");
528            db.execSQL("CREATE TRIGGER mms_words_delete AFTER DELETE ON part BEGIN DELETE FROM " +
529                    " words WHERE source_id = OLD._id AND table_to_use = 2; END;");
530
531            populateWordsTable(db);
532        } catch (Exception ex) {
533            Log.e(TAG, "got exception creating words table: " + ex.toString());
534        }
535    }
536
537    private void createIndices(SQLiteDatabase db) {
538        createThreadIdIndex(db);
539    }
540
541    private void createThreadIdIndex(SQLiteDatabase db) {
542        try {
543            db.execSQL("CREATE INDEX IF NOT EXISTS typeThreadIdIndex ON sms" +
544            " (type, thread_id);");
545        } catch (Exception ex) {
546            Log.e(TAG, "got exception creating indices: " + ex.toString());
547        }
548    }
549
550    private void createMmsTables(SQLiteDatabase db) {
551        // N.B.: Whenever the columns here are changed, the columns in
552        // {@ref MmsSmsProvider} must be changed to match.
553        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_PDU + " (" +
554                   Mms._ID + " INTEGER PRIMARY KEY," +
555                   Mms.THREAD_ID + " INTEGER," +
556                   Mms.DATE + " INTEGER," +
557                   Mms.DATE_SENT + " INTEGER DEFAULT 0," +
558                   Mms.MESSAGE_BOX + " INTEGER," +
559                   Mms.READ + " INTEGER DEFAULT 0," +
560                   Mms.MESSAGE_ID + " TEXT," +
561                   Mms.SUBJECT + " TEXT," +
562                   Mms.SUBJECT_CHARSET + " INTEGER," +
563                   Mms.CONTENT_TYPE + " TEXT," +
564                   Mms.CONTENT_LOCATION + " TEXT," +
565                   Mms.EXPIRY + " INTEGER," +
566                   Mms.MESSAGE_CLASS + " TEXT," +
567                   Mms.MESSAGE_TYPE + " INTEGER," +
568                   Mms.MMS_VERSION + " INTEGER," +
569                   Mms.MESSAGE_SIZE + " INTEGER," +
570                   Mms.PRIORITY + " INTEGER," +
571                   Mms.READ_REPORT + " INTEGER," +
572                   Mms.REPORT_ALLOWED + " INTEGER," +
573                   Mms.RESPONSE_STATUS + " INTEGER," +
574                   Mms.STATUS + " INTEGER," +
575                   Mms.TRANSACTION_ID + " TEXT," +
576                   Mms.RETRIEVE_STATUS + " INTEGER," +
577                   Mms.RETRIEVE_TEXT + " TEXT," +
578                   Mms.RETRIEVE_TEXT_CHARSET + " INTEGER," +
579                   Mms.READ_STATUS + " INTEGER," +
580                   Mms.CONTENT_CLASS + " INTEGER," +
581                   Mms.RESPONSE_TEXT + " TEXT," +
582                   Mms.DELIVERY_TIME + " INTEGER," +
583                   Mms.DELIVERY_REPORT + " INTEGER," +
584                   Mms.LOCKED + " INTEGER DEFAULT 0," +
585                   Mms.SEEN + " INTEGER DEFAULT 0" +
586                   Mms.TEXT_ONLY + " INTEGER DEFAULT 0" +
587                   ");");
588
589        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_ADDR + " (" +
590                   Addr._ID + " INTEGER PRIMARY KEY," +
591                   Addr.MSG_ID + " INTEGER," +
592                   Addr.CONTACT_ID + " INTEGER," +
593                   Addr.ADDRESS + " TEXT," +
594                   Addr.TYPE + " INTEGER," +
595                   Addr.CHARSET + " INTEGER);");
596
597        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_PART + " (" +
598                   Part._ID + " INTEGER PRIMARY KEY," +
599                   Part.MSG_ID + " INTEGER," +
600                   Part.SEQ + " INTEGER DEFAULT 0," +
601                   Part.CONTENT_TYPE + " TEXT," +
602                   Part.NAME + " TEXT," +
603                   Part.CHARSET + " INTEGER," +
604                   Part.CONTENT_DISPOSITION + " TEXT," +
605                   Part.FILENAME + " TEXT," +
606                   Part.CONTENT_ID + " TEXT," +
607                   Part.CONTENT_LOCATION + " TEXT," +
608                   Part.CT_START + " INTEGER," +
609                   Part.CT_TYPE + " TEXT," +
610                   Part._DATA + " TEXT," +
611                   Part.TEXT + " TEXT);");
612
613        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_RATE + " (" +
614                   Rate.SENT_TIME + " INTEGER);");
615
616        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_DRM + " (" +
617                   BaseColumns._ID + " INTEGER PRIMARY KEY," +
618                   "_data TEXT);");
619    }
620
621    private void createMmsTriggers(SQLiteDatabase db) {
622        // Cleans up parts when a MM is deleted.
623        db.execSQL("CREATE TRIGGER part_cleanup DELETE ON " + MmsProvider.TABLE_PDU + " " +
624                   "BEGIN " +
625                   "  DELETE FROM " + MmsProvider.TABLE_PART +
626                   "  WHERE " + Part.MSG_ID + "=old._id;" +
627                   "END;");
628
629        // Cleans up address info when a MM is deleted.
630        db.execSQL("CREATE TRIGGER addr_cleanup DELETE ON " + MmsProvider.TABLE_PDU + " " +
631                   "BEGIN " +
632                   "  DELETE FROM " + MmsProvider.TABLE_ADDR +
633                   "  WHERE " + Addr.MSG_ID + "=old._id;" +
634                   "END;");
635
636        // Delete obsolete delivery-report, read-report while deleting their
637        // associated Send.req.
638        db.execSQL("CREATE TRIGGER cleanup_delivery_and_read_report " +
639                   "AFTER DELETE ON " + MmsProvider.TABLE_PDU + " " +
640                   "WHEN old." + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_SEND_REQ + " " +
641                   "BEGIN " +
642                   "  DELETE FROM " + MmsProvider.TABLE_PDU +
643                   "  WHERE (" + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_DELIVERY_IND +
644                   "    OR " + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_READ_ORIG_IND +
645                   ")" +
646                   "    AND " + Mms.MESSAGE_ID + "=old." + Mms.MESSAGE_ID + "; " +
647                   "END;");
648
649        // Update threads table to indicate whether attachments exist when
650        // parts are inserted or deleted.
651        db.execSQL(PART_UPDATE_THREADS_ON_INSERT_TRIGGER);
652        db.execSQL(PART_UPDATE_THREADS_ON_UPDATE_TRIGGER);
653        db.execSQL(PART_UPDATE_THREADS_ON_DELETE_TRIGGER);
654        db.execSQL(PDU_UPDATE_THREADS_ON_UPDATE_TRIGGER);
655    }
656
657    private void createSmsTables(SQLiteDatabase db) {
658        // N.B.: Whenever the columns here are changed, the columns in
659        // {@ref MmsSmsProvider} must be changed to match.
660        db.execSQL("CREATE TABLE sms (" +
661                   "_id INTEGER PRIMARY KEY," +
662                   "thread_id INTEGER," +
663                   "address TEXT," +
664                   "person INTEGER," +
665                   "date INTEGER," +
666                   "date_sent INTEGER DEFAULT 0," +
667                   "protocol INTEGER," +
668                   "read INTEGER DEFAULT 0," +
669                   "status INTEGER DEFAULT -1," + // a TP-Status value
670                                                  // or -1 if it
671                                                  // status hasn't
672                                                  // been received
673                   "type INTEGER," +
674                   "reply_path_present INTEGER," +
675                   "subject TEXT," +
676                   "body TEXT," +
677                   "service_center TEXT," +
678                   "locked INTEGER DEFAULT 0," +
679                   "error_code INTEGER DEFAULT 0," +
680                   "seen INTEGER DEFAULT 0" +
681                   ");");
682
683        /**
684         * This table is used by the SMS dispatcher to hold
685         * incomplete partial messages until all the parts arrive.
686         */
687        db.execSQL("CREATE TABLE raw (" +
688                   "_id INTEGER PRIMARY KEY," +
689                   "date INTEGER," +
690                   "reference_number INTEGER," + // one per full message
691                   "count INTEGER," + // the number of parts
692                   "sequence INTEGER," + // the part number of this message
693                   "destination_port INTEGER," +
694                   "address TEXT," +
695                   "pdu TEXT);"); // the raw PDU for this part
696
697        db.execSQL("CREATE TABLE attachments (" +
698                   "sms_id INTEGER," +
699                   "content_url TEXT," +
700                   "offset INTEGER);");
701
702        /**
703         * This table is used by the SMS dispatcher to hold pending
704         * delivery status report intents.
705         */
706        db.execSQL("CREATE TABLE sr_pending (" +
707                   "reference_number INTEGER," +
708                   "action TEXT," +
709                   "data TEXT);");
710    }
711
712    private void createCommonTables(SQLiteDatabase db) {
713        // TODO Ensure that each entry is removed when the last use of
714        // any address equivalent to its address is removed.
715
716        /**
717         * This table maps the first instance seen of any particular
718         * MMS/SMS address to an ID, which is then used as its
719         * canonical representation.  If the same address or an
720         * equivalent address (as determined by our Sqlite
721         * PHONE_NUMBERS_EQUAL extension) is seen later, this same ID
722         * will be used. The _id is created with AUTOINCREMENT so it
723         * will never be reused again if a recipient is deleted.
724         */
725        db.execSQL("CREATE TABLE canonical_addresses (" +
726                   "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
727                   "address TEXT);");
728
729        /**
730         * This table maps the subject and an ordered set of recipient
731         * IDs, separated by spaces, to a unique thread ID.  The IDs
732         * come from the canonical_addresses table.  This works
733         * because messages are considered to be part of the same
734         * thread if they have the same subject (or a null subject)
735         * and the same set of recipients.
736         */
737        db.execSQL("CREATE TABLE threads (" +
738                   Threads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
739                   Threads.DATE + " INTEGER DEFAULT 0," +
740                   Threads.MESSAGE_COUNT + " INTEGER DEFAULT 0," +
741                   Threads.RECIPIENT_IDS + " TEXT," +
742                   Threads.SNIPPET + " TEXT," +
743                   Threads.SNIPPET_CHARSET + " INTEGER DEFAULT 0," +
744                   Threads.READ + " INTEGER DEFAULT 1," +
745                   Threads.TYPE + " INTEGER DEFAULT 0," +
746                   Threads.ERROR + " INTEGER DEFAULT 0," +
747                   Threads.HAS_ATTACHMENT + " INTEGER DEFAULT 0);");
748
749        /**
750         * This table stores the queue of messages to be sent/downloaded.
751         */
752        db.execSQL("CREATE TABLE " + MmsSmsProvider.TABLE_PENDING_MSG +" (" +
753                   PendingMessages._ID + " INTEGER PRIMARY KEY," +
754                   PendingMessages.PROTO_TYPE + " INTEGER," +
755                   PendingMessages.MSG_ID + " INTEGER," +
756                   PendingMessages.MSG_TYPE + " INTEGER," +
757                   PendingMessages.ERROR_TYPE + " INTEGER," +
758                   PendingMessages.ERROR_CODE + " INTEGER," +
759                   PendingMessages.RETRY_INDEX + " INTEGER NOT NULL DEFAULT 0," +
760                   PendingMessages.DUE_TIME + " INTEGER," +
761                   PendingMessages.LAST_TRY + " INTEGER);");
762
763    }
764
765    // TODO Check the query plans for these triggers.
766    private void createCommonTriggers(SQLiteDatabase db) {
767        // Updates threads table whenever a message is added to pdu.
768        db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON " +
769                   MmsProvider.TABLE_PDU + " " +
770                   PDU_UPDATE_THREAD_CONSTRAINTS +
771                   PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
772
773        // Updates threads table whenever a message is added to sms.
774        db.execSQL("CREATE TRIGGER sms_update_thread_on_insert AFTER INSERT ON sms " +
775                   SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
776
777        // Updates threads table whenever a message in pdu is updated.
778        db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
779                   "  UPDATE OF " + Mms.DATE + ", " + Mms.SUBJECT + ", " + Mms.MESSAGE_BOX +
780                   "  ON " + MmsProvider.TABLE_PDU + " " +
781                   PDU_UPDATE_THREAD_CONSTRAINTS +
782                   PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
783
784        // Updates threads table whenever a message in sms is updated.
785        db.execSQL("CREATE TRIGGER sms_update_thread_date_subject_on_update AFTER" +
786                   "  UPDATE OF " + Sms.DATE + ", " + Sms.BODY + ", " + Sms.TYPE +
787                   "  ON sms " +
788                   SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
789
790        // Updates threads table whenever a message in pdu is updated.
791        db.execSQL("CREATE TRIGGER pdu_update_thread_read_on_update AFTER" +
792                   "  UPDATE OF " + Mms.READ +
793                   "  ON " + MmsProvider.TABLE_PDU + " " +
794                   PDU_UPDATE_THREAD_CONSTRAINTS +
795                   "BEGIN " +
796                   PDU_UPDATE_THREAD_READ_BODY +
797                   "END;");
798
799        // Updates threads table whenever a message in sms is updated.
800        db.execSQL("CREATE TRIGGER sms_update_thread_read_on_update AFTER" +
801                   "  UPDATE OF " + Sms.READ +
802                   "  ON sms " +
803                   "BEGIN " +
804                   SMS_UPDATE_THREAD_READ_BODY +
805                   "END;");
806
807        // Update threads table whenever a message in pdu is deleted
808        db.execSQL("CREATE TRIGGER pdu_update_thread_on_delete " +
809                   "AFTER DELETE ON pdu " +
810                   "BEGIN " +
811                   "  UPDATE threads SET " +
812                   "     date = (strftime('%s','now') * 1000)" +
813                   "  WHERE threads._id = old." + Mms.THREAD_ID + "; " +
814                   UPDATE_THREAD_COUNT_ON_OLD +
815                   UPDATE_THREAD_SNIPPET_SNIPPET_CS_ON_DELETE +
816                   "END;");
817
818        // As of DATABASE_VERSION 55, we've removed these triggers that delete empty threads.
819        // These triggers interfere with saving drafts on brand new threads. Instead of
820        // triggers cleaning up empty threads, the empty threads should be cleaned up by
821        // an explicit call to delete with Threads.OBSOLETE_THREADS_URI.
822
823//        // When the last message in a thread is deleted, these
824//        // triggers ensure that the entry for its thread ID is removed
825//        // from the threads table.
826//        db.execSQL("CREATE TRIGGER delete_obsolete_threads_pdu " +
827//                   "AFTER DELETE ON pdu " +
828//                   "BEGIN " +
829//                   "  DELETE FROM threads " +
830//                   "  WHERE " +
831//                   "    _id = old.thread_id " +
832//                   "    AND _id NOT IN " +
833//                   "    (SELECT thread_id FROM sms " +
834//                   "     UNION SELECT thread_id from pdu); " +
835//                   "END;");
836//
837//        db.execSQL("CREATE TRIGGER delete_obsolete_threads_when_update_pdu " +
838//                   "AFTER UPDATE OF " + Mms.THREAD_ID + " ON pdu " +
839//                   "WHEN old." + Mms.THREAD_ID + " != new." + Mms.THREAD_ID + " " +
840//                   "BEGIN " +
841//                   "  DELETE FROM threads " +
842//                   "  WHERE " +
843//                   "    _id = old.thread_id " +
844//                   "    AND _id NOT IN " +
845//                   "    (SELECT thread_id FROM sms " +
846//                   "     UNION SELECT thread_id from pdu); " +
847//                   "END;");
848
849        // Insert pending status for M-Notification.ind or M-ReadRec.ind
850        // when they are inserted into Inbox/Outbox.
851        db.execSQL("CREATE TRIGGER insert_mms_pending_on_insert " +
852                   "AFTER INSERT ON pdu " +
853                   "WHEN new." + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND +
854                   "  OR new." + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_READ_REC_IND +
855                   " " +
856                   "BEGIN " +
857                   "  INSERT INTO " + MmsSmsProvider.TABLE_PENDING_MSG +
858                   "    (" + PendingMessages.PROTO_TYPE + "," +
859                   "     " + PendingMessages.MSG_ID + "," +
860                   "     " + PendingMessages.MSG_TYPE + "," +
861                   "     " + PendingMessages.ERROR_TYPE + "," +
862                   "     " + PendingMessages.ERROR_CODE + "," +
863                   "     " + PendingMessages.RETRY_INDEX + "," +
864                   "     " + PendingMessages.DUE_TIME + ") " +
865                   "  VALUES " +
866                   "    (" + MmsSms.MMS_PROTO + "," +
867                   "      new." + BaseColumns._ID + "," +
868                   "      new." + Mms.MESSAGE_TYPE + ",0,0,0,0);" +
869                   "END;");
870
871        // Insert pending status for M-Send.req when it is moved into Outbox.
872        db.execSQL("CREATE TRIGGER insert_mms_pending_on_update " +
873                   "AFTER UPDATE ON pdu " +
874                   "WHEN new." + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_SEND_REQ +
875                   "  AND new." + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_OUTBOX +
876                   "  AND old." + Mms.MESSAGE_BOX + "!=" + Mms.MESSAGE_BOX_OUTBOX + " " +
877                   "BEGIN " +
878                   "  INSERT INTO " + MmsSmsProvider.TABLE_PENDING_MSG +
879                   "    (" + PendingMessages.PROTO_TYPE + "," +
880                   "     " + PendingMessages.MSG_ID + "," +
881                   "     " + PendingMessages.MSG_TYPE + "," +
882                   "     " + PendingMessages.ERROR_TYPE + "," +
883                   "     " + PendingMessages.ERROR_CODE + "," +
884                   "     " + PendingMessages.RETRY_INDEX + "," +
885                   "     " + PendingMessages.DUE_TIME + ") " +
886                   "  VALUES " +
887                   "    (" + MmsSms.MMS_PROTO + "," +
888                   "      new." + BaseColumns._ID + "," +
889                   "      new." + Mms.MESSAGE_TYPE + ",0,0,0,0);" +
890                   "END;");
891
892        // When a message is moved out of Outbox, delete its pending status.
893        db.execSQL("CREATE TRIGGER delete_mms_pending_on_update " +
894                   "AFTER UPDATE ON " + MmsProvider.TABLE_PDU + " " +
895                   "WHEN old." + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_OUTBOX +
896                   "  AND new." + Mms.MESSAGE_BOX + "!=" + Mms.MESSAGE_BOX_OUTBOX + " " +
897                   "BEGIN " +
898                   "  DELETE FROM " + MmsSmsProvider.TABLE_PENDING_MSG +
899                   "  WHERE " + PendingMessages.MSG_ID + "=new._id; " +
900                   "END;");
901
902        // Delete pending status for a message when it is deleted.
903        db.execSQL("CREATE TRIGGER delete_mms_pending_on_delete " +
904                   "AFTER DELETE ON " + MmsProvider.TABLE_PDU + " " +
905                   "BEGIN " +
906                   "  DELETE FROM " + MmsSmsProvider.TABLE_PENDING_MSG +
907                   "  WHERE " + PendingMessages.MSG_ID + "=old._id; " +
908                   "END;");
909
910        // TODO Add triggers for SMS retry-status management.
911
912        // Update the error flag of threads when the error type of
913        // a pending MM is updated.
914        db.execSQL("CREATE TRIGGER update_threads_error_on_update_mms " +
915                   "  AFTER UPDATE OF err_type ON pending_msgs " +
916                   "  WHEN (OLD.err_type < 10 AND NEW.err_type >= 10)" +
917                   "    OR (OLD.err_type >= 10 AND NEW.err_type < 10) " +
918                   "BEGIN" +
919                   "  UPDATE threads SET error = " +
920                   "    CASE" +
921                   "      WHEN NEW.err_type >= 10 THEN error + 1" +
922                   "      ELSE error - 1" +
923                   "    END " +
924                   "  WHERE _id =" +
925                   "   (SELECT DISTINCT thread_id" +
926                   "    FROM pdu" +
927                   "    WHERE _id = NEW.msg_id); " +
928                   "END;");
929
930        // Update the error flag of threads when delete pending message.
931        db.execSQL("CREATE TRIGGER update_threads_error_on_delete_mms " +
932                   "  BEFORE DELETE ON pdu" +
933                   "  WHEN OLD._id IN (SELECT DISTINCT msg_id" +
934                   "                   FROM pending_msgs" +
935                   "                   WHERE err_type >= 10) " +
936                   "BEGIN " +
937                   "  UPDATE threads SET error = error - 1" +
938                   "  WHERE _id = OLD.thread_id; " +
939                   "END;");
940
941        // Update the error flag of threads while moving an MM out of Outbox,
942        // which was failed to be sent permanently.
943        db.execSQL("CREATE TRIGGER update_threads_error_on_move_mms " +
944                   "  BEFORE UPDATE OF msg_box ON pdu " +
945                   "  WHEN (OLD.msg_box = 4 AND NEW.msg_box != 4) " +
946                   "  AND (OLD._id IN (SELECT DISTINCT msg_id" +
947                   "                   FROM pending_msgs" +
948                   "                   WHERE err_type >= 10)) " +
949                   "BEGIN " +
950                   "  UPDATE threads SET error = error - 1" +
951                   "  WHERE _id = OLD.thread_id; " +
952                   "END;");
953
954        // Update the error flag of threads after a text message was
955        // failed to send/receive.
956        db.execSQL("CREATE TRIGGER update_threads_error_on_update_sms " +
957                   "  AFTER UPDATE OF type ON sms" +
958                   "  WHEN (OLD.type != 5 AND NEW.type = 5)" +
959                   "    OR (OLD.type = 5 AND NEW.type != 5) " +
960                   "BEGIN " +
961                   "  UPDATE threads SET error = " +
962                   "    CASE" +
963                   "      WHEN NEW.type = 5 THEN error + 1" +
964                   "      ELSE error - 1" +
965                   "    END " +
966                   "  WHERE _id = NEW.thread_id; " +
967                   "END;");
968    }
969
970    @Override
971    public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
972        Log.w(TAG, "Upgrading database from version " + oldVersion
973                + " to " + currentVersion + ".");
974
975        switch (oldVersion) {
976        case 40:
977            if (currentVersion <= 40) {
978                return;
979            }
980
981            db.beginTransaction();
982            try {
983                upgradeDatabaseToVersion41(db);
984                db.setTransactionSuccessful();
985            } catch (Throwable ex) {
986                Log.e(TAG, ex.getMessage(), ex);
987                break;
988            } finally {
989                db.endTransaction();
990            }
991            // fall through
992        case 41:
993            if (currentVersion <= 41) {
994                return;
995            }
996
997            db.beginTransaction();
998            try {
999                upgradeDatabaseToVersion42(db);
1000                db.setTransactionSuccessful();
1001            } catch (Throwable ex) {
1002                Log.e(TAG, ex.getMessage(), ex);
1003                break;
1004            } finally {
1005                db.endTransaction();
1006            }
1007            // fall through
1008        case 42:
1009            if (currentVersion <= 42) {
1010                return;
1011            }
1012
1013            db.beginTransaction();
1014            try {
1015                upgradeDatabaseToVersion43(db);
1016                db.setTransactionSuccessful();
1017            } catch (Throwable ex) {
1018                Log.e(TAG, ex.getMessage(), ex);
1019                break;
1020            } finally {
1021                db.endTransaction();
1022            }
1023            // fall through
1024        case 43:
1025            if (currentVersion <= 43) {
1026                return;
1027            }
1028
1029            db.beginTransaction();
1030            try {
1031                upgradeDatabaseToVersion44(db);
1032                db.setTransactionSuccessful();
1033            } catch (Throwable ex) {
1034                Log.e(TAG, ex.getMessage(), ex);
1035                break;
1036            } finally {
1037                db.endTransaction();
1038            }
1039            // fall through
1040        case 44:
1041            if (currentVersion <= 44) {
1042                return;
1043            }
1044
1045            db.beginTransaction();
1046            try {
1047                upgradeDatabaseToVersion45(db);
1048                db.setTransactionSuccessful();
1049            } catch (Throwable ex) {
1050                Log.e(TAG, ex.getMessage(), ex);
1051                break;
1052            } finally {
1053                db.endTransaction();
1054            }
1055            // fall through
1056        case 45:
1057            if (currentVersion <= 45) {
1058                return;
1059            }
1060            db.beginTransaction();
1061            try {
1062                upgradeDatabaseToVersion46(db);
1063                db.setTransactionSuccessful();
1064            } catch (Throwable ex) {
1065                Log.e(TAG, ex.getMessage(), ex);
1066                break;
1067            } finally {
1068                db.endTransaction();
1069            }
1070            // fall through
1071        case 46:
1072            if (currentVersion <= 46) {
1073                return;
1074            }
1075
1076            db.beginTransaction();
1077            try {
1078                upgradeDatabaseToVersion47(db);
1079                db.setTransactionSuccessful();
1080            } catch (Throwable ex) {
1081                Log.e(TAG, ex.getMessage(), ex);
1082                break;
1083            } finally {
1084                db.endTransaction();
1085            }
1086            // fall through
1087        case 47:
1088            if (currentVersion <= 47) {
1089                return;
1090            }
1091
1092            db.beginTransaction();
1093            try {
1094                upgradeDatabaseToVersion48(db);
1095                db.setTransactionSuccessful();
1096            } catch (Throwable ex) {
1097                Log.e(TAG, ex.getMessage(), ex);
1098                break;
1099            } finally {
1100                db.endTransaction();
1101            }
1102            // fall through
1103        case 48:
1104            if (currentVersion <= 48) {
1105                return;
1106            }
1107
1108            db.beginTransaction();
1109            try {
1110                createWordsTables(db);
1111                db.setTransactionSuccessful();
1112            } catch (Throwable ex) {
1113                Log.e(TAG, ex.getMessage(), ex);
1114                break;
1115            } finally {
1116                db.endTransaction();
1117            }
1118            // fall through
1119        case 49:
1120            if (currentVersion <= 49) {
1121                return;
1122            }
1123            db.beginTransaction();
1124            try {
1125                createThreadIdIndex(db);
1126                db.setTransactionSuccessful();
1127            } catch (Throwable ex) {
1128                Log.e(TAG, ex.getMessage(), ex);
1129                break; // force to destroy all old data;
1130            } finally {
1131                db.endTransaction();
1132            }
1133            // fall through
1134        case 50:
1135            if (currentVersion <= 50) {
1136                return;
1137            }
1138
1139            db.beginTransaction();
1140            try {
1141                upgradeDatabaseToVersion51(db);
1142                db.setTransactionSuccessful();
1143            } catch (Throwable ex) {
1144                Log.e(TAG, ex.getMessage(), ex);
1145                break;
1146            } finally {
1147                db.endTransaction();
1148            }
1149            // fall through
1150        case 51:
1151            if (currentVersion <= 51) {
1152                return;
1153            }
1154            // 52 was adding a new meta_data column, but that was removed.
1155            // fall through
1156        case 52:
1157            if (currentVersion <= 52) {
1158                return;
1159            }
1160
1161            db.beginTransaction();
1162            try {
1163                upgradeDatabaseToVersion53(db);
1164                db.setTransactionSuccessful();
1165            } catch (Throwable ex) {
1166                Log.e(TAG, ex.getMessage(), ex);
1167                break;
1168            } finally {
1169                db.endTransaction();
1170            }
1171            // fall through
1172        case 53:
1173            if (currentVersion <= 53) {
1174                return;
1175            }
1176
1177            db.beginTransaction();
1178            try {
1179                upgradeDatabaseToVersion54(db);
1180                db.setTransactionSuccessful();
1181            } catch (Throwable ex) {
1182                Log.e(TAG, ex.getMessage(), ex);
1183                break;
1184            } finally {
1185                db.endTransaction();
1186            }
1187            // fall through
1188        case 54:
1189            if (currentVersion <= 54) {
1190                return;
1191            }
1192
1193            db.beginTransaction();
1194            try {
1195                upgradeDatabaseToVersion55(db);
1196                db.setTransactionSuccessful();
1197            } catch (Throwable ex) {
1198                Log.e(TAG, ex.getMessage(), ex);
1199                break;
1200            } finally {
1201                db.endTransaction();
1202            }
1203            // fall through
1204        case 55:
1205            if (currentVersion <= 55) {
1206                return;
1207            }
1208
1209            db.beginTransaction();
1210            try {
1211                upgradeDatabaseToVersion56(db);
1212                db.setTransactionSuccessful();
1213            } catch (Throwable ex) {
1214                Log.e(TAG, ex.getMessage(), ex);
1215                break;
1216            } finally {
1217                db.endTransaction();
1218            }
1219            return;
1220        }
1221
1222        Log.e(TAG, "Destroying all old data.");
1223        dropAll(db);
1224        onCreate(db);
1225    }
1226
1227    private void dropAll(SQLiteDatabase db) {
1228        // Clean the database out in order to start over from scratch.
1229        // We don't need to drop our triggers here because SQLite automatically
1230        // drops a trigger when its attached database is dropped.
1231        db.execSQL("DROP TABLE IF EXISTS canonical_addresses");
1232        db.execSQL("DROP TABLE IF EXISTS threads");
1233        db.execSQL("DROP TABLE IF EXISTS " + MmsSmsProvider.TABLE_PENDING_MSG);
1234        db.execSQL("DROP TABLE IF EXISTS sms");
1235        db.execSQL("DROP TABLE IF EXISTS raw");
1236        db.execSQL("DROP TABLE IF EXISTS attachments");
1237        db.execSQL("DROP TABLE IF EXISTS thread_ids");
1238        db.execSQL("DROP TABLE IF EXISTS sr_pending");
1239        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_PDU + ";");
1240        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_ADDR + ";");
1241        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_PART + ";");
1242        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_RATE + ";");
1243        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_DRM + ";");
1244    }
1245
1246    private void upgradeDatabaseToVersion41(SQLiteDatabase db) {
1247        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_move_mms");
1248        db.execSQL("CREATE TRIGGER update_threads_error_on_move_mms " +
1249                   "  BEFORE UPDATE OF msg_box ON pdu " +
1250                   "  WHEN (OLD.msg_box = 4 AND NEW.msg_box != 4) " +
1251                   "  AND (OLD._id IN (SELECT DISTINCT msg_id" +
1252                   "                   FROM pending_msgs" +
1253                   "                   WHERE err_type >= 10)) " +
1254                   "BEGIN " +
1255                   "  UPDATE threads SET error = error - 1" +
1256                   "  WHERE _id = OLD.thread_id; " +
1257                   "END;");
1258    }
1259
1260    private void upgradeDatabaseToVersion42(SQLiteDatabase db) {
1261        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_delete");
1262        db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_sms");
1263        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_delete_sms");
1264    }
1265
1266    private void upgradeDatabaseToVersion43(SQLiteDatabase db) {
1267        // Add 'has_attachment' column to threads table.
1268        db.execSQL("ALTER TABLE threads ADD COLUMN has_attachment INTEGER DEFAULT 0");
1269
1270        updateThreadsAttachmentColumn(db);
1271
1272        // Add insert and delete triggers for keeping it up to date.
1273        db.execSQL(PART_UPDATE_THREADS_ON_INSERT_TRIGGER);
1274        db.execSQL(PART_UPDATE_THREADS_ON_DELETE_TRIGGER);
1275    }
1276
1277    private void upgradeDatabaseToVersion44(SQLiteDatabase db) {
1278        updateThreadsAttachmentColumn(db);
1279
1280        // add the update trigger for keeping the threads up to date.
1281        db.execSQL(PART_UPDATE_THREADS_ON_UPDATE_TRIGGER);
1282    }
1283
1284    private void upgradeDatabaseToVersion45(SQLiteDatabase db) {
1285        // Add 'locked' column to sms table.
1286        db.execSQL("ALTER TABLE sms ADD COLUMN " + Sms.LOCKED + " INTEGER DEFAULT 0");
1287
1288        // Add 'locked' column to pdu table.
1289        db.execSQL("ALTER TABLE pdu ADD COLUMN " + Mms.LOCKED + " INTEGER DEFAULT 0");
1290    }
1291
1292    private void upgradeDatabaseToVersion46(SQLiteDatabase db) {
1293        // add the "text" column for caching inline text (e.g. strings) instead of
1294        // putting them in an external file
1295        db.execSQL("ALTER TABLE part ADD COLUMN " + Part.TEXT + " TEXT");
1296
1297        Cursor textRows = db.query(
1298                "part",
1299                new String[] { Part._ID, Part._DATA, Part.TEXT},
1300                "ct = 'text/plain' OR ct == 'application/smil'",
1301                null,
1302                null,
1303                null,
1304                null);
1305        ArrayList<String> filesToDelete = new ArrayList<String>();
1306        try {
1307            db.beginTransaction();
1308            if (textRows != null) {
1309                int partDataColumn = textRows.getColumnIndex(Part._DATA);
1310
1311                // This code is imperfect in that we can't guarantee that all the
1312                // backing files get deleted.  For example if the system aborts after
1313                // the database is updated but before we complete the process of
1314                // deleting files.
1315                while (textRows.moveToNext()) {
1316                    String path = textRows.getString(partDataColumn);
1317                    if (path != null) {
1318                        try {
1319                            InputStream is = new FileInputStream(path);
1320                            byte [] data = new byte[is.available()];
1321                            is.read(data);
1322                            EncodedStringValue v = new EncodedStringValue(data);
1323                            db.execSQL("UPDATE part SET " + Part._DATA + " = NULL, " +
1324                                    Part.TEXT + " = ?", new String[] { v.getString() });
1325                            is.close();
1326                            filesToDelete.add(path);
1327                        } catch (IOException e) {
1328                            // TODO Auto-generated catch block
1329                            e.printStackTrace();
1330                        }
1331                    }
1332                }
1333            }
1334            db.setTransactionSuccessful();
1335        } finally {
1336            db.endTransaction();
1337            for (String pathToDelete : filesToDelete) {
1338                try {
1339                    (new File(pathToDelete)).delete();
1340                } catch (SecurityException ex) {
1341                    Log.e(TAG, "unable to clean up old mms file for " + pathToDelete, ex);
1342                }
1343            }
1344            if (textRows != null) {
1345                textRows.close();
1346            }
1347        }
1348    }
1349
1350    private void upgradeDatabaseToVersion47(SQLiteDatabase db) {
1351        updateThreadsAttachmentColumn(db);
1352
1353        // add the update trigger for keeping the threads up to date.
1354        db.execSQL(PDU_UPDATE_THREADS_ON_UPDATE_TRIGGER);
1355    }
1356
1357    private void upgradeDatabaseToVersion48(SQLiteDatabase db) {
1358        // Add 'error_code' column to sms table.
1359        db.execSQL("ALTER TABLE sms ADD COLUMN error_code INTEGER DEFAULT 0");
1360    }
1361
1362    private void upgradeDatabaseToVersion51(SQLiteDatabase db) {
1363        db.execSQL("ALTER TABLE sms add COLUMN seen INTEGER DEFAULT 0");
1364        db.execSQL("ALTER TABLE pdu add COLUMN seen INTEGER DEFAULT 0");
1365
1366        try {
1367            // update the existing sms and pdu tables so the new "seen" column is the same as
1368            // the "read" column for each row.
1369            ContentValues contentValues = new ContentValues();
1370            contentValues.put("seen", 1);
1371            int count = db.update("sms", contentValues, "read=1", null);
1372            Log.d(TAG, "[MmsSmsDb] upgradeDatabaseToVersion51: updated " + count +
1373                    " rows in sms table to have READ=1");
1374            count = db.update("pdu", contentValues, "read=1", null);
1375            Log.d(TAG, "[MmsSmsDb] upgradeDatabaseToVersion51: updated " + count +
1376                    " rows in pdu table to have READ=1");
1377        } catch (Exception ex) {
1378            Log.e(TAG, "[MmsSmsDb] upgradeDatabaseToVersion51 caught ", ex);
1379        }
1380    }
1381
1382    private void upgradeDatabaseToVersion53(SQLiteDatabase db) {
1383        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_read_on_update");
1384
1385        // Updates threads table whenever a message in pdu is updated.
1386        db.execSQL("CREATE TRIGGER pdu_update_thread_read_on_update AFTER" +
1387                   "  UPDATE OF " + Mms.READ +
1388                   "  ON " + MmsProvider.TABLE_PDU + " " +
1389                   PDU_UPDATE_THREAD_CONSTRAINTS +
1390                   "BEGIN " +
1391                   PDU_UPDATE_THREAD_READ_BODY +
1392                   "END;");
1393    }
1394
1395    private void upgradeDatabaseToVersion54(SQLiteDatabase db) {
1396        // Add 'date_sent' column to sms table.
1397        db.execSQL("ALTER TABLE sms ADD COLUMN " + Sms.DATE_SENT + " INTEGER DEFAULT 0");
1398
1399        // Add 'date_sent' column to pdu table.
1400        db.execSQL("ALTER TABLE pdu ADD COLUMN " + Mms.DATE_SENT + " INTEGER DEFAULT 0");
1401    }
1402
1403    private void upgradeDatabaseToVersion55(SQLiteDatabase db) {
1404        // Drop removed triggers
1405        db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_pdu");
1406        db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_when_update_pdu");
1407    }
1408
1409    private void upgradeDatabaseToVersion56(SQLiteDatabase db) {
1410        // Add 'text_only' column to pdu table.
1411        db.execSQL("ALTER TABLE " + MmsProvider.TABLE_PDU + " ADD COLUMN " + Mms.TEXT_ONLY +
1412                " INTEGER DEFAULT 0");
1413    }
1414
1415    @Override
1416    public synchronized SQLiteDatabase getWritableDatabase() {
1417        SQLiteDatabase db = super.getWritableDatabase();
1418
1419        if (!sTriedAutoIncrement) {
1420            sTriedAutoIncrement = true;
1421            boolean hasAutoIncrementThreads = hasAutoIncrement(db, "threads");
1422            boolean hasAutoIncrementAddresses = hasAutoIncrement(db, "canonical_addresses");
1423            Log.d(TAG, "[getWritableDatabase] hasAutoIncrementThreads: " + hasAutoIncrementThreads +
1424                    " hasAutoIncrementAddresses: " + hasAutoIncrementAddresses);
1425            boolean autoIncrementThreadsSuccess = true;
1426            boolean autoIncrementAddressesSuccess = true;
1427            if (!hasAutoIncrementThreads) {
1428                db.beginTransaction();
1429                try {
1430                    if (false && sFakeLowStorageTest) {
1431                        Log.d(TAG, "[getWritableDatabase] mFakeLowStorageTest is true " +
1432                                " - fake exception");
1433                        throw new Exception("FakeLowStorageTest");
1434                    }
1435                    upgradeThreadsTableToAutoIncrement(db);     // a no-op if already upgraded
1436                    db.setTransactionSuccessful();
1437                } catch (Throwable ex) {
1438                    Log.e(TAG, "Failed to add autoIncrement to threads;: " + ex.getMessage(), ex);
1439                    autoIncrementThreadsSuccess = false;
1440                } finally {
1441                    db.endTransaction();
1442                }
1443            }
1444            if (!hasAutoIncrementAddresses) {
1445                db.beginTransaction();
1446                try {
1447                    if (false && sFakeLowStorageTest) {
1448                        Log.d(TAG, "[getWritableDatabase] mFakeLowStorageTest is true " +
1449                        " - fake exception");
1450                        throw new Exception("FakeLowStorageTest");
1451                    }
1452                    upgradeAddressTableToAutoIncrement(db);     // a no-op if already upgraded
1453                    db.setTransactionSuccessful();
1454                } catch (Throwable ex) {
1455                    Log.e(TAG, "Failed to add autoIncrement to canonical_addresses: " +
1456                            ex.getMessage(), ex);
1457                    autoIncrementAddressesSuccess = false;
1458                } finally {
1459                    db.endTransaction();
1460                }
1461            }
1462            if (autoIncrementThreadsSuccess && autoIncrementAddressesSuccess) {
1463                if (mLowStorageMonitor != null) {
1464                    // We've already updated the database. This receiver is no longer necessary.
1465                    Log.d(TAG, "Unregistering mLowStorageMonitor - we've upgraded");
1466                    mContext.unregisterReceiver(mLowStorageMonitor);
1467                    mLowStorageMonitor = null;
1468                }
1469            } else {
1470                if (sFakeLowStorageTest) {
1471                    sFakeLowStorageTest = false;
1472                }
1473
1474                // We failed, perhaps because of low storage. Turn on a receiver to watch for
1475                // storage space.
1476                if (mLowStorageMonitor == null) {
1477                    Log.d(TAG, "[getWritableDatabase] turning on storage monitor");
1478                    mLowStorageMonitor = new LowStorageMonitor();
1479                    IntentFilter intentFilter = new IntentFilter();
1480                    intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
1481                    intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
1482                    mContext.registerReceiver(mLowStorageMonitor, intentFilter);
1483                }
1484            }
1485        }
1486        return db;
1487    }
1488
1489    // Determine whether a particular table has AUTOINCREMENT in its schema.
1490    private boolean hasAutoIncrement(SQLiteDatabase db, String tableName) {
1491        boolean result = false;
1492        String query = "SELECT sql FROM sqlite_master WHERE type='table' AND name='" +
1493                        tableName + "'";
1494        Cursor c = db.rawQuery(query, null);
1495        if (c != null) {
1496            try {
1497                if (c.moveToFirst()) {
1498                    String schema = c.getString(0);
1499                    result = schema != null ? schema.contains("AUTOINCREMENT") : false;
1500                    Log.d(TAG, "[MmsSmsDb] tableName: " + tableName + " hasAutoIncrement: " +
1501                            schema + " result: " + result);
1502                }
1503            } finally {
1504                c.close();
1505            }
1506        }
1507        return result;
1508    }
1509
1510    // upgradeThreadsTableToAutoIncrement() is called to add the AUTOINCREMENT keyword to
1511    // the threads table. This could fail if the user has a lot of conversations and not enough
1512    // storage to make a copy of the threads table. That's ok. This upgrade is optional. It'll
1513    // be called again next time the device is rebooted.
1514    private void upgradeThreadsTableToAutoIncrement(SQLiteDatabase db) {
1515        if (hasAutoIncrement(db, "threads")) {
1516            Log.d(TAG, "[MmsSmsDb] upgradeThreadsTableToAutoIncrement: already upgraded");
1517            return;
1518        }
1519        Log.d(TAG, "[MmsSmsDb] upgradeThreadsTableToAutoIncrement: upgrading");
1520
1521        // Make the _id of the threads table autoincrement so we never re-use thread ids
1522        // Have to create a new temp threads table. Copy all the info from the old table.
1523        // Drop the old table and rename the new table to that of the old.
1524        db.execSQL("CREATE TABLE threads_temp (" +
1525                Threads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1526                Threads.DATE + " INTEGER DEFAULT 0," +
1527                Threads.MESSAGE_COUNT + " INTEGER DEFAULT 0," +
1528                Threads.RECIPIENT_IDS + " TEXT," +
1529                Threads.SNIPPET + " TEXT," +
1530                Threads.SNIPPET_CHARSET + " INTEGER DEFAULT 0," +
1531                Threads.READ + " INTEGER DEFAULT 1," +
1532                Threads.TYPE + " INTEGER DEFAULT 0," +
1533                Threads.ERROR + " INTEGER DEFAULT 0," +
1534                Threads.HAS_ATTACHMENT + " INTEGER DEFAULT 0);");
1535
1536        db.execSQL("INSERT INTO threads_temp SELECT * from threads;");
1537        db.execSQL("DROP TABLE threads;");
1538        db.execSQL("ALTER TABLE threads_temp RENAME TO threads;");
1539    }
1540
1541    // upgradeAddressTableToAutoIncrement() is called to add the AUTOINCREMENT keyword to
1542    // the canonical_addresses table. This could fail if the user has a lot of people they've
1543    // messaged with and not enough storage to make a copy of the canonical_addresses table.
1544    // That's ok. This upgrade is optional. It'll be called again next time the device is rebooted.
1545    private void upgradeAddressTableToAutoIncrement(SQLiteDatabase db) {
1546        if (hasAutoIncrement(db, "canonical_addresses")) {
1547            Log.d(TAG, "[MmsSmsDb] upgradeAddressTableToAutoIncrement: already upgraded");
1548            return;
1549        }
1550        Log.d(TAG, "[MmsSmsDb] upgradeAddressTableToAutoIncrement: upgrading");
1551
1552        // Make the _id of the canonical_addresses table autoincrement so we never re-use ids
1553        // Have to create a new temp canonical_addresses table. Copy all the info from the old
1554        // table. Drop the old table and rename the new table to that of the old.
1555        db.execSQL("CREATE TABLE canonical_addresses_temp (_id INTEGER PRIMARY KEY AUTOINCREMENT," +
1556                "address TEXT);");
1557
1558        db.execSQL("INSERT INTO canonical_addresses_temp SELECT * from canonical_addresses;");
1559        db.execSQL("DROP TABLE canonical_addresses;");
1560        db.execSQL("ALTER TABLE canonical_addresses_temp RENAME TO canonical_addresses;");
1561    }
1562
1563    private class LowStorageMonitor extends BroadcastReceiver {
1564
1565        public LowStorageMonitor() {
1566        }
1567
1568        public void onReceive(Context context, Intent intent) {
1569            String action = intent.getAction();
1570
1571            Log.d(TAG, "[LowStorageMonitor] onReceive intent " + action);
1572
1573            if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
1574                sTriedAutoIncrement = false;    // try to upgrade on the next getWriteableDatabase
1575            }
1576        }
1577    }
1578
1579    private void updateThreadsAttachmentColumn(SQLiteDatabase db) {
1580        // Set the values of that column correctly based on the current
1581        // contents of the database.
1582        db.execSQL("UPDATE threads SET has_attachment=1 WHERE _id IN " +
1583                   "  (SELECT DISTINCT pdu.thread_id FROM part " +
1584                   "   JOIN pdu ON pdu._id=part.mid " +
1585                   "   WHERE part.ct != 'text/plain' AND part.ct != 'application/smil')");
1586    }
1587}
1588