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