1 /* 2 * Copyright (C) 2012 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.cellbroadcastreceiver; 18 19 import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRSRC_CBR; 20 import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRTYPE_PROVIDERINIT; 21 22 import android.annotation.NonNull; 23 import android.content.ContentProvider; 24 import android.content.ContentProviderClient; 25 import android.content.ContentResolver; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.content.UriMatcher; 29 import android.database.Cursor; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.database.sqlite.SQLiteQueryBuilder; 32 import android.net.Uri; 33 import android.os.AsyncTask; 34 import android.os.Bundle; 35 import android.os.UserManager; 36 import android.provider.Telephony; 37 import android.telephony.SmsCbCmasInfo; 38 import android.telephony.SmsCbEtwsInfo; 39 import android.telephony.SmsCbLocation; 40 import android.telephony.SmsCbMessage; 41 import android.text.TextUtils; 42 import android.util.Log; 43 44 import com.android.internal.annotations.VisibleForTesting; 45 46 import java.util.concurrent.CountDownLatch; 47 48 /** 49 * ContentProvider for the database of received cell broadcasts. 50 */ 51 public class CellBroadcastContentProvider extends ContentProvider { 52 private static final String TAG = "CellBroadcastContentProvider"; 53 54 /** URI matcher for ContentProvider queries. */ 55 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 56 57 /** Authority string for content URIs. */ 58 @VisibleForTesting 59 public static final String CB_AUTHORITY = "cellbroadcasts-app"; 60 61 /** Content URI for notifying observers. */ 62 static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts-app/"); 63 64 /** URI matcher type to get all cell broadcasts. */ 65 private static final int CB_ALL = 0; 66 67 /** URI matcher type to get a cell broadcast by ID. */ 68 private static final int CB_ALL_ID = 1; 69 70 /** MIME type for the list of all cell broadcasts. */ 71 private static final String CB_LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast"; 72 73 /** MIME type for an individual cell broadcast. */ 74 private static final String CB_TYPE = "vnd.android.cursor.item/cellbroadcast"; 75 76 public static final String CALL_MIGRATION_METHOD = "migrate-legacy-data"; 77 78 static { sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL)79 sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL); sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID)80 sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID); 81 } 82 83 /** 84 * The database for this content provider. Before using this we need to wait on 85 * mInitializedLatch, which counts down once initialization finishes in a background thread 86 */ 87 88 @VisibleForTesting 89 public CellBroadcastDatabaseHelper mOpenHelper; 90 91 // Latch which counts down from 1 when initialization in CellBroadcastOpenHelper.tryToMigrateV13 92 // is finished 93 private final CountDownLatch mInitializedLatch = new CountDownLatch(1); 94 95 /** 96 * Initialize content provider. 97 * @return true if the provider was successfully loaded, false otherwise 98 */ 99 @Override onCreate()100 public boolean onCreate() { 101 mOpenHelper = new CellBroadcastDatabaseHelper(getContext(), false); 102 // trigger this to create database explicitly. Otherwise the db will be created only after 103 // the first query/update/insertion. Data migration is done inside db creation and we want 104 // to migrate data from cellbroadcast-legacy immediately when upgrade to the mainline module 105 // rather than migrate after the first emergency alert. 106 // getReadable database will also call tryToMigrateV13 which copies the DB file to allow 107 // for safe rollbacks. 108 // This is done in a background thread to avoid triggering an ANR if the disk operations are 109 // too slow, and all other database uses should wait for the latch. 110 new Thread(() -> { 111 mOpenHelper.getReadableDatabase(); 112 mInitializedLatch.countDown(); 113 }).start(); 114 return true; 115 } 116 awaitInitAndGetWritableDatabase()117 protected SQLiteDatabase awaitInitAndGetWritableDatabase() { 118 while (mInitializedLatch.getCount() != 0) { 119 try { 120 mInitializedLatch.await(); 121 } catch (InterruptedException e) { 122 CellBroadcastReceiverMetrics.getInstance().logModuleError( 123 ERRSRC_CBR, ERRTYPE_PROVIDERINIT); 124 Log.e(TAG, "Interrupted while waiting for db initialization. e=" + e); 125 } 126 } 127 return mOpenHelper.getWritableDatabase(); 128 } 129 awaitInitAndGetReadableDatabase()130 protected SQLiteDatabase awaitInitAndGetReadableDatabase() { 131 while (mInitializedLatch.getCount() != 0) { 132 try { 133 mInitializedLatch.await(); 134 } catch (InterruptedException e) { 135 CellBroadcastReceiverMetrics.getInstance().logModuleError( 136 ERRSRC_CBR, ERRTYPE_PROVIDERINIT); 137 Log.e(TAG, "Interrupted while waiting for db initialization. e=" + e); 138 } 139 } 140 return mOpenHelper.getReadableDatabase(); 141 } 142 143 /** 144 * Return a cursor for the cell broadcast table. 145 * @param uri the URI to query. 146 * @param projection the list of columns to put into the cursor, or null. 147 * @param selection the selection criteria to apply when filtering rows, or null. 148 * @param selectionArgs values to replace ?s in selection string. 149 * @param sortOrder how the rows in the cursor should be sorted, or null to sort from most 150 * recently received to least recently received. 151 * @return a Cursor or null. 152 */ 153 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)154 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 155 String sortOrder) { 156 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 157 qb.setTables(CellBroadcastDatabaseHelper.TABLE_NAME); 158 159 int match = sUriMatcher.match(uri); 160 switch (match) { 161 case CB_ALL: 162 // get all broadcasts 163 break; 164 165 case CB_ALL_ID: 166 // get broadcast by ID 167 qb.appendWhere("(_id=" + uri.getPathSegments().get(0) + ')'); 168 break; 169 170 default: 171 Log.e(TAG, "Invalid query: " + uri); 172 throw new IllegalArgumentException("Unknown URI: " + uri); 173 } 174 175 String orderBy; 176 if (!TextUtils.isEmpty(sortOrder)) { 177 orderBy = sortOrder; 178 } else { 179 orderBy = Telephony.CellBroadcasts.DEFAULT_SORT_ORDER; 180 } 181 182 SQLiteDatabase db = awaitInitAndGetReadableDatabase(); 183 Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy); 184 if (c != null) { 185 c.setNotificationUri(getContext().getContentResolver(), CONTENT_URI); 186 } 187 return c; 188 } 189 190 /** 191 * Return the MIME type of the data at the specified URI. 192 * @param uri the URI to query. 193 * @return a MIME type string, or null if there is no type. 194 */ 195 @Override getType(Uri uri)196 public String getType(Uri uri) { 197 int match = sUriMatcher.match(uri); 198 switch (match) { 199 case CB_ALL: 200 return CB_LIST_TYPE; 201 202 case CB_ALL_ID: 203 return CB_TYPE; 204 205 default: 206 return null; 207 } 208 } 209 210 /** 211 * Insert a new row. This throws an exception, as the database can only be modified by 212 * calling custom methods in this class, and not via the ContentProvider interface. 213 * @param uri the content:// URI of the insertion request. 214 * @param values a set of column_name/value pairs to add to the database. 215 * @return the URI for the newly inserted item. 216 */ 217 @Override insert(Uri uri, ContentValues values)218 public Uri insert(Uri uri, ContentValues values) { 219 throw new UnsupportedOperationException("insert not supported"); 220 } 221 222 /** 223 * Delete one or more rows. This throws an exception, as the database can only be modified by 224 * calling custom methods in this class, and not via the ContentProvider interface. 225 * @param uri the full URI to query, including a row ID (if a specific record is requested). 226 * @param selection an optional restriction to apply to rows when deleting. 227 * @return the number of rows affected. 228 */ 229 @Override delete(Uri uri, String selection, String[] selectionArgs)230 public int delete(Uri uri, String selection, String[] selectionArgs) { 231 throw new UnsupportedOperationException("delete not supported"); 232 } 233 234 /** 235 * Update one or more rows. This throws an exception, as the database can only be modified by 236 * calling custom methods in this class, and not via the ContentProvider interface. 237 * @param uri the URI to query, potentially including the row ID. 238 * @param values a Bundle mapping from column names to new column values. 239 * @param selection an optional filter to match rows to update. 240 * @return the number of rows affected. 241 */ 242 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)243 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 244 throw new UnsupportedOperationException("update not supported"); 245 } 246 247 @Override call(String method, String name, Bundle args)248 public Bundle call(String method, String name, Bundle args) { 249 Log.d(TAG, "call:" 250 + " method=" + method 251 + " name=" + name 252 + " args=" + args); 253 // this is to handle a content-provider defined method: migration 254 if (CALL_MIGRATION_METHOD.equals(method)) { 255 mOpenHelper.migrateFromLegacyIfNeeded(awaitInitAndGetReadableDatabase()); 256 } 257 return null; 258 } 259 getContentValues(SmsCbMessage message)260 private ContentValues getContentValues(SmsCbMessage message) { 261 ContentValues cv = new ContentValues(); 262 cv.put(Telephony.CellBroadcasts.SLOT_INDEX, message.getSlotIndex()); 263 cv.put(Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, message.getGeographicalScope()); 264 SmsCbLocation location = message.getLocation(); 265 cv.put(Telephony.CellBroadcasts.PLMN, location.getPlmn()); 266 if (location.getLac() != -1) { 267 cv.put(Telephony.CellBroadcasts.LAC, location.getLac()); 268 } 269 if (location.getCid() != -1) { 270 cv.put(Telephony.CellBroadcasts.CID, location.getCid()); 271 } 272 cv.put(Telephony.CellBroadcasts.SERIAL_NUMBER, message.getSerialNumber()); 273 cv.put(Telephony.CellBroadcasts.SERVICE_CATEGORY, message.getServiceCategory()); 274 cv.put(Telephony.CellBroadcasts.LANGUAGE_CODE, message.getLanguageCode()); 275 cv.put(Telephony.CellBroadcasts.MESSAGE_BODY, message.getMessageBody()); 276 cv.put(Telephony.CellBroadcasts.DELIVERY_TIME, message.getReceivedTime()); 277 cv.put(Telephony.CellBroadcasts.MESSAGE_FORMAT, message.getMessageFormat()); 278 cv.put(Telephony.CellBroadcasts.MESSAGE_PRIORITY, message.getMessagePriority()); 279 280 SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo(); 281 if (etwsInfo != null) { 282 cv.put(Telephony.CellBroadcasts.ETWS_WARNING_TYPE, etwsInfo.getWarningType()); 283 } 284 285 SmsCbCmasInfo cmasInfo = message.getCmasWarningInfo(); 286 if (cmasInfo != null) { 287 cv.put(Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, cmasInfo.getMessageClass()); 288 cv.put(Telephony.CellBroadcasts.CMAS_CATEGORY, cmasInfo.getCategory()); 289 cv.put(Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, cmasInfo.getResponseType()); 290 cv.put(Telephony.CellBroadcasts.CMAS_SEVERITY, cmasInfo.getSeverity()); 291 cv.put(Telephony.CellBroadcasts.CMAS_URGENCY, cmasInfo.getUrgency()); 292 cv.put(Telephony.CellBroadcasts.CMAS_CERTAINTY, cmasInfo.getCertainty()); 293 } 294 295 return cv; 296 } 297 298 /** 299 * Internal method to insert a new Cell Broadcast into the database and notify observers. 300 * @param message the message to insert 301 * @return true if the broadcast is new, false if it's a duplicate broadcast. 302 */ 303 @VisibleForTesting insertNewBroadcast(SmsCbMessage message)304 public boolean insertNewBroadcast(SmsCbMessage message) { 305 SQLiteDatabase db = awaitInitAndGetWritableDatabase(); 306 ContentValues cv = getContentValues(message); 307 308 // Note: this method previously queried the database for duplicate message IDs, but this 309 // is not compatible with CMAS carrier requirements and could also cause other emergency 310 // alerts, e.g. ETWS, to not display if the database is filled with old messages. 311 // Use duplicate message ID detection in CellBroadcastAlertService instead of DB query. 312 long rowId = db.insert(CellBroadcastDatabaseHelper.TABLE_NAME, null, cv); 313 if (rowId == -1) { 314 Log.e(TAG, "failed to insert new broadcast into database"); 315 // Return true on DB write failure because we still want to notify the user. 316 // The SmsCbMessage will be passed with the intent, so the message will be 317 // displayed in the emergency alert dialog, or the dialog that is displayed when 318 // the user selects the notification for a non-emergency broadcast, even if the 319 // broadcast could not be written to the database. 320 } 321 return true; // broadcast is not a duplicate 322 } 323 324 /** 325 * Internal method to delete a cell broadcast by row ID and notify observers. 326 * @param rowId the row ID of the broadcast to delete 327 * @return true if the database was updated, false otherwise 328 */ 329 @VisibleForTesting deleteBroadcast(long rowId)330 public boolean deleteBroadcast(long rowId) { 331 SQLiteDatabase db = awaitInitAndGetWritableDatabase(); 332 333 int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, 334 Telephony.CellBroadcasts._ID + "=?", 335 new String[]{Long.toString(rowId)}); 336 if (rowCount != 0) { 337 return true; 338 } else { 339 Log.e(TAG, "failed to delete broadcast at row " + rowId); 340 return false; 341 } 342 } 343 344 /** 345 * Internal method to delete all cell broadcasts and notify observers. 346 * @return true if the database was updated, false otherwise 347 */ 348 @VisibleForTesting deleteAllBroadcasts()349 public boolean deleteAllBroadcasts() { 350 SQLiteDatabase db = awaitInitAndGetWritableDatabase(); 351 352 int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, null, null); 353 if (rowCount != 0) { 354 return true; 355 } else { 356 Log.e(TAG, "failed to delete all broadcasts"); 357 return false; 358 } 359 } 360 361 /** 362 * Internal method to mark a broadcast as read and notify observers. The broadcast can be 363 * identified by delivery time (for new alerts) or by row ID. The caller is responsible for 364 * decrementing the unread non-emergency alert count, if necessary. 365 * 366 * @param columnName the column name to query (ID or delivery time) 367 * @param columnValue the ID or delivery time of the broadcast to mark read 368 * @return true if the database was updated, false otherwise 369 */ markBroadcastRead(String columnName, long columnValue)370 boolean markBroadcastRead(String columnName, long columnValue) { 371 SQLiteDatabase db = awaitInitAndGetWritableDatabase(); 372 373 ContentValues cv = new ContentValues(1); 374 cv.put(Telephony.CellBroadcasts.MESSAGE_READ, 1); 375 376 String whereClause = columnName + "=?"; 377 String[] whereArgs = new String[]{Long.toString(columnValue)}; 378 379 int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause, whereArgs); 380 if (rowCount != 0) { 381 return true; 382 } else { 383 Log.e(TAG, "failed to mark broadcast read: " + columnName + " = " + columnValue); 384 return false; 385 } 386 } 387 388 /** 389 * Internal method to mark a broadcast received in direct boot mode. After user unlocks, mark 390 * all messages not in direct boot mode. 391 * 392 * @param columnName the column name to query (ID or delivery time) 393 * @param columnValue the ID or delivery time of the broadcast to mark read 394 * @param isSmsSyncPending whether the message was pending for SMS inbox synchronization 395 * @return true if the database was updated, false otherwise 396 */ 397 @VisibleForTesting markBroadcastSmsSyncPending(String columnName, long columnValue, boolean isSmsSyncPending)398 public boolean markBroadcastSmsSyncPending(String columnName, long columnValue, 399 boolean isSmsSyncPending) { 400 SQLiteDatabase db = awaitInitAndGetWritableDatabase(); 401 402 ContentValues cv = new ContentValues(1); 403 cv.put(CellBroadcastDatabaseHelper.SMS_SYNC_PENDING, isSmsSyncPending ? 1 : 0); 404 405 String whereClause = columnName + "=?"; 406 String[] whereArgs = new String[]{Long.toString(columnValue)}; 407 408 int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause, 409 whereArgs); 410 if (rowCount != 0) { 411 return true; 412 } else { 413 Log.e(TAG, "failed to mark broadcast pending for sms inbox sync: " + isSmsSyncPending 414 + " where: " + columnName + " = " + columnValue); 415 return false; 416 } 417 } 418 419 /** 420 * Write message to sms inbox if pending. e.g, when receive alerts in direct boot mode, we 421 * might need to sync message to sms inbox after user unlock. 422 * @param context 423 */ 424 425 @VisibleForTesting resyncToSmsInbox(@onNull Context context)426 public void resyncToSmsInbox(@NonNull Context context) { 427 // query all messages currently marked as sms inbox sync pending 428 try (Cursor cursor = query( 429 CellBroadcastContentProvider.CONTENT_URI, 430 CellBroadcastDatabaseHelper.QUERY_COLUMNS, 431 CellBroadcastDatabaseHelper.SMS_SYNC_PENDING + "=1", 432 null, null)) { 433 if (cursor != null) { 434 while (cursor.moveToNext()) { 435 SmsCbMessage message = CellBroadcastCursorAdapter 436 .createFromCursor(context, cursor); 437 if (message != null) { 438 Log.d(TAG, "handling message received pending for sms sync: " 439 + message.toString()); 440 writeMessageToSmsInbox(message, context); 441 // mark message received in direct mode was handled 442 markBroadcastSmsSyncPending( 443 Telephony.CellBroadcasts.DELIVERY_TIME, 444 message.getReceivedTime(), false); 445 } 446 } 447 } 448 } 449 } 450 451 /** 452 * Write displayed cellbroadcast messages to sms inbox 453 * 454 * @param message The cell broadcast message. 455 */ 456 @VisibleForTesting writeMessageToSmsInbox(@onNull SmsCbMessage message, @NonNull Context context)457 public void writeMessageToSmsInbox(@NonNull SmsCbMessage message, @NonNull Context context) { 458 UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); 459 if (!userManager.isSystemUser()) { 460 // SMS database is single-user mode, discard non-system users to avoid inserting twice. 461 Log.d(TAG, "ignoring writeMessageToSmsInbox due to non-system user"); 462 return; 463 } 464 // Note SMS database is not direct boot aware for privacy reasons, we should only interact 465 // with sms db until users has unlocked. 466 if (!userManager.isUserUnlocked()) { 467 Log.d(TAG, "ignoring writeMessageToSmsInbox due to direct boot mode"); 468 // need to retry after user unlock 469 markBroadcastSmsSyncPending(Telephony.CellBroadcasts.DELIVERY_TIME, 470 message.getReceivedTime(), true); 471 return; 472 } 473 // composing SMS 474 ContentValues cv = new ContentValues(); 475 cv.put(Telephony.Sms.Inbox.BODY, message.getMessageBody()); 476 cv.put(Telephony.Sms.Inbox.DATE, message.getReceivedTime()); 477 cv.put(Telephony.Sms.Inbox.SUBSCRIPTION_ID, message.getSubscriptionId()); 478 cv.put(Telephony.Sms.Inbox.SUBJECT, context.getString( 479 CellBroadcastResources.getDialogTitleResource(context, message))); 480 cv.put(Telephony.Sms.Inbox.ADDRESS, 481 CellBroadcastResources.getSmsSenderAddressResourceEnglishString(context, message)); 482 cv.put(Telephony.Sms.Inbox.THREAD_ID, Telephony.Threads.getOrCreateThreadId(context, 483 CellBroadcastResources.getSmsSenderAddressResourceEnglishString(context, message))); 484 if (CellBroadcastSettings.getResourcesByOperator(context, message.getSubscriptionId(), 485 CellBroadcastReceiver.getRoamingOperatorSupported(context)) 486 .getBoolean(R.bool.always_mark_sms_read)) { 487 // Always mark SMS message READ. End users expect when they read new CBS messages, 488 // the unread alert count in the notification should be decreased, as they thought it 489 // was coming from SMS. Now we are marking those SMS as read (SMS now serve as a message 490 // history purpose) and that should give clear messages to end-users that alerts are not 491 // from the SMS app but CellBroadcast and they should tap the notification to read alert 492 // in order to see decreased unread message count. 493 cv.put(Telephony.Sms.Inbox.READ, 1); 494 } 495 Uri uri = context.getContentResolver().insert(Telephony.Sms.Inbox.CONTENT_URI, cv); 496 if (uri == null) { 497 Log.e(TAG, "writeMessageToSmsInbox: failed"); 498 } else { 499 Log.d(TAG, "writeMessageToSmsInbox: succeed uri = " + uri); 500 } 501 } 502 503 /** Callback for users of AsyncCellBroadcastOperation. */ 504 interface CellBroadcastOperation { 505 /** 506 * Perform an operation using the specified provider. 507 * @param provider the CellBroadcastContentProvider to use 508 * @return true if any rows were changed, false otherwise 509 */ execute(CellBroadcastContentProvider provider)510 boolean execute(CellBroadcastContentProvider provider); 511 } 512 513 /** 514 * Async task to call this content provider's internal methods on a background thread. 515 * The caller supplies the CellBroadcastOperation object to call for this provider. 516 */ 517 static class AsyncCellBroadcastTask extends AsyncTask<CellBroadcastOperation, Void, Void> { 518 /** Reference to this app's content resolver. */ 519 private ContentResolver mContentResolver; 520 AsyncCellBroadcastTask(ContentResolver contentResolver)521 AsyncCellBroadcastTask(ContentResolver contentResolver) { 522 mContentResolver = contentResolver; 523 } 524 525 /** 526 * Perform a generic operation on the CellBroadcastContentProvider. 527 * @param params the CellBroadcastOperation object to call for this provider 528 * @return void 529 */ 530 @Override doInBackground(CellBroadcastOperation... params)531 protected Void doInBackground(CellBroadcastOperation... params) { 532 ContentProviderClient cpc = mContentResolver.acquireContentProviderClient( 533 CellBroadcastContentProvider.CB_AUTHORITY); 534 CellBroadcastContentProvider provider = (CellBroadcastContentProvider) 535 cpc.getLocalContentProvider(); 536 537 if (provider != null) { 538 try { 539 boolean changed = params[0].execute(provider); 540 if (changed) { 541 Log.d(TAG, "database changed: notifying observers..."); 542 mContentResolver.notifyChange(CONTENT_URI, null, false); 543 } 544 } finally { 545 cpc.release(); 546 } 547 } else { 548 Log.e(TAG, "getLocalContentProvider() returned null"); 549 } 550 551 mContentResolver = null; // free reference to content resolver 552 return null; 553 } 554 } 555 } 556