blob: f7dea4eb38f3d6ad1d0d234a00444ebae96ecf1c [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
19import android.app.AppOpsManager;
20import android.content.ContentProvider;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.UriMatcher;
25import android.content.pm.PackageManager;
26import android.database.Cursor;
27import android.database.sqlite.SQLiteDatabase;
28import android.database.sqlite.SQLiteOpenHelper;
29import android.database.sqlite.SQLiteQueryBuilder;
30import android.net.Uri;
31import android.os.Binder;
32import android.os.Process;
33import android.provider.Telephony.CellBroadcasts;
Jack Yu66ad02d2019-11-18 19:55:39 -080034import android.telephony.Rlog;
Jordan Liu11775052019-10-22 14:42:07 -070035import android.text.TextUtils;
36import android.util.Log;
37
38import com.android.internal.annotations.VisibleForTesting;
39
40import java.util.Arrays;
41
42/**
43 * The content provider that provides access of cell broadcast message to application.
44 * Permission {@link android.permission.READ_CELL_BROADCASTS} is required for querying the cell
45 * broadcast message. Only phone process has the permission to write/update the database via this
46 * provider.
47 */
48public class CellBroadcastProvider extends ContentProvider {
49 /** Interface for read/write permission check. */
50 public interface PermissionChecker {
51 /** Return {@code True} if the caller has the permission to write/update the database. */
52 boolean hasWritePermission();
53
54 /** Return {@code True} if the caller has the permission to query the complete database. */
55 boolean hasReadPermission();
56
57 /**
58 * Return {@code True} if the caller has the permission to query the database for
59 * cell broadcast message history.
60 */
61 boolean hasReadPermissionForHistory();
62 }
63
64 private static final String TAG = CellBroadcastProvider.class.getSimpleName();
65
66 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
67
68 /** Database name. */
69 private static final String DATABASE_NAME = "cellbroadcasts.db";
70
71 /** Database version. */
72 private static final int DATABASE_VERSION = 2;
73
74 /** URI matcher for ContentProvider queries. */
75 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
76
77 /** URI matcher type to get all cell broadcasts. */
78 private static final int ALL = 0;
79
80 /**
81 * URI matcher type for get all message history, this is used primarily for default
82 * cellbroadcast app or messaging app to display message history. some information is not
83 * exposed for messaging history, e.g, messages which are out of broadcast geometrics will not
84 * be delivered to end users thus will not be returned as message history query result.
85 */
86 private static final int MESSAGE_HISTORY = 1;
87
88 /** MIME type for the list of all cell broadcasts. */
89 private static final String LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast";
90
91 /** Table name of cell broadcast message. */
92 @VisibleForTesting
93 public static final String CELL_BROADCASTS_TABLE_NAME = "cell_broadcasts";
94
95 /** Authority string for content URIs. */
96 @VisibleForTesting
97 public static final String AUTHORITY = "cellbroadcasts";
98
99 /** Content uri of this provider. */
100 public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
101
Jordan Liub51f6932019-11-15 11:13:59 -0800102 /**
Jordan Liub51f6932019-11-15 11:13:59 -0800103 * Local definition of the query columns for instantiating
104 * {@link android.telephony.SmsCbMessage} objects.
105 */
106 public static final String[] QUERY_COLUMNS = {
Jack Yu515f34c2019-11-20 23:19:29 -0800107 CellBroadcasts._ID,
108 CellBroadcasts.SLOT_INDEX,
109 CellBroadcasts.SUB_ID,
110 CellBroadcasts.GEOGRAPHICAL_SCOPE,
111 CellBroadcasts.PLMN,
112 CellBroadcasts.LAC,
113 CellBroadcasts.CID,
114 CellBroadcasts.SERIAL_NUMBER,
115 CellBroadcasts.SERVICE_CATEGORY,
116 CellBroadcasts.LANGUAGE_CODE,
117 CellBroadcasts.MESSAGE_BODY,
118 CellBroadcasts.MESSAGE_FORMAT,
119 CellBroadcasts.MESSAGE_PRIORITY,
120 CellBroadcasts.ETWS_WARNING_TYPE,
121 CellBroadcasts.CMAS_MESSAGE_CLASS,
122 CellBroadcasts.CMAS_CATEGORY,
123 CellBroadcasts.CMAS_RESPONSE_TYPE,
124 CellBroadcasts.CMAS_SEVERITY,
125 CellBroadcasts.CMAS_URGENCY,
126 CellBroadcasts.CMAS_CERTAINTY,
127 CellBroadcasts.RECEIVED_TIME,
128 CellBroadcasts.MESSAGE_BROADCASTED,
129 CellBroadcasts.GEOMETRIES,
130 CellBroadcasts.MAXIMUM_WAIT_TIME
Jordan Liub51f6932019-11-15 11:13:59 -0800131 };
132
Jordan Liu11775052019-10-22 14:42:07 -0700133 @VisibleForTesting
134 public PermissionChecker mPermissionChecker;
135
136 /** The database helper for this content provider. */
137 @VisibleForTesting
138 public SQLiteOpenHelper mDbHelper;
139
140 static {
141 sUriMatcher.addURI(AUTHORITY, null, ALL);
142 sUriMatcher.addURI(AUTHORITY, "history", MESSAGE_HISTORY);
143 }
144
145 public CellBroadcastProvider() {}
146
147 @VisibleForTesting
148 public CellBroadcastProvider(PermissionChecker permissionChecker) {
149 mPermissionChecker = permissionChecker;
150 }
151
152 @Override
153 public boolean onCreate() {
154 mDbHelper = new CellBroadcastDatabaseHelper(getContext());
155 mPermissionChecker = new CellBroadcastPermissionChecker();
156 setAppOps(AppOpsManager.OP_READ_CELL_BROADCASTS, AppOpsManager.OP_NONE);
157 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) {
Jack Yu66ad02d2019-11-18 19:55:39 -0800183 Rlog.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) {
Jack Yu66ad02d2019-11-18 19:55:39 -0800223 Rlog.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 {
Jack Yu66ad02d2019-11-18 19:55:39 -0800238 Rlog.e(TAG, "Insert record failed because of unknown reason, uri = " + uri);
Jordan Liu11775052019-10-22 14:42:07 -0700239 return null;
240 }
241 default:
242 throw new IllegalArgumentException(
243 "Insert method doesn't support this uri = " + uri);
244 }
245 }
246
247 @Override
248 public int delete(Uri uri, String selection, String[] selectionArgs) {
249 checkWritePermission();
250
251 if (DBG) {
Jack Yu66ad02d2019-11-18 19:55:39 -0800252 Rlog.d(TAG, "delete:"
Jordan Liu11775052019-10-22 14:42:07 -0700253 + " uri = " + uri
254 + " selection = " + selection
255 + " selectionArgs = " + Arrays.toString(selectionArgs));
256 }
257
258 switch (sUriMatcher.match(uri)) {
259 case ALL:
260 return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME,
261 selection, selectionArgs);
262 default:
263 throw new IllegalArgumentException(
264 "Delete method doesn't support this uri = " + uri);
265 }
266 }
267
268 @Override
269 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
270 checkWritePermission();
271
272 if (DBG) {
Jack Yu66ad02d2019-11-18 19:55:39 -0800273 Rlog.d(TAG, "update:"
Jordan Liu11775052019-10-22 14:42:07 -0700274 + " uri = " + uri
275 + " values = {" + values + "}"
276 + " selection = " + selection
277 + " selectionArgs = " + Arrays.toString(selectionArgs));
278 }
279
280 switch (sUriMatcher.match(uri)) {
281 case ALL:
282 int rowCount = getWritableDatabase().update(
283 CELL_BROADCASTS_TABLE_NAME,
284 values,
285 selection,
286 selectionArgs);
287 if (rowCount > 0) {
288 getContext().getContentResolver().notifyChange(uri, null /* observer */);
289 }
290 return rowCount;
291 default:
292 throw new IllegalArgumentException(
293 "Update method doesn't support this uri = " + uri);
294 }
295 }
296
297 /**
298 * Returns a string used to create the cell broadcast table. This is exposed so the unit test
299 * can construct its own in-memory database to match the cell broadcast db.
300 */
301 @VisibleForTesting
302 public static String getStringForCellBroadcastTableCreation(String tableName) {
303 return "CREATE TABLE " + tableName + " ("
304 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
Jack Yu515f34c2019-11-20 23:19:29 -0800305 + CellBroadcasts.SUB_ID + " INTEGER,"
Jordan Liu11775052019-10-22 14:42:07 -0700306 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
307 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
308 + CellBroadcasts.PLMN + " TEXT,"
309 + CellBroadcasts.LAC + " INTEGER,"
310 + CellBroadcasts.CID + " INTEGER,"
311 + CellBroadcasts.SERIAL_NUMBER + " INTEGER,"
312 + CellBroadcasts.SERVICE_CATEGORY + " INTEGER,"
313 + CellBroadcasts.LANGUAGE_CODE + " TEXT,"
314 + CellBroadcasts.MESSAGE_BODY + " TEXT,"
315 + CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
316 + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
317 + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
318 + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER,"
319 + CellBroadcasts.CMAS_CATEGORY + " INTEGER,"
320 + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER,"
321 + CellBroadcasts.CMAS_SEVERITY + " INTEGER,"
322 + CellBroadcasts.CMAS_URGENCY + " INTEGER,"
323 + CellBroadcasts.CMAS_CERTAINTY + " INTEGER,"
324 + CellBroadcasts.RECEIVED_TIME + " BIGINT,"
325 + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0,"
326 + CellBroadcasts.GEOMETRIES + " TEXT,"
327 + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);";
328 }
329
330 private SQLiteDatabase getWritableDatabase() {
331 return mDbHelper.getWritableDatabase();
332 }
333
334 private SQLiteDatabase getReadableDatabase() {
335 return mDbHelper.getReadableDatabase();
336 }
337
338 private void checkWritePermission() {
339 if (!mPermissionChecker.hasWritePermission()) {
340 throw new SecurityException(
341 "No permission to write CellBroadcast provider");
342 }
343 }
344
345 private void checkReadPermission(Uri uri) {
346 int match = sUriMatcher.match(uri);
347 switch (match) {
348 case ALL:
349 if (!mPermissionChecker.hasReadPermission()) {
350 throw new SecurityException(
351 "No permission to read CellBroadcast provider");
352 }
353 break;
354 case MESSAGE_HISTORY:
355 // TODO: if we plan to allow apps to query db in framework, we should migrate data
356 // first before deprecating app's database. otherwise users will lose all history.
357 if (!mPermissionChecker.hasReadPermissionForHistory()) {
358 throw new SecurityException(
359 "No permission to read CellBroadcast provider for message history");
360 }
361 break;
362 default:
363 return;
364 }
365 }
366
367 private class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
368 CellBroadcastDatabaseHelper(Context context) {
369 super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
370 }
371
372 @Override
373 public void onCreate(SQLiteDatabase db) {
374 db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
375 }
376
377 @Override
378 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
379 if (DBG) {
Jack Yu66ad02d2019-11-18 19:55:39 -0800380 Rlog.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
Jordan Liu11775052019-10-22 14:42:07 -0700381 }
Jack Yu515f34c2019-11-20 23:19:29 -0800382 if (oldVersion < 2) {
Jordan Liu11775052019-10-22 14:42:07 -0700383 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
384 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
Jack Yu66ad02d2019-11-18 19:55:39 -0800385 Rlog.d(TAG, "add slotIndex column");
Jordan Liu11775052019-10-22 14:42:07 -0700386 }
387 }
388 }
389
390 private class CellBroadcastPermissionChecker implements PermissionChecker {
391 @Override
392 public boolean hasWritePermission() {
Chen Xu8b4b6bc2019-11-26 00:24:19 -0800393 // Only the telephony system compontents e.g, Cellbroadcast service has the write
394 // permission to modify this provider.
395 int status = getContext().checkCallingOrSelfPermission(
396 "android.permission.GRANT_RUNTIME_PERMISSIONS_TO_TELEPHONY_DEFAULTS");
397 if (status == PackageManager.PERMISSION_GRANTED) {
398 return true;
399 }
400 return false;
Jordan Liu11775052019-10-22 14:42:07 -0700401 }
402
403 @Override
404 public boolean hasReadPermission() {
Chen Xu8b4b6bc2019-11-26 00:24:19 -0800405 // Only the telephony system compontents e.g, Cellbroadcast service has the read
406 // permission to access this provider.
407 int status = getContext().checkCallingOrSelfPermission(
408 "android.permission.GRANT_RUNTIME_PERMISSIONS_TO_TELEPHONY_DEFAULTS");
409 if (status == PackageManager.PERMISSION_GRANTED) {
410 return true;
411 }
412 return false;
Jordan Liu11775052019-10-22 14:42:07 -0700413 }
414
415 @Override
416 public boolean hasReadPermissionForHistory() {
417 int status = getContext().checkCallingOrSelfPermission(
418 "android.permission.RECEIVE_EMERGENCY_BROADCAST");
419 if (status == PackageManager.PERMISSION_GRANTED) {
420 return true;
421 }
422 return false;
423 }
424 }
425}