blob: 0a191a6f730aa0677979027b17cfec67cb572f5c [file] [log] [blame]
/*
* Copyright (C) 2023 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 android.telephonyprovider.cts;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static java.util.Map.entry;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.provider.Telephony.Carriers;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.ApiTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Map;
/**
* Tests for the APN database exposed by {@link Carriers}.
*/
@ApiTest(apis = {"android.provider.Telephony.Carriers#CONTENT_URI"})
@RunWith(AndroidJUnit4.class)
public class ApnTest {
private static final String TAG = "ApnTest";
private static final Uri CARRIER_TABLE_URI = Carriers.CONTENT_URI;
private static final String NAME = "carrierName";
private static final String APN = "apn";
private static final String PROXY = "proxy";
private static final String PORT = "port";
private static final String MMSC = "mmsc";
private static final String MMSPROXY = "mmsproxy";
private static final String MMSPORT = "mmsport";
private static final String USER = "user";
private static final String PASSWORD = "password";
private static final String AUTH_TYPE = "auth_type";
private static final String TYPE = "type";
private static final String PROTOCOL = "protocol";
private static final String ROAMING_PROTOCOL = "roaming_protocol";
private static final String CARRIER_ENABLED = "true";
private static final String NETWORK_TYPE_BITMASK = "0";
private static final String BEARER = "0";
private static final String CARRIER_ID = String.valueOf(TelephonyManager.UNKNOWN_CARRIER_ID);
private static final Map<String, String> APN_MAP =
Map.ofEntries(
entry(Carriers.NAME, NAME),
entry(Carriers.APN, APN),
entry(Carriers.PROXY, PROXY),
entry(Carriers.PORT, PORT),
entry(Carriers.MMSC, MMSC),
entry(Carriers.MMSPROXY, MMSPROXY),
entry(Carriers.MMSPORT, MMSPORT),
entry(Carriers.USER, USER),
entry(Carriers.PASSWORD, PASSWORD),
entry(Carriers.AUTH_TYPE, AUTH_TYPE),
entry(Carriers.TYPE, TYPE),
entry(Carriers.PROTOCOL, PROTOCOL),
entry(Carriers.ROAMING_PROTOCOL, ROAMING_PROTOCOL),
entry(Carriers.CARRIER_ENABLED, CARRIER_ENABLED),
entry(Carriers.NETWORK_TYPE_BITMASK, NETWORK_TYPE_BITMASK),
entry(Carriers.BEARER, BEARER),
entry(Carriers.CARRIER_ID, CARRIER_ID));
// make sure the numeric is empty aka default value
private static final String TEST_APN_SELECTION = Carriers.NUMERIC + "= '' AND "
+ Carriers.CARRIER_ID + "=?";
private static final String[] TEST_APN_SELECTION_ARGS = {CARRIER_ID};
private static final String[] TEST_APN_PROJECTION =
APN_MAP.keySet().toArray(new String[APN_MAP.size()]);
private Context mContext;
private ContentResolver mContentResolver;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getInstrumentation().getContext();
assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY));
mContentResolver = mContext.getContentResolver();
}
@After
public void tearDown() throws Exception {
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
return;
}
// Ensures APNs are deleted in case a test threw.
InstrumentationRegistry.getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity();
try {
// Delete the test APN (potentially redundant for deletion test).
mContentResolver.delete(CARRIER_TABLE_URI, TEST_APN_SELECTION, TEST_APN_SELECTION_ARGS);
} finally {
InstrumentationRegistry.getInstrumentation()
.getUiAutomation()
.dropShellPermissionIdentity();
}
}
/* The CTS tests are intended to test APN insertion with {@link
* android.Manifest.permission.READ_PHONE_STATE} and makes use of {@link
* android.Manifest.permission.WRITE_APN_SETTINGS} instead of carrier privileges. This covers
* a class of errors where APIs that require READ_PHONE_STATE are called during APN updates.
* In these cases, if a caller has READ_PHONE_STATE, it is important to ensure that the binder
* identity is appropriately cleared or handled. Otherwise, the calling UID may not match
* the calling package during permission checks.
*/
@Test
public void testPermissionCheckForApnInsertion_success() {
try {
// Insert the test APN.
insertTestApnAndValidateInsertion(/* grantPermission= */ true);
} catch (SecurityException e) {
fail(
"Test failed due to security exception. Permissions may be incorrectly checked"
+ " or managed. "
+ Log.getStackTraceString(e));
} catch (Exception e) {
fail("Test failed due to exception. " + Log.getStackTraceString(e));
}
}
@Test
public void testPermissionCheckForApnInsertion_noWriteApnSettings() {
assertThrows(
SecurityException.class,
() -> insertTestApnAndValidateInsertion(/* grantPermission= */ false));
}
@Test
public void testPermissionCheckForApnUpdate_success() {
try {
// Insert the test APN.
insertTestApnAndValidateInsertion(/* grantPermission= */ true);
grantWriteApnSettings();
// Create an APN entry to update.
ContentValues contentValues = makeDefaultContentValues();
// Update the APN.
final String newApn = "newapn";
contentValues.put(Carriers.APN, newApn);
final int updateCount =
mContentResolver.update(
CARRIER_TABLE_URI,
contentValues,
TEST_APN_SELECTION,
TEST_APN_SELECTION_ARGS);
assertWithMessage("Unexpected number of rows updated").that(updateCount).isEqualTo(1);
// Verify the updated value.
Cursor cursor = queryTestApn();
assertWithMessage("Failed to query the table").that(cursor).isNotNull();
assertWithMessage("Unexpected number of APNs returned by cursor")
.that(cursor.getCount())
.isEqualTo(1);
cursor.moveToFirst();
assertWithMessage("Unexpected value returned by cursor")
.that(cursor.getString(cursor.getColumnIndex(Carriers.APN)))
.isEqualTo(newApn);
} catch (SecurityException e) {
fail(
"Test failed due to security exception. Permissions may be incorrectly checked"
+ " or managed. "
+ Log.getStackTraceString(e));
} catch (Exception e) {
fail("Test failed due to exception. " + Log.getStackTraceString(e));
}
}
@Test
public void testPermissionCheckForApnUpdate_noWriteApnSettings() {
try {
// Insert the test APN.
insertTestApnAndValidateInsertion(/* grantPermission= */ true);
// Create an APN entry to update.
ContentValues contentValues = makeDefaultContentValues();
// Attempt to update the APN without WRITE_APN_SETTINGS.
contentValues.put(Carriers.APN, "newapn");
assertThrows(
SecurityException.class,
() ->
mContentResolver.update(
CARRIER_TABLE_URI,
contentValues,
TEST_APN_SELECTION,
TEST_APN_SELECTION_ARGS));
} catch (SecurityException e) {
fail(
"Test failed due to security exception. Permissions may be incorrectly checked"
+ " or managed. "
+ Log.getStackTraceString(e));
} catch (Exception e) {
fail("Test failed due to exception. " + Log.getStackTraceString(e));
}
}
@Test
public void testPermissionCheckForApnDeletion_success() {
try {
insertTestApnAndValidateInsertion(/* grantPermission= */ true);
grantWriteApnSettings();
// Delete the APN.
int numberOfRowsDeleted =
mContentResolver.delete(
CARRIER_TABLE_URI, TEST_APN_SELECTION, TEST_APN_SELECTION_ARGS);
assertWithMessage("Unexpected number of rows deleted")
.that(numberOfRowsDeleted)
.isEqualTo(1);
// Verify that deleted values are gone.
Cursor cursor = queryTestApn();
assertWithMessage("Unexpected number of rows deleted")
.that(cursor.getCount())
.isEqualTo(0);
} catch (SecurityException e) {
fail(
"Test failed due to security exception. Permissions may be incorrectly checked"
+ " or managed. "
+ Log.getStackTraceString(e));
} catch (Exception e) {
fail("Test failed due to exception. " + Log.getStackTraceString(e));
}
}
@Test
public void testPermissionCheckForApnDeletion_noWriteApnSettings() {
try {
insertTestApnAndValidateInsertion(/* grantPermission= */ true);
// Attempt to delete the APN without WRITE_APN_SETTINGS.
assertThrows(
SecurityException.class,
() ->
mContentResolver.delete(
CARRIER_TABLE_URI,
TEST_APN_SELECTION,
TEST_APN_SELECTION_ARGS));
} catch (SecurityException e) {
fail(
"Test failed due to security exception. Permissions may be incorrectly checked"
+ " or managed. "
+ Log.getStackTraceString(e));
} catch (Exception e) {
fail("Test failed due to exception. " + Log.getStackTraceString(e));
}
}
/**
* Verify that the test APN setting can be inserted, queried, and validated
* by {@link Carriers#CARRIER_ID} without {@link Carriers#NUMERIC} when
* the permission is granted
*
* @param grantPermission whether to grant WRITE_APN_SETTINGS prior to insertion
*/
private void insertTestApnAndValidateInsertion(boolean grantPermission) throws Exception {
if (grantPermission) {
grantWriteApnSettings();
}
// Create a set of column_name/value pairs to add to the database.
ContentValues contentValues = makeDefaultContentValues();
// Insert the value into database. Without permissions, this is expected to throw.
Uri newUri = mContentResolver.insert(CARRIER_TABLE_URI, contentValues);
assertWithMessage("Failed to insert to table").that(newUri).isNotNull();
Cursor cursor = queryTestApn();
// Verify that the inserted value match the results of the query.
assertWithMessage("Failed to query the table").that(cursor).isNotNull();
assertWithMessage("Unexpected number of APNs returned by cursor")
.that(cursor.getCount())
.isEqualTo(1);
cursor.moveToFirst();
for (Map.Entry<String, String> entry : APN_MAP.entrySet()) {
assertWithMessage("Unexpected value returned by cursor")
.that(cursor.getString(cursor.getColumnIndex(entry.getKey())))
.isEqualTo(entry.getValue());
}
if (grantPermission) {
InstrumentationRegistry.getInstrumentation()
.getUiAutomation()
.dropShellPermissionIdentity();
}
}
/** Grants WRITE_APN_SETTINGS through shell identity. */
private void grantWriteApnSettings() {
InstrumentationRegistry.getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity("android.permission.WRITE_APN_SETTINGS");
}
/** Returns a cursor corresponding to the test APN value. */
private Cursor queryTestApn() {
return mContentResolver.query(
CARRIER_TABLE_URI,
TEST_APN_PROJECTION,
TEST_APN_SELECTION,
TEST_APN_SELECTION_ARGS,
/* sortOrder= */ null);
}
private ContentValues makeDefaultContentValues() {
ContentValues contentValues = new ContentValues();
APN_MAP.entrySet().stream()
.forEach(entry -> contentValues.put(entry.getKey(), entry.getValue()));
return contentValues;
}
}