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