diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7631950..06372d3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -24,7 +24,7 @@ @@ -33,6 +33,13 @@ android:exported="true" android:multiprocess="false" /> + + + + + + + + + diff --git a/src/com/android/providers/telephony/BlacklistProvider.java b/src/com/android/providers/telephony/BlacklistProvider.java new file mode 100644 index 0000000..44b3c92 --- /dev/null +++ b/src/com/android/providers/telephony/BlacklistProvider.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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 android.app.backup.BackupManager; +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.provider.Telephony.Blacklist; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; + +public class BlacklistProvider extends ContentProvider { + private static final String TAG = "BlacklistProvider"; + private static final boolean DEBUG = true; + + private static final String DATABASE_NAME = "blacklist.db"; + private static final int DATABASE_VERSION = 3; + + private static final String BLACKLIST_TABLE = "blacklist"; + private static final String COLUMN_NORMALIZED = "normalized_number"; + + private static final int BL_ALL = 0; + private static final int BL_ID = 1; + private static final int BL_NUMBER = 2; + private static final int BL_PHONE = 3; + private static final int BL_MESSAGE = 4; + + private static final UriMatcher + sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + static { + sURIMatcher.addURI("blacklist", null, BL_ALL); + sURIMatcher.addURI("blacklist", "#", BL_ID); + sURIMatcher.addURI("blacklist", "bynumber/*", BL_NUMBER); + sURIMatcher.addURI("blacklist", "phone", BL_PHONE); + sURIMatcher.addURI("blacklist", "message", BL_MESSAGE); + } + + private static class DatabaseHelper extends SQLiteOpenHelper { + // Context to access resources with + private Context mContext; + + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + mContext = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + // Set up the database schema + db.execSQL("CREATE TABLE " + BLACKLIST_TABLE + + "(_id INTEGER PRIMARY KEY," + + "number TEXT," + + "normalized_number TEXT," + + "is_regex INTEGER," + + "phone INTEGER DEFAULT 0," + + "message INTEGER DEFAULT 0);"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2) { + // drop the uniqueness constraint that was present on the DB in V1 + db.execSQL("ALTER TABLE " + BLACKLIST_TABLE + + " RENAME TO " + BLACKLIST_TABLE + "_old;"); + onCreate(db); + db.execSQL("INSERT INTO " + BLACKLIST_TABLE + + " SELECT * FROM " + BLACKLIST_TABLE + "_old;"); + } + + if (oldVersion < 3) { + // update the normalized number column, v1 and v2 didn't handle + // alphanumeric 'numbers' correctly + + Cursor rows = db.query(BLACKLIST_TABLE, + new String[] { Blacklist._ID, Blacklist.NUMBER }, + null, null, null, null, null); + + try { + db.beginTransaction(); + if (rows != null) { + ContentValues cv = new ContentValues(); + String[] rowId = new String[1]; + + while (rows.moveToNext()) { + rowId[0] = rows.getString(0); + cv.clear(); + cv.put(COLUMN_NORMALIZED, normalizeNumber(rows.getString(1))); + db.update(BLACKLIST_TABLE, cv, Blacklist._ID + "= ?", rowId); + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + if (rows != null) { + rows.close(); + } + } + } + } + } + + private DatabaseHelper mOpenHelper; + private BackupManager mBackupManager; + + @Override + public boolean onCreate() { + mOpenHelper = new DatabaseHelper(getContext()); + mBackupManager = new BackupManager(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + + qb.setTables(BLACKLIST_TABLE); + + // Generate the body of the query. + int match = sURIMatcher.match(uri); + if (DEBUG) { + Log.v(TAG, "Query uri=" + uri + ", match=" + match); + } + + switch (match) { + case BL_ALL: + break; + case BL_ID: + qb.appendWhere(Blacklist._ID + " = " + uri.getLastPathSegment()); + break; + case BL_NUMBER: { + String number = normalizeNumber(uri.getLastPathSegment()); + boolean regex = uri.getBooleanQueryParameter(Blacklist.REGEX_KEY, false); + + if (regex) { + qb.appendWhere("\"" + number + "\" like " + COLUMN_NORMALIZED); + } else { + qb.appendWhere(COLUMN_NORMALIZED + " = \"" + number + "\""); + } + break; + } + case BL_PHONE: + qb.appendWhere(Blacklist.PHONE_MODE + " != 0"); + break; + case BL_MESSAGE: + qb.appendWhere(Blacklist.MESSAGE_MODE + " != 0"); + break; + default: + Log.e(TAG, "query: invalid request: " + uri); + return null; + } + + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = Blacklist.DEFAULT_SORT_ORDER; + } + + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); + + ret.setNotificationUri(getContext().getContentResolver(), uri); + + return ret; + } + + @Override + public String getType(Uri uri) { + switch (sURIMatcher.match(uri)) { + case BL_ALL: + case BL_PHONE: + case BL_MESSAGE: + return "vnd.android.cursor.dir/blacklist-entry"; + case BL_ID: + case BL_NUMBER: + return "vnd.android.cursor.item/blacklist-entry"; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + } + + @Override + public Uri insert(Uri uri, ContentValues initialValues) { + int match = sURIMatcher.match(uri); + if (DEBUG) { + Log.v(TAG, "Insert uri=" + uri + ", match=" + match); + } + + if (match != BL_ALL) { + return null; + } + + ContentValues values = validateAndPrepareContentValues(initialValues, null); + if (values == null) { + Log.e(TAG, "Invalid insert values " + initialValues); + return null; + } + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long rowID = db.insert(BLACKLIST_TABLE, null, values); + if (rowID <= 0) { + return null; + } + + if (DEBUG) Log.d(TAG, "inserted " + values + " rowID = " + rowID); + notifyChange(); + + return ContentUris.withAppendedId(Blacklist.CONTENT_URI, rowID); + } + + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + int count = 0; + int match = sURIMatcher.match(uri); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + + if (DEBUG) { + Log.v(TAG, "Delete uri=" + uri + ", match=" + match); + } + + switch (match) { + case BL_ALL: + break; + case BL_ID: + if (where != null || whereArgs != null) { + throw new UnsupportedOperationException( + "Cannot delete URI " + uri + " with a where clause"); + } + where = Blacklist._ID + " = ?"; + whereArgs = new String[] { uri.getLastPathSegment() }; + break; + case BL_NUMBER: + if (where != null || whereArgs != null) { + throw new UnsupportedOperationException( + "Cannot delete URI " + uri + " with a where clause"); + } + where = COLUMN_NORMALIZED + " = ?"; + whereArgs = new String[] { normalizeNumber(uri.getLastPathSegment()) }; + break; + default: + throw new UnsupportedOperationException("Cannot delete that URI: " + uri); + } + + count = db.delete(BLACKLIST_TABLE, where, whereArgs); + if (DEBUG) Log.d(TAG, "delete result count " + count); + + if (count > 0) { + notifyChange(); + } + + return count; + } + + @Override + public int update(Uri uri, ContentValues initialValues, String where, String[] whereArgs) { + int count = 0; + int match = sURIMatcher.match(uri); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + + if (DEBUG) { + Log.v(TAG, "Update uri=" + uri + ", match=" + match); + } + + String uriNumber = match == BL_NUMBER ? uri.getLastPathSegment() : null; + ContentValues values = validateAndPrepareContentValues(initialValues, uriNumber); + if (values == null) { + Log.e(TAG, "Invalid update values " + initialValues); + return 0; + } + + switch (match) { + case BL_ALL: + count = db.update(BLACKLIST_TABLE, values, where, whereArgs); + break; + case BL_NUMBER: + if (where != null || whereArgs != null) { + throw new UnsupportedOperationException( + "Cannot update URI " + uri + " with a where clause"); + } + db.beginTransaction(); + try { + count = db.update(BLACKLIST_TABLE, values, COLUMN_NORMALIZED + " = ?", + new String[] { normalizeNumber(uriNumber) }); + if (count == 0) { + // convenience: fall back to insert if number wasn't present + if (db.insert(BLACKLIST_TABLE, null, values) > 0) { + count = 1; + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + break; + case BL_ID: + if (where != null || whereArgs != null) { + throw new UnsupportedOperationException( + "Cannot update URI " + uri + " with a where clause"); + } + count = db.update(BLACKLIST_TABLE, values, Blacklist._ID + " = ?", + new String[] { uri.getLastPathSegment() }); + break; + default: + throw new UnsupportedOperationException("Cannot update that URI: " + uri); + } + + if (DEBUG) Log.d(TAG, "Update result count " + count); + + if (count > 0) { + notifyChange(); + } + + return count; + } + + private ContentValues validateAndPrepareContentValues( + ContentValues initialValues, String uriNumber) { + ContentValues values = new ContentValues(initialValues); + + // apps are not supposed to mess with the normalized number or the regex state + values.remove(COLUMN_NORMALIZED); + values.remove(Blacklist.IS_REGEX); + + // on 'upsert', insert the number passed via URI if no other number was specified + if (uriNumber != null && !values.containsKey(Blacklist.NUMBER)) { + values.put(Blacklist.NUMBER, uriNumber); + } + + if (values.containsKey(Blacklist.NUMBER)) { + String number = values.getAsString(Blacklist.NUMBER); + if (TextUtils.isEmpty(number)) { + return null; + } + + String normalizedNumber = normalizeNumber(number); + boolean isRegex = normalizedNumber.indexOf('%') >= 0 + || normalizedNumber.indexOf('_') >= 0; + + values.put(COLUMN_NORMALIZED, normalizedNumber); + values.put(Blacklist.IS_REGEX, isRegex ? 1 : 0); + } + + return values; + } + + private void notifyChange() { + getContext().getContentResolver().notifyChange(Blacklist.CONTENT_URI, null); + mBackupManager.dataChanged(); + } + + // mostly a copy of PhoneNumberUtils.normalizeNumber, + // with the exception of support for regex characters + private static String normalizeNumber(String number) { + int len = number.length(); + StringBuilder ret = new StringBuilder(len); + + for (int i = 0; i < len; i++) { + char c = number.charAt(i); + // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.) + int digit = Character.digit(c, 10); + if (digit != -1) { + ret.append(digit); + } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(number)); + } else if (i == 0 && c == '+') { + ret.append(c); + } else if (c == '*') { + // replace regex match-multiple character by SQL equivalent + ret.append('%'); + } else if (c == '.') { + // replace regex-match-single character by SQL equivalent + ret.append('_'); + } + } + + return ret.toString(); + } +} diff --git a/src/com/android/providers/telephony/MmsProvider.java b/src/com/android/providers/telephony/MmsProvider.java index ac7c1f9..8094641 100644 --- a/src/com/android/providers/telephony/MmsProvider.java +++ b/src/com/android/providers/telephony/MmsProvider.java @@ -48,6 +48,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.HashSet; import android.provider.Telephony.Threads; /** @@ -595,6 +596,10 @@ public int delete(Uri uri, String selection, selectionArgs, uri); } else if (TABLE_PART.equals(table)) { deletedRows = deleteParts(db, finalSelection, selectionArgs); + // Because the trigger for part table is deleted + // We need to update word and threads table after delete sth in part table + cleanUpWords(db); + updateHasAttachment(db); } else if (TABLE_DRM.equals(table)) { deletedRows = deleteTempDrmData(db, finalSelection, selectionArgs); } else { @@ -609,12 +614,13 @@ public int delete(Uri uri, String selection, static int deleteMessages(Context context, SQLiteDatabase db, String selection, String[] selectionArgs, Uri uri) { - Cursor cursor = db.query(TABLE_PDU, new String[] { Mms._ID }, + Cursor cursor = db.query(TABLE_PDU, new String[] { Mms._ID, Mms.THREAD_ID }, selection, selectionArgs, null, null, null); if (cursor == null) { return 0; } + HashSet threadIds = new HashSet(); try { if (cursor.getCount() == 0) { return 0; @@ -623,12 +629,22 @@ static int deleteMessages(Context context, SQLiteDatabase db, while (cursor.moveToNext()) { deleteParts(db, Part.MSG_ID + " = ?", new String[] { String.valueOf(cursor.getLong(0)) }); + threadIds.add(cursor.getLong(1)); } + // Because the trigger for part table is deleted + // We need to update word and threads table after delete sth in part table + cleanUpWords(db); + updateHasAttachment(db); } finally { cursor.close(); } int count = db.delete(TABLE_PDU, selection, selectionArgs); + // The triggers used to update threads after delete pdu is deleted + // Remenber to update threads which related to the deleted pdus + for (long thread : threadIds) { + MmsSmsDatabaseHelper.updateThread(db, thread); + } if (count > 0) { Intent intent = new Intent(Mms.Intents.CONTENT_CHANGED_ACTION); intent.putExtra(Mms.Intents.DELETED_CONTENTS, uri); @@ -640,6 +656,18 @@ static int deleteMessages(Context context, SQLiteDatabase db, return count; } + private static void cleanUpWords(SQLiteDatabase db) { + db.execSQL("DELETE FROM words WHERE source_id not in (select _id from part) AND " + + "table_to_use = 2"); + } + + private static void updateHasAttachment(SQLiteDatabase db) { + db.execSQL("UPDATE threads SET has_attachment = CASE " + + "(SELECT COUNT(*) FROM part JOIN pdu WHERE part.mid = pdu._id AND " + + "pdu.thread_id = threads._id AND part.ct != 'text/plain' " + + "AND part.ct != 'application/smil') WHEN 0 THEN 0 ELSE 1 END"); + } + private static int deleteParts(SQLiteDatabase db, String selection, String[] selectionArgs) { return deleteDataRows(db, TABLE_PART, selection, selectionArgs); diff --git a/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java b/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java index 4f23be8..ed01a95 100644 --- a/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java +++ b/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java @@ -74,19 +74,6 @@ public class MmsSmsDatabaseHelper extends SQLiteOpenHelper { " 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" + @@ -136,20 +123,6 @@ public class MmsSmsDatabaseHelper extends SQLiteOpenHelper { 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. @@ -176,28 +149,6 @@ public class MmsSmsDatabaseHelper extends SQLiteOpenHelper { " 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 = @@ -215,7 +166,7 @@ public class MmsSmsDatabaseHelper extends SQLiteOpenHelper { private static boolean sFakeLowStorageTest = false; // for testing only static final String DATABASE_NAME = "mmssms.db"; - static final int DATABASE_VERSION = 57; + static final int DATABASE_VERSION = 60; private final Context mContext; private LowStorageMonitor mLowStorageMonitor; @@ -546,6 +497,7 @@ private void createWordsTables(SQLiteDatabase db) { private void createIndices(SQLiteDatabase db) { createThreadIdIndex(db); + createPduPartIndex(db); } private void createThreadIdIndex(SQLiteDatabase db) { @@ -557,6 +509,15 @@ private void createThreadIdIndex(SQLiteDatabase db) { } } + private void createPduPartIndex(SQLiteDatabase db) { + try { + db.execSQL("CREATE INDEX IF NOT EXISTS index_part ON " + MmsProvider.TABLE_PART + + " (mid);"); + } 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. @@ -593,7 +554,8 @@ private void createMmsTables(SQLiteDatabase db) { Mms.DELIVERY_REPORT + " INTEGER," + Mms.LOCKED + " INTEGER DEFAULT 0," + Mms.SEEN + " INTEGER DEFAULT 0," + - Mms.TEXT_ONLY + " INTEGER DEFAULT 0" + + Mms.TEXT_ONLY + " INTEGER DEFAULT 0," + + "sub_id INTEGER DEFAULT 0" + ");"); db.execSQL("CREATE TABLE " + MmsProvider.TABLE_ADDR + " (" + @@ -667,9 +629,6 @@ private void createMmsTriggers(SQLiteDatabase db) { 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); @@ -757,18 +716,6 @@ private void createMmsTriggers(SQLiteDatabase db) { 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 " + @@ -835,8 +782,11 @@ private void createSmsTables(SQLiteDatabase db) { "body TEXT," + "service_center TEXT," + "locked INTEGER DEFAULT 0," + + "sub_id INTEGER DEFAULT 0," + // sub_id : 0 for subscription 1 + // sub_id : 1 for subscription 2 "error_code INTEGER DEFAULT 0," + - "seen INTEGER DEFAULT 0" + + "seen INTEGER DEFAULT 0," + + "pri INTEGER DEFAULT -1" + ");"); /** @@ -1273,6 +1223,45 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { } finally { db.endTransaction(); } + // fall through + case 57: + if (currentVersion <= 57) { + return; + } + // skip version 58 + // fall through + case 58: + if (currentVersion <= 58) { + return; + } + + db.beginTransaction(); + try { + upgradeDatabaseToVersion59(db); + } catch (Throwable ex) { + Log.e(TAG, ex.getMessage(), ex); + // OK to fail here, this might be present already + } finally { + db.setTransactionSuccessful(); + 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(); + } + return; } @@ -1328,7 +1317,6 @@ private void upgradeDatabaseToVersion43(SQLiteDatabase 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) { @@ -1474,6 +1462,20 @@ private void upgradeDatabaseToVersion57(SQLiteDatabase db) { db.execSQL("DELETE FROM " + MmsProvider.TABLE_PDU + " WHERE " + Mms.THREAD_ID + " IS NULL"); } + private void upgradeDatabaseToVersion59(SQLiteDatabase db) { + // Add 'sub_id' column to pdu table. + db.execSQL("ALTER TABLE " + MmsProvider.TABLE_PDU + " ADD COLUMN sub_id" + + " INTEGER DEFAULT 0"); + db.execSQL("ALTER TABLE " + SmsProvider.TABLE_SMS + " ADD COLUMN sub_id" + + " INTEGER DEFAULT 0"); + } + + private void upgradeDatabaseToVersion60(SQLiteDatabase db) { + // Add priority column + db.execSQL("ALTER TABLE " + SmsProvider.TABLE_SMS + " ADD COLUMN pri" + + " INTEGER DEFAULT -1"); + } + @Override public synchronized SQLiteDatabase getWritableDatabase() { SQLiteDatabase db = super.getWritableDatabase(); @@ -1752,7 +1754,8 @@ private void upgradePduTableToAutoIncrement(SQLiteDatabase db) { Mms.DELIVERY_REPORT + " INTEGER," + Mms.LOCKED + " INTEGER DEFAULT 0," + Mms.SEEN + " INTEGER DEFAULT 0," + - Mms.TEXT_ONLY + " INTEGER DEFAULT 0" + + Mms.TEXT_ONLY + " INTEGER DEFAULT 0," + + "sub_id INTEGER DEFAULT 0" + ");"); db.execSQL("INSERT INTO pdu_temp SELECT * from pdu;"); diff --git a/src/com/android/providers/telephony/MmsSmsProvider.java b/src/com/android/providers/telephony/MmsSmsProvider.java index a15335f..347e246 100644 --- a/src/com/android/providers/telephony/MmsSmsProvider.java +++ b/src/com/android/providers/telephony/MmsSmsProvider.java @@ -134,7 +134,7 @@ public class MmsSmsProvider extends ContentProvider { // table. private static final String[] SMS_ONLY_COLUMNS = { "address", "body", "person", "reply_path_present", - "service_center", "status", "subject", "type", "error_code" }; + "service_center", "status", "subject", "type", "error_code", "pri" }; // These are all the columns that appear in the "threads" table. private static final String[] THREADS_COLUMNS = { diff --git a/src/com/android/providers/telephony/SmsProvider.java b/src/com/android/providers/telephony/SmsProvider.java index c36a0d2..28fb970 100644 --- a/src/com/android/providers/telephony/SmsProvider.java +++ b/src/com/android/providers/telephony/SmsProvider.java @@ -607,7 +607,8 @@ public int delete(Uri url, String where, String[] whereArgs) { */ private int deleteMessageFromIcc(String messageIndexString) { SmsManager smsManager = SmsManager.getDefault(); - + // use phone app permissions to avoid UID mismatch in AppOpsManager.noteOp() call + long token = Binder.clearCallingIdentity(); try { return smsManager.deleteMessageFromIcc( Integer.parseInt(messageIndexString)) @@ -619,6 +620,7 @@ private int deleteMessageFromIcc(String messageIndexString) { ContentResolver cr = getContext().getContentResolver(); cr.notifyChange(ICC_URI, null); + Binder.restoreCallingIdentity(token); } } diff --git a/src/com/android/providers/telephony/TelephonyBackupAgent.java b/src/com/android/providers/telephony/TelephonyBackupAgent.java new file mode 100644 index 0000000..677efad --- /dev/null +++ b/src/com/android/providers/telephony/TelephonyBackupAgent.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2013 The CyanogenMod 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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; +import java.util.zip.CRC32; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import android.app.backup.BackupDataInput; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupAgentHelper; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.Telephony.Blacklist; +import android.text.TextUtils; +import android.util.Log; + +/** + * Performs backup and restore of the telephony data. + * At the moment this is restricted to the blacklist data. + */ +public class TelephonyBackupAgent extends BackupAgentHelper { + private static final String TAG = "TelephonyBackupAgent"; + + private static final String KEY_BLACKLIST = "blacklist"; + + private static final int STATE_BLACKLIST = 0; + private static final int STATE_SIZE = 1; + + private static final String SEPARATOR = "|"; + + private static final byte[] EMPTY_DATA = new byte[0]; + + private static final int COLUMN_NUMBER = 0; + private static final int COLUMN_PHONE_MODE = 1; + private static final int COLUMN_MESSAGE_MODE = 2; + + private static final String[] PROJECTION = { + Blacklist.NUMBER, + Blacklist.PHONE_MODE, + Blacklist.MESSAGE_MODE + }; + + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + byte[] blacklistData = getBlacklist(); + long[] stateChecksums = readOldChecksums(oldState); + + stateChecksums[STATE_BLACKLIST] = + writeIfChanged(stateChecksums[STATE_BLACKLIST], KEY_BLACKLIST, + blacklistData, data); + + writeNewChecksums(stateChecksums, newState); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, + ParcelFileDescriptor newState) throws IOException { + while (data.readNextHeader()) { + final String key = data.getKey(); + final int size = data.getDataSize(); + if (KEY_BLACKLIST.equals(key)) { + restoreBlacklist(data); + } else { + data.skipEntityData(); + } + } + } + + private long[] readOldChecksums(ParcelFileDescriptor oldState) throws IOException { + long[] stateChecksums = new long[STATE_SIZE]; + + DataInputStream dataInput = new DataInputStream( + new FileInputStream(oldState.getFileDescriptor())); + for (int i = 0; i < STATE_SIZE; i++) { + try { + stateChecksums[i] = dataInput.readLong(); + } catch (EOFException eof) { + break; + } + } + dataInput.close(); + return stateChecksums; + } + + private void writeNewChecksums(long[] checksums, ParcelFileDescriptor newState) + throws IOException { + DataOutputStream dataOutput = new DataOutputStream( + new FileOutputStream(newState.getFileDescriptor())); + for (int i = 0; i < STATE_SIZE; i++) { + dataOutput.writeLong(checksums[i]); + } + dataOutput.close(); + } + + private long writeIfChanged(long oldChecksum, String key, byte[] data, + BackupDataOutput output) { + CRC32 checkSummer = new CRC32(); + checkSummer.update(data); + long newChecksum = checkSummer.getValue(); + if (oldChecksum == newChecksum) { + return oldChecksum; + } + try { + output.writeEntityHeader(key, data.length); + output.writeEntityData(data, data.length); + } catch (IOException ioe) { + // Bail + } + return newChecksum; + } + + private byte[] getBlacklist() { + Cursor cursor = getContentResolver().query(Blacklist.CONTENT_URI, PROJECTION, + null, null, Blacklist.DEFAULT_SORT_ORDER); + if (cursor == null) { + return EMPTY_DATA; + } + if (!cursor.moveToFirst()) { + Log.e(TAG, "Couldn't read from the cursor"); + cursor.close(); + return EMPTY_DATA; + } + + byte[] sizeBytes = new byte[4]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(cursor.getCount() * 20); + try { + GZIPOutputStream gzip = new GZIPOutputStream(baos); + while (!cursor.isAfterLast()) { + String number = cursor.getString(COLUMN_NUMBER); + int phoneMode = cursor.getInt(COLUMN_PHONE_MODE); + int messageMode = cursor.getInt(COLUMN_MESSAGE_MODE); + // TODO: escape the string + String out = number + SEPARATOR + phoneMode + SEPARATOR + messageMode; + byte[] line = out.getBytes(); + writeInt(sizeBytes, 0, line.length); + gzip.write(sizeBytes); + gzip.write(line); + cursor.moveToNext(); + } + gzip.finish(); + } catch (IOException ioe) { + Log.e(TAG, "Couldn't compress the blacklist", ioe); + return EMPTY_DATA; + } finally { + cursor.close(); + } + return baos.toByteArray(); + } + + private void restoreBlacklist(BackupDataInput data) { + ContentValues cv = new ContentValues(2); + byte[] blacklistCompressed = new byte[data.getDataSize()]; + byte[] blacklist = null; + try { + data.readEntityData(blacklistCompressed, 0, blacklistCompressed.length); + GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(blacklistCompressed)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] tempData = new byte[1024]; + int got; + while ((got = gzip.read(tempData)) > 0) { + baos.write(tempData, 0, got); + } + gzip.close(); + blacklist = baos.toByteArray(); + } catch (IOException ioe) { + Log.e(TAG, "Couldn't read and uncompress entity data", ioe); + return; + } + + int pos = 0; + while (pos + 4 < blacklist.length) { + int length = readInt(blacklist, pos); + pos += 4; + if (pos + length > blacklist.length) { + Log.e(TAG, "Insufficient data"); + } + String line = new String(blacklist, pos, length); + pos += length; + // TODO: unescape the string + StringTokenizer st = new StringTokenizer(line, SEPARATOR); + try { + String number = st.nextToken(); + int phoneMode = Integer.parseInt(st.nextToken()); + int messageMode = Integer.parseInt(st.nextToken()); + + if (!TextUtils.isEmpty(number)) { + cv.clear(); + cv.put(Blacklist.NUMBER, number); + cv.put(Blacklist.PHONE_MODE, phoneMode); + cv.put(Blacklist.MESSAGE_MODE, messageMode); + + Uri uri = Blacklist.CONTENT_FILTER_BYNUMBER_URI.buildUpon() + .appendPath(number).build(); + getContentResolver().update(uri, cv, null, null); + } + } catch (NoSuchElementException nsee) { + Log.e(TAG, "Token format error\n" + nsee); + } catch (NumberFormatException nfe) { + Log.e(TAG, "Number format error\n" + nfe); + } + } + } + + /** + * Write an int in BigEndian into the byte array. + * @param out byte array + * @param pos current pos in array + * @param value integer to write + * @return the index after adding the size of an int (4) + */ + private int writeInt(byte[] out, int pos, int value) { + out[pos + 0] = (byte) ((value >> 24) & 0xFF); + out[pos + 1] = (byte) ((value >> 16) & 0xFF); + out[pos + 2] = (byte) ((value >> 8) & 0xFF); + out[pos + 3] = (byte) ((value >> 0) & 0xFF); + return pos + 4; + } + + private int readInt(byte[] in, int pos) { + int result = + ((in[pos ] & 0xFF) << 24) | + ((in[pos + 1] & 0xFF) << 16) | + ((in[pos + 2] & 0xFF) << 8) | + ((in[pos + 3] & 0xFF) << 0); + return result; + } +} diff --git a/src/com/android/providers/telephony/TelephonyProvider.java b/src/com/android/providers/telephony/TelephonyProvider.java index b67aac0..89f6dc4 100644 --- a/src/com/android/providers/telephony/TelephonyProvider.java +++ b/src/com/android/providers/telephony/TelephonyProvider.java @@ -33,6 +33,7 @@ import android.net.Uri; import android.os.Environment; import android.provider.Telephony; +import android.text.TextUtils; import android.util.Log; import android.util.Xml; @@ -55,7 +56,7 @@ public class TelephonyProvider extends ContentProvider private static final String DATABASE_NAME = "telephony.db"; private static final boolean DBG = true; - private static final int DATABASE_VERSION = 8 << 16; + private static final int DATABASE_VERSION = 9 << 16; private static final int URL_TELEPHONY = 1; private static final int URL_CURRENT = 2; private static final int URL_ID = 3; @@ -152,6 +153,24 @@ public void onCreate(SQLiteDatabase db) { initDatabase(db); } + private int getDefaultPreferredApnId(SQLiteDatabase db) { + int id = -1; + String configPref = mContext.getResources().getString(R.string.config_preferred_apn, ""); + if (!TextUtils.isEmpty(configPref)) { + String[] s = configPref.split(","); + if (s.length == 3) { + Cursor c = db.query("carriers", new String[] { "_id" }, + "apn='" + s[0] + "' AND mcc='" + s[1] + "' AND mnc='" + s[2] + "'", + null, null, null, null); + if (c.moveToFirst()) { + id = c.getInt(0); + } + c.close(); + } + } + return id; + } + private void initDatabase(SQLiteDatabase db) { // Read internal APNS data Resources r = mContext.getResources(); @@ -241,6 +260,12 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { " ADD COLUMN mvno_match_data TEXT DEFAULT '';"); oldVersion = 8 << 16 | 6; } + if (oldVersion < (9 << 16 | 6)) { + // Add preferred field to the APN. The XML file does not change. + db.execSQL("ALTER TABLE " + CARRIERS_TABLE + + " ADD COLUMN preferred BOOLEAN DEFAULT 0;"); + oldVersion = 9 << 16 | 6; + } } /** @@ -403,7 +428,33 @@ private void setPreferredApnId(Long id) { private long getPreferredApnId() { SharedPreferences sp = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); - return sp.getLong(COLUMN_APN_ID, -1); + long id = sp.getLong(COLUMN_APN_ID, -1); + if (id == -1) { + id = getDefaultPreferredApnId(); + if (id > -1) { + setPreferredApnId(id); + } + } + return id; + } + + private long getDefaultPreferredApnId() { + long id = -1; + String configPref = getContext().getResources().getString(R.string.config_preferred_apn, ""); + if (!TextUtils.isEmpty(configPref)) { + String[] s = configPref.split(","); + if (s.length == 3) { + Cursor c = mOpenHelper.getReadableDatabase().query("carriers", new String[] { "_id" }, + "apn='" + s[0] + "' AND mcc='" + s[1] + "' AND mnc='" + s[2] + "'", + null, null, null, null); + if (c.moveToFirst()) { + id = c.getLong(0); + } + c.close(); + } + } + Log.d(TAG, "Preferred APN: " + id); + return id; } @Override @@ -748,5 +799,6 @@ private void restoreDefaultAPN() { } setPreferredApnId((long)-1); mOpenHelper.initDatabase(db); + setPreferredApnId(getDefaultPreferredApnId()); } }