• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.CellBroadcastMessage;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 /**
36  * ContentProvider for the database of received cell broadcasts.
37  */
38 public 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 {
sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL)63         sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL);
sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID)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
onCreate()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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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
getType(Uri uri)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
insert(Uri uri, ContentValues values)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
delete(Uri uri, String selection, String[] selectionArgs)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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)180     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
181         throw new UnsupportedOperationException("update not supported");
182     }
183 
184     /**
185      * Internal method to insert a new Cell Broadcast into the database and notify observers.
186      * @param message the message to insert
187      * @return true if the broadcast is new, false if it's a duplicate broadcast.
188      */
insertNewBroadcast(CellBroadcastMessage message)189     boolean insertNewBroadcast(CellBroadcastMessage message) {
190         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
191         ContentValues cv = message.getContentValues();
192 
193         // Note: this method previously queried the database for duplicate message IDs, but this
194         // is not compatible with CMAS carrier requirements and could also cause other emergency
195         // alerts, e.g. ETWS, to not display if the database is filled with old messages.
196         // Use duplicate message ID detection in CellBroadcastAlertService instead of DB query.
197 
198         long rowId = db.insert(CellBroadcastDatabaseHelper.TABLE_NAME, null, cv);
199         if (rowId == -1) {
200             Log.e(TAG, "failed to insert new broadcast into database");
201             // Return true on DB write failure because we still want to notify the user.
202             // The CellBroadcastMessage will be passed with the intent, so the message will be
203             // displayed in the emergency alert dialog, or the dialog that is displayed when
204             // the user selects the notification for a non-emergency broadcast, even if the
205             // broadcast could not be written to the database.
206         }
207         return true;    // broadcast is not a duplicate
208     }
209 
210     /**
211      * Internal method to delete a cell broadcast by row ID and notify observers.
212      * @param rowId the row ID of the broadcast to delete
213      * @return true if the database was updated, false otherwise
214      */
deleteBroadcast(long rowId)215     boolean deleteBroadcast(long rowId) {
216         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
217 
218         int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME,
219                 Telephony.CellBroadcasts._ID + "=?",
220                 new String[]{Long.toString(rowId)});
221         if (rowCount != 0) {
222             return true;
223         } else {
224             Log.e(TAG, "failed to delete broadcast at row " + rowId);
225             return false;
226         }
227     }
228 
229     /**
230      * Internal method to delete all cell broadcasts and notify observers.
231      * @return true if the database was updated, false otherwise
232      */
deleteAllBroadcasts()233     boolean deleteAllBroadcasts() {
234         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
235 
236         int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, null, null);
237         if (rowCount != 0) {
238             return true;
239         } else {
240             Log.e(TAG, "failed to delete all broadcasts");
241             return false;
242         }
243     }
244 
245     /**
246      * Internal method to mark a broadcast as read and notify observers. The broadcast can be
247      * identified by delivery time (for new alerts) or by row ID. The caller is responsible for
248      * decrementing the unread non-emergency alert count, if necessary.
249      *
250      * @param columnName the column name to query (ID or delivery time)
251      * @param columnValue the ID or delivery time of the broadcast to mark read
252      * @return true if the database was updated, false otherwise
253      */
markBroadcastRead(String columnName, long columnValue)254     boolean markBroadcastRead(String columnName, long columnValue) {
255         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
256 
257         ContentValues cv = new ContentValues(1);
258         cv.put(Telephony.CellBroadcasts.MESSAGE_READ, 1);
259 
260         String whereClause = columnName + "=?";
261         String[] whereArgs = new String[]{Long.toString(columnValue)};
262 
263         int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause, whereArgs);
264         if (rowCount != 0) {
265             return true;
266         } else {
267             Log.e(TAG, "failed to mark broadcast read: " + columnName + " = " + columnValue);
268             return false;
269         }
270     }
271 
272     /** Callback for users of AsyncCellBroadcastOperation. */
273     interface CellBroadcastOperation {
274         /**
275          * Perform an operation using the specified provider.
276          * @param provider the CellBroadcastContentProvider to use
277          * @return true if any rows were changed, false otherwise
278          */
execute(CellBroadcastContentProvider provider)279         boolean execute(CellBroadcastContentProvider provider);
280     }
281 
282     /**
283      * Async task to call this content provider's internal methods on a background thread.
284      * The caller supplies the CellBroadcastOperation object to call for this provider.
285      */
286     static class AsyncCellBroadcastTask extends AsyncTask<CellBroadcastOperation, Void, Void> {
287         /** Reference to this app's content resolver. */
288         private ContentResolver mContentResolver;
289 
AsyncCellBroadcastTask(ContentResolver contentResolver)290         AsyncCellBroadcastTask(ContentResolver contentResolver) {
291             mContentResolver = contentResolver;
292         }
293 
294         /**
295          * Perform a generic operation on the CellBroadcastContentProvider.
296          * @param params the CellBroadcastOperation object to call for this provider
297          * @return void
298          */
299         @Override
doInBackground(CellBroadcastOperation... params)300         protected Void doInBackground(CellBroadcastOperation... params) {
301             ContentProviderClient cpc = mContentResolver.acquireContentProviderClient(
302                     CellBroadcastContentProvider.CB_AUTHORITY);
303             CellBroadcastContentProvider provider = (CellBroadcastContentProvider)
304                     cpc.getLocalContentProvider();
305 
306             if (provider != null) {
307                 try {
308                     boolean changed = params[0].execute(provider);
309                     if (changed) {
310                         Log.d(TAG, "database changed: notifying observers...");
311                         mContentResolver.notifyChange(CONTENT_URI, null, false);
312                     }
313                 } finally {
314                     cpc.release();
315                 }
316             } else {
317                 Log.e(TAG, "getLocalContentProvider() returned null");
318             }
319 
320             mContentResolver = null;    // free reference to content resolver
321             return null;
322         }
323     }
324 }
325