blob: 2b3fc63e47c0bd0ed8e3a99c1197421dac29d3dd [file] [log] [blame]
Jordan Liu26e820c2019-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 Liu26e820c2019-10-22 14:42:07 -070019import android.content.ContentProvider;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.UriMatcher;
24import android.content.pm.PackageManager;
25import android.database.Cursor;
26import android.database.sqlite.SQLiteDatabase;
27import android.database.sqlite.SQLiteOpenHelper;
28import android.database.sqlite.SQLiteQueryBuilder;
29import android.net.Uri;
30import android.os.Binder;
31import android.os.Process;
32import 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. */
70 private static final int DATABASE_VERSION = 2;
71
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 Liud3eeb402019-11-15 11:13:59 -0800100 /**
Jordan Liud3eeb402019-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 Yu0bd7e432019-11-20 23:19:29 -0800105 CellBroadcasts._ID,
106 CellBroadcasts.SLOT_INDEX,
107 CellBroadcasts.SUB_ID,
108 CellBroadcasts.GEOGRAPHICAL_SCOPE,
109 CellBroadcasts.PLMN,
110 CellBroadcasts.LAC,
111 CellBroadcasts.CID,
112 CellBroadcasts.SERIAL_NUMBER,
113 CellBroadcasts.SERVICE_CATEGORY,
114 CellBroadcasts.LANGUAGE_CODE,
115 CellBroadcasts.MESSAGE_BODY,
116 CellBroadcasts.MESSAGE_FORMAT,
117 CellBroadcasts.MESSAGE_PRIORITY,
118 CellBroadcasts.ETWS_WARNING_TYPE,
119 CellBroadcasts.CMAS_MESSAGE_CLASS,
120 CellBroadcasts.CMAS_CATEGORY,
121 CellBroadcasts.CMAS_RESPONSE_TYPE,
122 CellBroadcasts.CMAS_SEVERITY,
123 CellBroadcasts.CMAS_URGENCY,
124 CellBroadcasts.CMAS_CERTAINTY,
125 CellBroadcasts.RECEIVED_TIME,
126 CellBroadcasts.MESSAGE_BROADCASTED,
127 CellBroadcasts.GEOMETRIES,
128 CellBroadcasts.MAXIMUM_WAIT_TIME
Jordan Liud3eeb402019-11-15 11:13:59 -0800129 };
130
Jordan Liu26e820c2019-10-22 14:42:07 -0700131 @VisibleForTesting
132 public PermissionChecker mPermissionChecker;
133
134 /** The database helper for this content provider. */
135 @VisibleForTesting
136 public SQLiteOpenHelper mDbHelper;
137
138 static {
139 sUriMatcher.addURI(AUTHORITY, null, ALL);
140 sUriMatcher.addURI(AUTHORITY, "history", MESSAGE_HISTORY);
141 }
142
143 public CellBroadcastProvider() {}
144
145 @VisibleForTesting
146 public CellBroadcastProvider(PermissionChecker permissionChecker) {
147 mPermissionChecker = permissionChecker;
148 }
149
150 @Override
151 public boolean onCreate() {
152 mDbHelper = new CellBroadcastDatabaseHelper(getContext());
153 mPermissionChecker = new CellBroadcastPermissionChecker();
Jordan Liu26e820c2019-10-22 14:42:07 -0700154 return true;
155 }
156
157 /**
158 * Return the MIME type of the data at the specified URI.
159 *
160 * @param uri the URI to query.
161 * @return a MIME type string, or null if there is no type.
162 */
163 @Override
164 public String getType(Uri uri) {
165 int match = sUriMatcher.match(uri);
166 switch (match) {
167 case ALL:
168 return LIST_TYPE;
169 default:
170 return null;
171 }
172 }
173
174 @Override
175 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
176 String sortOrder) {
177 checkReadPermission(uri);
178
179 if (DBG) {
Jordan Liu75ebf632019-12-16 15:35:27 -0800180 Log.d(TAG, "query:"
Jordan Liu26e820c2019-10-22 14:42:07 -0700181 + " uri = " + uri
182 + " projection = " + Arrays.toString(projection)
183 + " selection = " + selection
184 + " selectionArgs = " + Arrays.toString(selectionArgs)
185 + " sortOrder = " + sortOrder);
186 }
187 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
188 qb.setStrict(true); // a little protection from injection attacks
189 qb.setTables(CELL_BROADCASTS_TABLE_NAME);
190
191 String orderBy;
192 if (!TextUtils.isEmpty(sortOrder)) {
193 orderBy = sortOrder;
194 } else {
195 orderBy = CellBroadcasts.RECEIVED_TIME + " DESC";
196 }
197
198 int match = sUriMatcher.match(uri);
199 switch (match) {
200 case ALL:
201 return getReadableDatabase().query(
202 CELL_BROADCASTS_TABLE_NAME, projection, selection, selectionArgs,
203 null /* groupBy */, null /* having */, orderBy);
204 case MESSAGE_HISTORY:
205 // limit projections to certain columns. limit result to broadcasted messages only.
206 qb.appendWhere(CellBroadcasts.MESSAGE_BROADCASTED + "=1");
207 return qb.query(getReadableDatabase(), projection, selection, selectionArgs, null,
208 null, orderBy);
209 default:
210 throw new IllegalArgumentException(
211 "Query method doesn't support this uri = " + uri);
212 }
213 }
214
215 @Override
216 public Uri insert(Uri uri, ContentValues values) {
217 checkWritePermission();
218
219 if (DBG) {
Jordan Liu75ebf632019-12-16 15:35:27 -0800220 Log.d(TAG, "insert:"
Jordan Liu26e820c2019-10-22 14:42:07 -0700221 + " uri = " + uri
222 + " contentValue = " + values);
223 }
224
225 switch (sUriMatcher.match(uri)) {
226 case ALL:
227 long row = getWritableDatabase().insertOrThrow(CELL_BROADCASTS_TABLE_NAME, null,
228 values);
229 if (row > 0) {
230 Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
231 getContext().getContentResolver()
232 .notifyChange(CONTENT_URI, null /* observer */);
233 return newUri;
234 } else {
Jordan Liu75ebf632019-12-16 15:35:27 -0800235 Log.e(TAG, "Insert record failed because of unknown reason, uri = " + uri);
Jordan Liu26e820c2019-10-22 14:42:07 -0700236 return null;
237 }
238 default:
239 throw new IllegalArgumentException(
240 "Insert method doesn't support this uri = " + uri);
241 }
242 }
243
244 @Override
245 public int delete(Uri uri, String selection, String[] selectionArgs) {
246 checkWritePermission();
247
248 if (DBG) {
Jordan Liu75ebf632019-12-16 15:35:27 -0800249 Log.d(TAG, "delete:"
Jordan Liu26e820c2019-10-22 14:42:07 -0700250 + " uri = " + uri
251 + " selection = " + selection
252 + " selectionArgs = " + Arrays.toString(selectionArgs));
253 }
254
255 switch (sUriMatcher.match(uri)) {
256 case ALL:
257 return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME,
258 selection, selectionArgs);
259 default:
260 throw new IllegalArgumentException(
261 "Delete method doesn't support this uri = " + uri);
262 }
263 }
264
265 @Override
266 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
267 checkWritePermission();
268
269 if (DBG) {
Jordan Liu75ebf632019-12-16 15:35:27 -0800270 Log.d(TAG, "update:"
Jordan Liu26e820c2019-10-22 14:42:07 -0700271 + " uri = " + uri
272 + " values = {" + values + "}"
273 + " selection = " + selection
274 + " selectionArgs = " + Arrays.toString(selectionArgs));
275 }
276
277 switch (sUriMatcher.match(uri)) {
278 case ALL:
279 int rowCount = getWritableDatabase().update(
280 CELL_BROADCASTS_TABLE_NAME,
281 values,
282 selection,
283 selectionArgs);
284 if (rowCount > 0) {
285 getContext().getContentResolver().notifyChange(uri, null /* observer */);
286 }
287 return rowCount;
288 default:
289 throw new IllegalArgumentException(
290 "Update method doesn't support this uri = " + uri);
291 }
292 }
293
294 /**
295 * Returns a string used to create the cell broadcast table. This is exposed so the unit test
296 * can construct its own in-memory database to match the cell broadcast db.
297 */
298 @VisibleForTesting
299 public static String getStringForCellBroadcastTableCreation(String tableName) {
300 return "CREATE TABLE " + tableName + " ("
301 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
Jack Yu0bd7e432019-11-20 23:19:29 -0800302 + CellBroadcasts.SUB_ID + " INTEGER,"
Jordan Liu26e820c2019-10-22 14:42:07 -0700303 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
304 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
305 + CellBroadcasts.PLMN + " TEXT,"
306 + CellBroadcasts.LAC + " INTEGER,"
307 + CellBroadcasts.CID + " INTEGER,"
308 + CellBroadcasts.SERIAL_NUMBER + " INTEGER,"
309 + CellBroadcasts.SERVICE_CATEGORY + " INTEGER,"
310 + CellBroadcasts.LANGUAGE_CODE + " TEXT,"
311 + CellBroadcasts.MESSAGE_BODY + " TEXT,"
312 + CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
313 + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
314 + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
315 + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER,"
316 + CellBroadcasts.CMAS_CATEGORY + " INTEGER,"
317 + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER,"
318 + CellBroadcasts.CMAS_SEVERITY + " INTEGER,"
319 + CellBroadcasts.CMAS_URGENCY + " INTEGER,"
320 + CellBroadcasts.CMAS_CERTAINTY + " INTEGER,"
321 + CellBroadcasts.RECEIVED_TIME + " BIGINT,"
322 + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0,"
323 + CellBroadcasts.GEOMETRIES + " TEXT,"
324 + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);";
325 }
326
327 private SQLiteDatabase getWritableDatabase() {
328 return mDbHelper.getWritableDatabase();
329 }
330
331 private SQLiteDatabase getReadableDatabase() {
332 return mDbHelper.getReadableDatabase();
333 }
334
335 private void checkWritePermission() {
336 if (!mPermissionChecker.hasWritePermission()) {
337 throw new SecurityException(
338 "No permission to write CellBroadcast provider");
339 }
340 }
341
342 private void checkReadPermission(Uri uri) {
343 int match = sUriMatcher.match(uri);
344 switch (match) {
345 case ALL:
346 if (!mPermissionChecker.hasReadPermission()) {
347 throw new SecurityException(
348 "No permission to read CellBroadcast provider");
349 }
350 break;
351 case MESSAGE_HISTORY:
352 // TODO: if we plan to allow apps to query db in framework, we should migrate data
353 // first before deprecating app's database. otherwise users will lose all history.
354 if (!mPermissionChecker.hasReadPermissionForHistory()) {
355 throw new SecurityException(
356 "No permission to read CellBroadcast provider for message history");
357 }
358 break;
359 default:
360 return;
361 }
362 }
363
364 private class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
365 CellBroadcastDatabaseHelper(Context context) {
366 super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
367 }
368
369 @Override
370 public void onCreate(SQLiteDatabase db) {
371 db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
372 }
373
374 @Override
375 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
376 if (DBG) {
Jordan Liu75ebf632019-12-16 15:35:27 -0800377 Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
Jordan Liu26e820c2019-10-22 14:42:07 -0700378 }
Jack Yu0bd7e432019-11-20 23:19:29 -0800379 if (oldVersion < 2) {
Jordan Liu26e820c2019-10-22 14:42:07 -0700380 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
381 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
Jordan Liu75ebf632019-12-16 15:35:27 -0800382 Log.d(TAG, "add slotIndex column");
Jordan Liu26e820c2019-10-22 14:42:07 -0700383 }
384 }
385 }
386
387 private class CellBroadcastPermissionChecker implements PermissionChecker {
388 @Override
389 public boolean hasWritePermission() {
390 // Only the phone and network statck process has the write permission to modify this
391 // provider.
392 return Binder.getCallingUid() == Process.PHONE_UID
393 || Binder.getCallingUid() == Process.NETWORK_STACK_UID;
394 }
395
396 @Override
397 public boolean hasReadPermission() {
398 // Only the phone and network stack process has the read permission to query data from
399 // this provider.
400 return Binder.getCallingUid() == Process.PHONE_UID
401 || Binder.getCallingUid() == Process.NETWORK_STACK_UID;
402 }
403
404 @Override
405 public boolean hasReadPermissionForHistory() {
406 int status = getContext().checkCallingOrSelfPermission(
407 "android.permission.RECEIVE_EMERGENCY_BROADCAST");
408 if (status == PackageManager.PERMISSION_GRANTED) {
409 return true;
410 }
411 return false;
412 }
413 }
414}