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