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