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