blob: 2946fba6f996f07fe78b2201921ddbab2ee77306 [file] [log] [blame]
/*
* Copyright (C) 2011 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.cellbroadcastreceiver;
import static java.nio.file.Files.copy;
import android.annotation.NonNull;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.provider.Telephony;
import android.provider.Telephony.CellBroadcasts;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.io.File;
/**
* Open, create, and upgrade the cell broadcast SQLite database. Previously an inner class of
* {@code CellBroadcastDatabase}, this is now a top-level class. The column definitions in
* {@code CellBroadcastDatabase} have been moved to {@link Telephony.CellBroadcasts} in the
* framework, to simplify access to this database from third-party apps.
*/
public class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "CellBroadcastDatabaseHelper";
/**
* Database version 1: initial version (support removed)
* Database version 2-9: (reserved for OEM database customization) (support removed)
* Database version 10: adds ETWS and CMAS columns and CDMA support (support removed)
* Database version 11: adds delivery time index
* Database version 12: add slotIndex
* Database version 13: add smsSyncPending
*/
private static final int DATABASE_VERSION = 13;
private static final String OLD_DATABASE_NAME = "cell_broadcasts.db";
private static final String DATABASE_NAME_V13 = "cell_broadcasts_v13.db";
@VisibleForTesting
public static final String TABLE_NAME = "broadcasts";
// Preference key for whether the data migration from pre-R CBR app was complete.
public static final String KEY_LEGACY_DATA_MIGRATION = "legacy_data_migration";
/**
* Is the message pending for sms synchronization.
* when received cellbroadcast message in direct boot mode, we will retry synchronizing
* alert message to sms inbox after user unlock if needed.
* <P>Type: Boolean</P>
*/
public static final String SMS_SYNC_PENDING = "isSmsSyncPending";
/*
* Query columns for instantiating SmsCbMessage.
*/
public static final String[] QUERY_COLUMNS = {
Telephony.CellBroadcasts._ID,
Telephony.CellBroadcasts.SLOT_INDEX,
Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE,
Telephony.CellBroadcasts.PLMN,
Telephony.CellBroadcasts.LAC,
Telephony.CellBroadcasts.CID,
Telephony.CellBroadcasts.SERIAL_NUMBER,
Telephony.CellBroadcasts.SERVICE_CATEGORY,
Telephony.CellBroadcasts.LANGUAGE_CODE,
Telephony.CellBroadcasts.MESSAGE_BODY,
Telephony.CellBroadcasts.DELIVERY_TIME,
Telephony.CellBroadcasts.MESSAGE_READ,
Telephony.CellBroadcasts.MESSAGE_FORMAT,
Telephony.CellBroadcasts.MESSAGE_PRIORITY,
Telephony.CellBroadcasts.ETWS_WARNING_TYPE,
Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS,
Telephony.CellBroadcasts.CMAS_CATEGORY,
Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE,
Telephony.CellBroadcasts.CMAS_SEVERITY,
Telephony.CellBroadcasts.CMAS_URGENCY,
Telephony.CellBroadcasts.CMAS_CERTAINTY
};
/**
* Returns a string used to create the cell broadcast table. This is exposed so the unit test
* can construct its own in-memory database to match the cell broadcast db.
*/
@VisibleForTesting
public static String getStringForCellBroadcastTableCreation(String tableName) {
return "CREATE TABLE " + tableName + " ("
+ CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
+ CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
+ CellBroadcasts.PLMN + " TEXT,"
+ CellBroadcasts.LAC + " INTEGER,"
+ CellBroadcasts.CID + " INTEGER,"
+ CellBroadcasts.SERIAL_NUMBER + " INTEGER,"
+ Telephony.CellBroadcasts.SERVICE_CATEGORY + " INTEGER,"
+ Telephony.CellBroadcasts.LANGUAGE_CODE + " TEXT,"
+ Telephony.CellBroadcasts.MESSAGE_BODY + " TEXT,"
+ Telephony.CellBroadcasts.DELIVERY_TIME + " INTEGER,"
+ Telephony.CellBroadcasts.MESSAGE_READ + " INTEGER,"
+ Telephony.CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
+ Telephony.CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
+ Telephony.CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
+ Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER,"
+ Telephony.CellBroadcasts.CMAS_CATEGORY + " INTEGER,"
+ Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER,"
+ Telephony.CellBroadcasts.CMAS_SEVERITY + " INTEGER,"
+ Telephony.CellBroadcasts.CMAS_URGENCY + " INTEGER,"
+ Telephony.CellBroadcasts.CMAS_CERTAINTY + " INTEGER,"
+ SMS_SYNC_PENDING + " BOOLEAN);";
}
private final Context mContext;
final boolean mLegacyProvider;
private ContentProviderClient mOverrideContentProviderClient = null;
@VisibleForTesting
public CellBroadcastDatabaseHelper(Context context, boolean legacyProvider) {
super(context, DATABASE_NAME_V13, null, DATABASE_VERSION);
mContext = context;
mLegacyProvider = legacyProvider;
}
@VisibleForTesting
public CellBroadcastDatabaseHelper(Context context, boolean legacyProvider, String dbName) {
super(context, dbName, null, DATABASE_VERSION);
mContext = context;
mLegacyProvider = legacyProvider;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(getStringForCellBroadcastTableCreation(TABLE_NAME));
db.execSQL("CREATE INDEX IF NOT EXISTS deliveryTimeIndex ON " + TABLE_NAME
+ " (" + Telephony.CellBroadcasts.DELIVERY_TIME + ");");
if (!mLegacyProvider) {
migrateFromLegacyIfNeeded(db);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == newVersion) {
return;
}
// always log database upgrade
log("Upgrading DB from version " + oldVersion + " to " + newVersion);
if (oldVersion < 12) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN "
+ Telephony.CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
}
if (oldVersion < 13) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + SMS_SYNC_PENDING
+ " BOOLEAN DEFAULT 0;");
}
}
private synchronized void tryToMigrateV13() {
File oldDb = mContext.getDatabasePath(OLD_DATABASE_NAME);
File newDb = mContext.getDatabasePath(DATABASE_NAME_V13);
if (!oldDb.exists()) {
return;
}
// We do the DB copy in two scenarios:
// 1. device receives v13 upgrade.
// 2. device receives v13 upgrade, gets rollback to v12, then receives v13 upgrade again.
// If the DB is modified after rollback, we want to copy those changes again.
if (!newDb.exists() || oldDb.lastModified() > newDb.lastModified()) {
try {
// copy() requires that the destination file does not exist
Log.d(TAG, "copying to v13 db");
if (newDb.exists()) newDb.delete();
copy(oldDb.toPath(), newDb.toPath());
} catch (Exception e) {
// If the copy failed we don't know if the db is in a safe state, so just delete it
// and continue with an empty new db. Ignore the exception and just log an error.
mContext.deleteDatabase(DATABASE_NAME_V13);
loge("could not copy DB to v13. e=" + e);
}
}
// else the V13 database has already been created.
}
@Override
public SQLiteDatabase getReadableDatabase() {
tryToMigrateV13();
return super.getReadableDatabase();
}
@Override
public SQLiteDatabase getWritableDatabase() {
tryToMigrateV13();
return super.getWritableDatabase();
}
@VisibleForTesting
public void setOverrideContentProviderClient(ContentProviderClient client) {
mOverrideContentProviderClient = client;
}
private ContentProviderClient getContentProviderClient() {
if (mOverrideContentProviderClient != null) {
return mOverrideContentProviderClient;
}
return mContext.getContentResolver()
.acquireContentProviderClient(Telephony.CellBroadcasts.AUTHORITY_LEGACY);
}
/**
* This is the migration logic to accommodate OEMs move to mainlined CBR for the first time.
* When the db is initially created, this is called once to
* migrate predefined data through {@link Telephony.CellBroadcasts#AUTHORITY_LEGACY_URI}
* from OEM app.
*/
@VisibleForTesting
public void migrateFromLegacyIfNeeded(@NonNull SQLiteDatabase db) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
if (sp.getBoolean(CellBroadcastDatabaseHelper.KEY_LEGACY_DATA_MIGRATION, false)) {
log("Data migration was complete already");
return;
}
try (ContentProviderClient client = getContentProviderClient()) {
if (client == null) {
log("No legacy provider available for migration");
return;
}
db.beginTransaction();
log("Starting migration from legacy provider");
// migration columns are same as query columns
try (Cursor c = client.query(Telephony.CellBroadcasts.AUTHORITY_LEGACY_URI,
QUERY_COLUMNS, null, null, null)) {
final ContentValues values = new ContentValues();
while (c.moveToNext()) {
values.clear();
for (String column : QUERY_COLUMNS) {
copyFromCursorToContentValues(column, c, values);
}
// remove the primary key to avoid UNIQUE constraint failure.
values.remove(Telephony.CellBroadcasts._ID);
try {
if (db.insert(TABLE_NAME, null, values) == -1) {
// We only have one shot to migrate data, so log and
// keep marching forward
loge("Failed to insert " + values + "; continuing");
}
} catch (Exception e) {
// If insert for one message fails, continue with other messages
loge("Failed to insert " + values + " due to exception: " + e);
}
}
log("Finished migration from legacy provider");
} catch (RemoteException e) {
throw new IllegalStateException(e);
} finally {
// if beginTransaction() is called then setTransactionSuccessful() must be called.
// This is a nested begin/end transcation block -- since this is called from
// onCreate() which is inside another block in SQLiteOpenHelper. If a nested
// transaction fails, all transaction fail and that would result in table not being
// created (it's created in onCreate()).
db.setTransactionSuccessful();
db.endTransaction();
}
} catch (Exception e) {
// We have to guard ourselves against any weird behavior of the
// legacy provider by trying to catch everything
loge("Failed migration from legacy provider: " + e);
} finally {
// Mark data migration was triggered to make sure this is done only once.
sp.edit().putBoolean(KEY_LEGACY_DATA_MIGRATION, true).commit();
}
}
public static void copyFromCursorToContentValues(@NonNull String column, @NonNull Cursor cursor,
@NonNull ContentValues values) {
final int index = cursor.getColumnIndex(column);
if (index != -1) {
if (cursor.isNull(index)) {
values.putNull(column);
} else {
values.put(column, cursor.getString(index));
}
}
}
private static void log(String msg) {
Log.d(TAG, msg);
}
private static void loge(String msg) {
Log.e(TAG, msg);
}
}