/* * 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. *

Type: Boolean

*/ 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); } }