1 /*
2  * Copyright (C) 2019 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.cellbroadcastservice;
18 
19 import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERR_FAILED_TO_INSERT_TO_DB;
20 
21 import android.content.ContentProvider;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.UriMatcher;
27 import android.content.pm.PackageManager;
28 import android.database.Cursor;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.database.sqlite.SQLiteOpenHelper;
31 import android.database.sqlite.SQLiteQueryBuilder;
32 import android.net.Uri;
33 import android.provider.Telephony;
34 import android.provider.Telephony.CellBroadcasts;
35 import android.text.TextUtils;
36 import android.util.Log;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import java.util.Arrays;
41 
42 /**
43  * The content provider that provides access of cell broadcast message to application.
44  * Permission {@link com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY} is
45  * required for querying the cell broadcast message. Only the Cell Broadcast module should have this
46  * permission.
47  */
48 public class CellBroadcastProvider extends ContentProvider {
49     private static final String TAG = CellBroadcastProvider.class.getSimpleName();
50 
51     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
52 
53     /** Database name. */
54     private static final String DATABASE_NAME = "cellbroadcasts.db";
55 
56     /** Database version. */
57     @VisibleForTesting
58     public static final int DATABASE_VERSION = 4;
59 
60     /** URI matcher for ContentProvider queries. */
61     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
62 
63     /** URI matcher type to get all cell broadcasts. */
64     private static final int ALL = 0;
65 
66     /**
67      * URI matcher type for get all message history, this is used primarily for default
68      * cellbroadcast app or messaging app to display message history. some information is not
69      * exposed for messaging history, e.g, messages which are out of broadcast geometrics will not
70      * be delivered to end users thus will not be returned as message history query result.
71      */
72     private static final int MESSAGE_HISTORY = 1;
73 
74     /**
75      * URI matcher type for update message which are being displayed to end-users.
76      */
77     private static final int MESSAGE_DISPLAYED = 2;
78 
79     /** MIME type for the list of all cell broadcasts. */
80     private static final String LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast";
81 
82     /** Table name of cell broadcast message. */
83     @VisibleForTesting
84     public static final String CELL_BROADCASTS_TABLE_NAME = "cell_broadcasts";
85 
86     /** Authority string for content URIs. */
87     @VisibleForTesting
88     public static final String AUTHORITY = "cellbroadcasts";
89 
90     /** Content uri of this provider. */
91     public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
92 
93     /**
94      * Local definition of the query columns for instantiating
95      * {@link android.telephony.SmsCbMessage} objects.
96      */
97     public static final String[] QUERY_COLUMNS = {
98             CellBroadcasts._ID,
99             CellBroadcasts.SLOT_INDEX,
100             CellBroadcasts.SUBSCRIPTION_ID,
101             CellBroadcasts.GEOGRAPHICAL_SCOPE,
102             CellBroadcasts.PLMN,
103             CellBroadcasts.LAC,
104             CellBroadcasts.CID,
105             CellBroadcasts.SERIAL_NUMBER,
106             CellBroadcasts.SERVICE_CATEGORY,
107             CellBroadcasts.LANGUAGE_CODE,
108             CellBroadcasts.DATA_CODING_SCHEME,
109             CellBroadcasts.MESSAGE_BODY,
110             CellBroadcasts.MESSAGE_FORMAT,
111             CellBroadcasts.MESSAGE_PRIORITY,
112             CellBroadcasts.ETWS_WARNING_TYPE,
113             CellBroadcasts.ETWS_IS_PRIMARY,
114             CellBroadcasts.CMAS_MESSAGE_CLASS,
115             CellBroadcasts.CMAS_CATEGORY,
116             CellBroadcasts.CMAS_RESPONSE_TYPE,
117             CellBroadcasts.CMAS_SEVERITY,
118             CellBroadcasts.CMAS_URGENCY,
119             CellBroadcasts.CMAS_CERTAINTY,
120             CellBroadcasts.RECEIVED_TIME,
121             CellBroadcasts.LOCATION_CHECK_TIME,
122             CellBroadcasts.MESSAGE_BROADCASTED,
123             CellBroadcasts.MESSAGE_DISPLAYED,
124             CellBroadcasts.GEOMETRIES,
125             CellBroadcasts.MAXIMUM_WAIT_TIME
126     };
127 
128     @VisibleForTesting
129     public CellBroadcastPermissionChecker mPermissionChecker;
130 
131     /** The database helper for this content provider. */
132     @VisibleForTesting
133     public SQLiteOpenHelper mDbHelper;
134 
135     static {
sUriMatcher.addURI(AUTHORITY, null, ALL)136         sUriMatcher.addURI(AUTHORITY, null, ALL);
sUriMatcher.addURI(AUTHORITY, "history", MESSAGE_HISTORY)137         sUriMatcher.addURI(AUTHORITY, "history", MESSAGE_HISTORY);
sUriMatcher.addURI(AUTHORITY, "displayed", MESSAGE_DISPLAYED)138         sUriMatcher.addURI(AUTHORITY, "displayed", MESSAGE_DISPLAYED);
139     }
140 
CellBroadcastProvider()141     public CellBroadcastProvider() {}
142 
143     @VisibleForTesting
CellBroadcastProvider(CellBroadcastPermissionChecker permissionChecker)144     public CellBroadcastProvider(CellBroadcastPermissionChecker permissionChecker) {
145         mPermissionChecker = permissionChecker;
146     }
147 
148     @Override
onCreate()149     public boolean onCreate() {
150         mDbHelper = new CellBroadcastDatabaseHelper(getContext());
151         mPermissionChecker = new CellBroadcastPermissionChecker();
152         return true;
153     }
154 
155     /**
156      * Return the MIME type of the data at the specified URI.
157      *
158      * @param uri the URI to query.
159      * @return a MIME type string, or null if there is no type.
160      */
161     @Override
getType(Uri uri)162     public String getType(Uri uri) {
163         int match = sUriMatcher.match(uri);
164         switch (match) {
165             case ALL:
166                 return LIST_TYPE;
167             default:
168                 return null;
169         }
170     }
171 
172     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)173     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
174             String sortOrder) {
175         checkReadPermission(uri);
176 
177         if (DBG) {
178             Log.d(TAG, "query:"
179                     + " uri = " + uri
180                     + " projection = " + Arrays.toString(projection)
181                     + " selection = " + selection
182                     + " selectionArgs = " + Arrays.toString(selectionArgs)
183                     + " sortOrder = " + sortOrder);
184         }
185         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
186         qb.setStrict(true); // a little protection from injection attacks
187         qb.setTables(CELL_BROADCASTS_TABLE_NAME);
188 
189         String orderBy;
190         if (!TextUtils.isEmpty(sortOrder)) {
191             orderBy = sortOrder;
192         } else {
193             orderBy = CellBroadcasts.RECEIVED_TIME + " DESC";
194         }
195 
196         int match = sUriMatcher.match(uri);
197         switch (match) {
198             case ALL:
199                 return getReadableDatabase().query(
200                         CELL_BROADCASTS_TABLE_NAME, projection, selection, selectionArgs,
201                         null /* groupBy */, null /* having */, orderBy);
202             case MESSAGE_HISTORY:
203                 // limit projections to certain columns. limit result to broadcasted messages only.
204                 qb.appendWhere(CellBroadcasts.MESSAGE_BROADCASTED  + "=1");
205                 return qb.query(getReadableDatabase(), projection, selection, selectionArgs, null,
206                         null, orderBy);
207             default:
208                 throw new IllegalArgumentException(
209                         "Query method doesn't support this uri = " + uri);
210         }
211     }
212 
213     @Override
insert(Uri uri, ContentValues values)214     public Uri insert(Uri uri, ContentValues values) {
215         checkWritePermission();
216 
217         if (DBG) {
218             Log.d(TAG, "insert:"
219                     + " uri = " + uri
220                     + " contentValue = " + values);
221         }
222 
223         switch (sUriMatcher.match(uri)) {
224             case ALL:
225                 long row = getWritableDatabase().insertOrThrow(CELL_BROADCASTS_TABLE_NAME, null,
226                         values);
227                 if (row > 0) {
228                     Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
229                     getContext().getContentResolver()
230                             .notifyChange(CONTENT_URI, null /* observer */);
231                     return newUri;
232                 } else {
233                     String errorString = "uri=" + uri.toString() + " values=" + values;
234                     // 1000 character limit for error logs
235                     if (errorString.length() > 1000) {
236                         errorString = errorString.substring(0, 1000);
237                     }
238                     CellBroadcastServiceMetrics.getInstance().logMessageError(
239                             ERR_FAILED_TO_INSERT_TO_DB, errorString);
240                     Log.e(TAG, "Insert record failed because of unknown reason. " + errorString);
241                     return null;
242                 }
243             default:
244                 String errorString = "Insert method doesn't support this uri="
245                         + uri.toString() + " values=" + values;
246                 // 1000 character limit for error logs
247                 if (errorString.length() > 1000) {
248                     errorString = errorString.substring(0, 1000);
249                 }
250                 CellBroadcastServiceMetrics.getInstance().logMessageError(
251                         ERR_FAILED_TO_INSERT_TO_DB, errorString);
252                 throw new IllegalArgumentException(errorString);
253         }
254     }
255 
256     @Override
delete(Uri uri, String selection, String[] selectionArgs)257     public int delete(Uri uri, String selection, String[] selectionArgs) {
258         checkWritePermission();
259 
260         if (DBG) {
261             Log.d(TAG, "delete:"
262                     + " uri = " + uri
263                     + " selection = " + selection
264                     + " selectionArgs = " + Arrays.toString(selectionArgs));
265         }
266 
267         switch (sUriMatcher.match(uri)) {
268             case ALL:
269                 return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME,
270                         selection, selectionArgs);
271             default:
272                 throw new IllegalArgumentException(
273                         "Delete method doesn't support this uri = " + uri);
274         }
275     }
276 
277     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)278     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
279         checkWritePermission();
280 
281         if (DBG) {
282             Log.d(TAG, "update:"
283                     + " uri = " + uri
284                     + " values = {" + values + "}"
285                     + " selection = " + selection
286                     + " selectionArgs = " + Arrays.toString(selectionArgs));
287         }
288 
289         int rowCount = 0;
290         switch (sUriMatcher.match(uri)) {
291             case ALL:
292                 rowCount = getWritableDatabase().update(
293                         CELL_BROADCASTS_TABLE_NAME,
294                         values,
295                         selection,
296                         selectionArgs);
297                 if (rowCount > 0) {
298                     getContext().getContentResolver().notifyChange(uri, null /* observer */,
299                             ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS
300                                     | ContentResolver.NOTIFY_SYNC_TO_NETWORK );
301                 }
302                 return rowCount;
303             case MESSAGE_DISPLAYED:
304                 // mark message was displayed to the end-users.
305                 values.put(Telephony.CellBroadcasts.MESSAGE_DISPLAYED, 1);
306                 rowCount = getWritableDatabase().update(
307                         CELL_BROADCASTS_TABLE_NAME,
308                         values,
309                         selection,
310                         selectionArgs);
311                 if (rowCount > 0) {
312                     // update was succeed. the row number of the updated message.
313                     try (Cursor ret = query(CellBroadcasts.CONTENT_URI,
314                             new String[]{CellBroadcasts._ID},
315                             selection, selectionArgs, null)) {
316                         if (ret != null && ret.moveToFirst()) {
317                             int rowNumber = ret.getInt(ret.getColumnIndex(CellBroadcasts._ID));
318                             Log.d(TAG, "notify contentObservers for the displayed message, row: "
319                                     + rowNumber);
320                             getContext().getContentResolver().notifyChange(
321                                     Uri.withAppendedPath(CONTENT_URI,
322                                             "displayed/" + rowNumber), null, true);
323                         }
324                     } catch (Exception ex) {
325                         Log.e(TAG, "exception during update message displayed:  " + ex.toString());
326                     }
327                 }
328                 return rowCount;
329             default:
330                 throw new IllegalArgumentException(
331                         "Update method doesn't support this uri = " + uri);
332         }
333     }
334 
335     /**
336      * Returns a string used to create the cell broadcast table. This is exposed so the unit test
337      * can construct its own in-memory database to match the cell broadcast db.
338      */
339     @VisibleForTesting
getStringForCellBroadcastTableCreation(String tableName)340     public static String getStringForCellBroadcastTableCreation(String tableName) {
341         return "CREATE TABLE " + tableName + " ("
342                 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
343                 + CellBroadcasts.SUBSCRIPTION_ID + " INTEGER,"
344                 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
345                 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
346                 + CellBroadcasts.PLMN + " TEXT,"
347                 + CellBroadcasts.LAC + " INTEGER,"
348                 + CellBroadcasts.CID + " INTEGER,"
349                 + CellBroadcasts.SERIAL_NUMBER + " INTEGER,"
350                 + CellBroadcasts.SERVICE_CATEGORY + " INTEGER,"
351                 + CellBroadcasts.LANGUAGE_CODE + " TEXT,"
352                 + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0,"
353                 + CellBroadcasts.MESSAGE_BODY + " TEXT,"
354                 + CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
355                 + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
356                 + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
357                 + CellBroadcasts.ETWS_IS_PRIMARY + " BOOLEAN DEFAULT 0,"
358                 + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER,"
359                 + CellBroadcasts.CMAS_CATEGORY + " INTEGER,"
360                 + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER,"
361                 + CellBroadcasts.CMAS_SEVERITY + " INTEGER,"
362                 + CellBroadcasts.CMAS_URGENCY + " INTEGER,"
363                 + CellBroadcasts.CMAS_CERTAINTY + " INTEGER,"
364                 + CellBroadcasts.RECEIVED_TIME + " BIGINT,"
365                 + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1,"
366                 + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0,"
367                 + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 0,"
368                 + CellBroadcasts.GEOMETRIES + " TEXT,"
369                 + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);";
370     }
371 
getWritableDatabase()372     private SQLiteDatabase getWritableDatabase() {
373         return mDbHelper.getWritableDatabase();
374     }
375 
getReadableDatabase()376     private SQLiteDatabase getReadableDatabase() {
377         return mDbHelper.getReadableDatabase();
378     }
379 
checkWritePermission()380     private void checkWritePermission() {
381         if (!mPermissionChecker.hasFullAccessPermission()) {
382             throw new SecurityException(
383                     "No permission to write CellBroadcast provider");
384         }
385     }
386 
checkReadPermission(Uri uri)387     private void checkReadPermission(Uri uri) {
388         int match = sUriMatcher.match(uri);
389         switch (match) {
390             case ALL:
391                 if (!mPermissionChecker.hasFullAccessPermission()) {
392                     throw new SecurityException(
393                             "No permission to read CellBroadcast provider");
394                 }
395                 break;
396             case MESSAGE_HISTORY:
397                 // The normal read permission android.permission.READ_CELL_BROADCASTS
398                 // is defined in AndroidManifest.xml and is enfored by the platform.
399                 // So no additional check is required here.
400                 break;
401             default:
402                 return;
403         }
404     }
405 
406     @VisibleForTesting
407     public static class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
CellBroadcastDatabaseHelper(Context context)408         public CellBroadcastDatabaseHelper(Context context) {
409             super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
410         }
411 
412         @Override
onCreate(SQLiteDatabase db)413         public void onCreate(SQLiteDatabase db) {
414             db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
415         }
416 
417         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)418         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
419             if (DBG) {
420                 Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
421             }
422             if (oldVersion < 2) {
423                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
424                         + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
425                 Log.d(TAG, "add slotIndex column");
426             }
427 
428             if (oldVersion < 3) {
429                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
430                         + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0;");
431                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
432                         + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1;");
433                 // Specifically for upgrade, the message displayed should be true. For newly arrived
434                 // message, default should be false.
435                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
436                         + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 1;");
437                 Log.d(TAG, "add dcs, location check time, and message displayed column.");
438             }
439 
440             if (oldVersion < 4) {
441                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
442                         + CellBroadcasts.ETWS_IS_PRIMARY + " BOOLEAN DEFAULT 0;");
443                 Log.d(TAG, "add ETWS is_primary column.");
444             }
445         }
446     }
447 
448     /**
449      * Cell broadcast permission checker.
450      */
451     public class CellBroadcastPermissionChecker {
452         /**
453          * @return {@code true} if the caller has permission to fully access the cell broadcast
454          * provider.
455          */
hasFullAccessPermission()456         public boolean hasFullAccessPermission() {
457             int status = getContext().checkCallingOrSelfPermission(
458                     "com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY");
459             return status == PackageManager.PERMISSION_GRANTED;
460         }
461     }
462 }
463