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