blob: a9494fb02c7a865c1f6cea3b2ddb01a32e1f79d0 [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 static com.android.providers.telephony.SmsProvider.NO_ERROR_CODE;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DatabaseErrorHandler;
import android.database.DefaultDatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.storage.StorageManager;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.Telephony;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Mms.Addr;
import android.provider.Telephony.Mms.Part;
import android.provider.Telephony.Mms.Rate;
import android.provider.Telephony.MmsSms;
import android.provider.Telephony.MmsSms.PendingMessages;
import android.provider.Telephony.Sms;
import android.provider.Telephony.Sms.Intents;
import android.provider.Telephony.Threads;
import android.telephony.SubscriptionManager;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.PhoneFactory;
import com.google.android.mms.pdu.EncodedStringValue;
import com.google.android.mms.pdu.PduHeaders;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A {@link SQLiteOpenHelper} that handles DB management of SMS and MMS tables.
*
* From N, SMS and MMS tables are split into two groups with different levels of encryption.
* - the raw table, which lives inside DE(Device Encrypted) storage.
* - all other tables, which lives under CE(Credential Encrypted) storage.
*
* All tables are created by this class in the same database that can live either in DE or CE
* storage. But not all tables in the same database should be used. Only DE tables should be used
* in the database created in DE and only CE tables should be used in the database created in CE.
* The only exception is a non-FBE device migrating from M to N, in which case the DE and CE tables
* will actually live inside the same storage/database.
*
* This class provides methods to create instances that manage databases in different storage.
* It's the responsibility of the clients of this class to make sure the right instance is
* used to access tables that are supposed to live inside the intended storage.
*/
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 sDeInstance = null;
private static MmsSmsDatabaseHelper sCeInstance = null;
private static MmsSmsDatabaseErrorHandler sDbErrorHandler = null;
private static final String[] BIND_ARGS_NONE = new String[0];
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 = 67;
private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000;
private final Context mContext;
private LowStorageMonitor mLowStorageMonitor;
// SharedPref key used to check if initial create has been done (if onCreate has already been
// called once)
private static final String INITIAL_CREATE_DONE = "initial_create_done";
// cache for INITIAL_CREATE_DONE shared pref so access to it can be avoided when possible
private static AtomicBoolean sInitialCreateDone = new AtomicBoolean(false);
/**
* The primary purpose of this DatabaseErrorHandler is to broadcast an intent on corruption and
* print a Log.wtf so database corruption can be caught earlier.
*/
private static class MmsSmsDatabaseErrorHandler implements DatabaseErrorHandler {
private DefaultDatabaseErrorHandler mDefaultDatabaseErrorHandler
= new DefaultDatabaseErrorHandler();
private Context mContext;
MmsSmsDatabaseErrorHandler(Context context) {
mContext = context;
}
@Override
public void onCorruption(SQLiteDatabase dbObj) {
String logMsg = "Corruption reported by sqlite on database: " + dbObj.getPath();
localLogWtf(logMsg);
sendDbLostIntent(mContext, true);
// Let the default error handler take other actions
mDefaultDatabaseErrorHandler.onCorruption(dbObj);
}
}
@VisibleForTesting
MmsSmsDatabaseHelper(Context context, MmsSmsDatabaseErrorHandler dbErrorHandler) {
super(context, DATABASE_NAME, null, DATABASE_VERSION, dbErrorHandler);
mContext = context;
// Memory optimization - close idle connections after 30s of inactivity
setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
setWriteAheadLoggingEnabled(false);
try {
PhoneFactory.addLocalLog(TAG, 100);
} catch (IllegalArgumentException e) {
// ignore
}
}
private static synchronized MmsSmsDatabaseErrorHandler getDbErrorHandler(Context context) {
if (sDbErrorHandler == null) {
sDbErrorHandler = new MmsSmsDatabaseErrorHandler(context);
}
return sDbErrorHandler;
}
private static void sendDbLostIntent(Context context, boolean isCorrupted) {
// Broadcast ACTION_SMS_MMS_DB_LOST
Intent intent = new Intent(Sms.Intents.ACTION_SMS_MMS_DB_LOST);
intent.putExtra(Sms.Intents.EXTRA_IS_CORRUPTED, isCorrupted);
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
context.sendBroadcast(intent);
}
/**
* Returns a singleton helper for the combined MMS and SMS database in device encrypted storage.
*/
/* package */ static synchronized MmsSmsDatabaseHelper getInstanceForDe(Context context) {
if (sDeInstance == null) {
Context deContext = ProviderUtil.getDeviceEncryptedContext(context);
sDeInstance = new MmsSmsDatabaseHelper(deContext, getDbErrorHandler(deContext));
}
return sDeInstance;
}
/**
* Returns a singleton helper for the combined MMS and SMS database in credential encrypted
* storage. If FBE is not available, use the device encrypted storage instead.
*/
/* package */ static synchronized MmsSmsDatabaseHelper getInstanceForCe(Context context) {
if (sCeInstance == null) {
if (StorageManager.isFileEncryptedNativeOrEmulated()) {
Context ceContext = ProviderUtil.getCredentialEncryptedContext(context);
sCeInstance = new MmsSmsDatabaseHelper(ceContext, getDbErrorHandler(ceContext));
} else {
sCeInstance = getInstanceForDe(context);
}
}
return sCeInstance;
}
/**
* 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(MmsSmsProvider.TABLE_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();
sb.append("_id NOT IN (");
while (iter.hasNext()) {
sb.append(iter.next());
if (iter.hasNext()) {
sb.append(",");
}
}
sb.append(")");
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) {
updateThreads(db, null, null);
return;
}
updateThreads(db, "(thread_id = ?)", new String[]{ String.valueOf(thread_id) });
}
/**
* Update all threads containing SMS matching the 'where' condition. Note that the condition
* is applied to individual messages in the sms table, NOT the threads table.
*/
public static void updateThreads(SQLiteDatabase db, String where, String[] whereArgs) {
if (where == null) {
where = "1";
}
if (whereArgs == null) {
whereArgs = BIND_ARGS_NONE;
}
db.beginTransaction();
try {
// Delete rows in the threads table if
// there are no more messages attached to it in either
// the sms or pdu tables.
// Note that we do this regardless of whether they match 'where'.
int rows = db.delete(MmsSmsProvider.TABLE_THREADS,
"_id NOT IN (" +
" SELECT DISTINCT thread_id FROM sms WHERE thread_id IS NOT NULL" +
" UNION" +
" SELECT DISTINCT thread_id FROM pdu WHERE thread_id IS NOT NULL)",
null);
if (rows > 0) {
// If this deleted a row, let's remove orphaned canonical_addresses
removeUnferencedCanonicalAddresses(db);
}
// 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" +
" WHERE " + Sms.THREAD_ID + " = threads._id" +
" AND sms." + Sms.TYPE + " != 3" +
" ) + (" +
" SELECT COUNT(pdu._id) FROM pdu" +
" WHERE " + Mms.THREAD_ID + " = threads._id" +
" AND (m_type=132 OR m_type=130 OR m_type=128)" +
" AND " + Mms.MESSAGE_BOX + " != 3" +
" )" +
" WHERE EXISTS (" +
" SELECT _id" +
" FROM sms" +
" WHERE thread_id = threads._id" +
" AND (" + where + ")" +
" LIMIT 1" +
" );",
whereArgs);
// 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(
" WITH matches AS (" +
" SELECT date * 1000 AS date, sub AS snippet, sub_cs AS snippet_cs, thread_id" +
" FROM pdu" +
" WHERE thread_id = threads._id" +
" UNION" +
" SELECT date, body AS snippet, 0 AS snippet_cs, thread_id" +
" FROM sms" +
" WHERE thread_id = threads._id" +
" ORDER BY date DESC" +
" LIMIT 1" +
" )" +
" UPDATE threads" +
" SET date = (SELECT date FROM matches)," +
" snippet = (SELECT snippet FROM matches)," +
" snippet_cs = (SELECT snippet_cs FROM matches)" +
" WHERE EXISTS (" +
" SELECT _id" +
" FROM sms" +
" WHERE thread_id = threads._id" +
" AND (" + where + ")" +
" LIMIT 1" +
" );",
whereArgs);
// 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.
db.execSQL(
" UPDATE threads" +
" SET error = EXISTS (" +
" SELECT type" +
" FROM sms" +
" WHERE type=" + Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED +
" AND thread_id = threads._id" +
" )" +
" WHERE EXISTS (" +
" SELECT _id" +
" FROM sms" +
" WHERE thread_id = threads._id" +
" AND (" + where + ")" +
" LIMIT 1" +
" );",
whereArgs);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
} finally {
db.endTransaction();
}
}
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) {
localLog("onCreate: Creating all SMS-MMS tables.");
createMmsTables(db);
createSmsTables(db);
createCommonTables(db);
createCommonTriggers(db);
createMmsTriggers(db);
createWordsTables(db);
createIndices(db);
// if FBE is not supported, or if this onCreate is for CE partition database
if (!StorageManager.isFileEncryptedNativeOrEmulated()
|| (mContext != null && mContext.isCredentialProtectedStorage())) {
localLog("onCreate: broadcasting ACTION_SMS_MMS_DB_CREATED");
// Broadcast ACTION_SMS_MMS_DB_CREATED
Intent intent = new Intent(Sms.Intents.ACTION_SMS_MMS_DB_CREATED);
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
if (isInitialCreateDone()) {
// this onCreate is called after onCreate was called once initially. The db file
// disappeared mysteriously?
localLogWtf("onCreate: was already called once earlier");
intent.putExtra(Intents.EXTRA_IS_INITIAL_CREATE, false);
} else {
setInitialCreateDone();
intent.putExtra(Intents.EXTRA_IS_INITIAL_CREATE, true);
}
mContext.sendBroadcast(intent);
}
}
private static void localLog(String logMsg) {
Log.d(TAG, logMsg);
PhoneFactory.localLog(TAG, logMsg);
}
private static void localLogWtf(String logMsg) {
Log.wtf(TAG, logMsg);
PhoneFactory.localLog(TAG, logMsg);
}
private boolean isInitialCreateDone() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
return sp.getBoolean(INITIAL_CREATE_DONE, false);
}
private void setInitialCreateDone() {
if (!sInitialCreateDone.getAndSet(true)) {
SharedPreferences.Editor editor
= PreferenceManager.getDefaultSharedPreferences(mContext).edit();
editor.putBoolean(INITIAL_CREATE_DONE, true);
editor.commit();
}
}
// 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;");
populateWordsTable(db);
} catch (Exception ex) {
Log.e(TAG, "got exception creating words table: " + ex.toString());
}
}
private void createIndices(SQLiteDatabase db) {
createThreadIdIndex(db);
createThreadIdDateIndex(db);
createPartMidIndex(db);
createAddrMsgIdIndex(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 createThreadIdDateIndex(SQLiteDatabase db) {
try {
db.execSQL("CREATE INDEX IF NOT EXISTS threadIdDateIndex ON sms" +
" (thread_id, date);");
} catch (Exception ex) {
Log.e(TAG, "got exception creating indices: " + ex.toString());
}
}
private void createPartMidIndex(SQLiteDatabase db) {
try {
db.execSQL("CREATE INDEX IF NOT EXISTS partMidIndex ON part (mid)");
} catch (Exception ex) {
Log.e(TAG, "got exception creating indices: " + ex.toString());
}
}
private void createAddrMsgIdIndex(SQLiteDatabase db) {
try {
db.execSQL("CREATE INDEX IF NOT EXISTS addrMsgIdIndex ON addr (msg_id)");
} catch (Exception ex) {
Log.e(TAG, "got exception creating indices: " + ex.toString());
}
}
@VisibleForTesting
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 AUTOINCREMENT," +
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.SUBSCRIPTION_ID + " INTEGER DEFAULT "
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID + ", " +
Mms.SEEN + " INTEGER DEFAULT 0," +
Mms.CREATOR + " TEXT," +
Mms.TEXT_ONLY + " 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 AUTOINCREMENT," +
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);");
// Restricted view of pdu table, only sent/received messages without wap pushes
db.execSQL("CREATE VIEW " + MmsProvider.VIEW_PDU_RESTRICTED + " AS " +
"SELECT * FROM " + MmsProvider.TABLE_PDU + " WHERE " +
"(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX +
" OR " +
Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_SENT + ")" +
" AND " +
"(" + Mms.MESSAGE_TYPE + "!=" + PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND + ");");
}
// Unlike the other trigger-creating functions, this function can be called multiple times
// without harm.
private void createMmsTriggers(SQLiteDatabase db) {
// Cleans up parts when a MM is deleted.
db.execSQL("DROP TRIGGER IF EXISTS part_cleanup");
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("DROP TRIGGER IF EXISTS addr_cleanup");
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("DROP TRIGGER IF EXISTS cleanup_delivery_and_read_report");
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;");
db.execSQL("DROP TRIGGER IF EXISTS update_threads_on_insert_part");
db.execSQL(PART_UPDATE_THREADS_ON_INSERT_TRIGGER);
db.execSQL("DROP TRIGGER IF EXISTS update_threads_on_update_part");
db.execSQL(PART_UPDATE_THREADS_ON_UPDATE_TRIGGER);
db.execSQL("DROP TRIGGER IF EXISTS update_threads_on_delete_part");
db.execSQL(PART_UPDATE_THREADS_ON_DELETE_TRIGGER);
db.execSQL("DROP TRIGGER IF EXISTS update_threads_on_update_pdu");
db.execSQL(PDU_UPDATE_THREADS_ON_UPDATE_TRIGGER);
// Delete pending status for a message when it is deleted.
db.execSQL("DROP TRIGGER IF EXISTS delete_mms_pending_on_delete");
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;");
// When a message is moved out of Outbox, delete its pending status.
db.execSQL("DROP TRIGGER IF EXISTS delete_mms_pending_on_update");
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;");
// Insert pending status for M-Notification.ind or M-ReadRec.ind
// when they are inserted into Inbox/Outbox.
db.execSQL("DROP TRIGGER IF EXISTS insert_mms_pending_on_insert");
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("DROP TRIGGER IF EXISTS insert_mms_pending_on_update");
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;");
// monitor the mms table
db.execSQL("DROP TRIGGER IF EXISTS mms_words_update");
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("DROP TRIGGER IF EXISTS mms_words_delete");
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;");
// Updates threads table whenever a message in pdu is updated.
db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_date_subject_on_update");
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);
// Update threads table whenever a message in pdu is deleted
db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_delete");
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;");
// Updates threads table whenever a message is added to pdu.
db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_insert");
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 in pdu is updated.
db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_read_on_update");
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;");
// Update the error flag of threads when delete pending message.
db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_delete_mms");
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("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;");
}
@VisibleForTesting
public static String CREATE_SMS_TABLE_STRING =
"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," +
"sub_id INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID + ", " +
"error_code INTEGER DEFAULT " + NO_ERROR_CODE + ", " +
"creator TEXT," +
"seen INTEGER DEFAULT 0" +
");";
@VisibleForTesting
public static String CREATE_ATTACHMENTS_TABLE_STRING =
"CREATE TABLE attachments (" +
"sms_id INTEGER," +
"content_url TEXT," +
"offset INTEGER);";
/**
* This table is used by the SMS dispatcher to hold
* incomplete partial messages until all the parts arrive.
*/
@VisibleForTesting
public static String CREATE_RAW_TABLE_STRING =
"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," +
"sub_id INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID + ", " +
"pdu TEXT," + // the raw PDU for this part
"deleted INTEGER DEFAULT 0," + // bool to indicate if row is deleted
"message_body TEXT," + // message body
"display_originating_addr TEXT);";
// email address if from an email gateway, otherwise same as address
@VisibleForTesting
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_SMS_TABLE_STRING);
db.execSQL(CREATE_RAW_TABLE_STRING);
db.execSQL(CREATE_ATTACHMENTS_TABLE_STRING);
/**
* 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);");
// Restricted view of sms table, only sent/received messages
db.execSQL("CREATE VIEW " + SmsProvider.VIEW_SMS_RESTRICTED + " AS " +
"SELECT * FROM " + SmsProvider.TABLE_SMS + " WHERE " +
Sms.TYPE + "=" + Sms.MESSAGE_TYPE_INBOX +
" OR " +
Sms.TYPE + "=" + Sms.MESSAGE_TYPE_SENT + ";");
if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
// Create a table to keep track of changes to SMS table - specifically on update to read
// and deletion of msgs
db.execSQL("CREATE TABLE sms_changes (" +
"_id INTEGER PRIMARY KEY," +
"orig_rowid INTEGER," +
"sub_id INTEGER," +
"type INTEGER," +
"new_read_status INTEGER" +
");");
db.execSQL("CREATE TRIGGER sms_update_on_read_change_row " +
"AFTER UPDATE OF read ON sms WHEN NEW.read != OLD.read " +
"BEGIN " +
" INSERT INTO sms_changes VALUES(null, NEW._id, NEW.sub_id, " +
"0, NEW.read); " +
"END;");
db.execSQL("CREATE TRIGGER sms_delete_change_row " +
"AFTER DELETE ON sms " +
"BEGIN " +
" INSERT INTO sms_changes values(null, OLD._id, OLD.sub_id, 1, null); " +
"END;");
}
}
@VisibleForTesting
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.ARCHIVED + " INTEGER DEFAULT 0," +
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.SUBSCRIPTION_ID + " INTEGER DEFAULT " +
SubscriptionManager.INVALID_SUBSCRIPTION_ID + ", " +
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 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 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 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;");
// 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;");
// 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 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();
}
// fall through
case 55:
if (currentVersion <= 55) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion56(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 56:
if (currentVersion <= 56) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion57(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 57:
if (currentVersion <= 57) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion58(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 58:
if (currentVersion <= 58) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion59(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 59:
if (currentVersion <= 59) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion60(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 60:
if (currentVersion <= 60) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion61(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 61:
if (currentVersion <= 61) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion62(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 62:
if (currentVersion <= 62) {
return;
}
db.beginTransaction();
try {
// upgrade to 63: just add a happy little index.
createThreadIdDateIndex(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 63:
if (currentVersion <= 63) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion64(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 64:
if (currentVersion <= 64) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion65(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 65:
if (currentVersion <= 65) {
return;
}
db.beginTransaction();
try {
upgradeDatabaseToVersion66(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break;
} finally {
db.endTransaction();
}
// fall through
case 66:
if (currentVersion <= 66) {
return;
}
db.beginTransaction();
try {
createPartMidIndex(db);
createAddrMsgIdIndex(db);
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, ex.getMessage(), ex);
break; // force to destroy all old data;
} finally {
db.endTransaction();
}
// fall through
}
Log.e(TAG, "Destroying all old data.");
localLog("onUpgrade: Calling dropAll() and onCreate(). Upgrading database"
+ " from version " + oldVersion + " to " + currentVersion + "failed.");
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.
localLog("****DROPPING ALL SMS-MMS TABLES****");
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 " + NO_ERROR_CODE);
}
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");
}
private void upgradeDatabaseToVersion56(SQLiteDatabase db) {
// Add 'text_only' column to pdu table.
db.execSQL("ALTER TABLE " + MmsProvider.TABLE_PDU + " ADD COLUMN " + Mms.TEXT_ONLY +
" INTEGER DEFAULT 0");
}
private void upgradeDatabaseToVersion57(SQLiteDatabase db) {
// Clear out bad rows, those with empty threadIds, from the pdu table.
db.execSQL("DELETE FROM " + MmsProvider.TABLE_PDU + " WHERE " + Mms.THREAD_ID + " IS NULL");
}
private void upgradeDatabaseToVersion58(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + MmsProvider.TABLE_PDU +
" ADD COLUMN " + Mms.SUBSCRIPTION_ID
+ " INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID);
db.execSQL("ALTER TABLE " + MmsSmsProvider.TABLE_PENDING_MSG
+" ADD COLUMN " + "pending_sub_id"
+ " INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID);
db.execSQL("ALTER TABLE " + SmsProvider.TABLE_SMS
+ " ADD COLUMN " + Sms.SUBSCRIPTION_ID
+ " INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID);
db.execSQL("ALTER TABLE " + SmsProvider.TABLE_RAW
+" ADD COLUMN " + Sms.SUBSCRIPTION_ID
+ " INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID);
}
private void upgradeDatabaseToVersion59(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + MmsProvider.TABLE_PDU +" ADD COLUMN "
+ Mms.CREATOR + " TEXT");
db.execSQL("ALTER TABLE " + SmsProvider.TABLE_SMS +" ADD COLUMN "
+ Sms.CREATOR + " TEXT");
}
private void upgradeDatabaseToVersion60(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + MmsSmsProvider.TABLE_THREADS +" ADD COLUMN "
+ Threads.ARCHIVED + " INTEGER DEFAULT 0");
}
private void upgradeDatabaseToVersion61(SQLiteDatabase db) {
db.execSQL("CREATE VIEW " + SmsProvider.VIEW_SMS_RESTRICTED + " AS " +
"SELECT * FROM " + SmsProvider.TABLE_SMS + " WHERE " +
Sms.TYPE + "=" + Sms.MESSAGE_TYPE_INBOX +
" OR " +
Sms.TYPE + "=" + Sms.MESSAGE_TYPE_SENT + ";");
db.execSQL("CREATE VIEW " + MmsProvider.VIEW_PDU_RESTRICTED + " AS " +
"SELECT * FROM " + MmsProvider.TABLE_PDU + " WHERE " +
"(" + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_INBOX +
" OR " +
Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_SENT + ")" +
" AND " +
"(" + Mms.MESSAGE_TYPE + "!=" + PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND + ");");
}
private void upgradeDatabaseToVersion62(SQLiteDatabase db) {
// When a non-FBE device is upgraded to N, all MMS attachment files are moved from
// /data/data to /data/user_de. We need to update the paths stored in the parts table to
// reflect this change.
String newPartsDirPath;
try {
newPartsDirPath = mContext.getDir(MmsProvider.PARTS_DIR_NAME, 0).getCanonicalPath();
}
catch (IOException e){
Log.e(TAG, "openFile: check file path failed " + e, e);
return;
}
// The old path of the part files will be something like this:
// /data/data/0/com.android.providers.telephony/app_parts
// The new path of the part files will be something like this:
// /data/user_de/0/com.android.providers.telephony/app_parts
int partsDirIndex = newPartsDirPath.lastIndexOf(
File.separator, newPartsDirPath.lastIndexOf(MmsProvider.PARTS_DIR_NAME));
String partsDirName = newPartsDirPath.substring(partsDirIndex) + File.separator;
// The query to update the part path will be:
// UPDATE part SET _data = '/data/user_de/0/com.android.providers.telephony' ||
// SUBSTR(_data, INSTR(_data, '/app_parts/'))
// WHERE INSTR(_data, '/app_parts/') > 0
db.execSQL("UPDATE " + MmsProvider.TABLE_PART +
" SET " + Part._DATA + " = '" + newPartsDirPath.substring(0, partsDirIndex) + "' ||" +
" SUBSTR(" + Part._DATA + ", INSTR(" + Part._DATA + ", '" + partsDirName + "'))" +
" WHERE INSTR(" + Part._DATA + ", '" + partsDirName + "') > 0");
}
private void upgradeDatabaseToVersion64(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + SmsProvider.TABLE_RAW +" ADD COLUMN deleted INTEGER DEFAULT 0");
}
private void upgradeDatabaseToVersion65(SQLiteDatabase db) {
// aosp and internal code diverged at version 63. Aosp did createThreadIdDateIndex() on
// upgrading to 63, whereas internal (nyc) added column 'deleted'. A device upgrading from
// nyc will have columns deleted and message_body in raw table with version 64, but not
// createThreadIdDateIndex()
try {
db.execSQL("ALTER TABLE " + SmsProvider.TABLE_RAW + " ADD COLUMN message_body TEXT");
} catch (SQLiteException e) {
Log.w(TAG, "[upgradeDatabaseToVersion65] Exception adding column message_body; " +
"trying createThreadIdDateIndex() instead: " + e);
createThreadIdDateIndex(db);
}
}
private void upgradeDatabaseToVersion66(SQLiteDatabase db) {
try {
db.execSQL("ALTER TABLE " + SmsProvider.TABLE_RAW
+ " ADD COLUMN display_originating_addr TEXT");
} catch (SQLiteException e) {
Log.e(TAG, "[upgradeDatabaseToVersion66] Exception adding column "
+ "display_originating_addr; " + e);
}
}
@Override
public synchronized SQLiteDatabase getReadableDatabase() {
SQLiteDatabase db = super.getWritableDatabase();
// getReadableDatabase gets or creates a database. So we know for sure that a database has
// already been created at this point.
if (mContext.isCredentialProtectedStorage()) {
setInitialCreateDone();
}
return db;
}
@Override
public synchronized SQLiteDatabase getWritableDatabase() {
SQLiteDatabase db = super.getWritableDatabase();
// getWritableDatabase gets or creates a database. So we know for sure that a database has
// already been created at this point.
if (mContext.isCredentialProtectedStorage()) {
setInitialCreateDone();
}
if (!sTriedAutoIncrement) {
sTriedAutoIncrement = true;
boolean hasAutoIncrementThreads = hasAutoIncrement(db, MmsSmsProvider.TABLE_THREADS);
boolean hasAutoIncrementAddresses = hasAutoIncrement(db, "canonical_addresses");
boolean hasAutoIncrementPart = hasAutoIncrement(db, "part");
boolean hasAutoIncrementPdu = hasAutoIncrement(db, "pdu");
String logMsg = "[getWritableDatabase]" +
" hasAutoIncrementThreads: " + hasAutoIncrementThreads +
" hasAutoIncrementAddresses: " + hasAutoIncrementAddresses +
" hasAutoIncrementPart: " + hasAutoIncrementPart +
" hasAutoIncrementPdu: " + hasAutoIncrementPdu;
Log.d(TAG, logMsg);
localLog(logMsg);
boolean autoIncrementThreadsSuccess = true;
boolean autoIncrementAddressesSuccess = true;
boolean autoIncrementPartSuccess = true;
boolean autoIncrementPduSuccess = 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 (!hasAutoIncrementPart) {
db.beginTransaction();
try {
if (false && sFakeLowStorageTest) {
Log.d(TAG, "[getWritableDatabase] mFakeLowStorageTest is true " +
" - fake exception");
throw new Exception("FakeLowStorageTest");
}
upgradePartTableToAutoIncrement(db); // a no-op if already upgraded
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, "Failed to add autoIncrement to part: " +
ex.getMessage(), ex);
autoIncrementPartSuccess = false;
} finally {
db.endTransaction();
}
}
if (!hasAutoIncrementPdu) {
db.beginTransaction();
try {
if (false && sFakeLowStorageTest) {
Log.d(TAG, "[getWritableDatabase] mFakeLowStorageTest is true " +
" - fake exception");
throw new Exception("FakeLowStorageTest");
}
upgradePduTableToAutoIncrement(db); // a no-op if already upgraded
db.setTransactionSuccessful();
} catch (Throwable ex) {
Log.e(TAG, "Failed to add autoIncrement to pdu: " +
ex.getMessage(), ex);
autoIncrementPduSuccess = false;
} finally {
db.endTransaction();
}
}
if (autoIncrementThreadsSuccess &&
autoIncrementAddressesSuccess &&
autoIncrementPartSuccess &&
autoIncrementPduSuccess) {
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, MmsSmsProvider.TABLE_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;");
}
// upgradePartTableToAutoIncrement() is called to add the AUTOINCREMENT keyword to
// the part table. This could fail if the user has a lot of sound/video/picture attachments
// and not enough storage to make a copy of the part table.
// That's ok. This upgrade is optional. It'll be called again next time the device is rebooted.
private void upgradePartTableToAutoIncrement(SQLiteDatabase db) {
if (hasAutoIncrement(db, "part")) {
Log.d(TAG, "[MmsSmsDb] upgradePartTableToAutoIncrement: already upgraded");
return;
}
Log.d(TAG, "[MmsSmsDb] upgradePartTableToAutoIncrement: upgrading");
// Make the _id of the part table autoincrement so we never re-use ids
// Have to create a new temp part 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 part_temp (" +
Part._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
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("INSERT INTO part_temp SELECT * from part;");
db.execSQL("DROP TABLE part;");
db.execSQL("ALTER TABLE part_temp RENAME TO part;");
// part-related triggers get tossed when the part table is dropped -- rebuild them.
createMmsTriggers(db);
}
// upgradePduTableToAutoIncrement() is called to add the AUTOINCREMENT keyword to
// the pdu table. This could fail if the user has a lot of mms messages
// and not enough storage to make a copy of the pdu table.
// That's ok. This upgrade is optional. It'll be called again next time the device is rebooted.
private void upgradePduTableToAutoIncrement(SQLiteDatabase db) {
if (hasAutoIncrement(db, "pdu")) {
Log.d(TAG, "[MmsSmsDb] upgradePduTableToAutoIncrement: already upgraded");
return;
}
Log.d(TAG, "[MmsSmsDb] upgradePduTableToAutoIncrement: upgrading");
// Make the _id of the part table autoincrement so we never re-use ids
// Have to create a new temp part 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 pdu_temp (" +
Mms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
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.SUBSCRIPTION_ID + " INTEGER DEFAULT "
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID + ", " +
Mms.SEEN + " INTEGER DEFAULT 0," +
Mms.TEXT_ONLY + " INTEGER DEFAULT 0" +
");");
db.execSQL("INSERT INTO pdu_temp SELECT * from pdu;");
db.execSQL("DROP TABLE pdu;");
db.execSQL("ALTER TABLE pdu_temp RENAME TO pdu;");
// pdu-related triggers get tossed when the part table is dropped -- rebuild them.
createMmsTriggers(db);
}
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')");
}
}