DO NOT MERGE Fix the remaining holes for access to APN data
am: 30a633ac17
Change-Id: Ie79a4cdd030c6adea280aa55de4690ebff58dfc8
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 4ae9acf..3b95bce 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -107,6 +107,12 @@
android:multiprocess="false"
android:writePermission="android.permission.MODIFY_PHONE_STATE" />
+ <provider android:name="CellBroadcastProvider"
+ android:authorities="cellbroadcasts_fwk"
+ android:exported="true"
+ android:singleUser="true"
+ android:multiprocess="false" />
+
<provider android:name="HbpcdLookupProvider"
android:authorities="hbpcd_lookup"
android:exported="true"
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..ee02cd3
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsTelephonyProviderTestCases"
+ }
+ ]
+}
diff --git a/src/com/android/providers/telephony/CellBroadcastProvider.java b/src/com/android/providers/telephony/CellBroadcastProvider.java
new file mode 100644
index 0000000..c50fbf2
--- /dev/null
+++ b/src/com/android/providers/telephony/CellBroadcastProvider.java
@@ -0,0 +1,316 @@
+/*
+ * 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.providers.telephony;
+
+import android.app.AppOpsManager;
+import android.content.ContentProvider;
+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.net.Uri;
+import android.os.Binder;
+import android.os.Process;
+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 android.permission.READ_CELL_BROADCASTS} is required for querying the cell
+ * broadcast message. Only phone process has the permission to write/update the database via this
+ * provider.
+ */
+public class CellBroadcastProvider extends ContentProvider {
+ /** Interface for read/write permission check. */
+ public interface PermissionChecker {
+ /** Return {@code True} if the caller has the permission to write/update the database. */
+ boolean hasWritePermission();
+
+ /** Return {@code True} if the caller has the permission to query the database. */
+ boolean hasReadPermission();
+ }
+
+ 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. */
+ private static final int DATABASE_VERSION = 1;
+
+ /** 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;
+
+ /** 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_fwk";
+
+ /** Content uri of this provider. */
+ public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts_fwk");
+
+ @VisibleForTesting
+ public PermissionChecker mPermissionChecker;
+
+ /** The database helper for this content provider. */
+ @VisibleForTesting
+ public SQLiteOpenHelper mDbHelper;
+
+ static {
+ sUriMatcher.addURI(AUTHORITY, null, ALL);
+ }
+
+ public CellBroadcastProvider() {}
+
+ @VisibleForTesting
+ public CellBroadcastProvider(PermissionChecker permissionChecker) {
+ mPermissionChecker = permissionChecker;
+ }
+
+ @Override
+ public boolean onCreate() {
+ mDbHelper = new CellBroadcastDatabaseHelper(getContext());
+ mPermissionChecker = new CellBroadcastPermissionChecker();
+ setAppOps(AppOpsManager.OP_READ_CELL_BROADCASTS, AppOpsManager.OP_NONE);
+ 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();
+
+ if (DBG) {
+ Log.d(TAG, "query:"
+ + " uri = " + uri
+ + " projection = " + Arrays.toString(projection)
+ + " selection = " + selection
+ + " selectionArgs = " + Arrays.toString(selectionArgs)
+ + " sortOrder = " + sortOrder);
+ }
+
+ 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);
+ 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 {
+ Log.e(TAG, "Insert record failed because of unknown reason, uri = " + uri);
+ return null;
+ }
+ default:
+ throw new IllegalArgumentException(
+ "Insert method doesn't support this uri = " + uri);
+ }
+ }
+
+ @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));
+ }
+
+ switch (sUriMatcher.match(uri)) {
+ case ALL:
+ int rowCount = getWritableDatabase().update(
+ CELL_BROADCASTS_TABLE_NAME,
+ values,
+ selection,
+ selectionArgs);
+ if (rowCount > 0) {
+ getContext().getContentResolver().notifyChange(uri, null /* observer */);
+ }
+ return rowCount;
+ default:
+ throw new IllegalArgumentException(
+ "Update method doesn't support this uri = " + uri);
+ }
+ }
+
+ @VisibleForTesting
+ public static String getStringForCellBroadcastTableCreation(String tableName) {
+ return "CREATE TABLE " + tableName + " ("
+ + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + 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.MESSAGE_BODY + " TEXT,"
+ + CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
+ + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
+ + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
+ + 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.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0,"
+ + CellBroadcasts.GEOMETRIES + " TEXT);";
+ }
+
+ private SQLiteDatabase getWritableDatabase() {
+ return mDbHelper.getWritableDatabase();
+ }
+
+ private SQLiteDatabase getReadableDatabase() {
+ return mDbHelper.getReadableDatabase();
+ }
+
+ private void checkWritePermission() {
+ if (!mPermissionChecker.hasWritePermission()) {
+ throw new SecurityException(
+ "No permission to write CellBroadcast provider");
+ }
+ }
+
+ private void checkReadPermission() {
+ if (!mPermissionChecker.hasReadPermission()) {
+ throw new SecurityException(
+ "No permission to read CellBroadcast provider");
+ }
+ }
+
+ private class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
+ 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) {}
+ }
+
+ private class CellBroadcastPermissionChecker implements PermissionChecker {
+ @Override
+ public boolean hasWritePermission() {
+ // Only the phone process has the write permission to modify this provider.
+ return Binder.getCallingUid() == Process.PHONE_UID;
+ }
+
+ @Override
+ public boolean hasReadPermission() {
+ // Only the phone process has the read permission to query data from this provider.
+ return Binder.getCallingUid() == Process.PHONE_UID;
+ }
+ }
+}
diff --git a/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java b/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java
index 5fc36d2..4add85d 100644
--- a/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java
+++ b/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java
@@ -17,6 +17,7 @@
package com.android.providers.telephony;
import static android.provider.Telephony.RcsColumns.IS_RCS_TABLE_SCHEMA_CODE_COMPLETE;
+import static com.android.internal.telephony.SmsResponse.NO_ERROR_CODE;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
@@ -1012,7 +1013,7 @@
"service_center TEXT," +
"locked INTEGER DEFAULT 0," +
"sub_id INTEGER DEFAULT " + SubscriptionManager.INVALID_SUBSCRIPTION_ID + ", " +
- "error_code INTEGER DEFAULT 0," +
+ "error_code INTEGER DEFAULT " + NO_ERROR_CODE + ", " +
"creator TEXT," +
"seen INTEGER DEFAULT 0" +
");";
@@ -1816,7 +1817,7 @@
private void upgradeDatabaseToVersion48(SQLiteDatabase db) {
// Add 'error_code' column to sms table.
- db.execSQL("ALTER TABLE sms ADD COLUMN error_code INTEGER DEFAULT 0");
+ db.execSQL("ALTER TABLE sms ADD COLUMN error_code INTEGER DEFAULT " + NO_ERROR_CODE);
}
private void upgradeDatabaseToVersion51(SQLiteDatabase db) {
diff --git a/src/com/android/providers/telephony/MmsSmsProvider.java b/src/com/android/providers/telephony/MmsSmsProvider.java
index 8c3555c..1170aad 100644
--- a/src/com/android/providers/telephony/MmsSmsProvider.java
+++ b/src/com/android/providers/telephony/MmsSmsProvider.java
@@ -305,6 +305,7 @@
private SQLiteOpenHelper mOpenHelper;
private boolean mUseStrictPhoneNumberComparation;
+ private int mMinMatch;
private static final String METHOD_IS_RESTORING = "is_restoring";
private static final String IS_RESTORING_KEY = "restoring";
@@ -316,6 +317,9 @@
mUseStrictPhoneNumberComparation =
getContext().getResources().getBoolean(
com.android.internal.R.bool.config_use_strict_phone_number_comparation);
+ mMinMatch =
+ getContext().getResources().getInteger(
+ com.android.internal.R.integer.config_phonenumber_compare_min_match);
TelephonyBackupAgent.DeferredSmsMmsRestoreService.startIfFilesExist(getContext());
return true;
}
@@ -542,7 +546,7 @@
selectionArgs = new String[] { refinedAddress };
} else {
selection += " OR PHONE_NUMBERS_EQUAL(address, ?, " +
- (mUseStrictPhoneNumberComparation ? 1 : 0) + ")";
+ (mUseStrictPhoneNumberComparation ? "1)" : "0, " + mMinMatch + ")");
selectionArgs = new String[] { refinedAddress, refinedAddress };
}
@@ -997,13 +1001,14 @@
* FROM pdu, (SELECT msg_id AS address_msg_id
* FROM addr
* WHERE (address='<phoneNumber>' OR
- * PHONE_NUMBERS_EQUAL(addr.address, '<phoneNumber>', 1/0)))
+ * PHONE_NUMBERS_EQUAL(addr.address, '<phoneNumber>', 1/0, none/mMinMatch)))
* AS matching_addresses
* WHERE pdu._id = matching_addresses.address_msg_id
* UNION
* SELECT ...
* FROM sms
- * WHERE (address='<phoneNumber>' OR PHONE_NUMBERS_EQUAL(sms.address, '<phoneNumber>', 1/0));
+ * WHERE (address='<phoneNumber>' OR
+ * PHONE_NUMBERS_EQUAL(sms.address, '<phoneNumber>', 1/0, none/mMinMatch));
*/
private Cursor getMessagesByPhoneNumber(
String phoneNumber, String[] projection, String selection,
@@ -1018,7 +1023,7 @@
selection,
"(address=" + escapedPhoneNumber + " OR PHONE_NUMBERS_EQUAL(address, " +
escapedPhoneNumber +
- (mUseStrictPhoneNumberComparation ? ", 1))" : ", 0))"));
+ (mUseStrictPhoneNumberComparation ? ", 1))" : ", 0, " + mMinMatch + "))"));
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@@ -1030,7 +1035,7 @@
"FROM addr WHERE (address=" + escapedPhoneNumber +
" OR PHONE_NUMBERS_EQUAL(addr.address, " +
escapedPhoneNumber +
- (mUseStrictPhoneNumberComparation ? ", 1))) " : ", 0))) ") +
+ (mUseStrictPhoneNumberComparation ? ", 1))) " : ", 0, " + mMinMatch + "))) ") +
"AS matching_addresses");
smsQueryBuilder.setTables(smsTable);
diff --git a/src/com/android/providers/telephony/SmsProvider.java b/src/com/android/providers/telephony/SmsProvider.java
index e2e6d10..ddfa14c 100644
--- a/src/com/android/providers/telephony/SmsProvider.java
+++ b/src/com/android/providers/telephony/SmsProvider.java
@@ -16,6 +16,8 @@
package com.android.providers.telephony;
+import static com.android.internal.telephony.SmsResponse.NO_ERROR_CODE;
+
import android.annotation.NonNull;
import android.app.AppOpsManager;
import android.content.ContentProvider;
@@ -75,7 +77,7 @@
// N.B.: These columns must appear in the same order as the
// calls to add appear in convertIccToSms.
"service_center_address", // getServiceCenterAddress
- "address", // getDisplayOriginatingAddress
+ "address", // getDisplayOriginatingAddress or getRecipientAddress
"message_class", // getMessageClass
"body", // getDisplayMessageBody
"date", // getTimestampMillis
@@ -83,9 +85,9 @@
"index_on_icc", // getIndexOnIcc
"is_status_report", // isStatusReportMessage
"transport_type", // Always "sms".
- "type", // Always MESSAGE_TYPE_ALL.
+ "type", // depend on getStatusOnIcc
"locked", // Always 0 (false).
- "error_code", // Always 0
+ "error_code", // Always -1 (NO_ERROR_CODE), previously it was 0 always.
"_id"
};
@@ -292,21 +294,39 @@
}
private Object[] convertIccToSms(SmsMessage message, int id) {
+ int statusOnIcc = message.getStatusOnIcc();
+ int type = Sms.MESSAGE_TYPE_ALL;
+ switch (statusOnIcc) {
+ case SmsManager.STATUS_ON_ICC_READ:
+ case SmsManager.STATUS_ON_ICC_UNREAD:
+ type = Sms.MESSAGE_TYPE_INBOX;
+ break;
+ case SmsManager.STATUS_ON_ICC_SENT:
+ type = Sms.MESSAGE_TYPE_SENT;
+ break;
+ case SmsManager.STATUS_ON_ICC_UNSENT:
+ type = Sms.MESSAGE_TYPE_OUTBOX;
+ break;
+ }
// N.B.: These calls must appear in the same order as the
// columns appear in ICC_COLUMNS.
Object[] row = new Object[13];
row[0] = message.getServiceCenterAddress();
- row[1] = message.getDisplayOriginatingAddress();
+ row[1] =
+ (type == Sms.MESSAGE_TYPE_INBOX)
+ ? message.getDisplayOriginatingAddress()
+ : message.getRecipientAddress();
+
row[2] = String.valueOf(message.getMessageClass());
row[3] = message.getDisplayMessageBody();
row[4] = message.getTimestampMillis();
- row[5] = Sms.STATUS_NONE;
+ row[5] = statusOnIcc;
row[6] = message.getIndexOnIcc();
row[7] = message.isStatusReportMessage();
row[8] = "sms";
- row[9] = TextBasedSmsColumns.MESSAGE_TYPE_ALL;
+ row[9] = type;
row[10] = 0; // locked
- row[11] = 0; // error_code
+ row[11] = NO_ERROR_CODE;
row[12] = id;
return row;
}
@@ -632,7 +652,12 @@
db.insert(TABLE_WORDS, Telephony.MmsSms.WordsTable.INDEXED_TEXT, cv);
}
if (rowID > 0) {
- Uri uri = Uri.withAppendedPath(url, String.valueOf(rowID));
+ Uri uri = null;
+ if (table == TABLE_SMS) {
+ uri = Uri.withAppendedPath(Sms.CONTENT_URI, String.valueOf(rowID));
+ } else {
+ uri = Uri.withAppendedPath(url, String.valueOf(rowID));
+ }
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.d(TAG, "insert " + uri + " succeeded");
}
diff --git a/src/com/android/providers/telephony/TelephonyProvider.java b/src/com/android/providers/telephony/TelephonyProvider.java
index d9e368d..0aefc62 100644
--- a/src/com/android/providers/telephony/TelephonyProvider.java
+++ b/src/com/android/providers/telephony/TelephonyProvider.java
@@ -2558,25 +2558,6 @@
if (isNewBuild) {
if (!apnSourceServiceExists(getContext())) {
- // Call getReadableDatabase() to make sure onUpgrade is called
- if (VDBG) log("onCreate: calling getReadableDatabase to trigger onUpgrade");
- SQLiteDatabase db = getReadableDatabase();
-
- // Get rid of old preferred apn shared preferences
- SubscriptionManager sm = SubscriptionManager.from(getContext());
- if (sm != null) {
- List<SubscriptionInfo> subInfoList = sm.getAllSubscriptionInfoList();
- for (SubscriptionInfo subInfo : subInfoList) {
- SharedPreferences spPrefFile = getContext().getSharedPreferences(
- PREF_FILE_APN + subInfo.getSubscriptionId(), Context.MODE_PRIVATE);
- if (spPrefFile != null) {
- SharedPreferences.Editor editor = spPrefFile.edit();
- editor.clear();
- editor.apply();
- }
- }
- }
-
// Update APN DB
updateApnDb();
}
@@ -2690,26 +2671,6 @@
private void deletePreferredApnId() {
SharedPreferences sp = getContext().getSharedPreferences(PREF_FILE_APN,
Context.MODE_PRIVATE);
-
- // Before deleting, save actual preferred apns (not the ids) in a separate SP.
- // NOTE: This code to call setPreferredApn() can be removed since the function is now called
- // from setPreferredApnId(). However older builds (pre oc-mr1) do not have that change, so
- // when devices upgrade from those builds and this function is called, this code is needed
- // otherwise the preferred APN will be lost.
- Map<String, ?> allPrefApnId = sp.getAll();
- for (String key : allPrefApnId.keySet()) {
- // extract subId from key by removing COLUMN_APN_ID
- try {
- int subId = Integer.parseInt(key.replace(COLUMN_APN_ID, ""));
- long apnId = getPreferredApnId(subId, false);
- if (apnId != INVALID_APN_ID) {
- setPreferredApn(apnId, subId);
- }
- } catch (Exception e) {
- loge("Skipping over key " + key + " due to exception " + e);
- }
- }
-
SharedPreferences.Editor editor = sp.edit();
editor.clear();
editor.apply();
diff --git a/tests/src/com/android/providers/telephony/CellBroadcastProviderTest.java b/tests/src/com/android/providers/telephony/CellBroadcastProviderTest.java
new file mode 100644
index 0000000..b5ff07b
--- /dev/null
+++ b/tests/src/com/android/providers/telephony/CellBroadcastProviderTest.java
@@ -0,0 +1,384 @@
+/*
+ * 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.providers.telephony;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+
+import junit.framework.TestCase;
+
+import android.content.ContentValues;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Telephony.CellBroadcasts;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.util.Log;
+
+import com.android.providers.telephony.CellBroadcastProvider.PermissionChecker;
+
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class CellBroadcastProviderTest extends TestCase {
+ private static final String TAG = CellBroadcastProviderTest.class.getSimpleName();
+
+ public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts_fwk");
+
+ private static final int GEO_SCOPE = 1;
+ private static final String PLMN = "123456";
+ private static final int LAC = 13;
+ private static final int CID = 123;
+ private static final int SERIAL_NUMBER = 17984;
+ private static final int SERVICE_CATEGORY = 4379;
+ private static final String LANGUAGE_CODE = "en";
+ private static final String MESSAGE_BODY = "AMBER Alert: xxxx";
+ private static final int MESSAGE_FORMAT = 1;
+ private static final int MESSAGE_PRIORITY = 3;
+ private static final int ETWS_WARNING_TYPE = 1;
+ private static final int CMAS_MESSAGE_CLASS = 1;
+ private static final int CMAS_CATEGORY = 6;
+ private static final int CMAS_RESPONSE_TYPE = 1;
+ private static final int CMAS_SEVERITY = 2;
+ private static final int CMAS_URGENCY = 3;
+ private static final int CMAS_CERTAINTY = 4;
+ private static final int RECEIVED_TIME = 1562792637;
+ private static final int MESSAGE_BROADCASTED = 1;
+ private static final String GEOMETRIES_COORDINATES
+ = "polygon|0,0|0,1|1,1|1,0;circle|0,0|100";
+
+ private static final String SELECT_BY_ID = CellBroadcasts._ID + "=?";
+
+ private static final String[] QUERY_COLUMNS = {
+ CellBroadcasts._ID,
+ CellBroadcasts.GEOGRAPHICAL_SCOPE,
+ CellBroadcasts.PLMN,
+ CellBroadcasts.LAC,
+ CellBroadcasts.CID,
+ CellBroadcasts.SERIAL_NUMBER,
+ CellBroadcasts.SERVICE_CATEGORY,
+ CellBroadcasts.LANGUAGE_CODE,
+ CellBroadcasts.MESSAGE_BODY,
+ CellBroadcasts.MESSAGE_FORMAT,
+ CellBroadcasts.MESSAGE_PRIORITY,
+ CellBroadcasts.ETWS_WARNING_TYPE,
+ CellBroadcasts.CMAS_MESSAGE_CLASS,
+ CellBroadcasts.CMAS_CATEGORY,
+ CellBroadcasts.CMAS_RESPONSE_TYPE,
+ CellBroadcasts.CMAS_SEVERITY,
+ CellBroadcasts.CMAS_URGENCY,
+ CellBroadcasts.CMAS_CERTAINTY,
+ CellBroadcasts.RECEIVED_TIME,
+ CellBroadcasts.MESSAGE_BROADCASTED,
+ CellBroadcasts.GEOMETRIES
+ };
+
+ private CellBroadcastProviderTestable mCellBroadcastProviderTestable;
+ private MockContextWithProvider mContext;
+ private MockContentResolver mContentResolver;
+
+ @Mock
+ private PermissionChecker mMockPermissionChecker;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ doReturn(true).when(mMockPermissionChecker).hasReadPermission();
+ doReturn(true).when(mMockPermissionChecker).hasWritePermission();
+
+ mCellBroadcastProviderTestable = new CellBroadcastProviderTestable(mMockPermissionChecker);
+ mContext = new MockContextWithProvider(mCellBroadcastProviderTestable);
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mCellBroadcastProviderTestable.closeDatabase();
+ super.tearDown();
+ }
+
+ @Test
+ public void testUpdate() {
+ // Insert a cellbroadcast to the database.
+ ContentValues cv = fakeCellBroadcast();
+ Uri uri = mContentResolver.insert(CONTENT_URI, cv);
+ assertThat(uri).isNotNull();
+
+ // Change some fields of this cell broadcast.
+ int messageBroadcasted = 1 - cv.getAsInteger(CellBroadcasts.MESSAGE_BROADCASTED);
+ int receivedTime = 1234555555;
+ cv.put(CellBroadcasts.MESSAGE_BROADCASTED, messageBroadcasted);
+ cv.put(CellBroadcasts.RECEIVED_TIME, receivedTime);
+ mContentResolver.update(CONTENT_URI, cv, SELECT_BY_ID,
+ new String[] { uri.getLastPathSegment() });
+
+ // Query and check if the update is successed.
+ Cursor cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS,
+ SELECT_BY_ID, new String[] { uri.getLastPathSegment() }, null /* orderBy */);
+ cursor.moveToNext();
+
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.RECEIVED_TIME)))
+ .isEqualTo(receivedTime);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_BROADCASTED)))
+ .isEqualTo(messageBroadcasted);
+ cursor.close();
+ }
+
+ @Test
+ public void testUpdate_WithoutWritePermission_fail() {
+ ContentValues cv = fakeCellBroadcast();
+ Uri uri = mContentResolver.insert(CONTENT_URI, cv);
+ assertThat(uri).isNotNull();
+
+ // Revoke the write permission
+ doReturn(false).when(mMockPermissionChecker).hasWritePermission();
+
+ try {
+ mContentResolver.update(CONTENT_URI, cv, SELECT_BY_ID,
+ new String[] { uri.getLastPathSegment() });
+ fail();
+ } catch (SecurityException ex) {
+ // pass the test
+ }
+ }
+
+ @Test
+ public void testGetAllCellBroadcast() {
+ // Insert some cell broadcasts which message_broadcasted is false
+ int messageNotBroadcastedCount = 5;
+ ContentValues cv = fakeCellBroadcast();
+ cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 0);
+ for (int i = 0; i < messageNotBroadcastedCount; i++) {
+ mContentResolver.insert(CONTENT_URI, cv);
+ }
+
+ // Insert some cell broadcasts which message_broadcasted is true
+ int messageBroadcastedCount = 6;
+ cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1);
+ for (int i = 0; i < messageBroadcastedCount; i++) {
+ mContentResolver.insert(CONTENT_URI, cv);
+ }
+
+ // Query the broadcast with message_broadcasted is false
+ Cursor cursor = mContentResolver.query(
+ CONTENT_URI,
+ QUERY_COLUMNS,
+ String.format("%s=?", CellBroadcasts.MESSAGE_BROADCASTED), /* selection */
+ new String[] {"0"}, /* selectionArgs */
+ null /* sortOrder */);
+ assertThat(cursor.getCount()).isEqualTo(messageNotBroadcastedCount);
+ }
+
+ @Test
+ public void testDelete_withoutWritePermission_throwSecurityException() {
+ Uri uri = mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+ assertThat(uri).isNotNull();
+
+ // Revoke the write permission
+ doReturn(false).when(mMockPermissionChecker).hasWritePermission();
+
+ try {
+ mContentResolver.delete(CONTENT_URI, SELECT_BY_ID,
+ new String[] { uri.getLastPathSegment() });
+ fail();
+ } catch (SecurityException ex) {
+ // pass the test
+ }
+ }
+
+
+ @Test
+ public void testDelete_oneRecord_success() {
+ // Insert a cellbroadcast to the database.
+ ContentValues cv = fakeCellBroadcast();
+ Uri uri = mContentResolver.insert(CONTENT_URI, cv);
+ assertThat(uri).isNotNull();
+
+ String[] selectionArgs = new String[] { uri.getLastPathSegment() };
+
+ // Ensure the cell broadcast is inserted.
+ Cursor cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS,
+ SELECT_BY_ID, selectionArgs, null /* orderBy */);
+ assertThat(cursor.getCount()).isEqualTo(1);
+ cursor.close();
+
+ // Delete the cell broadcast
+ int rowCount = mContentResolver.delete(CONTENT_URI, SELECT_BY_ID,
+ selectionArgs);
+ assertThat(rowCount).isEqualTo(1);
+
+ // Ensure the cell broadcast is deleted.
+ cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS, SELECT_BY_ID,
+ selectionArgs, null /* orderBy */);
+ assertThat(cursor.getCount()).isEqualTo(0);
+ cursor.close();
+ }
+
+ @Test
+ public void testDelete_all_success() {
+ // Insert a cellbroadcast to the database.
+ mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+ mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+
+ // Ensure the cell broadcast are inserted.
+ Cursor cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS,
+ null /* selection */, null /* selectionArgs */, null /* orderBy */);
+ assertThat(cursor.getCount()).isEqualTo(2);
+ cursor.close();
+
+ // Delete all cell broadcasts.
+ int rowCount = mContentResolver.delete(
+ CONTENT_URI, null /* selection */, null /* selectionArgs */);
+ assertThat(rowCount).isEqualTo(2);
+ cursor.close();
+
+ // Ensure all cell broadcasts are deleted.
+ cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS,
+ null /* selection */, null /* selectionArgs */, null /* orderBy */);
+ assertThat(cursor.getCount()).isEqualTo(0);
+ cursor.close();
+ }
+
+ @Test
+ public void testInsert_withoutWritePermission_fail() {
+ doReturn(false).when(mMockPermissionChecker).hasWritePermission();
+
+ try {
+ mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+ fail();
+ } catch (SecurityException ex) {
+ // pass the test
+ }
+ }
+
+ @Test
+ public void testInsertAndQuery() {
+ // Insert a cell broadcast message
+ Uri uri = mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+
+ // Verify that the return uri is not null and the record is inserted into the database
+ // correctly.
+ assertThat(uri).isNotNull();
+ Cursor cursor = mContentResolver.query(
+ CONTENT_URI, QUERY_COLUMNS, SELECT_BY_ID,
+ new String[] { uri.getLastPathSegment() }, null /* orderBy */);
+ assertThat(cursor.getCount()).isEqualTo(1);
+
+ cursor.moveToNext();
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.GEOGRAPHICAL_SCOPE)))
+ .isEqualTo(GEO_SCOPE);
+ assertThat(cursor.getString(cursor.getColumnIndexOrThrow(CellBroadcasts.PLMN)))
+ .isEqualTo(PLMN);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.LAC))).isEqualTo(LAC);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CID))).isEqualTo(CID);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.SERIAL_NUMBER)))
+ .isEqualTo(SERIAL_NUMBER);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.SERVICE_CATEGORY)))
+ .isEqualTo(SERVICE_CATEGORY);
+ assertThat(cursor.getString(cursor.getColumnIndexOrThrow(CellBroadcasts.LANGUAGE_CODE)))
+ .isEqualTo(LANGUAGE_CODE);
+ assertThat(cursor.getString(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_BODY)))
+ .isEqualTo(MESSAGE_BODY);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_FORMAT)))
+ .isEqualTo(MESSAGE_FORMAT);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_PRIORITY)))
+ .isEqualTo(MESSAGE_PRIORITY);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.ETWS_WARNING_TYPE)))
+ .isEqualTo(ETWS_WARNING_TYPE);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_MESSAGE_CLASS)))
+ .isEqualTo(CMAS_MESSAGE_CLASS);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_CATEGORY)))
+ .isEqualTo(CMAS_CATEGORY);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_RESPONSE_TYPE)))
+ .isEqualTo(CMAS_RESPONSE_TYPE);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_SEVERITY)))
+ .isEqualTo(CMAS_SEVERITY);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_URGENCY)))
+ .isEqualTo(CMAS_URGENCY);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_CERTAINTY)))
+ .isEqualTo(CMAS_CERTAINTY);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.RECEIVED_TIME)))
+ .isEqualTo(RECEIVED_TIME);
+ assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_BROADCASTED)))
+ .isEqualTo(MESSAGE_BROADCASTED);
+ assertThat(cursor.getString(cursor.getColumnIndexOrThrow(
+ CellBroadcasts.GEOMETRIES))).isEqualTo(GEOMETRIES_COORDINATES);
+ }
+
+ /**
+ * This is used to give the CellBroadcastProviderTest a mocked context which takes a
+ * CellBroadcastProvider and attaches it to the ContentResolver.
+ */
+ private class MockContextWithProvider extends MockContext {
+ private final MockContentResolver mResolver;
+
+ public MockContextWithProvider(CellBroadcastProviderTestable cellBroadcastProvider) {
+ mResolver = new MockContentResolver();
+ cellBroadcastProvider.initializeForTesting(this);
+
+ // Add given cellBroadcastProvider to mResolver, so that mResolver can send queries
+ // to the provider.
+ mResolver.addProvider(CellBroadcastProvider.AUTHORITY, cellBroadcastProvider);
+ }
+
+ @Override
+ public MockContentResolver getContentResolver() {
+ return mResolver;
+ }
+
+
+ @Override
+ public Object getSystemService(String name) {
+ Log.d(TAG, "getSystemService: returning null");
+ return null;
+ }
+
+ @Override
+ public int checkCallingOrSelfPermission(String permission) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ }
+
+ private static ContentValues fakeCellBroadcast() {
+ ContentValues cv = new ContentValues();
+ cv.put(CellBroadcasts.GEOGRAPHICAL_SCOPE, GEO_SCOPE);
+ cv.put(CellBroadcasts.PLMN, PLMN);
+ cv.put(CellBroadcasts.LAC, LAC);
+ cv.put(CellBroadcasts.CID, CID);
+ cv.put(CellBroadcasts.SERIAL_NUMBER, SERIAL_NUMBER);
+ cv.put(CellBroadcasts.SERVICE_CATEGORY, SERVICE_CATEGORY);
+ cv.put(CellBroadcasts.LANGUAGE_CODE, LANGUAGE_CODE);
+ cv.put(CellBroadcasts.MESSAGE_BODY, MESSAGE_BODY);
+ cv.put(CellBroadcasts.MESSAGE_FORMAT, MESSAGE_FORMAT);
+ cv.put(CellBroadcasts.MESSAGE_PRIORITY, MESSAGE_PRIORITY);
+ cv.put(CellBroadcasts.ETWS_WARNING_TYPE, ETWS_WARNING_TYPE);
+ cv.put(CellBroadcasts.CMAS_MESSAGE_CLASS, CMAS_MESSAGE_CLASS);
+ cv.put(CellBroadcasts.CMAS_CATEGORY, CMAS_CATEGORY);
+ cv.put(CellBroadcasts.CMAS_RESPONSE_TYPE, CMAS_RESPONSE_TYPE);
+ cv.put(CellBroadcasts.CMAS_SEVERITY, CMAS_SEVERITY);
+ cv.put(CellBroadcasts.CMAS_URGENCY, CMAS_URGENCY);
+ cv.put(CellBroadcasts.CMAS_CERTAINTY, CMAS_CERTAINTY);
+ cv.put(CellBroadcasts.RECEIVED_TIME, RECEIVED_TIME);
+ cv.put(CellBroadcasts.MESSAGE_BROADCASTED, MESSAGE_BROADCASTED);
+ cv.put(CellBroadcasts.GEOMETRIES, GEOMETRIES_COORDINATES);
+ return cv;
+ }
+}
diff --git a/tests/src/com/android/providers/telephony/CellBroadcastProviderTestable.java b/tests/src/com/android/providers/telephony/CellBroadcastProviderTestable.java
new file mode 100644
index 0000000..8334312
--- /dev/null
+++ b/tests/src/com/android/providers/telephony/CellBroadcastProviderTestable.java
@@ -0,0 +1,71 @@
+/*
+ * 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.providers.telephony;
+
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+public class CellBroadcastProviderTestable extends CellBroadcastProvider {
+ private static final String TAG = CellBroadcastProviderTestable.class.getSimpleName();
+
+ public CellBroadcastProviderTestable(PermissionChecker permissionChecker) {
+ super(permissionChecker);
+ }
+
+ @Override
+ public boolean onCreate() {
+ // DO NOT call super.onCreate(), otherwise the permission checker will be override.
+ Log.d(TAG, "CellBroadcastProviderTestable onCreate");
+ mDbHelper = new InMemoryCellBroadcastProviderDbHelper();
+ return true;
+ }
+
+ public void closeDatabase() {
+ mDbHelper.close();
+ }
+
+ public static class InMemoryCellBroadcastProviderDbHelper extends SQLiteOpenHelper {
+ public InMemoryCellBroadcastProviderDbHelper() {
+ super(InstrumentationRegistry.getTargetContext(),
+ null, // db file name is null for in-memory db
+ null, // CursorFactory is null by default
+ 1); // db version is no-op for tests
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.d(TAG, "IN MEMORY DB CREATED");
+ db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
+ }
+
+ public void initializeForTesting(Context context) {
+ ProviderInfo providerInfo = new ProviderInfo();
+ providerInfo.authority = CellBroadcastProvider.AUTHORITY;
+
+ // Add context to given carrierIdProvider
+ attachInfoForTesting(context, providerInfo);
+ }
+}