blob: 8b1a7479ca0b5e627cc90fe054c3d8285da46e15 [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
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;
34import android.text.TextUtils;
35import android.util.Log;
36
37import com.android.internal.annotations.VisibleForTesting;
38
39import java.util.Arrays;
40
41/**
42 * The content provider that provides access of cell broadcast message to application.
43 * Permission {@link android.permission.READ_CELL_BROADCASTS} is required for querying the cell
44 * broadcast message. Only phone process has the permission to write/update the database via this
45 * provider.
46 */
47public class CellBroadcastProvider extends ContentProvider {
48 /** Interface for read/write permission check. */
49 public interface PermissionChecker {
50 /** Return {@code True} if the caller has the permission to write/update the database. */
51 boolean hasWritePermission();
52
53 /** Return {@code True} if the caller has the permission to query the complete database. */
54 boolean hasReadPermission();
55
56 /**
57 * Return {@code True} if the caller has the permission to query the database for
58 * cell broadcast message history.
59 */
60 boolean hasReadPermissionForHistory();
61 }
62
63 private static final String TAG = CellBroadcastProvider.class.getSimpleName();
64
65 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
66
67 /** Database name. */
68 private static final String DATABASE_NAME = "cellbroadcasts.db";
69
70 /** Database version. */
71 private static final int DATABASE_VERSION = 2;
72
73 /** URI matcher for ContentProvider queries. */
74 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
75
76 /** URI matcher type to get all cell broadcasts. */
77 private static final int ALL = 0;
78
79 /**
80 * URI matcher type for get all message history, this is used primarily for default
81 * cellbroadcast app or messaging app to display message history. some information is not
82 * exposed for messaging history, e.g, messages which are out of broadcast geometrics will not
83 * be delivered to end users thus will not be returned as message history query result.
84 */
85 private static final int MESSAGE_HISTORY = 1;
86
87 /** MIME type for the list of all cell broadcasts. */
88 private static final String LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast";
89
90 /** Table name of cell broadcast message. */
91 @VisibleForTesting
92 public static final String CELL_BROADCASTS_TABLE_NAME = "cell_broadcasts";
93
94 /** Authority string for content URIs. */
95 @VisibleForTesting
96 public static final String AUTHORITY = "cellbroadcasts";
97
98 /** Content uri of this provider. */
99 public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
100
101 @VisibleForTesting
102 public PermissionChecker mPermissionChecker;
103
104 /** The database helper for this content provider. */
105 @VisibleForTesting
106 public SQLiteOpenHelper mDbHelper;
107
108 static {
109 sUriMatcher.addURI(AUTHORITY, null, ALL);
110 sUriMatcher.addURI(AUTHORITY, "history", MESSAGE_HISTORY);
111 }
112
113 public CellBroadcastProvider() {}
114
115 @VisibleForTesting
116 public CellBroadcastProvider(PermissionChecker permissionChecker) {
117 mPermissionChecker = permissionChecker;
118 }
119
120 @Override
121 public boolean onCreate() {
122 mDbHelper = new CellBroadcastDatabaseHelper(getContext());
123 mPermissionChecker = new CellBroadcastPermissionChecker();
124 setAppOps(AppOpsManager.OP_READ_CELL_BROADCASTS, AppOpsManager.OP_NONE);
125 return true;
126 }
127
128 /**
129 * Return the MIME type of the data at the specified URI.
130 *
131 * @param uri the URI to query.
132 * @return a MIME type string, or null if there is no type.
133 */
134 @Override
135 public String getType(Uri uri) {
136 int match = sUriMatcher.match(uri);
137 switch (match) {
138 case ALL:
139 return LIST_TYPE;
140 default:
141 return null;
142 }
143 }
144
145 @Override
146 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
147 String sortOrder) {
148 checkReadPermission(uri);
149
150 if (DBG) {
151 Log.d(TAG, "query:"
152 + " uri = " + uri
153 + " projection = " + Arrays.toString(projection)
154 + " selection = " + selection
155 + " selectionArgs = " + Arrays.toString(selectionArgs)
156 + " sortOrder = " + sortOrder);
157 }
158 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
159 qb.setStrict(true); // a little protection from injection attacks
160 qb.setTables(CELL_BROADCASTS_TABLE_NAME);
161
162 String orderBy;
163 if (!TextUtils.isEmpty(sortOrder)) {
164 orderBy = sortOrder;
165 } else {
166 orderBy = CellBroadcasts.RECEIVED_TIME + " DESC";
167 }
168
169 int match = sUriMatcher.match(uri);
170 switch (match) {
171 case ALL:
172 return getReadableDatabase().query(
173 CELL_BROADCASTS_TABLE_NAME, projection, selection, selectionArgs,
174 null /* groupBy */, null /* having */, orderBy);
175 case MESSAGE_HISTORY:
176 // limit projections to certain columns. limit result to broadcasted messages only.
177 qb.appendWhere(CellBroadcasts.MESSAGE_BROADCASTED + "=1");
178 return qb.query(getReadableDatabase(), projection, selection, selectionArgs, null,
179 null, orderBy);
180 default:
181 throw new IllegalArgumentException(
182 "Query method doesn't support this uri = " + uri);
183 }
184 }
185
186 @Override
187 public Uri insert(Uri uri, ContentValues values) {
188 checkWritePermission();
189
190 if (DBG) {
191 Log.d(TAG, "insert:"
192 + " uri = " + uri
193 + " contentValue = " + values);
194 }
195
196 switch (sUriMatcher.match(uri)) {
197 case ALL:
198 long row = getWritableDatabase().insertOrThrow(CELL_BROADCASTS_TABLE_NAME, null,
199 values);
200 if (row > 0) {
201 Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
202 getContext().getContentResolver()
203 .notifyChange(CONTENT_URI, null /* observer */);
204 return newUri;
205 } else {
206 Log.e(TAG, "Insert record failed because of unknown reason, uri = " + uri);
207 return null;
208 }
209 default:
210 throw new IllegalArgumentException(
211 "Insert method doesn't support this uri = " + uri);
212 }
213 }
214
215 @Override
216 public int delete(Uri uri, String selection, String[] selectionArgs) {
217 checkWritePermission();
218
219 if (DBG) {
220 Log.d(TAG, "delete:"
221 + " uri = " + uri
222 + " selection = " + selection
223 + " selectionArgs = " + Arrays.toString(selectionArgs));
224 }
225
226 switch (sUriMatcher.match(uri)) {
227 case ALL:
228 return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME,
229 selection, selectionArgs);
230 default:
231 throw new IllegalArgumentException(
232 "Delete method doesn't support this uri = " + uri);
233 }
234 }
235
236 @Override
237 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
238 checkWritePermission();
239
240 if (DBG) {
241 Log.d(TAG, "update:"
242 + " uri = " + uri
243 + " values = {" + values + "}"
244 + " selection = " + selection
245 + " selectionArgs = " + Arrays.toString(selectionArgs));
246 }
247
248 switch (sUriMatcher.match(uri)) {
249 case ALL:
250 int rowCount = getWritableDatabase().update(
251 CELL_BROADCASTS_TABLE_NAME,
252 values,
253 selection,
254 selectionArgs);
255 if (rowCount > 0) {
256 getContext().getContentResolver().notifyChange(uri, null /* observer */);
257 }
258 return rowCount;
259 default:
260 throw new IllegalArgumentException(
261 "Update method doesn't support this uri = " + uri);
262 }
263 }
264
265 /**
266 * Returns a string used to create the cell broadcast table. This is exposed so the unit test
267 * can construct its own in-memory database to match the cell broadcast db.
268 */
269 @VisibleForTesting
270 public static String getStringForCellBroadcastTableCreation(String tableName) {
271 return "CREATE TABLE " + tableName + " ("
272 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
273 + CellBroadcasts.SUB_ID + " INTEGER,"
274 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
275 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
276 + CellBroadcasts.PLMN + " TEXT,"
277 + CellBroadcasts.LAC + " INTEGER,"
278 + CellBroadcasts.CID + " INTEGER,"
279 + CellBroadcasts.SERIAL_NUMBER + " INTEGER,"
280 + CellBroadcasts.SERVICE_CATEGORY + " INTEGER,"
281 + CellBroadcasts.LANGUAGE_CODE + " TEXT,"
282 + CellBroadcasts.MESSAGE_BODY + " TEXT,"
283 + CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
284 + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
285 + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
286 + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER,"
287 + CellBroadcasts.CMAS_CATEGORY + " INTEGER,"
288 + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER,"
289 + CellBroadcasts.CMAS_SEVERITY + " INTEGER,"
290 + CellBroadcasts.CMAS_URGENCY + " INTEGER,"
291 + CellBroadcasts.CMAS_CERTAINTY + " INTEGER,"
292 + CellBroadcasts.RECEIVED_TIME + " BIGINT,"
293 + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0,"
294 + CellBroadcasts.GEOMETRIES + " TEXT,"
295 + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);";
296 }
297
298 private SQLiteDatabase getWritableDatabase() {
299 return mDbHelper.getWritableDatabase();
300 }
301
302 private SQLiteDatabase getReadableDatabase() {
303 return mDbHelper.getReadableDatabase();
304 }
305
306 private void checkWritePermission() {
307 if (!mPermissionChecker.hasWritePermission()) {
308 throw new SecurityException(
309 "No permission to write CellBroadcast provider");
310 }
311 }
312
313 private void checkReadPermission(Uri uri) {
314 int match = sUriMatcher.match(uri);
315 switch (match) {
316 case ALL:
317 if (!mPermissionChecker.hasReadPermission()) {
318 throw new SecurityException(
319 "No permission to read CellBroadcast provider");
320 }
321 break;
322 case MESSAGE_HISTORY:
323 // TODO: if we plan to allow apps to query db in framework, we should migrate data
324 // first before deprecating app's database. otherwise users will lose all history.
325 if (!mPermissionChecker.hasReadPermissionForHistory()) {
326 throw new SecurityException(
327 "No permission to read CellBroadcast provider for message history");
328 }
329 break;
330 default:
331 return;
332 }
333 }
334
335 private class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
336 CellBroadcastDatabaseHelper(Context context) {
337 super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
338 }
339
340 @Override
341 public void onCreate(SQLiteDatabase db) {
342 db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
343 }
344
345 @Override
346 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
347 if (DBG) {
348 Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
349 }
350 if (newVersion == 2) {
351 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
352 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
353 Log.d(TAG, "add slotIndex column");
354 }
355 }
356 }
357
358 private class CellBroadcastPermissionChecker implements PermissionChecker {
359 @Override
360 public boolean hasWritePermission() {
361 // Only the phone and network statck process has the write permission to modify this
362 // provider.
363 return Binder.getCallingUid() == Process.PHONE_UID
364 || Binder.getCallingUid() == Process.NETWORK_STACK_UID;
365 }
366
367 @Override
368 public boolean hasReadPermission() {
369 // Only the phone and network stack process has the read permission to query data from
370 // this provider.
371 return Binder.getCallingUid() == Process.PHONE_UID
372 || Binder.getCallingUid() == Process.NETWORK_STACK_UID;
373 }
374
375 @Override
376 public boolean hasReadPermissionForHistory() {
377 int status = getContext().checkCallingOrSelfPermission(
378 "android.permission.RECEIVE_EMERGENCY_BROADCAST");
379 if (status == PackageManager.PERMISSION_GRANTED) {
380 return true;
381 }
382 return false;
383 }
384 }
385}