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 {
sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL)64         sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL);
sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID)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
onCreate()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
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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
getType(Uri uri)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
insert(Uri uri, ContentValues values)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
delete(Uri uri, String selection, String[] selectionArgs)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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)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      */
insertNewBroadcast(CellBroadcastMessage message)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      */
deleteBroadcast(long rowId)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      */
deleteAllBroadcasts()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      */
markBroadcastRead(String columnName, long columnValue)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          */
execute(CellBroadcastContentProvider provider)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 
AsyncCellBroadcastTask(ContentResolver contentResolver)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
doInBackground(CellBroadcastOperation... params)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