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