1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cellbroadcastreceiver; 18 19 import static java.nio.file.Files.copy; 20 21 import android.annotation.NonNull; 22 import android.content.ContentProviderClient; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.SharedPreferences; 26 import android.database.Cursor; 27 import android.database.sqlite.SQLiteDatabase; 28 import android.database.sqlite.SQLiteOpenHelper; 29 import android.os.RemoteException; 30 import android.preference.PreferenceManager; 31 import android.provider.Telephony; 32 import android.provider.Telephony.CellBroadcasts; 33 import android.util.Log; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 37 import java.io.File; 38 39 /** 40 * Open, create, and upgrade the cell broadcast SQLite database. Previously an inner class of 41 * {@code CellBroadcastDatabase}, this is now a top-level class. The column definitions in 42 * {@code CellBroadcastDatabase} have been moved to {@link Telephony.CellBroadcasts} in the 43 * framework, to simplify access to this database from third-party apps. 44 */ 45 public class CellBroadcastDatabaseHelper extends SQLiteOpenHelper { 46 47 private static final String TAG = "CellBroadcastDatabaseHelper"; 48 49 /** 50 * Database version 1: initial version (support removed) 51 * Database version 2-9: (reserved for OEM database customization) (support removed) 52 * Database version 10: adds ETWS and CMAS columns and CDMA support (support removed) 53 * Database version 11: adds delivery time index 54 * Database version 12: add slotIndex 55 * Database version 13: add smsSyncPending 56 */ 57 private static final int DATABASE_VERSION = 13; 58 59 private static final String OLD_DATABASE_NAME = "cell_broadcasts.db"; 60 private static final String DATABASE_NAME_V13 = "cell_broadcasts_v13.db"; 61 @VisibleForTesting 62 public static final String TABLE_NAME = "broadcasts"; 63 64 // Preference key for whether the data migration from pre-R CBR app was complete. 65 public static final String KEY_LEGACY_DATA_MIGRATION = "legacy_data_migration"; 66 67 /** 68 * Is the message pending for sms synchronization. 69 * when received cellbroadcast message in direct boot mode, we will retry synchronizing 70 * alert message to sms inbox after user unlock if needed. 71 * <P>Type: Boolean</P> 72 */ 73 public static final String SMS_SYNC_PENDING = "isSmsSyncPending"; 74 75 /* 76 * Query columns for instantiating SmsCbMessage. 77 */ 78 public static final String[] QUERY_COLUMNS = { 79 Telephony.CellBroadcasts._ID, 80 Telephony.CellBroadcasts.SLOT_INDEX, 81 Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, 82 Telephony.CellBroadcasts.PLMN, 83 Telephony.CellBroadcasts.LAC, 84 Telephony.CellBroadcasts.CID, 85 Telephony.CellBroadcasts.SERIAL_NUMBER, 86 Telephony.CellBroadcasts.SERVICE_CATEGORY, 87 Telephony.CellBroadcasts.LANGUAGE_CODE, 88 Telephony.CellBroadcasts.MESSAGE_BODY, 89 Telephony.CellBroadcasts.DELIVERY_TIME, 90 Telephony.CellBroadcasts.MESSAGE_READ, 91 Telephony.CellBroadcasts.MESSAGE_FORMAT, 92 Telephony.CellBroadcasts.MESSAGE_PRIORITY, 93 Telephony.CellBroadcasts.ETWS_WARNING_TYPE, 94 Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, 95 Telephony.CellBroadcasts.CMAS_CATEGORY, 96 Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, 97 Telephony.CellBroadcasts.CMAS_SEVERITY, 98 Telephony.CellBroadcasts.CMAS_URGENCY, 99 Telephony.CellBroadcasts.CMAS_CERTAINTY 100 }; 101 102 /** 103 * Returns a string used to create the cell broadcast table. This is exposed so the unit test 104 * can construct its own in-memory database to match the cell broadcast db. 105 */ 106 @VisibleForTesting getStringForCellBroadcastTableCreation(String tableName)107 public static String getStringForCellBroadcastTableCreation(String tableName) { 108 return "CREATE TABLE " + tableName + " (" 109 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 110 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0," 111 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER," 112 + CellBroadcasts.PLMN + " TEXT," 113 + CellBroadcasts.LAC + " INTEGER," 114 + CellBroadcasts.CID + " INTEGER," 115 + CellBroadcasts.SERIAL_NUMBER + " INTEGER," 116 + Telephony.CellBroadcasts.SERVICE_CATEGORY + " INTEGER," 117 + Telephony.CellBroadcasts.LANGUAGE_CODE + " TEXT," 118 + Telephony.CellBroadcasts.MESSAGE_BODY + " TEXT," 119 + Telephony.CellBroadcasts.DELIVERY_TIME + " INTEGER," 120 + Telephony.CellBroadcasts.MESSAGE_READ + " INTEGER," 121 + Telephony.CellBroadcasts.MESSAGE_FORMAT + " INTEGER," 122 + Telephony.CellBroadcasts.MESSAGE_PRIORITY + " INTEGER," 123 + Telephony.CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER," 124 + Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER," 125 + Telephony.CellBroadcasts.CMAS_CATEGORY + " INTEGER," 126 + Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER," 127 + Telephony.CellBroadcasts.CMAS_SEVERITY + " INTEGER," 128 + Telephony.CellBroadcasts.CMAS_URGENCY + " INTEGER," 129 + Telephony.CellBroadcasts.CMAS_CERTAINTY + " INTEGER," 130 + SMS_SYNC_PENDING + " BOOLEAN);"; 131 } 132 133 private final Context mContext; 134 final boolean mLegacyProvider; 135 136 private ContentProviderClient mOverrideContentProviderClient = null; 137 138 @VisibleForTesting CellBroadcastDatabaseHelper(Context context, boolean legacyProvider)139 public CellBroadcastDatabaseHelper(Context context, boolean legacyProvider) { 140 super(context, DATABASE_NAME_V13, null, DATABASE_VERSION); 141 mContext = context; 142 mLegacyProvider = legacyProvider; 143 } 144 145 @VisibleForTesting CellBroadcastDatabaseHelper(Context context, boolean legacyProvider, String dbName)146 public CellBroadcastDatabaseHelper(Context context, boolean legacyProvider, String dbName) { 147 super(context, dbName, null, DATABASE_VERSION); 148 mContext = context; 149 mLegacyProvider = legacyProvider; 150 } 151 152 @Override onCreate(SQLiteDatabase db)153 public void onCreate(SQLiteDatabase db) { 154 db.execSQL(getStringForCellBroadcastTableCreation(TABLE_NAME)); 155 156 db.execSQL("CREATE INDEX IF NOT EXISTS deliveryTimeIndex ON " + TABLE_NAME 157 + " (" + Telephony.CellBroadcasts.DELIVERY_TIME + ");"); 158 if (!mLegacyProvider) { 159 migrateFromLegacyIfNeeded(db); 160 } 161 } 162 163 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)164 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 165 if (oldVersion == newVersion) { 166 return; 167 } 168 // always log database upgrade 169 log("Upgrading DB from version " + oldVersion + " to " + newVersion); 170 171 if (oldVersion < 12) { 172 db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " 173 + Telephony.CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;"); 174 } 175 if (oldVersion < 13) { 176 db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + SMS_SYNC_PENDING 177 + " BOOLEAN DEFAULT 0;"); 178 } 179 } 180 tryToMigrateV13()181 private synchronized void tryToMigrateV13() { 182 File oldDb = mContext.getDatabasePath(OLD_DATABASE_NAME); 183 File newDb = mContext.getDatabasePath(DATABASE_NAME_V13); 184 if (!oldDb.exists()) { 185 return; 186 } 187 // We do the DB copy in two scenarios: 188 // 1. device receives v13 upgrade. 189 // 2. device receives v13 upgrade, gets rollback to v12, then receives v13 upgrade again. 190 // If the DB is modified after rollback, we want to copy those changes again. 191 if (!newDb.exists() || oldDb.lastModified() > newDb.lastModified()) { 192 try { 193 // copy() requires that the destination file does not exist 194 Log.d(TAG, "copying to v13 db"); 195 if (newDb.exists()) newDb.delete(); 196 copy(oldDb.toPath(), newDb.toPath()); 197 } catch (Exception e) { 198 // If the copy failed we don't know if the db is in a safe state, so just delete it 199 // and continue with an empty new db. Ignore the exception and just log an error. 200 mContext.deleteDatabase(DATABASE_NAME_V13); 201 loge("could not copy DB to v13. e=" + e); 202 } 203 } 204 // else the V13 database has already been created. 205 } 206 207 @Override getReadableDatabase()208 public SQLiteDatabase getReadableDatabase() { 209 tryToMigrateV13(); 210 return super.getReadableDatabase(); 211 } 212 213 @Override getWritableDatabase()214 public SQLiteDatabase getWritableDatabase() { 215 tryToMigrateV13(); 216 return super.getWritableDatabase(); 217 } 218 219 @VisibleForTesting setOverrideContentProviderClient(ContentProviderClient client)220 public void setOverrideContentProviderClient(ContentProviderClient client) { 221 mOverrideContentProviderClient = client; 222 } 223 getContentProviderClient()224 private ContentProviderClient getContentProviderClient() { 225 if (mOverrideContentProviderClient != null) { 226 return mOverrideContentProviderClient; 227 } 228 return mContext.getContentResolver() 229 .acquireContentProviderClient(Telephony.CellBroadcasts.AUTHORITY_LEGACY); 230 } 231 232 /** 233 * This is the migration logic to accommodate OEMs move to mainlined CBR for the first time. 234 * When the db is initially created, this is called once to 235 * migrate predefined data through {@link Telephony.CellBroadcasts#AUTHORITY_LEGACY_URI} 236 * from OEM app. 237 */ 238 @VisibleForTesting migrateFromLegacyIfNeeded(@onNull SQLiteDatabase db)239 public void migrateFromLegacyIfNeeded(@NonNull SQLiteDatabase db) { 240 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); 241 if (sp.getBoolean(CellBroadcastDatabaseHelper.KEY_LEGACY_DATA_MIGRATION, false)) { 242 log("Data migration was complete already"); 243 return; 244 } 245 246 try (ContentProviderClient client = getContentProviderClient()) { 247 if (client == null) { 248 log("No legacy provider available for migration"); 249 return; 250 } 251 252 db.beginTransaction(); 253 log("Starting migration from legacy provider"); 254 // migration columns are same as query columns 255 try (Cursor c = client.query(Telephony.CellBroadcasts.AUTHORITY_LEGACY_URI, 256 QUERY_COLUMNS, null, null, null)) { 257 final ContentValues values = new ContentValues(); 258 while (c.moveToNext()) { 259 values.clear(); 260 for (String column : QUERY_COLUMNS) { 261 copyFromCursorToContentValues(column, c, values); 262 } 263 // remove the primary key to avoid UNIQUE constraint failure. 264 values.remove(Telephony.CellBroadcasts._ID); 265 266 try { 267 if (db.insert(TABLE_NAME, null, values) == -1) { 268 // We only have one shot to migrate data, so log and 269 // keep marching forward 270 loge("Failed to insert " + values + "; continuing"); 271 } 272 } catch (Exception e) { 273 // If insert for one message fails, continue with other messages 274 loge("Failed to insert " + values + " due to exception: " + e); 275 } 276 } 277 278 log("Finished migration from legacy provider"); 279 } catch (RemoteException e) { 280 throw new IllegalStateException(e); 281 } finally { 282 // if beginTransaction() is called then setTransactionSuccessful() must be called. 283 // This is a nested begin/end transcation block -- since this is called from 284 // onCreate() which is inside another block in SQLiteOpenHelper. If a nested 285 // transaction fails, all transaction fail and that would result in table not being 286 // created (it's created in onCreate()). 287 db.setTransactionSuccessful(); 288 db.endTransaction(); 289 } 290 } catch (Exception e) { 291 // We have to guard ourselves against any weird behavior of the 292 // legacy provider by trying to catch everything 293 loge("Failed migration from legacy provider: " + e); 294 } finally { 295 // Mark data migration was triggered to make sure this is done only once. 296 sp.edit().putBoolean(KEY_LEGACY_DATA_MIGRATION, true).commit(); 297 } 298 } 299 copyFromCursorToContentValues(@onNull String column, @NonNull Cursor cursor, @NonNull ContentValues values)300 public static void copyFromCursorToContentValues(@NonNull String column, @NonNull Cursor cursor, 301 @NonNull ContentValues values) { 302 final int index = cursor.getColumnIndex(column); 303 if (index != -1) { 304 if (cursor.isNull(index)) { 305 values.putNull(column); 306 } else { 307 values.put(column, cursor.getString(index)); 308 } 309 } 310 } 311 log(String msg)312 private static void log(String msg) { 313 Log.d(TAG, msg); 314 } 315 loge(String msg)316 private static void loge(String msg) { 317 Log.e(TAG, msg); 318 } 319 } 320