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