| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.cellbroadcastservice; |
| |
| import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__FAILED_TO_INSERT_TO_DB; |
| |
| import android.content.ContentProvider; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.UriMatcher; |
| import android.content.pm.PackageManager; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteOpenHelper; |
| import android.database.sqlite.SQLiteQueryBuilder; |
| import android.net.Uri; |
| import android.provider.Telephony; |
| import android.provider.Telephony.CellBroadcasts; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.util.Arrays; |
| |
| /** |
| * The content provider that provides access of cell broadcast message to application. |
| * Permission {@link com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY} is |
| * required for querying the cell broadcast message. Only the Cell Broadcast module should have this |
| * permission. |
| */ |
| public class CellBroadcastProvider extends ContentProvider { |
| private static final String TAG = CellBroadcastProvider.class.getSimpleName(); |
| |
| private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| /** Database name. */ |
| private static final String DATABASE_NAME = "cellbroadcasts.db"; |
| |
| /** Database version. */ |
| @VisibleForTesting |
| public static final int DATABASE_VERSION = 4; |
| |
| /** URI matcher for ContentProvider queries. */ |
| private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
| |
| /** URI matcher type to get all cell broadcasts. */ |
| private static final int ALL = 0; |
| |
| /** |
| * URI matcher type for get all message history, this is used primarily for default |
| * cellbroadcast app or messaging app to display message history. some information is not |
| * exposed for messaging history, e.g, messages which are out of broadcast geometrics will not |
| * be delivered to end users thus will not be returned as message history query result. |
| */ |
| private static final int MESSAGE_HISTORY = 1; |
| |
| /** |
| * URI matcher type for update message which are being displayed to end-users. |
| */ |
| private static final int MESSAGE_DISPLAYED = 2; |
| |
| /** MIME type for the list of all cell broadcasts. */ |
| private static final String LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast"; |
| |
| /** Table name of cell broadcast message. */ |
| @VisibleForTesting |
| public static final String CELL_BROADCASTS_TABLE_NAME = "cell_broadcasts"; |
| |
| /** Authority string for content URIs. */ |
| @VisibleForTesting |
| public static final String AUTHORITY = "cellbroadcasts"; |
| |
| /** Content uri of this provider. */ |
| public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts"); |
| |
| /** |
| * Local definition of the query columns for instantiating |
| * {@link android.telephony.SmsCbMessage} objects. |
| */ |
| public static final String[] QUERY_COLUMNS = { |
| CellBroadcasts._ID, |
| CellBroadcasts.SLOT_INDEX, |
| CellBroadcasts.SUBSCRIPTION_ID, |
| CellBroadcasts.GEOGRAPHICAL_SCOPE, |
| CellBroadcasts.PLMN, |
| CellBroadcasts.LAC, |
| CellBroadcasts.CID, |
| CellBroadcasts.SERIAL_NUMBER, |
| CellBroadcasts.SERVICE_CATEGORY, |
| CellBroadcasts.LANGUAGE_CODE, |
| CellBroadcasts.DATA_CODING_SCHEME, |
| CellBroadcasts.MESSAGE_BODY, |
| CellBroadcasts.MESSAGE_FORMAT, |
| CellBroadcasts.MESSAGE_PRIORITY, |
| CellBroadcasts.ETWS_WARNING_TYPE, |
| CellBroadcasts.ETWS_IS_PRIMARY, |
| CellBroadcasts.CMAS_MESSAGE_CLASS, |
| CellBroadcasts.CMAS_CATEGORY, |
| CellBroadcasts.CMAS_RESPONSE_TYPE, |
| CellBroadcasts.CMAS_SEVERITY, |
| CellBroadcasts.CMAS_URGENCY, |
| CellBroadcasts.CMAS_CERTAINTY, |
| CellBroadcasts.RECEIVED_TIME, |
| CellBroadcasts.LOCATION_CHECK_TIME, |
| CellBroadcasts.MESSAGE_BROADCASTED, |
| CellBroadcasts.MESSAGE_DISPLAYED, |
| CellBroadcasts.GEOMETRIES, |
| CellBroadcasts.MAXIMUM_WAIT_TIME |
| }; |
| |
| @VisibleForTesting |
| public CellBroadcastPermissionChecker mPermissionChecker; |
| |
| /** The database helper for this content provider. */ |
| @VisibleForTesting |
| public SQLiteOpenHelper mDbHelper; |
| |
| static { |
| sUriMatcher.addURI(AUTHORITY, null, ALL); |
| sUriMatcher.addURI(AUTHORITY, "history", MESSAGE_HISTORY); |
| sUriMatcher.addURI(AUTHORITY, "displayed", MESSAGE_DISPLAYED); |
| } |
| |
| public CellBroadcastProvider() {} |
| |
| @VisibleForTesting |
| public CellBroadcastProvider(CellBroadcastPermissionChecker permissionChecker) { |
| mPermissionChecker = permissionChecker; |
| } |
| |
| @Override |
| public boolean onCreate() { |
| mDbHelper = new CellBroadcastDatabaseHelper(getContext()); |
| mPermissionChecker = new CellBroadcastPermissionChecker(); |
| return true; |
| } |
| |
| /** |
| * Return the MIME type of the data at the specified URI. |
| * |
| * @param uri the URI to query. |
| * @return a MIME type string, or null if there is no type. |
| */ |
| @Override |
| public String getType(Uri uri) { |
| int match = sUriMatcher.match(uri); |
| switch (match) { |
| case ALL: |
| return LIST_TYPE; |
| default: |
| return null; |
| } |
| } |
| |
| @Override |
| public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, |
| String sortOrder) { |
| checkReadPermission(uri); |
| |
| if (DBG) { |
| Log.d(TAG, "query:" |
| + " uri = " + uri |
| + " projection = " + Arrays.toString(projection) |
| + " selection = " + selection |
| + " selectionArgs = " + Arrays.toString(selectionArgs) |
| + " sortOrder = " + sortOrder); |
| } |
| SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); |
| qb.setStrict(true); // a little protection from injection attacks |
| qb.setTables(CELL_BROADCASTS_TABLE_NAME); |
| |
| String orderBy; |
| if (!TextUtils.isEmpty(sortOrder)) { |
| orderBy = sortOrder; |
| } else { |
| orderBy = CellBroadcasts.RECEIVED_TIME + " DESC"; |
| } |
| |
| int match = sUriMatcher.match(uri); |
| switch (match) { |
| case ALL: |
| return getReadableDatabase().query( |
| CELL_BROADCASTS_TABLE_NAME, projection, selection, selectionArgs, |
| null /* groupBy */, null /* having */, orderBy); |
| case MESSAGE_HISTORY: |
| // limit projections to certain columns. limit result to broadcasted messages only. |
| qb.appendWhere(CellBroadcasts.MESSAGE_BROADCASTED + "=1"); |
| return qb.query(getReadableDatabase(), projection, selection, selectionArgs, null, |
| null, orderBy); |
| default: |
| throw new IllegalArgumentException( |
| "Query method doesn't support this uri = " + uri); |
| } |
| } |
| |
| @Override |
| public Uri insert(Uri uri, ContentValues values) { |
| checkWritePermission(); |
| |
| if (DBG) { |
| Log.d(TAG, "insert:" |
| + " uri = " + uri |
| + " contentValue = " + values); |
| } |
| |
| switch (sUriMatcher.match(uri)) { |
| case ALL: |
| long row = getWritableDatabase().insertOrThrow(CELL_BROADCASTS_TABLE_NAME, null, |
| values); |
| if (row > 0) { |
| Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row); |
| getContext().getContentResolver() |
| .notifyChange(CONTENT_URI, null /* observer */); |
| return newUri; |
| } else { |
| String errorString = "uri=" + uri.toString() + " values=" + values; |
| // 1000 character limit for error logs |
| if (errorString.length() > 1000) { |
| errorString = errorString.substring(0, 1000); |
| } |
| CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR, |
| CELL_BROADCAST_MESSAGE_ERROR__TYPE__FAILED_TO_INSERT_TO_DB, |
| errorString); |
| Log.e(TAG, "Insert record failed because of unknown reason. " + errorString); |
| return null; |
| } |
| default: |
| String errorString = "Insert method doesn't support this uri=" |
| + uri.toString() + " values=" + values; |
| // 1000 character limit for error logs |
| if (errorString.length() > 1000) { |
| errorString = errorString.substring(0, 1000); |
| } |
| CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR, |
| CELL_BROADCAST_MESSAGE_ERROR__TYPE__FAILED_TO_INSERT_TO_DB, errorString); |
| throw new IllegalArgumentException(errorString); |
| } |
| } |
| |
| @Override |
| public int delete(Uri uri, String selection, String[] selectionArgs) { |
| checkWritePermission(); |
| |
| if (DBG) { |
| Log.d(TAG, "delete:" |
| + " uri = " + uri |
| + " selection = " + selection |
| + " selectionArgs = " + Arrays.toString(selectionArgs)); |
| } |
| |
| switch (sUriMatcher.match(uri)) { |
| case ALL: |
| return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME, |
| selection, selectionArgs); |
| default: |
| throw new IllegalArgumentException( |
| "Delete method doesn't support this uri = " + uri); |
| } |
| } |
| |
| @Override |
| public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
| checkWritePermission(); |
| |
| if (DBG) { |
| Log.d(TAG, "update:" |
| + " uri = " + uri |
| + " values = {" + values + "}" |
| + " selection = " + selection |
| + " selectionArgs = " + Arrays.toString(selectionArgs)); |
| } |
| |
| int rowCount = 0; |
| switch (sUriMatcher.match(uri)) { |
| case ALL: |
| rowCount = getWritableDatabase().update( |
| CELL_BROADCASTS_TABLE_NAME, |
| values, |
| selection, |
| selectionArgs); |
| if (rowCount > 0) { |
| getContext().getContentResolver().notifyChange(uri, null /* observer */, |
| ContentResolver.NOTIFY_SKIP_NOTIFY_FOR_DESCENDANTS |
| | ContentResolver.NOTIFY_SYNC_TO_NETWORK ); |
| } |
| return rowCount; |
| case MESSAGE_DISPLAYED: |
| // mark message was displayed to the end-users. |
| values.put(Telephony.CellBroadcasts.MESSAGE_DISPLAYED, 1); |
| rowCount = getWritableDatabase().update( |
| CELL_BROADCASTS_TABLE_NAME, |
| values, |
| selection, |
| selectionArgs); |
| if (rowCount > 0) { |
| // update was succeed. the row number of the updated message. |
| try (Cursor ret = query(CellBroadcasts.CONTENT_URI, |
| new String[]{CellBroadcasts._ID}, |
| selection, selectionArgs, null)) { |
| if (ret != null && ret.moveToFirst()) { |
| int rowNumber = ret.getInt(ret.getColumnIndex(CellBroadcasts._ID)); |
| Log.d(TAG, "notify contentObservers for the displayed message, row: " |
| + rowNumber); |
| getContext().getContentResolver().notifyChange( |
| Uri.withAppendedPath(CONTENT_URI, |
| "displayed/" + rowNumber), null, true); |
| } |
| } catch (Exception ex) { |
| Log.e(TAG, "exception during update message displayed: " + ex.toString()); |
| } |
| } |
| return rowCount; |
| default: |
| throw new IllegalArgumentException( |
| "Update method doesn't support this uri = " + uri); |
| } |
| } |
| |
| /** |
| * Returns a string used to create the cell broadcast table. This is exposed so the unit test |
| * can construct its own in-memory database to match the cell broadcast db. |
| */ |
| @VisibleForTesting |
| public static String getStringForCellBroadcastTableCreation(String tableName) { |
| return "CREATE TABLE " + tableName + " (" |
| + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," |
| + CellBroadcasts.SUBSCRIPTION_ID + " INTEGER," |
| + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0," |
| + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER," |
| + CellBroadcasts.PLMN + " TEXT," |
| + CellBroadcasts.LAC + " INTEGER," |
| + CellBroadcasts.CID + " INTEGER," |
| + CellBroadcasts.SERIAL_NUMBER + " INTEGER," |
| + CellBroadcasts.SERVICE_CATEGORY + " INTEGER," |
| + CellBroadcasts.LANGUAGE_CODE + " TEXT," |
| + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0," |
| + CellBroadcasts.MESSAGE_BODY + " TEXT," |
| + CellBroadcasts.MESSAGE_FORMAT + " INTEGER," |
| + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER," |
| + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER," |
| + CellBroadcasts.ETWS_IS_PRIMARY + " BOOLEAN DEFAULT 0," |
| + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER," |
| + CellBroadcasts.CMAS_CATEGORY + " INTEGER," |
| + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER," |
| + CellBroadcasts.CMAS_SEVERITY + " INTEGER," |
| + CellBroadcasts.CMAS_URGENCY + " INTEGER," |
| + CellBroadcasts.CMAS_CERTAINTY + " INTEGER," |
| + CellBroadcasts.RECEIVED_TIME + " BIGINT," |
| + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1," |
| + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0," |
| + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 0," |
| + CellBroadcasts.GEOMETRIES + " TEXT," |
| + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);"; |
| } |
| |
| private SQLiteDatabase getWritableDatabase() { |
| return mDbHelper.getWritableDatabase(); |
| } |
| |
| private SQLiteDatabase getReadableDatabase() { |
| return mDbHelper.getReadableDatabase(); |
| } |
| |
| private void checkWritePermission() { |
| if (!mPermissionChecker.hasFullAccessPermission()) { |
| throw new SecurityException( |
| "No permission to write CellBroadcast provider"); |
| } |
| } |
| |
| private void checkReadPermission(Uri uri) { |
| int match = sUriMatcher.match(uri); |
| switch (match) { |
| case ALL: |
| if (!mPermissionChecker.hasFullAccessPermission()) { |
| throw new SecurityException( |
| "No permission to read CellBroadcast provider"); |
| } |
| break; |
| case MESSAGE_HISTORY: |
| // The normal read permission android.permission.READ_CELL_BROADCASTS |
| // is defined in AndroidManifest.xml and is enfored by the platform. |
| // So no additional check is required here. |
| break; |
| default: |
| return; |
| } |
| } |
| |
| @VisibleForTesting |
| public static class CellBroadcastDatabaseHelper extends SQLiteOpenHelper { |
| public CellBroadcastDatabaseHelper(Context context) { |
| super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION); |
| } |
| |
| @Override |
| public void onCreate(SQLiteDatabase db) { |
| db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME)); |
| } |
| |
| @Override |
| public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
| if (DBG) { |
| Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion); |
| } |
| if (oldVersion < 2) { |
| db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " |
| + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;"); |
| Log.d(TAG, "add slotIndex column"); |
| } |
| |
| if (oldVersion < 3) { |
| db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " |
| + CellBroadcasts.DATA_CODING_SCHEME + " INTEGER DEFAULT 0;"); |
| db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " |
| + CellBroadcasts.LOCATION_CHECK_TIME + " BIGINT DEFAULT -1;"); |
| // Specifically for upgrade, the message displayed should be true. For newly arrived |
| // message, default should be false. |
| db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " |
| + CellBroadcasts.MESSAGE_DISPLAYED + " BOOLEAN DEFAULT 1;"); |
| Log.d(TAG, "add dcs, location check time, and message displayed column."); |
| } |
| |
| if (oldVersion < 4) { |
| db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN " |
| + CellBroadcasts.ETWS_IS_PRIMARY + " BOOLEAN DEFAULT 0;"); |
| Log.d(TAG, "add ETWS is_primary column."); |
| } |
| } |
| } |
| |
| /** |
| * Cell broadcast permission checker. |
| */ |
| public class CellBroadcastPermissionChecker { |
| /** |
| * @return {@code true} if the caller has permission to fully access the cell broadcast |
| * provider. |
| */ |
| public boolean hasFullAccessPermission() { |
| int status = getContext().checkCallingOrSelfPermission( |
| "com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY"); |
| return status == PackageManager.PERMISSION_GRANTED; |
| } |
| } |
| } |