• 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.annotation.NonNull;
20 import android.content.ContentProvider;
21 import android.content.ContentProviderClient;
22 import android.content.ContentResolver;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.content.UriMatcher;
26 import android.database.Cursor;
27 import android.database.sqlite.SQLiteDatabase;
28 import android.database.sqlite.SQLiteQueryBuilder;
29 import android.net.Uri;
30 import android.os.AsyncTask;
31 import android.os.Bundle;
32 import android.os.UserManager;
33 import android.provider.Telephony;
34 import android.telephony.SmsCbCmasInfo;
35 import android.telephony.SmsCbEtwsInfo;
36 import android.telephony.SmsCbLocation;
37 import android.telephony.SmsCbMessage;
38 import android.text.TextUtils;
39 import android.util.Log;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 
43 import java.util.concurrent.CountDownLatch;
44 
45 /**
46  * ContentProvider for the database of received cell broadcasts.
47  */
48 public class CellBroadcastContentProvider extends ContentProvider {
49     private static final String TAG = "CellBroadcastContentProvider";
50 
51     /** URI matcher for ContentProvider queries. */
52     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
53 
54     /** Authority string for content URIs. */
55     @VisibleForTesting
56     public static final String CB_AUTHORITY = "cellbroadcasts-app";
57 
58     /** Content URI for notifying observers. */
59     static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts-app/");
60 
61     /** URI matcher type to get all cell broadcasts. */
62     private static final int CB_ALL = 0;
63 
64     /** URI matcher type to get a cell broadcast by ID. */
65     private static final int CB_ALL_ID = 1;
66 
67     /** MIME type for the list of all cell broadcasts. */
68     private static final String CB_LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast";
69 
70     /** MIME type for an individual cell broadcast. */
71     private static final String CB_TYPE = "vnd.android.cursor.item/cellbroadcast";
72 
73     public static final String CALL_MIGRATION_METHOD = "migrate-legacy-data";
74 
75     static {
sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL)76         sUriMatcher.addURI(CB_AUTHORITY, null, CB_ALL);
sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID)77         sUriMatcher.addURI(CB_AUTHORITY, "#", CB_ALL_ID);
78     }
79 
80     /**
81      * The database for this content provider. Before using this we need to wait on
82      * mInitializedLatch, which counts down once initialization finishes in a background thread
83      */
84 
85     @VisibleForTesting
86     public CellBroadcastDatabaseHelper mOpenHelper;
87 
88     // Latch which counts down from 1 when initialization in CellBroadcastOpenHelper.tryToMigrateV13
89     // is finished
90     private final CountDownLatch mInitializedLatch = new CountDownLatch(1);
91 
92     /**
93      * Initialize content provider.
94      * @return true if the provider was successfully loaded, false otherwise
95      */
96     @Override
onCreate()97     public boolean onCreate() {
98         mOpenHelper = new CellBroadcastDatabaseHelper(getContext(), false);
99         // trigger this to create database explicitly. Otherwise the db will be created only after
100         // the first query/update/insertion. Data migration is done inside db creation and we want
101         // to migrate data from cellbroadcast-legacy immediately when upgrade to the mainline module
102         // rather than migrate after the first emergency alert.
103         // getReadable database will also call tryToMigrateV13 which copies the DB file to allow
104         // for safe rollbacks.
105         // This is done in a background thread to avoid triggering an ANR if the disk operations are
106         // too slow, and all other database uses should wait for the latch.
107         new Thread(() -> {
108             mOpenHelper.getReadableDatabase();
109             mInitializedLatch.countDown();
110         }).start();
111         return true;
112     }
113 
awaitInitAndGetWritableDatabase()114     protected SQLiteDatabase awaitInitAndGetWritableDatabase() {
115         while (mInitializedLatch.getCount() != 0) {
116             try {
117                 mInitializedLatch.await();
118             } catch (InterruptedException e) {
119                 Log.e(TAG, "Interrupted while waiting for db initialization. e=" + e);
120             }
121         }
122         return mOpenHelper.getWritableDatabase();
123     }
124 
awaitInitAndGetReadableDatabase()125     protected SQLiteDatabase awaitInitAndGetReadableDatabase() {
126         while (mInitializedLatch.getCount() != 0) {
127             try {
128                 mInitializedLatch.await();
129             } catch (InterruptedException e) {
130                 Log.e(TAG, "Interrupted while waiting for db initialization. e=" + e);
131             }
132         }
133         return mOpenHelper.getReadableDatabase();
134     }
135 
136     /**
137      * Return a cursor for the cell broadcast table.
138      * @param uri the URI to query.
139      * @param projection the list of columns to put into the cursor, or null.
140      * @param selection the selection criteria to apply when filtering rows, or null.
141      * @param selectionArgs values to replace ?s in selection string.
142      * @param sortOrder how the rows in the cursor should be sorted, or null to sort from most
143      *  recently received to least recently received.
144      * @return a Cursor or null.
145      */
146     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)147     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
148             String sortOrder) {
149         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
150         qb.setTables(CellBroadcastDatabaseHelper.TABLE_NAME);
151 
152         int match = sUriMatcher.match(uri);
153         switch (match) {
154             case CB_ALL:
155                 // get all broadcasts
156                 break;
157 
158             case CB_ALL_ID:
159                 // get broadcast by ID
160                 qb.appendWhere("(_id=" + uri.getPathSegments().get(0) + ')');
161                 break;
162 
163             default:
164                 Log.e(TAG, "Invalid query: " + uri);
165                 throw new IllegalArgumentException("Unknown URI: " + uri);
166         }
167 
168         String orderBy;
169         if (!TextUtils.isEmpty(sortOrder)) {
170             orderBy = sortOrder;
171         } else {
172             orderBy = Telephony.CellBroadcasts.DEFAULT_SORT_ORDER;
173         }
174 
175         SQLiteDatabase db = awaitInitAndGetReadableDatabase();
176         Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
177         if (c != null) {
178             c.setNotificationUri(getContext().getContentResolver(), CONTENT_URI);
179         }
180         return c;
181     }
182 
183     /**
184      * Return the MIME type of the data at the specified URI.
185      * @param uri the URI to query.
186      * @return a MIME type string, or null if there is no type.
187      */
188     @Override
getType(Uri uri)189     public String getType(Uri uri) {
190         int match = sUriMatcher.match(uri);
191         switch (match) {
192             case CB_ALL:
193                 return CB_LIST_TYPE;
194 
195             case CB_ALL_ID:
196                 return CB_TYPE;
197 
198             default:
199                 return null;
200         }
201     }
202 
203     /**
204      * Insert a new row. This throws an exception, as the database can only be modified by
205      * calling custom methods in this class, and not via the ContentProvider interface.
206      * @param uri the content:// URI of the insertion request.
207      * @param values a set of column_name/value pairs to add to the database.
208      * @return the URI for the newly inserted item.
209      */
210     @Override
insert(Uri uri, ContentValues values)211     public Uri insert(Uri uri, ContentValues values) {
212         throw new UnsupportedOperationException("insert not supported");
213     }
214 
215     /**
216      * Delete one or more rows. This throws an exception, as the database can only be modified by
217      * calling custom methods in this class, and not via the ContentProvider interface.
218      * @param uri the full URI to query, including a row ID (if a specific record is requested).
219      * @param selection an optional restriction to apply to rows when deleting.
220      * @return the number of rows affected.
221      */
222     @Override
delete(Uri uri, String selection, String[] selectionArgs)223     public int delete(Uri uri, String selection, String[] selectionArgs) {
224         throw new UnsupportedOperationException("delete not supported");
225     }
226 
227     /**
228      * Update one or more rows. This throws an exception, as the database can only be modified by
229      * calling custom methods in this class, and not via the ContentProvider interface.
230      * @param uri the URI to query, potentially including the row ID.
231      * @param values a Bundle mapping from column names to new column values.
232      * @param selection an optional filter to match rows to update.
233      * @return the number of rows affected.
234      */
235     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)236     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
237         throw new UnsupportedOperationException("update not supported");
238     }
239 
240     @Override
call(String method, String name, Bundle args)241     public Bundle call(String method, String name, Bundle args) {
242         Log.d(TAG, "call:"
243                 + " method=" + method
244                 + " name=" + name
245                 + " args=" + args);
246         // this is to handle a content-provider defined method: migration
247         if (CALL_MIGRATION_METHOD.equals(method)) {
248             mOpenHelper.migrateFromLegacyIfNeeded(awaitInitAndGetReadableDatabase());
249         }
250         return null;
251     }
252 
getContentValues(SmsCbMessage message)253     private ContentValues getContentValues(SmsCbMessage message) {
254         ContentValues cv = new ContentValues();
255         cv.put(Telephony.CellBroadcasts.SLOT_INDEX, message.getSlotIndex());
256         cv.put(Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, message.getGeographicalScope());
257         SmsCbLocation location = message.getLocation();
258         cv.put(Telephony.CellBroadcasts.PLMN, location.getPlmn());
259         if (location.getLac() != -1) {
260             cv.put(Telephony.CellBroadcasts.LAC, location.getLac());
261         }
262         if (location.getCid() != -1) {
263             cv.put(Telephony.CellBroadcasts.CID, location.getCid());
264         }
265         cv.put(Telephony.CellBroadcasts.SERIAL_NUMBER, message.getSerialNumber());
266         cv.put(Telephony.CellBroadcasts.SERVICE_CATEGORY, message.getServiceCategory());
267         cv.put(Telephony.CellBroadcasts.LANGUAGE_CODE, message.getLanguageCode());
268         cv.put(Telephony.CellBroadcasts.MESSAGE_BODY, message.getMessageBody());
269         cv.put(Telephony.CellBroadcasts.DELIVERY_TIME, message.getReceivedTime());
270         cv.put(Telephony.CellBroadcasts.MESSAGE_FORMAT, message.getMessageFormat());
271         cv.put(Telephony.CellBroadcasts.MESSAGE_PRIORITY, message.getMessagePriority());
272 
273         SmsCbEtwsInfo etwsInfo = message.getEtwsWarningInfo();
274         if (etwsInfo != null) {
275             cv.put(Telephony.CellBroadcasts.ETWS_WARNING_TYPE, etwsInfo.getWarningType());
276         }
277 
278         SmsCbCmasInfo cmasInfo = message.getCmasWarningInfo();
279         if (cmasInfo != null) {
280             cv.put(Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, cmasInfo.getMessageClass());
281             cv.put(Telephony.CellBroadcasts.CMAS_CATEGORY, cmasInfo.getCategory());
282             cv.put(Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, cmasInfo.getResponseType());
283             cv.put(Telephony.CellBroadcasts.CMAS_SEVERITY, cmasInfo.getSeverity());
284             cv.put(Telephony.CellBroadcasts.CMAS_URGENCY, cmasInfo.getUrgency());
285             cv.put(Telephony.CellBroadcasts.CMAS_CERTAINTY, cmasInfo.getCertainty());
286         }
287 
288         return cv;
289     }
290 
291     /**
292      * Internal method to insert a new Cell Broadcast into the database and notify observers.
293      * @param message the message to insert
294      * @return true if the broadcast is new, false if it's a duplicate broadcast.
295      */
296     @VisibleForTesting
insertNewBroadcast(SmsCbMessage message)297     public boolean insertNewBroadcast(SmsCbMessage message) {
298         SQLiteDatabase db = awaitInitAndGetWritableDatabase();
299         ContentValues cv = getContentValues(message);
300 
301         // Note: this method previously queried the database for duplicate message IDs, but this
302         // is not compatible with CMAS carrier requirements and could also cause other emergency
303         // alerts, e.g. ETWS, to not display if the database is filled with old messages.
304         // Use duplicate message ID detection in CellBroadcastAlertService instead of DB query.
305         long rowId = db.insert(CellBroadcastDatabaseHelper.TABLE_NAME, null, cv);
306         if (rowId == -1) {
307             Log.e(TAG, "failed to insert new broadcast into database");
308             // Return true on DB write failure because we still want to notify the user.
309             // The SmsCbMessage will be passed with the intent, so the message will be
310             // displayed in the emergency alert dialog, or the dialog that is displayed when
311             // the user selects the notification for a non-emergency broadcast, even if the
312             // broadcast could not be written to the database.
313         }
314         return true;    // broadcast is not a duplicate
315     }
316 
317     /**
318      * Internal method to delete a cell broadcast by row ID and notify observers.
319      * @param rowId the row ID of the broadcast to delete
320      * @return true if the database was updated, false otherwise
321      */
322     @VisibleForTesting
deleteBroadcast(long rowId)323     public boolean deleteBroadcast(long rowId) {
324         SQLiteDatabase db = awaitInitAndGetWritableDatabase();
325 
326         int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME,
327                 Telephony.CellBroadcasts._ID + "=?",
328                 new String[]{Long.toString(rowId)});
329         if (rowCount != 0) {
330             return true;
331         } else {
332             Log.e(TAG, "failed to delete broadcast at row " + rowId);
333             return false;
334         }
335     }
336 
337     /**
338      * Internal method to delete all cell broadcasts and notify observers.
339      * @return true if the database was updated, false otherwise
340      */
341     @VisibleForTesting
deleteAllBroadcasts()342     public boolean deleteAllBroadcasts() {
343         SQLiteDatabase db = awaitInitAndGetWritableDatabase();
344 
345         int rowCount = db.delete(CellBroadcastDatabaseHelper.TABLE_NAME, null, null);
346         if (rowCount != 0) {
347             return true;
348         } else {
349             Log.e(TAG, "failed to delete all broadcasts");
350             return false;
351         }
352     }
353 
354     /**
355      * Internal method to mark a broadcast as read and notify observers. The broadcast can be
356      * identified by delivery time (for new alerts) or by row ID. The caller is responsible for
357      * decrementing the unread non-emergency alert count, if necessary.
358      *
359      * @param columnName the column name to query (ID or delivery time)
360      * @param columnValue the ID or delivery time of the broadcast to mark read
361      * @return true if the database was updated, false otherwise
362      */
markBroadcastRead(String columnName, long columnValue)363     boolean markBroadcastRead(String columnName, long columnValue) {
364         SQLiteDatabase db = awaitInitAndGetWritableDatabase();
365 
366         ContentValues cv = new ContentValues(1);
367         cv.put(Telephony.CellBroadcasts.MESSAGE_READ, 1);
368 
369         String whereClause = columnName + "=?";
370         String[] whereArgs = new String[]{Long.toString(columnValue)};
371 
372         int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause, whereArgs);
373         if (rowCount != 0) {
374             return true;
375         } else {
376             Log.e(TAG, "failed to mark broadcast read: " + columnName + " = " + columnValue);
377             return false;
378         }
379     }
380 
381     /**
382      * Internal method to mark a broadcast received in direct boot mode. After user unlocks, mark
383      * all messages not in direct boot mode.
384      *
385      * @param columnName the column name to query (ID or delivery time)
386      * @param columnValue the ID or delivery time of the broadcast to mark read
387      * @param isSmsSyncPending whether the message was pending for SMS inbox synchronization
388      * @return true if the database was updated, false otherwise
389      */
390     @VisibleForTesting
markBroadcastSmsSyncPending(String columnName, long columnValue, boolean isSmsSyncPending)391     public boolean markBroadcastSmsSyncPending(String columnName, long columnValue,
392             boolean isSmsSyncPending) {
393         SQLiteDatabase db = awaitInitAndGetWritableDatabase();
394 
395         ContentValues cv = new ContentValues(1);
396         cv.put(CellBroadcastDatabaseHelper.SMS_SYNC_PENDING, isSmsSyncPending ? 1 : 0);
397 
398         String whereClause = columnName + "=?";
399         String[] whereArgs = new String[]{Long.toString(columnValue)};
400 
401         int rowCount = db.update(CellBroadcastDatabaseHelper.TABLE_NAME, cv, whereClause,
402                 whereArgs);
403         if (rowCount != 0) {
404             return true;
405         } else {
406             Log.e(TAG, "failed to mark broadcast pending for sms inbox sync:  " + isSmsSyncPending
407                     + " where: " + columnName + " = " + columnValue);
408             return false;
409         }
410     }
411 
412     /**
413      * Write message to sms inbox if pending. e.g, when receive alerts in direct boot mode, we
414      * might need to sync message to sms inbox after user unlock.
415      * @param context
416      */
417 
418     @VisibleForTesting
resyncToSmsInbox(@onNull Context context)419     public void resyncToSmsInbox(@NonNull Context context) {
420         // query all messages currently marked as sms inbox sync pending
421         try (Cursor cursor = query(
422                 CellBroadcastContentProvider.CONTENT_URI,
423                 CellBroadcastDatabaseHelper.QUERY_COLUMNS,
424                 CellBroadcastDatabaseHelper.SMS_SYNC_PENDING + "=1",
425                 null, null)) {
426             if (cursor != null) {
427                 while (cursor.moveToNext()) {
428                     SmsCbMessage message = CellBroadcastCursorAdapter
429                             .createFromCursor(context, cursor);
430                     if (message != null) {
431                         Log.d(TAG, "handling message received pending for sms sync: "
432                                 + message.toString());
433                         writeMessageToSmsInbox(message, context);
434                         // mark message received in direct mode was handled
435                         markBroadcastSmsSyncPending(
436                                 Telephony.CellBroadcasts.DELIVERY_TIME,
437                                 message.getReceivedTime(), false);
438                     }
439                 }
440             }
441         }
442     }
443 
444     /**
445      * Write displayed cellbroadcast messages to sms inbox
446      *
447      * @param message The cell broadcast message.
448      */
449     @VisibleForTesting
writeMessageToSmsInbox(@onNull SmsCbMessage message, @NonNull Context context)450     public void writeMessageToSmsInbox(@NonNull SmsCbMessage message, @NonNull Context context) {
451         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
452         if (!userManager.isSystemUser()) {
453             // SMS database is single-user mode, discard non-system users to avoid inserting twice.
454             Log.d(TAG, "ignoring writeMessageToSmsInbox due to non-system user");
455             return;
456         }
457         // Note SMS database is not direct boot aware for privacy reasons, we should only interact
458         // with sms db until users has unlocked.
459         if (!userManager.isUserUnlocked()) {
460             Log.d(TAG, "ignoring writeMessageToSmsInbox due to direct boot mode");
461             // need to retry after user unlock
462             markBroadcastSmsSyncPending(Telephony.CellBroadcasts.DELIVERY_TIME,
463                         message.getReceivedTime(), true);
464             return;
465         }
466         // composing SMS
467         ContentValues cv = new ContentValues();
468         cv.put(Telephony.Sms.Inbox.BODY, message.getMessageBody());
469         cv.put(Telephony.Sms.Inbox.DATE, message.getReceivedTime());
470         cv.put(Telephony.Sms.Inbox.SUBSCRIPTION_ID, message.getSubscriptionId());
471         cv.put(Telephony.Sms.Inbox.SUBJECT, context.getString(
472                 CellBroadcastResources.getDialogTitleResource(context, message)));
473         cv.put(Telephony.Sms.Inbox.ADDRESS,
474                 CellBroadcastResources.getSmsSenderAddressResourceEnglishString(context, message));
475         cv.put(Telephony.Sms.Inbox.THREAD_ID, Telephony.Threads.getOrCreateThreadId(context,
476                 CellBroadcastResources.getSmsSenderAddressResourceEnglishString(context, message)));
477         if (CellBroadcastSettings.getResources(context, message.getSubscriptionId())
478                 .getBoolean(R.bool.always_mark_sms_read)) {
479             // Always mark SMS message READ. End users expect when they read new CBS messages,
480             // the unread alert count in the notification should be decreased, as they thought it
481             // was coming from SMS. Now we are marking those SMS as read (SMS now serve as a message
482             // history purpose) and that should give clear messages to end-users that alerts are not
483             // from the SMS app but CellBroadcast and they should tap the notification to read alert
484             // in order to see decreased unread message count.
485             cv.put(Telephony.Sms.Inbox.READ, 1);
486         }
487         Uri uri = context.getContentResolver().insert(Telephony.Sms.Inbox.CONTENT_URI, cv);
488         if (uri == null) {
489             Log.e(TAG, "writeMessageToSmsInbox: failed");
490         } else {
491             Log.d(TAG, "writeMessageToSmsInbox: succeed uri = " + uri);
492         }
493     }
494 
495     /** Callback for users of AsyncCellBroadcastOperation. */
496     interface CellBroadcastOperation {
497         /**
498          * Perform an operation using the specified provider.
499          * @param provider the CellBroadcastContentProvider to use
500          * @return true if any rows were changed, false otherwise
501          */
execute(CellBroadcastContentProvider provider)502         boolean execute(CellBroadcastContentProvider provider);
503     }
504 
505     /**
506      * Async task to call this content provider's internal methods on a background thread.
507      * The caller supplies the CellBroadcastOperation object to call for this provider.
508      */
509     static class AsyncCellBroadcastTask extends AsyncTask<CellBroadcastOperation, Void, Void> {
510         /** Reference to this app's content resolver. */
511         private ContentResolver mContentResolver;
512 
AsyncCellBroadcastTask(ContentResolver contentResolver)513         AsyncCellBroadcastTask(ContentResolver contentResolver) {
514             mContentResolver = contentResolver;
515         }
516 
517         /**
518          * Perform a generic operation on the CellBroadcastContentProvider.
519          * @param params the CellBroadcastOperation object to call for this provider
520          * @return void
521          */
522         @Override
doInBackground(CellBroadcastOperation... params)523         protected Void doInBackground(CellBroadcastOperation... params) {
524             ContentProviderClient cpc = mContentResolver.acquireContentProviderClient(
525                     CellBroadcastContentProvider.CB_AUTHORITY);
526             CellBroadcastContentProvider provider = (CellBroadcastContentProvider)
527                     cpc.getLocalContentProvider();
528 
529             if (provider != null) {
530                 try {
531                     boolean changed = params[0].execute(provider);
532                     if (changed) {
533                         Log.d(TAG, "database changed: notifying observers...");
534                         mContentResolver.notifyChange(CONTENT_URI, null, false);
535                     }
536                 } finally {
537                     cpc.release();
538                 }
539             } else {
540                 Log.e(TAG, "getLocalContentProvider() returned null");
541             }
542 
543             mContentResolver = null;    // free reference to content resolver
544             return null;
545         }
546     }
547 }
548