blob: a1032f5c9aabbb56816bb3fe28728ecd40acd6bf [file] [log] [blame]
Jordan Liu11775052019-10-22 14:42:07 -07001/*
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
17package com.android.cellbroadcastservice;
18
Jordan Liu62e183f2019-12-20 15:09:44 -080019import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__FAILED_TO_INSERT_TO_DB;
20
Jordan Liu11775052019-10-22 14:42:07 -070021import android.content.ContentProvider;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.UriMatcher;
26import android.content.pm.PackageManager;
27import android.database.Cursor;
28import android.database.sqlite.SQLiteDatabase;
29import android.database.sqlite.SQLiteOpenHelper;
30import android.database.sqlite.SQLiteQueryBuilder;
31import android.net.Uri;
Jordan Liu11775052019-10-22 14:42:07 -070032import android.provider.Telephony.CellBroadcasts;
33import android.text.TextUtils;
34import android.util.Log;
35
36import com.android.internal.annotations.VisibleForTesting;
37
38import java.util.Arrays;
39
40/**
41 * The content provider that provides access of cell broadcast message to application.
42 * Permission {@link android.permission.READ_CELL_BROADCASTS} is required for querying the cell
43 * broadcast message. Only phone process has the permission to write/update the database via this
44 * provider.
45 */
46public class CellBroadcastProvider extends ContentProvider {
47 /** Interface for read/write permission check. */
48 public interface PermissionChecker {
49 /** Return {@code True} if the caller has the permission to write/update the database. */
50 boolean hasWritePermission();
51
52 /** Return {@code True} if the caller has the permission to query the complete database. */
53 boolean hasReadPermission();
54
55 /**
56 * Return {@code True} if the caller has the permission to query the database for
57 * cell broadcast message history.
58 */
59 boolean hasReadPermissionForHistory();
60 }
61
62 private static final String TAG = CellBroadcastProvider.class.getSimpleName();
63
64 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
65
66 /** Database name. */
67 private static final String DATABASE_NAME = "cellbroadcasts.db";
68
69 /** Database version. */
Jack Yue6959112020-01-24 13:27:32 -080070 private static final int DATABASE_VERSION = 3;
Jordan Liu11775052019-10-22 14:42:07 -070071
72 /** URI matcher for ContentProvider queries. */
73 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
74
75 /** URI matcher type to get all cell broadcasts. */
76 private static final int ALL = 0;
77
78 /**
79 * URI matcher type for get all message history, this is used primarily for default
80 * cellbroadcast app or messaging app to display message history. some information is not
81 * exposed for messaging history, e.g, messages which are out of broadcast geometrics will not
82 * be delivered to end users thus will not be returned as message history query result.
83 */
84 private static final int MESSAGE_HISTORY = 1;
85
86 /** MIME type for the list of all cell broadcasts. */
87 private static final String LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast";
88
89 /** Table name of cell broadcast message. */
90 @VisibleForTesting
91 public static final String CELL_BROADCASTS_TABLE_NAME = "cell_broadcasts";
92
93 /** Authority string for content URIs. */
94 @VisibleForTesting
95 public static final String AUTHORITY = "cellbroadcasts";
96
97 /** Content uri of this provider. */
98 public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
99
Jordan Liub51f6932019-11-15 11:13:59 -0800100 /**
Jordan Liub51f6932019-11-15 11:13:59 -0800101 * Local definition of the query columns for instantiating
102 * {@link android.telephony.SmsCbMessage} objects.
103 */
104 public static final String[] QUERY_COLUMNS = {
Jack Yu515f34c2019-11-20 23:19:29 -0800105 CellBroadcasts._ID,
106 CellBroadcasts.SLOT_INDEX,
Jack Yufadeaa32019-12-19 14:12:40 -0800107 CellBroadcasts.SUBSCRIPTION_ID,
Jack Yu515f34c2019-11-20 23:19:29 -0800108 CellBroadcasts.GEOGRAPHICAL_SCOPE,
109 CellBroadcasts.PLMN,
110 CellBroadcasts.LAC,
111 CellBroadcasts.CID,
112 CellBroadcasts.SERIAL_NUMBER,
113 CellBroadcasts.SERVICE_CATEGORY,
114 CellBroadcasts.LANGUAGE_CODE,
Jack Yu28325462020-02-03 16:29:26 -0800115 CellBroadcasts.DATA_CODING_SCHEME,
Jack Yu515f34c2019-11-20 23:19:29 -0800116 CellBroadcasts.MESSAGE_BODY,
117 CellBroadcasts.MESSAGE_FORMAT,
118 CellBroadcasts.MESSAGE_PRIORITY,
119 CellBroadcasts.ETWS_WARNING_TYPE,
120 CellBroadcasts.CMAS_MESSAGE_CLASS,
121 CellBroadcasts.CMAS_CATEGORY,
122 CellBroadcasts.CMAS_RESPONSE_TYPE,
123 CellBroadcasts.CMAS_SEVERITY,
124 CellBroadcasts.CMAS_URGENCY,
125 CellBroadcasts.CMAS_CERTAINTY,
126 CellBroadcasts.RECEIVED_TIME,
Jack Yu28325462020-02-03 16:29:26 -0800127 CellBroadcasts.LOCATION_CHECK_TIME,
Jack Yu515f34c2019-11-20 23:19:29 -0800128 CellBroadcasts.MESSAGE_BROADCASTED,
Jack Yu28325462020-02-03 16:29:26 -0800129 CellBroadcasts.MESSAGE_DISPLAYED,
Jack Yu515f34c2019-11-20 23:19:29 -0800130 CellBroadcasts.GEOMETRIES,
131 CellBroadcasts.MAXIMUM_WAIT_TIME
Jordan Liub51f6932019-11-15 11:13:59 -0800132 };
133
Jordan Liu11775052019-10-22 14:42:07 -0700134 @VisibleForTesting
135 public PermissionChecker mPermissionChecker;
136
137 /** The database helper for this content provider. */
138 @VisibleForTesting
139 public SQLiteOpenHelper mDbHelper;
140
141 static {
142 sUriMatcher.addURI(AUTHORITY, null, ALL);
143 sUriMatcher.addURI(AUTHORITY, "history", MESSAGE_HISTORY);
144 }
145
146 public CellBroadcastProvider() {}
147
148 @VisibleForTesting
149 public CellBroadcastProvider(PermissionChecker permissionChecker) {
150 mPermissionChecker = permissionChecker;
151 }
152
153 @Override
154 public boolean onCreate() {
155 mDbHelper = new CellBroadcastDatabaseHelper(getContext());
156 mPermissionChecker = new CellBroadcastPermissionChecker();
Jordan Liu11775052019-10-22 14:42:07 -0700157 return true;
158 }
159
160 /**
161 * Return the MIME type of the data at the specified URI.
162 *
163 * @param uri the URI to query.
164 * @return a MIME type string, or null if there is no type.
165 */
166 @Override
167 public String getType(Uri uri) {
168 int match = sUriMatcher.match(uri);
169 switch (match) {
170 case ALL:
171 return LIST_TYPE;
172 default:
173 return null;
174 }
175 }
176
177 @Override
178 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
179 String sortOrder) {
180 checkReadPermission(uri);
181
182 if (DBG) {
Jordan Liue532d232019-12-16 15:35:27 -0800183 Log.d(TAG, "query:"
Jordan Liu11775052019-10-22 14:42:07 -0700184 + " uri = " + uri
185 + " projection = " + Arrays.toString(projection)
186 + " selection = " + selection
187 + " selectionArgs = " + Arrays.toString(selectionArgs)
188 + " sortOrder = " + sortOrder);
189 }
190 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
191 qb.setStrict(true); // a little protection from injection attacks
192 qb.setTables(CELL_BROADCASTS_TABLE_NAME);
193
194 String orderBy;
195 if (!TextUtils.isEmpty(sortOrder)) {
196 orderBy = sortOrder;
197 } else {
198 orderBy = CellBroadcasts.RECEIVED_TIME + " DESC";
199 }
200
201 int match = sUriMatcher.match(uri);
202 switch (match) {
203 case ALL:
204 return getReadableDatabase().query(
205 CELL_BROADCASTS_TABLE_NAME, projection, selection, selectionArgs,
206 null /* groupBy */, null /* having */, orderBy);
207 case MESSAGE_HISTORY:
208 // limit projections to certain columns. limit result to broadcasted messages only.
209 qb.appendWhere(CellBroadcasts.MESSAGE_BROADCASTED + "=1");
210 return qb.query(getReadableDatabase(), projection, selection, selectionArgs, null,
211 null, orderBy);
212 default:
213 throw new IllegalArgumentException(
214 "Query method doesn't support this uri = " + uri);
215 }
216 }
217
218 @Override
219 public Uri insert(Uri uri, ContentValues values) {
220 checkWritePermission();
221
222 if (DBG) {
Jordan Liue532d232019-12-16 15:35:27 -0800223 Log.d(TAG, "insert:"
Jordan Liu11775052019-10-22 14:42:07 -0700224 + " uri = " + uri
225 + " contentValue = " + values);
226 }
227
228 switch (sUriMatcher.match(uri)) {
229 case ALL:
230 long row = getWritableDatabase().insertOrThrow(CELL_BROADCASTS_TABLE_NAME, null,
231 values);
232 if (row > 0) {
233 Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
234 getContext().getContentResolver()
235 .notifyChange(CONTENT_URI, null /* observer */);
236 return newUri;
237 } else {
Jordan Liu62e183f2019-12-20 15:09:44 -0800238 String errorString = "uri=" + uri.toString() + " values=" + values;
239 // 1000 character limit for error logs
240 if (errorString.length() > 1000) {
241 errorString = errorString.substring(0, 1000);
242 }
243 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
244 CELL_BROADCAST_MESSAGE_ERROR__TYPE__FAILED_TO_INSERT_TO_DB,
245 errorString);
246 Log.e(TAG, "Insert record failed because of unknown reason. " + errorString);
Jordan Liu11775052019-10-22 14:42:07 -0700247 return null;
248 }
249 default:
Jordan Liu62e183f2019-12-20 15:09:44 -0800250 String errorString = "Insert method doesn't support this uri="
251 + uri.toString() + " values=" + values;
252 // 1000 character limit for error logs
253 if (errorString.length() > 1000) {
254 errorString = errorString.substring(0, 1000);
255 }
256 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR,
257 CELL_BROADCAST_MESSAGE_ERROR__TYPE__FAILED_TO_INSERT_TO_DB, errorString);
258 throw new IllegalArgumentException(errorString);
Jordan Liu11775052019-10-22 14:42:07 -0700259 }
260 }
261
262 @Override
263 public int delete(Uri uri, String selection, String[] selectionArgs) {
264 checkWritePermission();
265
266 if (DBG) {
Jordan Liue532d232019-12-16 15:35:27 -0800267 Log.d(TAG, "delete:"
Jordan Liu11775052019-10-22 14:42:07 -0700268 + " uri = " + uri
269 + " selection = " + selection
270 + " selectionArgs = " + Arrays.toString(selectionArgs));
271 }
272
273 switch (sUriMatcher.match(uri)) {
274 case ALL:
275 return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME,
276 selection, selectionArgs);
277 default:
278 throw new IllegalArgumentException(
279 "Delete method doesn't support this uri = " + uri);
280 }
281 }
282
283 @Override
284 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
285 checkWritePermission();
286
287 if (DBG) {
Jordan Liue532d232019-12-16 15:35:27 -0800288 Log.d(TAG, "update:"
Jordan Liu11775052019-10-22 14:42:07 -0700289 + " uri = " + uri
290 + " values = {" + values + "}"
291 + " selection = " + selection
292 + " selectionArgs = " + Arrays.toString(selectionArgs));
293 }
294
295 switch (sUriMatcher.match(uri)) {
296 case ALL:
297 int rowCount = getWritableDatabase().update(
298 CELL_BROADCASTS_TABLE_NAME,
299 values,
300 selection,
301 selectionArgs);
302 if (rowCount > 0) {
303 getContext().getContentResolver().notifyChange(uri, null /* observer */);
304 }
305 return rowCount;
306 default:
307 throw new IllegalArgumentException(
308 "Update method doesn't support this uri = " + uri);
309 }
310 }
311
312 /**
313 * Returns a string used to create the cell broadcast table. This is exposed so the unit test
314 * can construct its own in-memory database to match the cell broadcast db.
315 */
316 @VisibleForTesting
317 public static String getStringForCellBroadcastTableCreation(String tableName) {
318 return "CREATE TABLE " + tableName + " ("
319 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
Jack Yufadeaa32019-12-19 14:12:40 -0800320 + CellBroadcasts.SUBSCRIPTION_ID + " INTEGER,"
Jordan Liu11775052019-10-22 14:42:07 -0700321 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
322 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
323 + CellBroadcasts.PLMN + " TEXT,"
324 + CellBroadcasts.LAC + " INTEGER,"
325 + CellBroadcasts.CID + " INTEGER,"
326 + CellBroadcasts.SERIAL_NUMBER + " INTEGER,"
327 + CellBroadcasts.SERVICE_CATEGORY + " INTEGER,"
328 + CellBroadcasts.LANGUAGE_CODE + " TEXT,"
Jack Yue6959112020-01-24 13:27:32 -0800329 + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0,"
Jordan Liu11775052019-10-22 14:42:07 -0700330 + CellBroadcasts.MESSAGE_BODY + " TEXT,"
331 + CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
332 + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
333 + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
334 + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER,"
335 + CellBroadcasts.CMAS_CATEGORY + " INTEGER,"
336 + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER,"
337 + CellBroadcasts.CMAS_SEVERITY + " INTEGER,"
338 + CellBroadcasts.CMAS_URGENCY + " INTEGER,"
339 + CellBroadcasts.CMAS_CERTAINTY + " INTEGER,"
340 + CellBroadcasts.RECEIVED_TIME + " BIGINT,"
Jack Yue6959112020-01-24 13:27:32 -0800341 + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1,"
Jordan Liu11775052019-10-22 14:42:07 -0700342 + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0,"
Jack Yue6959112020-01-24 13:27:32 -0800343 + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 0,"
Jordan Liu11775052019-10-22 14:42:07 -0700344 + CellBroadcasts.GEOMETRIES + " TEXT,"
345 + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);";
346 }
347
348 private SQLiteDatabase getWritableDatabase() {
349 return mDbHelper.getWritableDatabase();
350 }
351
352 private SQLiteDatabase getReadableDatabase() {
353 return mDbHelper.getReadableDatabase();
354 }
355
356 private void checkWritePermission() {
357 if (!mPermissionChecker.hasWritePermission()) {
358 throw new SecurityException(
359 "No permission to write CellBroadcast provider");
360 }
361 }
362
363 private void checkReadPermission(Uri uri) {
364 int match = sUriMatcher.match(uri);
365 switch (match) {
366 case ALL:
367 if (!mPermissionChecker.hasReadPermission()) {
368 throw new SecurityException(
369 "No permission to read CellBroadcast provider");
370 }
371 break;
372 case MESSAGE_HISTORY:
373 // TODO: if we plan to allow apps to query db in framework, we should migrate data
374 // first before deprecating app's database. otherwise users will lose all history.
375 if (!mPermissionChecker.hasReadPermissionForHistory()) {
376 throw new SecurityException(
377 "No permission to read CellBroadcast provider for message history");
378 }
379 break;
380 default:
381 return;
382 }
383 }
384
385 private class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
386 CellBroadcastDatabaseHelper(Context context) {
387 super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
388 }
389
390 @Override
391 public void onCreate(SQLiteDatabase db) {
392 db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
393 }
394
395 @Override
396 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
397 if (DBG) {
Jordan Liue532d232019-12-16 15:35:27 -0800398 Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
Jordan Liu11775052019-10-22 14:42:07 -0700399 }
Jack Yu515f34c2019-11-20 23:19:29 -0800400 if (oldVersion < 2) {
Jordan Liu11775052019-10-22 14:42:07 -0700401 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
402 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
Jordan Liue532d232019-12-16 15:35:27 -0800403 Log.d(TAG, "add slotIndex column");
Jack Yue6959112020-01-24 13:27:32 -0800404 } else if (oldVersion < 3) {
405 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
406 + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0;");
407 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
408 + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1;");
409 // Specifically for upgrade, the message displayed should be true. For newly arrived
410 // message, default should be false.
411 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
412 + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 1;");
413 Log.d(TAG, "add dcs, location check time, and message displayed column.");
Jordan Liu11775052019-10-22 14:42:07 -0700414 }
415 }
416 }
417
418 private class CellBroadcastPermissionChecker implements PermissionChecker {
419 @Override
420 public boolean hasWritePermission() {
Chen Xu8b4b6bc2019-11-26 00:24:19 -0800421 // Only the telephony system compontents e.g, Cellbroadcast service has the write
422 // permission to modify this provider.
423 int status = getContext().checkCallingOrSelfPermission(
424 "android.permission.GRANT_RUNTIME_PERMISSIONS_TO_TELEPHONY_DEFAULTS");
425 if (status == PackageManager.PERMISSION_GRANTED) {
426 return true;
427 }
428 return false;
Jordan Liu11775052019-10-22 14:42:07 -0700429 }
430
431 @Override
432 public boolean hasReadPermission() {
Chen Xu8b4b6bc2019-11-26 00:24:19 -0800433 // Only the telephony system compontents e.g, Cellbroadcast service has the read
434 // permission to access this provider.
435 int status = getContext().checkCallingOrSelfPermission(
436 "android.permission.GRANT_RUNTIME_PERMISSIONS_TO_TELEPHONY_DEFAULTS");
437 if (status == PackageManager.PERMISSION_GRANTED) {
438 return true;
439 }
440 return false;
Jordan Liu11775052019-10-22 14:42:07 -0700441 }
442
443 @Override
444 public boolean hasReadPermissionForHistory() {
445 int status = getContext().checkCallingOrSelfPermission(
446 "android.permission.RECEIVE_EMERGENCY_BROADCAST");
447 if (status == PackageManager.PERMISSION_GRANTED) {
448 return true;
449 }
450 return false;
451 }
452 }
453}