blob: 3ba6e78239dcec7a79e2a6112fe72e8e28ea085f [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.telephony;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import android.provider.Telephony;
import android.provider.Telephony.Mms;
import android.provider.Telephony.MmsSms;
import android.provider.Telephony.Sms;
import android.provider.Telephony.Threads;
import android.provider.Telephony.Mms.Addr;
import android.provider.Telephony.Mms.Part;
import android.provider.Telephony.Mms.Rate;
import android.provider.Telephony.MmsSms.PendingMessages;
import android.util.Log;
import com.google.android.mms.pdu.EncodedStringValue;
import com.google.android.mms.pdu.PduHeaders;
public class MmsSmsDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "MmsSmsDatabaseHelper";
private static final String SMS_UPDATE_THREAD_READ_BODY =
" UPDATE threads SET read = " +
" CASE (SELECT COUNT(*)" +
" FROM sms" +
" WHERE " + Sms.READ + " = 0" +
" AND " + Sms.THREAD_ID + " = threads._id)" +
" WHEN 0 THEN 1" +
" ELSE 0" +
" END" +
" WHERE threads._id = new." + Sms.THREAD_ID + "; ";
private static final String UPDATE_THREAD_COUNT_ON_NEW =
" UPDATE threads SET message_count = " +
" (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
" ON threads._id = " + Sms.THREAD_ID +
" WHERE " + Sms.THREAD_ID + " = new.thread_id" +
" AND sms." + Sms.TYPE + " != 3) + " +
" (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
" ON threads._id = " + Mms.THREAD_ID +
" WHERE " + Mms.THREAD_ID + " = new.thread_id" +
" AND (m_type=132 OR m_type=130 OR m_type=128)" +
" AND " + Mms.MESSAGE_BOX + " != 3) " +
" WHERE threads._id = new.thread_id; ";
private static final String UPDATE_THREAD_COUNT_ON_OLD =
" UPDATE threads SET message_count = " +
" (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
" ON threads._id = " + Sms.THREAD_ID +
" WHERE " + Sms.THREAD_ID + " = old.thread_id" +
" AND sms." + Sms.TYPE + " != 3) + " +
" (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
" ON threads._id = " + Mms.THREAD_ID +
" WHERE " + Mms.THREAD_ID + " = old.thread_id" +
" AND (m_type=132 OR m_type=130 OR m_type=128)" +
" AND " + Mms.MESSAGE_BOX + " != 3) " +
" WHERE threads._id = old.thread_id; ";
private static final String SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE =
"BEGIN" +
" UPDATE threads SET" +
" date = (strftime('%s','now') * 1000), " +
" snippet = new." + Sms.BODY + ", " +
" snippet_cs = 0" +
" WHERE threads._id = new." + Sms.THREAD_ID + "; " +
UPDATE_THREAD_COUNT_ON_NEW +
SMS_UPDATE_THREAD_READ_BODY +
"END;";
private static final String PDU_UPDATE_THREAD_CONSTRAINTS =
" WHEN new." + Mms.MESSAGE_TYPE + "=" +
PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF +
" OR new." + Mms.MESSAGE_TYPE + "=" +
PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND +
" OR new." + Mms.MESSAGE_TYPE + "=" +
PduHeaders.MESSAGE_TYPE_SEND_REQ + " ";
// When looking in the pdu table for unread messages, only count messages that
// are displayed to the user. The constants are defined in PduHeaders and could be used
// here, but the string "(m_type=132 OR m_type=130 OR m_type=128)" is used throughout this
// file and so it is used here to be consistent.
// m_type=128 = MESSAGE_TYPE_SEND_REQ
// m_type=130 = MESSAGE_TYPE_NOTIFICATION_IND
// m_type=132 = MESSAGE_TYPE_RETRIEVE_CONF
private static final String PDU_UPDATE_THREAD_READ_BODY =
" UPDATE threads SET read = " +
" CASE (SELECT COUNT(*)" +
" FROM " + MmsProvider.TABLE_PDU +
" WHERE " + Mms.READ + " = 0" +
" AND " + Mms.THREAD_ID + " = threads._id " +
" AND (m_type=132 OR m_type=130 OR m_type=128)) " +
" WHEN 0 THEN 1" +
" ELSE 0" +
" END" +
" WHERE threads._id = new." + Mms.THREAD_ID + "; ";
private static final String PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE =
"BEGIN" +
" UPDATE threads SET" +
" date = (strftime('%s','now') * 1000), " +
" snippet = new." + Mms.SUBJECT + ", " +
" snippet_cs = new." + Mms.SUBJECT_CHARSET +
" WHERE threads._id = new." + Mms.THREAD_ID + "; " +
UPDATE_THREAD_COUNT_ON_NEW +
PDU_UPDATE_THREAD_READ_BODY +
"END;";
private static final String UPDATE_THREAD_SNIPPET_SNIPPET_CS_ON_DELETE =
" UPDATE threads SET snippet = " +
" (SELECT snippet FROM" +
" (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
" UNION SELECT date, body AS snippet, thread_id FROM sms)" +
" WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
" WHERE threads._id = OLD.thread_id; " +
" UPDATE threads SET snippet_cs = " +
" (SELECT snippet_cs FROM" +
" (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
" UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
" WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
" WHERE threads._id = OLD.thread_id; ";
// When a part is inserted, if it is not text/plain or application/smil
// (which both can exist with text-only MMSes), then there is an attachment.
// Set has_attachment=1 in the threads table for the thread in question.
private static final String PART_UPDATE_THREADS_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_threads_on_insert_part " +
" AFTER INSERT ON part " +
" WHEN new.ct != 'text/plain' AND new.ct != 'application/smil' " +
" BEGIN " +
" UPDATE threads SET has_attachment=1 WHERE _id IN " +
" (SELECT pdu.thread_id FROM part JOIN pdu ON pdu._id=part.mid " +
" WHERE part._id=new._id LIMIT 1); " +
" END";
// When the 'mid' column in the part table is updated, we need to run the trigger to update
// the threads table's has_attachment column, if the part is an attachment.
private static final String PART_UPDATE_THREADS_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_threads_on_update_part " +
" AFTER UPDATE of " + Part.MSG_ID + " ON part " +
" WHEN new.ct != 'text/plain' AND new.ct != 'application/smil' " +
" BEGIN " +
" UPDATE threads SET has_attachment=1 WHERE _id IN " +
" (SELECT pdu.thread_id FROM part JOIN pdu ON pdu._id=part.mid " +
" WHERE part._id=new._id LIMIT 1); " +
" END";
// When a part is deleted (with the same non-text/SMIL constraint as when
// we set has_attachment), update the threads table for all threads.
// Unfortunately we cannot update only the thread that the part was
// attached to, as it is possible that the part has been orphaned and
// the message it was attached to is already gone.
private static final String PART_UPDATE_THREADS_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_threads_on_delete_part " +
" AFTER DELETE ON part " +
" WHEN old.ct != 'text/plain' AND old.ct != 'application/smil' " +
" BEGIN " +
" UPDATE threads SET has_attachment = " +
" CASE " +
" (SELECT COUNT(*) FROM part JOIN pdu " +
" WHERE pdu.thread_id = threads._id " +
" AND part.ct != 'text/plain' AND part.ct != 'application/smil' " +
" AND part.mid = pdu._id)" +
" WHEN 0 THEN 0 " +
" ELSE 1 " +
" END; " +
" END";
// When the 'thread_id' column in the pdu table is updated, we need to run the trigger to update
// the threads table's has_attachment column, if the message has an attachment in 'part' table
private static final String PDU_UPDATE_THREADS_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_threads_on_update_pdu " +
" AFTER UPDATE of thread_id ON pdu " +
" BEGIN " +
" UPDATE threads SET has_attachment=1 WHERE _id IN " +
" (SELECT pdu.thread_id FROM part JOIN pdu " +
" WHERE part.ct != 'text/plain' AND part.ct != 'application/smil' " +
" AND part.mid = pdu._id);" +
" END";
private static MmsSmsDatabaseHelper sInstance = null;
private static boolean sTriedAutoIncrement = false;
private static boolean sFakeLowStorageTest = false; // for testing only
static final String DATABASE_NAME = "mmssms.db";
static final int DATABASE_VERSION = 55;
private final Context mContext;
private LowStorageMonitor mLowStorageMonitor;
private MmsSmsDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
}
/**
* Return a singleton helper for the combined MMS and SMS
* database.
*/
/* package */ static synchronized MmsSmsDatabaseHelper getInstance(Context context) {
if (sInstance == null) {
sInstance = new MmsSmsDatabaseHelper(context);
}
return sInstance;
}
/**
* Look through all the recipientIds referenced by the threads and then delete any
* unreferenced rows from the canonical_addresses table.
*/
private static void removeUnferencedCanonicalAddresses(SQLiteDatabase db) {
Cursor c = db.query("threads", new String[] { "recipient_ids" },
null, null, null, null, null);
if (c != null) {
try {
if (c.getCount() == 0) {
// no threads, delete all addresses
int rows = db.delete("canonical_addresses", null, null);
} else {
// Find all the referenced recipient_ids from the threads. recipientIds is
// a space-separated list of recipient ids: "1 14 21"
HashSet<Integer> recipientIds = new HashSet<Integer>();
while (c.moveToNext()) {
String[] recips = c.getString(0).split(" ");
for (String recip : recips) {
try {
int recipientId = Integer.parseInt(recip);
recipientIds.add(recipientId);
} catch (Exception e) {
}
}
}
// Now build a selection string of all the unique recipient ids
StringBuilder sb = new StringBuilder();
Iterator<Integer> iter = recipientIds.iterator();
while (iter.hasNext()) {
sb.append("_id != " + iter.next());
if (iter.hasNext()) {
sb.append(" AND ");
}
}
if (sb.length() > 0) {
int rows = db.delete("canonical_addresses", sb.toString(), null);
}
}
} finally {
c.close();
}
}
}
public static void updateThread(SQLiteDatabase db, long thread_id) {
if (thread_id < 0) {
updateAllThreads(db, null, null);
return;
}
// Delete the row for this thread in the threads table if
// there are no more messages attached to it in either
// the sms or pdu tables.
int rows = db.delete("threads",
"_id = ? AND _id NOT IN" +
" (SELECT thread_id FROM sms " +
" UNION SELECT thread_id FROM pdu)",
new String[] { String.valueOf(thread_id) });
if (rows > 0) {
// If this deleted a row, let's remove orphaned canonical_addresses and get outta here
removeUnferencedCanonicalAddresses(db);
return;
}
// Update the message count in the threads table as the sum
// of all messages in both the sms and pdu tables.
db.execSQL(
" UPDATE threads SET message_count = " +
" (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
" ON threads._id = " + Sms.THREAD_ID +
" WHERE " + Sms.THREAD_ID + " = " + thread_id +
" AND sms." + Sms.TYPE + " != 3) + " +
" (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
" ON threads._id = " + Mms.THREAD_ID +
" WHERE " + Mms.THREAD_ID + " = " + thread_id +
" AND (m_type=132 OR m_type=130 OR m_type=128)" +
" AND " + Mms.MESSAGE_BOX + " != 3) " +
" WHERE threads._id = " + thread_id + ";");
// Update the date and the snippet (and its character set) in
// the threads table to be that of the most recent message in
// the thread.
db.execSQL(
" UPDATE threads" +
" SET" +
" date =" +
" (SELECT date FROM" +
" (SELECT date * 1000 AS date, thread_id FROM pdu" +
" UNION SELECT date, thread_id FROM sms)" +
" WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)," +
" snippet =" +
" (SELECT snippet FROM" +
" (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
" UNION SELECT date, body AS snippet, thread_id FROM sms)" +
" WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)," +
" snippet_cs =" +
" (SELECT snippet_cs FROM" +
" (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
" UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
" WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)" +
" WHERE threads._id = " + thread_id + ";");
// Update the error column of the thread to indicate if there
// are any messages in it that have failed to send.
// First check to see if there are any messages with errors in this thread.
String query = "SELECT thread_id FROM sms WHERE type=" +
Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED +
" AND thread_id = " + thread_id +
" LIMIT 1";
int setError = 0;
Cursor c = db.rawQuery(query, null);
if (c != null) {
try {
setError = c.getCount(); // Because of the LIMIT 1, count will be 1 or 0.
} finally {
c.close();
}
}
// What's the current state of the error flag in the threads table?
String errorQuery = "SELECT error FROM threads WHERE _id = " + thread_id;
c = db.rawQuery(errorQuery, null);
if (c != null) {
try {
if (c.moveToNext()) {
int curError = c.getInt(0);
if (curError != setError) {
// The current thread error column differs, update it.
db.execSQL("UPDATE threads SET error=" + setError +
" WHERE _id = " + thread_id);
}
}
} finally {
c.close();
}
}
}
public static void updateAllThreads(SQLiteDatabase db, String where, String[] whereArgs) {
if (where == null) {
where = "";
} else {
where = "WHERE (" + where + ")";
}
String query = "SELECT _id FROM threads WHERE _id IN " +
"(SELECT DISTINCT thread_id FROM sms " + where + ")";
Cursor c = db.rawQuery(query, whereArgs);
if (c != null) {
try {
while (c.moveToNext()) {
updateThread(db, c.getInt(0));
}
} finally {
c.close();
}
}
// TODO: there are several db operations in this function. Lets wrap them in a
// transaction to make it faster.
// remove orphaned threads
db.delete("threads",
"_id NOT IN (SELECT DISTINCT thread_id FROM sms " +
"UNION SELECT DISTINCT thread_id FROM pdu)", null);
// remove orphaned canonical_addresses
removeUnferencedCanonicalAddresses(db);
}
public static int deleteOneSms(SQLiteDatabase db, int message_id) {
int thread_id = -1;
// Find the thread ID that the specified SMS belongs to.
Cursor c = db.query("sms", new String[] { "thread_id" },
"_id=" + message_id, null, null, null, null);
if (c != null) {
if (c.moveToFirst()) {
thread_id = c.getInt(0);
}
c.close();
}
// Delete the specified message.
int rows = db.delete("sms", "_id=" + message_id, null);
if (thread_id > 0) {
// Update its thread.
updateThread(db, thread_id);
}
return rows;
}
@Override
public void onCreate(SQLiteDatabase db) {
createMmsTables(db);
createSmsTables(db);
createCommonTables(db);
createCommonTriggers(db);
createMmsTriggers(db);
createWordsTables(db);
createIndices(db);
}
// When upgrading the database we need to populate the words
// table with the rows out of sms and part.
private void populateWordsTable(SQLiteDatabase db) {
final String TABLE_WORDS = "words";
{
Cursor smsRows = db.query(
"sms",
new String[] { Sms._ID, Sms.BODY },
null,
null,
null,
null,
null);
try {
if (smsRows != null) {
smsRows.moveToPosition(-1);
ContentValues cv = new ContentValues();
while (smsRows.moveToNext()) {
cv.clear();
long id = smsRows.getLong(0); // 0 for Sms._ID
String body = smsRows.getString(1); // 1 for Sms.BODY
cv.put(Telephony.MmsSms.WordsTable.ID, id);
cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, body);
cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, id);
cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 1);
db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv);
}
}
} finally {
if (smsRows != null) {
smsRows.close();
}
}
}
{
Cursor mmsRows = db.query(
"part",
new String[] { Part._ID, Part.TEXT },
"ct = 'text/plain'",
null,
null,
null,
null);
try {
if (mmsRows != null) {
mmsRows.moveToPosition(-1);
ContentValues cv = new ContentValues();
while (mmsRows.moveToNext()) {
cv.clear();
long id = mmsRows.getLong(0); // 0 for Part._ID
String body = mmsRows.getString(1); // 1 for Part.TEXT
cv.put(Telephony.MmsSms.WordsTable.ID, id);
cv.put(Telephony.MmsSms.WordsTable.INDEXED_TEXT, body);
cv.put(Telephony.MmsSms.WordsTable.SOURCE_ROW_ID, id);
cv.put(Telephony.MmsSms.WordsTable.TABLE_ID, 1);
db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv);
}
}
} finally {
if (mmsRows != null) {
mmsRows.close();
}
}
}
}
private void createWordsTables(SQLiteDatabase db) {
try {
db.execSQL("CREATE VIRTUAL TABLE words USING FTS3 (_id INTEGER PRIMARY KEY, index_text TEXT, source_id INTEGER, table_to_use INTEGER);");
// monitor the sms table
// NOTE don't handle inserts using a trigger because it has an unwanted
// side effect: the value returned for the last row ends up being the
// id of one of the trigger insert not the original row insert.
// Handle inserts manually in the provider.
db.execSQL("CREATE TRIGGER sms_words_update AFTER UPDATE ON sms BEGIN UPDATE words " +
" SET index_text = NEW.body WHERE (source_id=NEW._id AND table_to_use=1); " +
" END;");
db.execSQL("CREATE TRIGGER sms_words_delete AFTER DELETE ON sms BEGIN DELETE FROM " +
" words WHERE source_id = OLD._id AND table_to_use = 1; END;");
// monitor the mms table
db.execSQL("CREATE TRIGGER mms_words_update AFTER UPDATE ON part BEGIN UPDATE words " +
" SET index_text = NEW.text WHERE (source_id=NEW._id AND table_to_use=2); " +
" END;");
db.execSQL("CREATE TRIGGER mms_words_delete AFTER DELETE ON part BEGIN DELETE FROM " +
" words WHERE source_id = OLD._id AND table_to_use = 2; END;");
populateWordsTable(db);
} catch (Exception ex) {
Log.e(TAG, "got exception creating words table: " + ex.toString());
}
}
private void createIndices(SQLiteDatabase db) {
createThreadIdIndex(db);
}
private void createThreadIdIndex(SQLiteDatabase db) {
try {
db.execSQL("CREATE INDEX IF NOT EXISTS typeThreadIdIndex ON sms" +
" (type, thread_id);");
} catch (Exception ex) {
Log.e(TAG, "got exception creating indices: " + ex.toString());
}
}
private void createMmsTables(SQLiteDatabase db) {
// N.B.: Whenever the columns here are changed, the columns in
// {@ref MmsSmsProvider} must be changed to match.
db.execSQL("CREATE TABLE " + MmsProvider.TABLE_PDU + " (" +
Mms._ID + " INTEGER PRIMARY KEY," +
Mms.THREAD_ID + " INTEGER," +
Mms.DATE + " INTEGER," +
Mms.DATE_SENT + " INTEGER DEFAULT 0," +
Mms.MESSAGE_BOX + " INTEGER," +
Mms.READ + " INTEGER DEFAULT 0," +
Mms.MESSAGE_ID + " TEXT," +
Mms.SUBJECT + " TEXT," +
Mms.SUBJECT_CHARSET + " INTEGER," +
Mms.CONTENT_TYPE + " TEXT," +
Mms.CONTENT_LOCATION + " TEXT," +
Mms.EXPIRY + " INTEGER," +
Mms.MESSAGE_CLASS + " TEXT," +
Mms.MESSAGE_TYPE + " INTEGER," +
Mms.MMS_VERSION + " INTEGER," +
Mms.MESSAGE_SIZE + " INTEGER," +
Mms.PRIORITY + " INTEGER," +
Mms.READ_REPORT + " INTEGER," +
Mms.REPORT_ALLOWED + " INTEGER," +
Mms.RESPONSE_STATUS + " INTEGER," +
Mms.STATUS + " INTEGER," +
Mms.TRANSACTION_ID + " TEXT," +
Mms.RETRIEVE_STATUS + " INTEGER," +
Mms.RETRIEVE_TEXT + " TEXT," +
Mms.RETRIEVE_TEXT_CHARSET + " INTEGER," +
Mms.READ_STATUS + " INTEGER," +
Mms.CONTENT_CLASS + " INTEGER," +
Mms.RESPONSE_TEXT + " TEXT," +
Mms.DELIVERY_TIME + " INTEGER," +
Mms.DELIVERY_REPORT + " INTEGER," +
Mms.LOCKED + " INTEGER DEFAULT 0," +
Mms.SEEN + " INTEGER DEFAULT 0" +
");");
db.execSQL("CREATE TABLE " + MmsProvider.TABLE_ADDR + " (" +
Addr._ID + " INTEGER PRIMARY KEY," +
Addr.MSG_ID + " INTEGER," +
Addr.CONTACT_ID + " INTEGER," +
Addr.ADDRESS + " TEXT," +
Addr.TYPE + " INTEGER," +
Addr.CHARSET + " INTEGER);");
db.execSQL("CREATE TABLE " + MmsProvider.TABLE_PART + " (" +
Part._ID + " INTEGER PRIMARY KEY," +
Part.MSG_ID + " INTEGER," +
Part.SEQ + " INTEGER DEFAULT 0," +
Part.CONTENT_TYPE + " TEXT," +
Part.NAME + " TEXT," +
Part.CHARSET + " INTEGER," +
Part.CONTENT_DISPOSITION + " TEXT," +
Part.FILENAME + " TEXT," +
Part.CONTENT_ID + " TEXT," +
Part.CONTENT_LOCATION + " TEXT," +
Part.CT_START + " INTEGER," +
Part.CT_TYPE + " TEXT," +
Part._DATA + " TEXT," +
Part.TEXT + " TEXT);");
db.execSQL("CREATE TABLE " + MmsProvider.TABLE_RATE + " (" +
Rate.SENT_TIME + " INTEGER);");
db.execSQL("CREATE TABLE " + MmsProvider.TABLE_DRM + " (" +
BaseColumns._ID + " INTEGER PRIMARY KEY," +
"_data TEXT);");
}
private void createMmsTriggers(SQLiteDatabase db) {
// Cleans up parts when a MM is deleted.
db.execSQL("CREATE TRIGGER part_cleanup DELETE ON " + MmsProvider.TABLE_PDU + " " +
"BEGIN " +
" DELETE FROM " + MmsProvider.TABLE_PART +
" WHERE " + Part.MSG_ID + "=old._id;" +
"END;");
// Cleans up address info when a MM is deleted.
db.execSQL("CREATE TRIGGER addr_cleanup DELETE ON " + MmsProvider.TABLE_PDU + " " +
"BEGIN " +
" DELETE FROM " + MmsProvider.TABLE_ADDR +
" WHERE " + Addr.MSG_ID + "=old._id;" +
"END;");
// Delete obsolete delivery-report, read-report while deleting their
// associated Send.req.
db.execSQL("CREATE TRIGGER cleanup_delivery_and_read_report " +
"AFTER DELETE ON " + MmsProvider.TABLE_PDU + " " +
"WHEN old." + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_SEND_REQ + " " +
"BEGIN " +
" DELETE FROM " + MmsProvider.TABLE_PDU +
" WHERE (" + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_DELIVERY_IND +
" OR " + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_READ_ORIG_IND +
")" +
" AND " + Mms.MESSAGE_ID + "=old." + Mms.MESSAGE_ID + "; " +
"END;");
// Update threads table to indicate whether attachments exist when
// parts are inserted or deleted.
db.execSQL(PART_UPDATE_THREADS_ON_INSERT_TRIGGER);
db.execSQL(PART_UPDATE_THREADS_ON_UPDATE_TRIGGER);
db.execSQL(PART_UPDATE_THREADS_ON_DELETE_TRIGGER);
db.execSQL(PDU_UPDATE_THREADS_ON_UPDATE_TRIGGER);
}
private void createSmsTables(SQLiteDatabase db) {
// N.B.: Whenever the columns here are changed, the columns in
// {@ref MmsSmsProvider} must be changed to match.
db.execSQL("CREATE TABLE sms (" +
"_id INTEGER PRIMARY KEY," +
"thread_id INTEGER," +
"address TEXT," +
"person INTEGER," +
"date INTEGER," +
"date_sent INTEGER DEFAULT 0," +
"protocol INTEGER," +
"read INTEGER DEFAULT 0," +
"status INTEGER DEFAULT -1," + // a TP-Status value
// or -1 if it
// status hasn't
// been received
"type INTEGER," +
"reply_path_present INTEGER," +
"subject TEXT," +
"body TEXT," +
"service_center TEXT," +
"locked INTEGER DEFAULT 0," +
"error_code INTEGER DEFAULT 0," +
"seen INTEGER DEFAULT 0" +
");");
/**
* This table is used by the SMS dispatcher to hold
* incomplete partial messages until all the parts arrive.
*/
db.execSQL("CREATE TABLE raw (" +
"_id INTEGER PRIMARY KEY," +
"date INTEGER," +
"reference_number INTEGER," + // one per full message
"count INTEGER," + // the number of parts
"sequence INTEGER," + // the part number of this message
"destination_port INTEGER," +
"address TEXT," +
"pdu TEXT);"); // the raw PDU for this part
db.execSQL("CREATE TABLE attachments (" +
"sms_id INTEGER," +
"content_url TEXT," +
"offset INTEGER);");
/**
* This table is used by the SMS dispatcher to hold pending
* delivery status report intents.
*/
db.execSQL("CREATE TABLE sr_pending (" +
"reference_number INTEGER," +
"action TEXT," +
"data TEXT);");
}
private void createCommonTables(SQLiteDatabase db) {
// TODO Ensure that each entry is removed when the last use of
// any address equivalent to its address is removed.
/**
* This table maps the first instance seen of any particular
* MMS/SMS address to an ID, which is then used as its
* canonical representation. If the same address or an
* equivalent address (as determined by our Sqlite
* PHONE_NUMBERS_EQUAL extension) is seen later, this same ID
* will be used. The _id is created with AUTOINCREMENT so it
* will never be reused again if a recipient is deleted.
*/
db.execSQL("CREATE TABLE canonical_addresses (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
"address TEXT);");
/**
* This table maps the subject and an ordered set of recipient
* IDs, separated by spaces, to a unique thread ID. The IDs
* come from the canonical_addresses table. This works
* because messages are considered to be part of the same
* thread if they have the same subject (or a null subject)
* and the same set of recipients.
*/
db.execSQL("CREATE TABLE threads (" +
Threads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Threads.DATE + " INTEGER DEFAULT 0," +
Threads.MESSAGE_COUNT + " INTEGER DEFAULT 0," +
Threads.RECIPIENT_IDS + " TEXT," +
Threads.SNIPPET + " TEXT," +
Threads.SNIPPET_CHARSET + " INTEGER DEFAULT 0," +
Threads.READ + " INTEGER DEFAULT 1," +
Threads.TYPE + " INTEGER DEFAULT 0," +
Threads.ERROR + " INTEGER DEFAULT 0," +
Threads.HAS_ATTACHMENT + " INTEGER DEFAULT 0);");
/**
* This table stores the queue of messages to be sent/downloaded.
*/
db.execSQL("CREATE TABLE " + MmsSmsProvider.TABLE_PENDING_MSG +" (" +
PendingMessages._ID + " INTEGER PRIMARY KEY," +
PendingMessages.PROTO_TYPE + " INTEGER," +
PendingMessages.MSG_ID + " INTEGER," +
PendingMessages.MSG_TYPE + " INTEGER," +
PendingMessages.ERROR_TYPE + " INTEGER," +
PendingMessages.ERROR_CODE + " INTEGER," +
PendingMessages.RETRY_INDEX + " INTEGER NOT NULL DEFAULT 0," +
PendingMessages.DUE_TIME + " INTEGER," +
PendingMessages.LAST_TRY + " INTEGER);");
}
// TODO Check the query plans for these triggers.
private void createCommonTriggers(SQLiteDatabase db) {
// Updates threads table whenever a message is added to pdu.
db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON " +
MmsProvider.TABLE_PDU + " " +
PDU_UPDATE_THREAD_CONSTRAINTS +
PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
// Updates threads table whenever a message is added to sms.
db.execSQL("CREATE TRIGGER sms_update_thread_on_insert AFTER INSERT ON sms " +
SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
// Updates threads table whenever a message in pdu is updated.
db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
" UPDATE OF " + Mms.DATE + ", " + Mms.SUBJECT + ", " + Mms.MESSAGE_BOX +
" ON " + MmsProvider.TABLE_PDU + " " +
PDU_UPDATE_THREAD_CONSTRAINTS +
PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
// Updates threads table whenever a message in sms is updated.
db.execSQL("CREATE TRIGGER sms_update_thread_date_subject_on_update AFTER" +
" UPDATE OF " + Sms.DATE + ", " + Sms.BODY + ", " + Sms.TYPE +
" ON sms " +
SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
// Updates threads table whenever a message in pdu is updated.
db.execSQL("CREATE TRIGGER pdu_update_thread_read_on_update AFTER" +
" UPDATE OF " + Mms.READ +
" ON " + MmsProvider.TABLE_PDU + " " +
PDU_UPDATE_THREAD_CONSTRAINTS +
"BEGIN " +
PDU_UPDATE_THREAD_READ_BODY +
"END;");
// Updates threads table whenever a message in sms is updated.
db.execSQL("CREATE TRIGGER sms_update_thread_read_on_update AFTER" +
" UPDATE OF " + Sms.READ +
" ON sms " +
"BEGIN " +
SMS_UPDATE_THREAD_READ_BODY +
"END;");
// Update threads table whenever a message in pdu is deleted
db.execSQL("CREATE TRIGGER pdu_update_thread_on_delete " +
"AFTER DELETE ON pdu " +
"BEGIN " +
" UPDATE threads SET " +
" date = (strftime('%s','now') * 1000)" +
" WHERE threads._id = old." + Mms.THREAD_ID + "; " +
UPDATE_THREAD_COUNT_ON_OLD +
UPDATE_THREAD_SNIPPET_SNIPPET_CS_ON_DELETE +
"END;");
// As of DATABASE_VERSION 55, we've removed these triggers that delete empty threads.
// These triggers interfere with saving drafts on brand new threads. Instead of
// triggers cleaning up empty threads, the empty threads should be cleaned up by
// an explicit call to delete with Threads.OBSOLETE_THREADS_URI.
// // When the last message in a thread is deleted, these
// // triggers ensure that the entry for its thread ID is removed
// // from the threads table.
// db.execSQL("CREATE TRIGGER delete_obsolete_threads_pdu " +
// "AFTER DELETE ON pdu " +
// "BEGIN " +
// " DELETE FROM threads " +
// " WHERE " +
// " _id = old.thread_id " +
// " AND _id NOT IN " +
// " (SELECT thread_id FROM sms " +
// " UNION SELECT thread_id from pdu); " +
// "END;");
//
// db.execSQL("CREATE TRIGGER delete_obsolete_threads_when_update_pdu " +
// "AFTER UPDATE OF " + Mms.THREAD_ID + " ON pdu " +
// "WHEN old." + Mms.THREAD_ID + " != new." + Mms.THREAD_ID + " " +
// "BEGIN " +
// " DELETE FROM threads " +
// " WHERE " +
// " _id = old.thread_id " +
// " AND _id NOT IN " +
// " (SELECT thread_id FROM sms " +
// " UNION SELECT thread_id from pdu); " +
// "END;");
// Insert pending status for M-Notification.ind or M-ReadRec.ind
// when they are inserted into Inbox/Outbox.
db.execSQL("CREATE TRIGGER insert_mms_pending_on_insert " +
"AFTER INSERT ON pdu " +
"WHEN new." + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND +
" OR new." + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_READ_REC_IND +
" " +
"BEGIN " +
" INSERT INTO " + MmsSmsProvider.TABLE_PENDING_MSG +
" (" + PendingMessages.PROTO_TYPE + "," +
" " + PendingMessages.MSG_ID + "," +
" " + PendingMessages.MSG_TYPE + "," +
" " + PendingMessages.ERROR_TYPE + "," +
" " + PendingMessages.ERROR_CODE + "," +
" " + PendingMessages.RETRY_INDEX + "," +
" " + PendingMessages.DUE_TIME + ") " +
" VALUES " +
" (" + MmsSms.MMS_PROTO + "," +
" new." + BaseColumns._ID + "," +
" new." + Mms.MESSAGE_TYPE + ",0,0,0,0);" +
"END;");
// Insert pending status for M-Send.req when it is moved into Outbox.
db.execSQL("CREATE TRIGGER insert_mms_pending_on_update " +
"AFTER UPDATE ON pdu " +
"WHEN new." + Mms.MESSAGE_TYPE + "=" + PduHeaders.MESSAGE_TYPE_SEND_REQ +
" AND new." + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_OUTBOX +
" AND old." + Mms.MESSAGE_BOX + "!=" + Mms.MESSAGE_BOX_OUTBOX + " " +
"BEGIN " +
" INSERT INTO " + MmsSmsProvider.TABLE_PENDING_MSG +
" (" + PendingMessages.PROTO_TYPE + "," +
" " + PendingMessages.MSG_ID + "," +
" " + PendingMessages.MSG_TYPE + "," +
" " + PendingMessages.ERROR_TYPE + "," +
" " + PendingMessages.ERROR_CODE + "," +
" " + PendingMessages.RETRY_INDEX + "," +
" " + PendingMessages.DUE_TIME + ") " +
" VALUES " +
" (" + MmsSms.MMS_PROTO + "," +
" new." + BaseColumns._ID + "," +
" new." + Mms.MESSAGE_TYPE + ",0,0,0,0);" +
"END;");
// When a message is moved out of Outbox, delete its pending status.
db.execSQL("CREATE TRIGGER delete_mms_pending_on_update " +
"AFTER UPDATE ON " + MmsProvider.TABLE_PDU + " " +
"WHEN old." + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_OUTBOX +
" AND new." + Mms.MESSAGE_BOX + "!=" + Mms.MESSAGE_BOX_OUTBOX + " " +
"BEGIN " +
" DELETE FROM " + MmsSmsProvider.TABLE_PENDING_MSG +
" WHERE " + PendingMessages.MSG_ID + "=new._id; " +
"END;");
// Delete pending status for a message when it is deleted.
db.execSQL("CREATE TRIGGER delete_mms_pending_on_delete " +
"AFTER DELETE ON " + MmsProvider.TABLE_PDU + " " +
"BEGIN " +
" DELETE FROM " + MmsSmsProvider.TABLE_PENDING_MSG +
" WHERE " + PendingMessages.MSG_ID + "=old._id; " +
"END;");
// TODO Add triggers for SMS retry-status management.
// Update the error flag of threads when the error type of
// a pending MM is updated.
db.execSQL("CREATE TRIGGER update_threads_error_on_update_mms " +
" AFTER UPDATE OF err_type ON pending_msgs " +
" WHEN (OLD.err_type < 10 AND NEW.err_type >= 10)" +
" OR (OLD.err_type >= 10 AND NEW.err_type < 10) " +
"BEGIN" +
" UPDATE threads SET error = " +
" CASE" +
" WHEN NEW.err_type >= 10 THEN error + 1" +
" ELSE error - 1" +
" END " +
" WHERE _id =" +
" (SELECT DISTINCT thread_id" +
" FROM pdu" +
" WHERE _id = NEW.msg_id); " +
"END;");
// Update the error flag of threads when delete pending message.
db.execSQL("CREATE TRIGGER update_threads_error_on_delete_mms " +
" BEFORE DELETE ON pdu" +
" WHEN OLD._id IN (SELECT DISTINCT msg_id" +
" FROM pending_msgs" +
" WHERE err_type >= 10) " +
"BEGIN " +
" UPDATE threads SET error = error - 1" +
" WHERE _id = OLD.thread_id; " +
"END;");
// Update the error flag of threads while moving an MM out of Outbox,
// which was failed to be sent permanently.
db.execSQL("CREATE TRIGGER update_threads_error_on_move_mms " +
" BEFORE UPDATE OF msg_box ON pdu " +
" WHEN (OLD.msg_box = 4 AND NEW.msg_box != 4) " +
" AND (OLD._id IN (SELECT DISTINCT msg_id" +
" FROM pending_msgs" +
" WHERE err_type >= 10)) " +
"BEGIN " +
" UPDATE threads SET error = error - 1" +
" WHERE _id = OLD.thread_id; " +
"END;");
// Update the error flag of threads after a text message was
// failed to send/receive.
db.execSQL("CREATE TRIGGER update_threads_error_on_update_sms " +
" AFTER UPDATE OF type ON sms" +
" WHEN (OLD.type != 5 AND NEW.type = 5)" +
" OR (OLD.type = 5 AND NEW.type != 5) " +
"BEGIN " +
" UPDATE threads SET error = " +
" CASE" +
" WHEN NEW.type = 5 THEN error + 1" +
" ELSE error - 1" +
" END " +
" WHERE _id = NEW.thread_id; " +
"END;");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion
+ " to " + currentVersion + ".");
switch (oldVersion) {
case 40:
if (currentVersion <= 40) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion41(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 41:
if (currentVersion <= 41) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion42(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 42:
if (currentVersion <= 42) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion43(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 43:
if (currentVersion <= 43) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion44(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 44:
if (currentVersion <= 44) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion45(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 45:
if (currentVersion <= 45) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion46(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 46:
if (currentVersion <= 46) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion47(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 47:
if (currentVersion <= 47) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion48(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 48:
if (currentVersion <= 48) {
return;
}
db.beginTransaction();
try {
createWordsTables(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 49:
if (currentVersion <= 49) {
return;
}
db.beginTransaction();
try {
createThreadIdIndex(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break; // force to destroy all old data;
} finally {
db.endTransaction();
}
// fall through
case 50:
if (currentVersion <= 50) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion51(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 51:
if (currentVersion <= 51) {
return;
}
// 52 was adding a new meta_data column, but that was removed.
// fall through
case 52:
if (currentVersion <= 52) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion53(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 53:
if (currentVersion <= 53) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion54(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 54:
if (currentVersion <= 54) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion55(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
return;
}
Log.e(TAG, "Destroying all old data.");
dropAll(db);
onCreate(db);
}
private void dropAll(SQLiteDatabase db) {
// Clean the database out in order to start over from scratch.
// We don't need to drop our triggers here because SQLite automatically
// drops a trigger when its attached database is dropped.
db.execSQL("DROP TABLE IF EXISTS canonical_addresses");
db.execSQL("DROP TABLE IF EXISTS threads");
db.execSQL("DROP TABLE IF EXISTS " + MmsSmsProvider.TABLE_PENDING_MSG);
db.execSQL("DROP TABLE IF EXISTS sms");
db.execSQL("DROP TABLE IF EXISTS raw");
db.execSQL("DROP TABLE IF EXISTS attachments");
db.execSQL("DROP TABLE IF EXISTS thread_ids");
db.execSQL("DROP TABLE IF EXISTS sr_pending");
db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_PDU + ";");
db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_ADDR + ";");
db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_PART + ";");
db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_RATE + ";");
db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_DRM + ";");
}
private void upgradeDatabaseToVersion41(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_move_mms");
db.execSQL("CREATE TRIGGER update_threads_error_on_move_mms " +
" BEFORE UPDATE OF msg_box ON pdu " +
" WHEN (OLD.msg_box = 4 AND NEW.msg_box != 4) " +
" AND (OLD._id IN (SELECT DISTINCT msg_id" +
" FROM pending_msgs" +
" WHERE err_type >= 10)) " +
"BEGIN " +
" UPDATE threads SET error = error - 1" +
" WHERE _id = OLD.thread_id; " +
"END;");
}
private void upgradeDatabaseToVersion42(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_sms");
db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_delete_sms");
}
private void upgradeDatabaseToVersion43(SQLiteDatabase db) {
// Add 'has_attachment' column to threads table.
db.execSQL("ALTER TABLE threads ADD COLUMN has_attachment INTEGER DEFAULT 0");
updateThreadsAttachmentColumn(db);
// Add insert and delete triggers for keeping it up to date.
db.execSQL(PART_UPDATE_THREADS_ON_INSERT_TRIGGER);
db.execSQL(PART_UPDATE_THREADS_ON_DELETE_TRIGGER);
}
private void upgradeDatabaseToVersion44(SQLiteDatabase db) {
updateThreadsAttachmentColumn(db);
// add the update trigger for keeping the threads up to date.
db.execSQL(PART_UPDATE_THREADS_ON_UPDATE_TRIGGER);
}
private void upgradeDatabaseToVersion45(SQLiteDatabase db) {
// Add 'locked' column to sms table.
db.execSQL("ALTER TABLE sms ADD COLUMN " + Sms.LOCKED + " INTEGER DEFAULT 0");
// Add 'locked' column to pdu table.
db.execSQL("ALTER TABLE pdu ADD COLUMN " + Mms.LOCKED + " INTEGER DEFAULT 0");
}
private void upgradeDatabaseToVersion46(SQLiteDatabase db) {
// add the "text" column for caching inline text (e.g. strings) instead of
// putting them in an external file
db.execSQL("ALTER TABLE part ADD COLUMN " + Part.TEXT + " TEXT");
Cursor textRows = db.query(
"part",
new String[] { Part._ID, Part._DATA, Part.TEXT},
"ct = 'text/plain' OR ct == 'application/smil'",
null,
null,
null,
null);
ArrayList<String> filesToDelete = new ArrayList<String>();
try {
db.beginTransaction();
if (textRows != null) {
int partDataColumn = textRows.getColumnIndex(Part._DATA);
// This code is imperfect in that we can't guarantee that all the
// backing files get deleted. For example if the system aborts after
// the database is updated but before we complete the process of
// deleting files.
while (textRows.moveToNext()) {
String path = textRows.getString(partDataColumn);
if (path != null) {
try {
InputStream is = new FileInputStream(path);
byte [] data = new byte[is.available()];
is.read(data);
EncodedStringValue v = new EncodedStringValue(data);
db.execSQL("UPDATE part SET " + Part._DATA + " = NULL, " +
Part.TEXT + " = ?", new String[] { v.getString() });
is.close();
filesToDelete.add(path);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
for (String pathToDelete : filesToDelete) {
try {
(new File(pathToDelete)).delete();
} catch (SecurityException ex) {
Log.e(TAG, "unable to clean up old mms file for " + pathToDelete, ex);
}
}
if (textRows != null) {
textRows.close();
}
}
}
private void upgradeDatabaseToVersion47(SQLiteDatabase db) {
updateThreadsAttachmentColumn(db);
// add the update trigger for keeping the threads up to date.
db.execSQL(PDU_UPDATE_THREADS_ON_UPDATE_TRIGGER);
}
private void upgradeDatabaseToVersion48(SQLiteDatabase db) {
// Add 'error_code' column to sms table.
db.execSQL("ALTER TABLE sms ADD COLUMN error_code INTEGER DEFAULT 0");
}
private void upgradeDatabaseToVersion51(SQLiteDatabase db) {
db.execSQL("ALTER TABLE sms add COLUMN seen INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE pdu add COLUMN seen INTEGER DEFAULT 0");
try {
// update the existing sms and pdu tables so the new "seen" column is the same as
// the "read" column for each row.
ContentValues contentValues = new ContentValues();
contentValues.put("seen", 1);
int count = db.update("sms", contentValues, "read=1", null);
Log.d(TAG, "[MmsSmsDb] upgradeDatabaseToVersion51: updated " + count +
" rows in sms table to have READ=1");
count = db.update("pdu", contentValues, "read=1", null);
Log.d(TAG, "[MmsSmsDb] upgradeDatabaseToVersion51: updated " + count +
" rows in pdu table to have READ=1");
} catch (Exception ex) {
Log.e(TAG, "[MmsSmsDb] upgradeDatabaseToVersion51 caught ", ex);
}
}
private void upgradeDatabaseToVersion53(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_read_on_update");
// Updates threads table whenever a message in pdu is updated.
db.execSQL("CREATE TRIGGER pdu_update_thread_read_on_update AFTER" +
" UPDATE OF " + Mms.READ +
" ON " + MmsProvider.TABLE_PDU + " " +
PDU_UPDATE_THREAD_CONSTRAINTS +
"BEGIN " +
PDU_UPDATE_THREAD_READ_BODY +
"END;");
}
private void upgradeDatabaseToVersion54(SQLiteDatabase db) {
// Add 'date_sent' column to sms table.
db.execSQL("ALTER TABLE sms ADD COLUMN " + Sms.DATE_SENT + " INTEGER DEFAULT 0");
// Add 'date_sent' column to pdu table.
db.execSQL("ALTER TABLE pdu ADD COLUMN " + Mms.DATE_SENT + " INTEGER DEFAULT 0");
}
private void upgradeDatabaseToVersion55(SQLiteDatabase db) {
// Drop removed triggers
db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_pdu");
db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_when_update_pdu");
}
@Override
public synchronized SQLiteDatabase getWritableDatabase() {
SQLiteDatabase db = super.getWritableDatabase();
if (!sTriedAutoIncrement) {
sTriedAutoIncrement = true;
boolean hasAutoIncrementThreads = hasAutoIncrement(db, "threads");
boolean hasAutoIncrementAddresses = hasAutoIncrement(db, "canonical_addresses");
Log.d(TAG, "[getWritableDatabase] hasAutoIncrementThreads: " + hasAutoIncrementThreads +
" hasAutoIncrementAddresses: " + hasAutoIncrementAddresses);
boolean autoIncrementThreadsSuccess = true;
boolean autoIncrementAddressesSuccess = true;
if (!hasAutoIncrementThreads) {
db.beginTransaction();
try {
if (false && sFakeLowStorageTest) {
Log.d(TAG, "[getWritableDatabase] mFakeLowStorageTest is true " +
" - fake exception");
throw new Exception("FakeLowStorageTest");
}
upgradeThreadsTableToAutoIncrement(db); // a no-op if already upgraded
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, "Failed to add autoIncrement to threads;: " + ex.getMessage(), ex);
autoIncrementThreadsSuccess = false;
} finally {
db.endTransaction();
}
}
if (!hasAutoIncrementAddresses) {
db.beginTransaction();
try {
if (false && sFakeLowStorageTest) {
Log.d(TAG, "[getWritableDatabase] mFakeLowStorageTest is true " +
" - fake exception");
throw new Exception("FakeLowStorageTest");
}
upgradeAddressTableToAutoIncrement(db); // a no-op if already upgraded
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, "Failed to add autoIncrement to canonical_addresses: " +
ex.getMessage(), ex);
autoIncrementAddressesSuccess = false;
} finally {
db.endTransaction();
}
}
if (autoIncrementThreadsSuccess && autoIncrementAddressesSuccess) {
if (mLowStorageMonitor != null) {
// We've already updated the database. This receiver is no longer necessary.
Log.d(TAG, "Unregistering mLowStorageMonitor - we've upgraded");
mContext.unregisterReceiver(mLowStorageMonitor);
mLowStorageMonitor = null;
}
} else {
if (sFakeLowStorageTest) {
sFakeLowStorageTest = false;
}
// We failed, perhaps because of low storage. Turn on a receiver to watch for
// storage space.
if (mLowStorageMonitor == null) {
Log.d(TAG, "[getWritableDatabase] turning on storage monitor");
mLowStorageMonitor = new LowStorageMonitor();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
mContext.registerReceiver(mLowStorageMonitor, intentFilter);
}
}
}
return db;
}
// Determine whether a particular table has AUTOINCREMENT in its schema.
private boolean hasAutoIncrement(SQLiteDatabase db, String tableName) {
boolean result = false;
String query = "SELECT sql FROM sqlite_master WHERE type='table' AND name='" +
tableName + "'";
Cursor c = db.rawQuery(query, null);
if (c != null) {
try {
if (c.moveToFirst()) {
String schema = c.getString(0);
result = schema != null ? schema.contains("AUTOINCREMENT") : false;
Log.d(TAG, "[MmsSmsDb] tableName: " + tableName + " hasAutoIncrement: " +
schema + " result: " + result);
}
} finally {
c.close();
}
}
return result;
}
// upgradeThreadsTableToAutoIncrement() is called to add the AUTOINCREMENT keyword to
// the threads table. This could fail if the user has a lot of conversations and not enough
// storage to make a copy of the threads table. That's ok. This upgrade is optional. It'll
// be called again next time the device is rebooted.
private void upgradeThreadsTableToAutoIncrement(SQLiteDatabase db) {
if (hasAutoIncrement(db, "threads")) {
Log.d(TAG, "[MmsSmsDb] upgradeThreadsTableToAutoIncrement: already upgraded");
return;
}
Log.d(TAG, "[MmsSmsDb] upgradeThreadsTableToAutoIncrement: upgrading");
// Make the _id of the threads table autoincrement so we never re-use thread ids
// Have to create a new temp threads table. Copy all the info from the old table.
// Drop the old table and rename the new table to that of the old.
db.execSQL("CREATE TABLE threads_temp (" +
Threads._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Threads.DATE + " INTEGER DEFAULT 0," +
Threads.MESSAGE_COUNT + " INTEGER DEFAULT 0," +
Threads.RECIPIENT_IDS + " TEXT," +
Threads.SNIPPET + " TEXT," +
Threads.SNIPPET_CHARSET + " INTEGER DEFAULT 0," +
Threads.READ + " INTEGER DEFAULT 1," +
Threads.TYPE + " INTEGER DEFAULT 0," +
Threads.ERROR + " INTEGER DEFAULT 0," +
Threads.HAS_ATTACHMENT + " INTEGER DEFAULT 0);");
db.execSQL("INSERT INTO threads_temp SELECT * from threads;");
db.execSQL("DROP TABLE threads;");
db.execSQL("ALTER TABLE threads_temp RENAME TO threads;");
}
// upgradeAddressTableToAutoIncrement() is called to add the AUTOINCREMENT keyword to
// the canonical_addresses table. This could fail if the user has a lot of people they've
// messaged with and not enough storage to make a copy of the canonical_addresses table.
// That's ok. This upgrade is optional. It'll be called again next time the device is rebooted.
private void upgradeAddressTableToAutoIncrement(SQLiteDatabase db) {
if (hasAutoIncrement(db, "canonical_addresses")) {
Log.d(TAG, "[MmsSmsDb] upgradeAddressTableToAutoIncrement: already upgraded");
return;
}
Log.d(TAG, "[MmsSmsDb] upgradeAddressTableToAutoIncrement: upgrading");
// Make the _id of the canonical_addresses table autoincrement so we never re-use ids
// Have to create a new temp canonical_addresses table. Copy all the info from the old
// table. Drop the old table and rename the new table to that of the old.
db.execSQL("CREATE TABLE canonical_addresses_temp (_id INTEGER PRIMARY KEY AUTOINCREMENT," +
"address TEXT);");
db.execSQL("INSERT INTO canonical_addresses_temp SELECT * from canonical_addresses;");
db.execSQL("DROP TABLE canonical_addresses;");
db.execSQL("ALTER TABLE canonical_addresses_temp RENAME TO canonical_addresses;");
}
private class LowStorageMonitor extends BroadcastReceiver {
public LowStorageMonitor() {
}
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d(TAG, "[LowStorageMonitor] onReceive intent " + action);
if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
sTriedAutoIncrement = false; // try to upgrade on the next getWriteableDatabase
}
}
}
private void updateThreadsAttachmentColumn(SQLiteDatabase db) {
// Set the values of that column correctly based on the current
// contents of the database.
db.execSQL("UPDATE threads SET has_attachment=1 WHERE _id IN " +
" (SELECT DISTINCT pdu.thread_id FROM part " +
" JOIN pdu ON pdu._id=part.mid " +
" WHERE part.ct != 'text/plain' AND part.ct != 'application/smil')");
}
}