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.CellBroadcastMetrics.ERR_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 CellBroadcastServiceMetrics.getInstance().logMessageError( 239 ERR_FAILED_TO_INSERT_TO_DB, errorString); 240 Log.e(TAG, "Insert record failed because of unknown reason. " + errorString); 241 return null; 242 } 243 default: 244 String errorString = "Insert method doesn't support this uri=" 245 + uri.toString() + " values=" + values; 246 // 1000 character limit for error logs 247 if (errorString.length() > 1000) { 248 errorString = errorString.substring(0, 1000); 249 } 250 CellBroadcastServiceMetrics.getInstance().logMessageError( 251 ERR_FAILED_TO_INSERT_TO_DB, errorString); 252 throw new IllegalArgumentException(errorString); 253 } 254 } 255 256 @Override delete(Uri uri, String selection, String[] selectionArgs)257 public int delete(Uri uri, String selection, String[] selectionArgs) { 258 checkWritePermission(); 259 260 if (DBG) { 261 Log.d(TAG, "delete:" 262 + " uri = " + uri 263 + " selection = " + selection 264 + " selectionArgs = " + Arrays.toString(selectionArgs)); 265 } 266 267 switch (sUriMatcher.match(uri)) { 268 case ALL: 269 return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME, 270 selection, selectionArgs); 271 default: 272 throw new IllegalArgumentException( 273 "Delete method doesn't support this uri = " + uri); 274 } 275 } 276 277 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)278 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 279 checkWritePermission(); 280 281 if (DBG) { 282 Log.d(TAG, "update:" 283 + " uri = " + uri 284 + " values = {" + values + "}" 285 + " selection = " + selection 286 + " selectionArgs = " + Arrays.toString(selectionArgs)); 287 } 288 289 int rowCount = 0; 290 switch (sUriMatcher.match(uri)) { 291 case ALL: 292 rowCount = getWritableDatabase().update( 293 CELL_BROADCASTS_TABLE_NAME, 294 values, 295 selection, 296 selectionArgs); 297 if (rowCount > 0) { 298 getContext().getContentResolver().notifyChange(uri, null /* observer */, 299 ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS 300 | ContentResolver.NOTIFY_SYNC_TO_NETWORK ); 301 } 302 return rowCount; 303 case MESSAGE_DISPLAYED: 304 // mark message was displayed to the end-users. 305 values.put(Telephony.CellBroadcasts.MESSAGE_DISPLAYED, 1); 306 rowCount = getWritableDatabase().update( 307 CELL_BROADCASTS_TABLE_NAME, 308 values, 309 selection, 310 selectionArgs); 311 if (rowCount > 0) { 312 // update was succeed. the row number of the updated message. 313 try (Cursor ret = query(CellBroadcasts.CONTENT_URI, 314 new String[]{CellBroadcasts._ID}, 315 selection, selectionArgs, null)) { 316 if (ret != null && ret.moveToFirst()) { 317 int rowNumber = ret.getInt(ret.getColumnIndex(CellBroadcasts._ID)); 318 Log.d(TAG, "notify contentObservers for the displayed message, row: " 319 + rowNumber); 320 getContext().getContentResolver().notifyChange( 321 Uri.withAppendedPath(CONTENT_URI, 322 "displayed/" + rowNumber), null, true); 323 } 324 } catch (Exception ex) { 325 Log.e(TAG, "exception during update message displayed: " + ex.toString()); 326 } 327 } 328 return rowCount; 329 default: 330 throw new IllegalArgumentException( 331 "Update method doesn't support this uri = " + uri); 332 } 333 } 334 335 /** 336 * Returns a string used to create the cell broadcast table. This is exposed so the unit test 337 * can construct its own in-memory database to match the cell broadcast db. 338 */ 339 @VisibleForTesting getStringForCellBroadcastTableCreation(String tableName)340 public static String getStringForCellBroadcastTableCreation(String tableName) { 341 return "CREATE TABLE " + tableName + " (" 342 + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," 343 + CellBroadcasts.SUBSCRIPTION_ID + " INTEGER," 344 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0," 345 + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER," 346 + CellBroadcasts.PLMN + " TEXT," 347 + CellBroadcasts.LAC + " INTEGER," 348 + CellBroadcasts.CID + " INTEGER," 349 + CellBroadcasts.SERIAL_NUMBER + " INTEGER," 350 + CellBroadcasts.SERVICE_CATEGORY + " INTEGER," 351 + CellBroadcasts.LANGUAGE_CODE + " TEXT," 352 + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0," 353 + CellBroadcasts.MESSAGE_BODY + " TEXT," 354 + CellBroadcasts.MESSAGE_FORMAT + " INTEGER," 355 + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER," 356 + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER," 357 + CellBroadcasts.ETWS_IS_PRIMARY + " BOOLEAN DEFAULT 0," 358 + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER," 359 + CellBroadcasts.CMAS_CATEGORY + " INTEGER," 360 + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER," 361 + CellBroadcasts.CMAS_SEVERITY + " INTEGER," 362 + CellBroadcasts.CMAS_URGENCY + " INTEGER," 363 + CellBroadcasts.CMAS_CERTAINTY + " INTEGER," 364 + CellBroadcasts.RECEIVED_TIME + " BIGINT," 365 + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1," 366 + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0," 367 + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 0," 368 + CellBroadcasts.GEOMETRIES + " TEXT," 369 + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);"; 370 } 371 getWritableDatabase()372 private SQLiteDatabase getWritableDatabase() { 373 return mDbHelper.getWritableDatabase(); 374 } 375 getReadableDatabase()376 private SQLiteDatabase getReadableDatabase() { 377 return mDbHelper.getReadableDatabase(); 378 } 379 checkWritePermission()380 private void checkWritePermission() { 381 if (!mPermissionChecker.hasFullAccessPermission()) { 382 throw new SecurityException( 383 "No permission to write CellBroadcast provider"); 384 } 385 } 386 checkReadPermission(Uri uri)387 private void checkReadPermission(Uri uri) { 388 int match = sUriMatcher.match(uri); 389 switch (match) { 390 case ALL: 391 if (!mPermissionChecker.hasFullAccessPermission()) { 392 throw new SecurityException( 393 "No permission to read CellBroadcast provider"); 394 } 395 break; 396 case MESSAGE_HISTORY: 397 // The normal read permission android.permission.READ_CELL_BROADCASTS 398 // is defined in AndroidManifest.xml and is enfored by the platform. 399 // So no additional check is required here. 400 break; 401 default: 402 return; 403 } 404 } 405 406 @VisibleForTesting 407 public static class CellBroadcastDatabaseHelper extends SQLiteOpenHelper { CellBroadcastDatabaseHelper(Context context)408 public CellBroadcastDatabaseHelper(Context context) { 409 super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION); 410 } 411 412 @Override onCreate(SQLiteDatabase db)413 public void onCreate(SQLiteDatabase db) { 414 db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME)); 415 } 416 417 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)418 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 419 if (DBG) { 420 Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion); 421 } 422 if (oldVersion < 2) { 423 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " 424 + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;"); 425 Log.d(TAG, "add slotIndex column"); 426 } 427 428 if (oldVersion < 3) { 429 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " 430 + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0;"); 431 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " 432 + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1;"); 433 // Specifically for upgrade, the message displayed should be true. For newly arrived 434 // message, default should be false. 435 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " 436 + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 1;"); 437 Log.d(TAG, "add dcs, location check time, and message displayed column."); 438 } 439 440 if (oldVersion < 4) { 441 db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " 442 + CellBroadcasts.ETWS_IS_PRIMARY + " BOOLEAN DEFAULT 0;"); 443 Log.d(TAG, "add ETWS is_primary column."); 444 } 445 } 446 } 447 448 /** 449 * Cell broadcast permission checker. 450 */ 451 public class CellBroadcastPermissionChecker { 452 /** 453 * @return {@code true} if the caller has permission to fully access the cell broadcast 454 * provider. 455 */ hasFullAccessPermission()456 public boolean hasFullAccessPermission() { 457 int status = getContext().checkCallingOrSelfPermission( 458 "com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY"); 459 return status == PackageManager.PERMISSION_GRANTED; 460 } 461 } 462 } 463