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.content.ContentProvider; 20 import android.content.ContentProviderClient; 21 import android.content.ContentResolver; 22 import android.content.ContentValues; 23 import android.content.UriMatcher; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.database.sqlite.SQLiteOpenHelper; 27 import android.database.sqlite.SQLiteQueryBuilder; 28 import android.net.Uri; 29 import android.os.AsyncTask; 30 import android.provider.Telephony; 31 import android.telephony.SmsCbCmasInfo; 32 import android.telephony.SmsCbEtwsInfo; 33 import android.telephony.SmsCbLocation; 34 import android.telephony.SmsCbMessage; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import com.android.internal.annotations.VisibleForTesting; 38 39 /** 40 * ContentProvider for the database of received cell broadcasts. 41 */ 42 public class CellBroadcastContentProvider extends ContentProvider { 43 private static final String TAG = "CellBroadcastContentProvider"; 44 45 /** URI matcher for ContentProvider queries. */ 46 private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 47 48 /** Authority string for content URIs. */ 49 static final String CB_AUTHORITY = "cellbroadcasts-app"; 50 51 /** Content URI for notifying observers. */ 52 static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts-app/"); 53 54 /** URI matcher type to get all cell broadcasts. */ 55 private static final int CB_ALL = 0; 56 57 /** URI matcher type to get a cell broadcast by ID. */ 58 private static final int CB_ALL_ID = 1; 59 60 /** MIME type for the list of all cell broadcasts. */ 61 private static final String CB_LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast"; 62 63 /** MIME type for an individual cell broadcast. */ 64 private static final String CB_TYPE = "vnd.android.cursor.item/cellbroadcast"; 65 66 static { sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL)67 sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL); sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID)68 sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID); 69 } 70 71 /** The database for this content provider. */ 72 @VisibleForTesting 73 public SQLiteOpenHelper mOpenHelper; 74 75 /** 76 * Initialize content provider. 77 * @return true if the provider was successfully loaded, false otherwise 78 */ 79 @Override onCreate()80 public boolean onCreate() { 81 mOpenHelper = new CellBroadcastDatabaseHelper(getContext(), false); 82 // trigger this to create database explicitly. Otherwise the db will be created only after 83 // the first query/update/insertion. Data migration is done inside db creation and we want 84 // to migrate data from cellbroadcast-legacy immediately when upgrade to the mainline module 85 // rather than migrate after the first emergency alert. 86 mOpenHelper.getReadableDatabase(); 87 return true; 88 } 89 90 /** 91 * Return a cursor for the cell broadcast table. 92 * @param uri the URI to query. 93 * @param projection the list of columns to put into the cursor, or null. 94 * @param selection the selection criteria to apply when filtering rows, or null. 95 * @param selectionArgs values to replace ?s in selection string. 96 * @param sortOrder how the rows in the cursor should be sorted, or null to sort from most 97 * recently received to least recently received. 98 * @return a Cursor or null. 99 */ 100 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)101 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 102 String sortOrder) { 103 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 104 qb.setTables(CellBroadcastDatabaseHelper.TABLE_NAME); 105 106 int match = sUriMatcher.match(uri); 107 switch (match) { 108 case CB_ALL: 109 // get all broadcasts 110 break; 111 112 case CB_ALL_ID: 113 // get broadcast by ID 114 qb.appendWhere("(_id=" + uri.getPathSegments().get(0) + ')'); 115 break; 116 117 default: 118 Log.e(TAG, "Invalid query: " + uri); 119 throw new IllegalArgumentException("Unknown URI: " + uri); 120 } 121 122 String orderBy; 123 if (!TextUtils.isEmpty(sortOrder)) { 124 orderBy = sortOrder; 125 } else { 126 orderBy = Telephony.CellBroadcasts.DEFAULT_SORT_ORDER; 127 } 128 129 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 130 Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy); 131 if (c != null) { 132 c.setNotificationUri(getContext().getContentResolver(), CONTENT_URI); 133 } 134 return c; 135 } 136 137 /** 138 * Return the MIME type of the data at the specified URI. 139 * @param uri the URI to query. 140 * @return a MIME type string, or null if there is no type. 141 */ 142 @Override getType(Uri uri)143 public String getType(Uri uri) { 144 int match = sUriMatcher.match(uri); 145 switch (match) { 146 case CB_ALL: 147 return CB_LIST_TYPE; 148 149 case CB_ALL_ID: 150 return CB_TYPE; 151 152 default: 153 return null; 154 } 155 } 156 157 /** 158 * Insert a new row. This throws an exception, as the database can only be modified by 159 * calling custom methods in this class, and not via the ContentProvider interface. 160 * @param uri the content:// URI of the insertion request. 161 * @param values a set of column_name/value pairs to add to the database. 162 * @return the URI for the newly inserted item. 163 */ 164 @Override insert(Uri uri, ContentValues values)165 public Uri insert(Uri uri, ContentValues values) { 166 throw new UnsupportedOperationException("insert not supported"); 167 } 168 169 /** 170 * Delete one or more rows. This throws an exception, as the database can only be modified by 171 * calling custom methods in this class, and not via the ContentProvider interface. 172 * @param uri the full URI to query, including a row ID (if a specific record is requested). 173 * @param selection an optional restriction to apply to rows when deleting. 174 * @return the number of rows affected. 175 */ 176 @Override delete(Uri uri, String selection, String[] selectionArgs)177 public int delete(Uri uri, String selection, String[] selectionArgs) { 178 throw new UnsupportedOperationException("delete not supported"); 179 } 180 181 /** 182 * Update one or more rows. This throws an exception, as the database can only be modified by 183 * calling custom methods in this class, and not via the ContentProvider interface. 184 * @param uri the URI to query, potentially including the row ID. 185 * @param values a Bundle mapping from column names to new column values. 186 * @param selection an optional filter to match rows to update. 187 * @return the number of rows affected. 188 */ 189 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)190 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 191 throw new UnsupportedOperationException("update not supported"); 192 } 193 getContentValues(SmsCbMessage message)194 private ContentValues getContentValues(SmsCbMessage message) { 195 ContentValues cv = new ContentValues(); 196 cv.put(Telephony.CellBroadcasts.SLOT_INDEX, message.getSlotIndex()); 197 cv.put(Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, message.getGeographicalScope()); 198 SmsCbLocation location = message.getLocation(); 199 cv.put(Telephony.CellBroadcasts.PLMN, location.getPlmn()); 200 if (location.getLac() != -1) { 201 cv.put(Telephony.CellBroadcasts.LAC, location.getLac()); 202 } 203 if (location.getCid() != -1) { 204 cv.put(Telephony.CellBroadcasts.CID, location.getCid()); 205 } 206 cv.put(Telephony.CellBroadcasts.SERIAL_NUMBER, message.getSerialNumber()); 207 cv.put(Telephony.CellBroadcasts.SERVICE_CATEGORY, message.getServiceCategory()); 208 cv.put(Telephony.CellBroadcasts.LANGUAGE_CODE, message.getLanguageCode()); 209 cv.put(Telephony.CellBroadcasts.MESSAGE_BODY, message.getMessageBody()); 210 cv.put(Telephony.CellBroadcasts.DELIVERY_TIME, message.getReceivedTime()); 211 cv.put(Telephony.CellBroadcasts.MESSAGE_FORMAT, message.getMessageFormat()); 212 cv.put(Telephony.CellBroadcasts.MESSAGE_PRIORITY, message.getMessagePriority()); 213 214 SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo(); 215 if (etwsInfo != null) { 216 cv.put(Telephony.CellBroadcasts.ETWS_WARNING_TYPE, etwsInfo.getWarningType()); 217 } 218 219 SmsCbCmasInfo cmasInfo = message.getCmasWarningInfo(); 220 if (cmasInfo != null) { 221 cv.put(Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, cmasInfo.getMessageClass()); 222 cv.put(Telephony.CellBroadcasts.CMAS_CATEGORY, cmasInfo.getCategory()); 223 cv.put(Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, cmasInfo.getResponseType()); 224 cv.put(Telephony.CellBroadcasts.CMAS_SEVERITY, cmasInfo.getSeverity()); 225 cv.put(Telephony.CellBroadcasts.CMAS_URGENCY, cmasInfo.getUrgency()); 226 cv.put(Telephony.CellBroadcasts.CMAS_CERTAINTY, cmasInfo.getCertainty()); 227 } 228 229 return cv; 230 } 231 232 /** 233 * Internal method to insert a new Cell Broadcast into the database and notify observers. 234 * @param message the message to insert 235 * @return true if the broadcast is new, false if it's a duplicate broadcast. 236 */ 237 @VisibleForTesting insertNewBroadcast(SmsCbMessage message)238 public boolean insertNewBroadcast(SmsCbMessage message) { 239 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 240 ContentValues cv = getContentValues(message); 241 242 // Note: this method previously queried the database for duplicate message IDs, but this 243 // is not compatible with CMAS carrier requirements and could also cause other emergency 244 // alerts, e.g. ETWS, to not display if the database is filled with old messages. 245 // Use duplicate message ID detection in CellBroadcastAlertService instead of DB query. 246 long rowId = db.insert(CellBroadcastDatabaseHelper.TABLE_NAME, null, cv); 247 if (rowId == -1) { 248 Log.e(TAG, "failed to insert new broadcast into database"); 249 // Return true on DB write failure because we still want to notify the user. 250 // The SmsCbMessage will be passed with the intent, so the message will be 251 // displayed in the emergency alert dialog, or the dialog that is displayed when 252 // the user selects the notification for a non-emergency broadcast, even if the 253 // broadcast could not be written to the database. 254 } 255 return true; // broadcast is not a duplicate 256 } 257 258 /** 259 * Internal method to delete a cell broadcast by row ID and notify observers. 260 * @param rowId the row ID of the broadcast to delete 261 * @return true if the database was updated, false otherwise 262 */ 263 @VisibleForTesting deleteBroadcast(long rowId)264 public boolean deleteBroadcast(long rowId) { 265 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 266 267 int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, 268 Telephony.CellBroadcasts._ID + "=?", 269 new String[]{Long.toString(rowId)}); 270 if (rowCount != 0) { 271 return true; 272 } else { 273 Log.e(TAG, "failed to delete broadcast at row " + rowId); 274 return false; 275 } 276 } 277 278 /** 279 * Internal method to delete all cell broadcasts and notify observers. 280 * @return true if the database was updated, false otherwise 281 */ 282 @VisibleForTesting deleteAllBroadcasts()283 public boolean deleteAllBroadcasts() { 284 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 285 286 int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, null, null); 287 if (rowCount != 0) { 288 return true; 289 } else { 290 Log.e(TAG, "failed to delete all broadcasts"); 291 return false; 292 } 293 } 294 295 /** 296 * Internal method to mark a broadcast as read and notify observers. The broadcast can be 297 * identified by delivery time (for new alerts) or by row ID. The caller is responsible for 298 * decrementing the unread non-emergency alert count, if necessary. 299 * 300 * @param columnName the column name to query (ID or delivery time) 301 * @param columnValue the ID or delivery time of the broadcast to mark read 302 * @return true if the database was updated, false otherwise 303 */ markBroadcastRead(String columnName, long columnValue)304 boolean markBroadcastRead(String columnName, long columnValue) { 305 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 306 307 ContentValues cv = new ContentValues(1); 308 cv.put(Telephony.CellBroadcasts.MESSAGE_READ, 1); 309 310 String whereClause = columnName + "=?"; 311 String[] whereArgs = new String[]{Long.toString(columnValue)}; 312 313 int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause, whereArgs); 314 if (rowCount != 0) { 315 return true; 316 } else { 317 Log.e(TAG, "failed to mark broadcast read: " + columnName + " = " + columnValue); 318 return false; 319 } 320 } 321 322 /** Callback for users of AsyncCellBroadcastOperation. */ 323 interface CellBroadcastOperation { 324 /** 325 * Perform an operation using the specified provider. 326 * @param provider the CellBroadcastContentProvider to use 327 * @return true if any rows were changed, false otherwise 328 */ execute(CellBroadcastContentProvider provider)329 boolean execute(CellBroadcastContentProvider provider); 330 } 331 332 /** 333 * Async task to call this content provider's internal methods on a background thread. 334 * The caller supplies the CellBroadcastOperation object to call for this provider. 335 */ 336 static class AsyncCellBroadcastTask extends AsyncTask<CellBroadcastOperation, Void, Void> { 337 /** Reference to this app's content resolver. */ 338 private ContentResolver mContentResolver; 339 AsyncCellBroadcastTask(ContentResolver contentResolver)340 AsyncCellBroadcastTask(ContentResolver contentResolver) { 341 mContentResolver = contentResolver; 342 } 343 344 /** 345 * Perform a generic operation on the CellBroadcastContentProvider. 346 * @param params the CellBroadcastOperation object to call for this provider 347 * @return void 348 */ 349 @Override doInBackground(CellBroadcastOperation... params)350 protected Void doInBackground(CellBroadcastOperation... params) { 351 ContentProviderClient cpc = mContentResolver.acquireContentProviderClient( 352 CellBroadcastContentProvider.CB_AUTHORITY); 353 CellBroadcastContentProvider provider = (CellBroadcastContentProvider) 354 cpc.getLocalContentProvider(); 355 356 if (provider != null) { 357 try { 358 boolean changed = params[0].execute(provider); 359 if (changed) { 360 Log.d(TAG, "database changed: notifying observers..."); 361 mContentResolver.notifyChange(CONTENT_URI, null, false); 362 } 363 } finally { 364 cpc.release(); 365 } 366 } else { 367 Log.e(TAG, "getLocalContentProvider() returned null"); 368 } 369 370 mContentResolver = null; // free reference to content resolver 371 return null; 372 } 373 } 374 } 375