• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__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                     CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
239                             CELL_BROADCAST_MESSAGE_ERROR__TYPE__FAILED_TO_INSERT_TO_DB,
240                             errorString);
241                     Log.e(TAG, "Insert record failed because of unknown reason. " + errorString);
242                     return null;
243                 }
244             default:
245                 String errorString = "Insert method doesn't support this uri="
246                         + uri.toString() + " values=" + values;
247                 // 1000 character limit for error logs
248                 if (errorString.length() > 1000) {
249                     errorString = errorString.substring(0, 1000);
250                 }
251                 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
252                         CELL_BROADCAST_MESSAGE_ERROR__TYPE__FAILED_TO_INSERT_TO_DB, errorString);
253                 throw new IllegalArgumentException(errorString);
254         }
255     }
256 
257     @Override
delete(Uri uri, String selection, String[] selectionArgs)258     public int delete(Uri uri, String selection, String[] selectionArgs) {
259         checkWritePermission();
260 
261         if (DBG) {
262             Log.d(TAG, "delete:"
263                     + " uri = " + uri
264                     + " selection = " + selection
265                     + " selectionArgs = " + Arrays.toString(selectionArgs));
266         }
267 
268         switch (sUriMatcher.match(uri)) {
269             case ALL:
270                 return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME,
271                         selection, selectionArgs);
272             default:
273                 throw new IllegalArgumentException(
274                         "Delete method doesn't support this uri = " + uri);
275         }
276     }
277 
278     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)279     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
280         checkWritePermission();
281 
282         if (DBG) {
283             Log.d(TAG, "update:"
284                     + " uri = " + uri
285                     + " values = {" + values + "}"
286                     + " selection = " + selection
287                     + " selectionArgs = " + Arrays.toString(selectionArgs));
288         }
289 
290         int rowCount = 0;
291         switch (sUriMatcher.match(uri)) {
292             case ALL:
293                 rowCount = getWritableDatabase().update(
294                         CELL_BROADCASTS_TABLE_NAME,
295                         values,
296                         selection,
297                         selectionArgs);
298                 if (rowCount > 0) {
299                     getContext().getContentResolver().notifyChange(uri, null /* observer */,
300                             ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS
301                                     | ContentResolver.NOTIFY_SYNC_TO_NETWORK );
302                 }
303                 return rowCount;
304             case MESSAGE_DISPLAYED:
305                 // mark message was displayed to the end-users.
306                 values.put(Telephony.CellBroadcasts.MESSAGE_DISPLAYED, 1);
307                 rowCount = getWritableDatabase().update(
308                         CELL_BROADCASTS_TABLE_NAME,
309                         values,
310                         selection,
311                         selectionArgs);
312                 if (rowCount > 0) {
313                     // update was succeed. the row number of the updated message.
314                     try (Cursor ret = query(CellBroadcasts.CONTENT_URI,
315                             new String[]{CellBroadcasts._ID},
316                             selection, selectionArgs, null)) {
317                         if (ret != null && ret.moveToFirst()) {
318                             int rowNumber = ret.getInt(ret.getColumnIndex(CellBroadcasts._ID));
319                             Log.d(TAG, "notify contentObservers for the displayed message, row: "
320                                     + rowNumber);
321                             getContext().getContentResolver().notifyChange(
322                                     Uri.withAppendedPath(CONTENT_URI,
323                                             "displayed/" + rowNumber), null, true);
324                         }
325                     } catch (Exception ex) {
326                         Log.e(TAG, "exception during update message displayed:  " + ex.toString());
327                     }
328                 }
329                 return rowCount;
330             default:
331                 throw new IllegalArgumentException(
332                         "Update method doesn't support this uri = " + uri);
333         }
334     }
335 
336     /**
337      * Returns a string used to create the cell broadcast table. This is exposed so the unit test
338      * can construct its own in-memory database to match the cell broadcast db.
339      */
340     @VisibleForTesting
getStringForCellBroadcastTableCreation(String tableName)341     public static String getStringForCellBroadcastTableCreation(String tableName) {
342         return "CREATE TABLE " + tableName + " ("
343                 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
344                 + CellBroadcasts.SUBSCRIPTION_ID + " INTEGER,"
345                 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
346                 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
347                 + CellBroadcasts.PLMN + " TEXT,"
348                 + CellBroadcasts.LAC + " INTEGER,"
349                 + CellBroadcasts.CID + " INTEGER,"
350                 + CellBroadcasts.SERIAL_NUMBER + " INTEGER,"
351                 + CellBroadcasts.SERVICE_CATEGORY + " INTEGER,"
352                 + CellBroadcasts.LANGUAGE_CODE + " TEXT,"
353                 + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0,"
354                 + CellBroadcasts.MESSAGE_BODY + " TEXT,"
355                 + CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
356                 + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
357                 + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
358                 + CellBroadcasts.ETWS_IS_PRIMARY + " BOOLEAN DEFAULT 0,"
359                 + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER,"
360                 + CellBroadcasts.CMAS_CATEGORY + " INTEGER,"
361                 + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER,"
362                 + CellBroadcasts.CMAS_SEVERITY + " INTEGER,"
363                 + CellBroadcasts.CMAS_URGENCY + " INTEGER,"
364                 + CellBroadcasts.CMAS_CERTAINTY + " INTEGER,"
365                 + CellBroadcasts.RECEIVED_TIME + " BIGINT,"
366                 + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1,"
367                 + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0,"
368                 + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 0,"
369                 + CellBroadcasts.GEOMETRIES + " TEXT,"
370                 + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);";
371     }
372 
getWritableDatabase()373     private SQLiteDatabase getWritableDatabase() {
374         return mDbHelper.getWritableDatabase();
375     }
376 
getReadableDatabase()377     private SQLiteDatabase getReadableDatabase() {
378         return mDbHelper.getReadableDatabase();
379     }
380 
checkWritePermission()381     private void checkWritePermission() {
382         if (!mPermissionChecker.hasFullAccessPermission()) {
383             throw new SecurityException(
384                     "No permission to write CellBroadcast provider");
385         }
386     }
387 
checkReadPermission(Uri uri)388     private void checkReadPermission(Uri uri) {
389         int match = sUriMatcher.match(uri);
390         switch (match) {
391             case ALL:
392                 if (!mPermissionChecker.hasFullAccessPermission()) {
393                     throw new SecurityException(
394                             "No permission to read CellBroadcast provider");
395                 }
396                 break;
397             case MESSAGE_HISTORY:
398                 // The normal read permission android.permission.READ_CELL_BROADCASTS
399                 // is defined in AndroidManifest.xml and is enfored by the platform.
400                 // So no additional check is required here.
401                 break;
402             default:
403                 return;
404         }
405     }
406 
407     @VisibleForTesting
408     public static class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
CellBroadcastDatabaseHelper(Context context)409         public CellBroadcastDatabaseHelper(Context context) {
410             super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
411         }
412 
413         @Override
onCreate(SQLiteDatabase db)414         public void onCreate(SQLiteDatabase db) {
415             db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
416         }
417 
418         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)419         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
420             if (DBG) {
421                 Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
422             }
423             if (oldVersion < 2) {
424                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
425                         + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
426                 Log.d(TAG, "add slotIndex column");
427             }
428 
429             if (oldVersion < 3) {
430                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
431                         + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0;");
432                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
433                         + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1;");
434                 // Specifically for upgrade, the message displayed should be true. For newly arrived
435                 // message, default should be false.
436                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
437                         + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 1;");
438                 Log.d(TAG, "add dcs, location check time, and message displayed column.");
439             }
440 
441             if (oldVersion < 4) {
442                 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
443                         + CellBroadcasts.ETWS_IS_PRIMARY + " BOOLEAN DEFAULT 0;");
444                 Log.d(TAG, "add ETWS is_primary column.");
445             }
446         }
447     }
448 
449     /**
450      * Cell broadcast permission checker.
451      */
452     public class CellBroadcastPermissionChecker {
453         /**
454          * @return {@code true} if the caller has permission to fully access the cell broadcast
455          * provider.
456          */
hasFullAccessPermission()457         public boolean hasFullAccessPermission() {
458             int status = getContext().checkCallingOrSelfPermission(
459                     "com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY");
460             return status == PackageManager.PERMISSION_GRANTED;
461         }
462     }
463 }
464