blob: 6c5623d2c1393df300f2e15babcda7f59064fd26 [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.telephony.cts;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.provider.Telephony.Carriers;
import android.telephony.AccessNetworkConstants;
import android.telephony.PreciseDataConnectionState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.telephony.data.ApnSetting;
import android.text.TextUtils;
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.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
/**
* Ensures that APNs that use carrier ID instead of legacy identifiers such as MCCMNC, MVNO type and
* match data are able to establish a data connection.
*/
@ApiTest(
apis = {
"android.provider.Telephony.Carriers#CONTENT_URI",
"android.provider.Telephony.Carriers#CARRIER_ID"
})
@RunWith(AndroidJUnit4.class)
public class ApnCarrierIdTest {
private static final Uri CARRIER_TABLE_URI = Carriers.CONTENT_URI;
/**
* A base selection string of columns we use to query an APN in the APN database. This excludes
* the numeric/carrier ID.
*
* <p>While it would be ideal to include the Carrier.TYPE here, the ordering of APN types
* generated from ApnSetting may not match the ordering when we query the type from the APN
* database, which makes it more non-trivial to query using type.
*/
private static final String BASE_APN_SELECTION_COLUMNS =
generateSelectionString(
List.of(
Carriers.NAME,
Carriers.APN,
Carriers.PROTOCOL,
Carriers.ROAMING_PROTOCOL,
Carriers.NETWORK_TYPE_BITMASK));
private static final String APN_SELECTION_STRING_WITH_NUMERIC =
BASE_APN_SELECTION_COLUMNS + "AND " + Carriers.NUMERIC + "=?";
private static final String APN_SELECTION_STRING_WITH_CARRIER_ID =
BASE_APN_SELECTION_COLUMNS + "AND " + Carriers.CARRIER_ID + "=?";
// The wait time is padded to account for varying modem performance. Note that this is a
// timeout, not an enforced wait time, so in most cases, a callback will be received prior to
// the wait time elapsing.
private static final long WAIT_TIME_MILLIS = 10000L;
private Context mContext;
private ContentResolver mContentResolver;
private final Executor mSimpleExecutor = Runnable::run;
private TelephonyManager mTelephonyManager;
private PreciseDataConnectionState mPreciseDataConnectionState;
/**
* The original APN that belongs to the existing data connection. Required to re-insert it
* during teardown.
*/
private ContentValues mExistingApn;
/** Selection args for the carrier ID APN. Required to delete the test APN during teardown. */
private String[] mInsertedApnSelectionArgs;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getInstrumentation().getContext();
assumeTrue(mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY));
mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
if (mTelephonyManager.getSimState() != TelephonyManager.SIM_STATE_READY
|| mTelephonyManager.getSubscriptionId()
== SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
fail("This test requires a SIM card with an active subscription/data connection.");
}
InstrumentationRegistry.getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity();
PreciseDataConnectionStateListener preciseDataConnectionStateCallback =
new PreciseDataConnectionStateListener(
mTelephonyManager, /* desiredDataState= */ TelephonyManager.DATA_CONNECTED);
preciseDataConnectionStateCallback.awaitDataStateChanged(WAIT_TIME_MILLIS);
// The initial data state should be DATA_CONNECTED.
if (mPreciseDataConnectionState == null
|| mPreciseDataConnectionState.getState() != TelephonyManager.DATA_CONNECTED) {
fail("This test requires an active data connection.");
}
mContentResolver = mContext.getContentResolver();
}
@After
public void tearDown() {
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
return;
}
if (mInsertedApnSelectionArgs != null) {
int deleted =
mContentResolver.delete(
CARRIER_TABLE_URI,
APN_SELECTION_STRING_WITH_CARRIER_ID,
mInsertedApnSelectionArgs);
}
if (mExistingApn != null) {
PreciseDataConnectionStateListener pdcsCallback =
new PreciseDataConnectionStateListener(
mTelephonyManager,
/* desiredDataState= */ TelephonyManager.DATA_CONNECTED);
mContentResolver.insert(CARRIER_TABLE_URI, mExistingApn);
try {
pdcsCallback.awaitDataStateChanged(WAIT_TIME_MILLIS);
} catch (InterruptedException e) {
// do nothing - we just want to ensure the teardown is complete.
}
}
InstrumentationRegistry.getInstrumentation()
.getUiAutomation()
.dropShellPermissionIdentity();
}
/**
* Ensures that APNs that consist of a carrier ID column and no other identifying columns such
* as MCCMNC/numeric can establish a data connection.
*/
@Test
public void validateDataConnectionWithCarrierIdApn() throws Exception {
ApnSetting currentApn = mPreciseDataConnectionState.getApnSetting();
validateAndSetupInitialState(currentApn);
int carrierId = mTelephonyManager.getSimSpecificCarrierId();
ContentValues apnWithCarrierId = getApnWithCarrierId(currentApn, carrierId);
// Insert the carrier ID APN.
mPreciseDataConnectionState = null;
PreciseDataConnectionStateListener pdcsCallback =
new PreciseDataConnectionStateListener(
mTelephonyManager, /* desiredDataState= */ TelephonyManager.DATA_CONNECTED);
int rowsInserted =
mContentResolver.bulkInsert(
CARRIER_TABLE_URI, new ContentValues[] {apnWithCarrierId});
assertThat(rowsInserted).isEqualTo(1);
pdcsCallback.awaitDataStateChanged(WAIT_TIME_MILLIS);
// Generate selection arguments for the APN and store it so we can delete it in cleanup.
mInsertedApnSelectionArgs = generateSelectionArgs(currentApn, String.valueOf(carrierId));
// Ensure our APN value wasn't somehow overridden (such as in the event a carrier app
// exists).
assertThat(mPreciseDataConnectionState.getApnSetting().getCarrierId()).isEqualTo(carrierId);
assertThat(mPreciseDataConnectionState.getState())
.isEqualTo(TelephonyManager.DATA_CONNECTED);
}
/**
* Performs initial setup and validation for the test.
*
* <p>This skips the test if the existing APN already uses carrier ID. Otherwise, it deletes the
* existing APN and ensures data is disconnected.
*/
private void validateAndSetupInitialState(ApnSetting currentApn) throws Exception {
// Skip the test if the APN already uses carrier ID and data is connected.
assumeFalse(
"Skipping the test as the APN on the current SIM already uses carrier ID and has a"
+ " data connection.",
apnAlreadyUsesCarrierId(currentApn));
mPreciseDataConnectionState = null;
PreciseDataConnectionStateListener pdcsCallback =
new PreciseDataConnectionStateListener(
mTelephonyManager,
/* desiredDataState= */ TelephonyManager.DATA_DISCONNECTED);
int deletedRowCount =
mContentResolver.delete(
CARRIER_TABLE_URI,
APN_SELECTION_STRING_WITH_NUMERIC,
generateSelectionArgs(currentApn, currentApn.getOperatorNumeric()));
assertThat(deletedRowCount).isEqualTo(1);
// Store the APN so we can re-insert it once the test is complete.
mExistingApn = currentApn.toContentValues();
pdcsCallback.awaitDataStateChanged(WAIT_TIME_MILLIS);
// Data should disconnect without any identifying fields in the default APN.
assertThat(mPreciseDataConnectionState.getState())
.isEqualTo(TelephonyManager.DATA_DISCONNECTED);
}
private boolean apnAlreadyUsesCarrierId(ApnSetting apnSetting) {
return apnSetting.getCarrierId() != TelephonyManager.UNKNOWN_CARRIER_ID
&& TextUtils.isEmpty(apnSetting.getOperatorNumeric());
}
/**
* Replaces the existing APNs identifying fields with carrier ID and returns it as a
* ContentValues object.
*/
private ContentValues getApnWithCarrierId(ApnSetting apnSetting, int carrierId) {
ContentValues apnWithCarrierId = ApnSetting.makeApnSetting(apnSetting).toContentValues();
// Remove non carrier ID identifying fields and insert the carrier ID.
List<String> identifyingColumnsToDelete =
List.of(
Carriers.NUMERIC,
Carriers.MCC,
Carriers.MNC,
Carriers.MVNO_TYPE,
Carriers.MVNO_MATCH_DATA);
for (String identifyingColumn : identifyingColumnsToDelete) {
apnWithCarrierId.remove(identifyingColumn);
}
apnWithCarrierId.put(Carriers.CARRIER_ID, carrierId);
return apnWithCarrierId;
}
/** Generates a selection string for matching the given coluns in a database. */
private static String generateSelectionString(List<String> columns) {
return String.join("=? AND ", columns) + "=?";
}
/**
* Generates selection arguments for an APN.
*
* <p>The selection arguments are based on {@link #BASE_APN_SELECTION_COLUMNS} with the final
* argument being either the carrier ID or the numeric to match {@link
* #APN_SELECTION_STRING_WITH_NUMERIC} or {@link #APN_SELECTION_STRING_WITH_CARRIER_ID}.
*/
private String[] generateSelectionArgs(ApnSetting baseApn, String numericOrCarrierId) {
return new String[] {
baseApn.getEntryName(),
baseApn.getApnName(),
ApnSetting.getProtocolStringFromInt(baseApn.getProtocol()),
ApnSetting.getProtocolStringFromInt(baseApn.getRoamingProtocol()),
Integer.toString(baseApn.getNetworkTypeBitmask()),
numericOrCarrierId,
};
}
/**
* A oneshot PreciseDataConnectionState listener that listens for a desired data state change on
* a cellular network.
*
* <p>The listener will register itself once instantiated and will unregister itself after
* calling {@link PreciseDataConnectionStateListener#awaitDataStateChanged}
*/
private class PreciseDataConnectionStateListener extends TelephonyCallback
implements TelephonyCallback.PreciseDataConnectionStateListener {
private final int mDesiredDataState;
private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
private final Object mLock = new Object();
/**
* Instantiates and registers a PreciseDataConnectionStateListener instance.
*
* @param telephonyManager the TelephonyManager instance to register the callback on
* @param desiredDataState the data state that is expected after performing an action. A
* callback will only be fired for this state. See {@link
* #onPreciseDataConnectionStateChanged(PreciseDataConnectionState)} for additional
* information.
*/
PreciseDataConnectionStateListener(
TelephonyManager telephonyManager, int desiredDataState) {
mDesiredDataState = desiredDataState;
mPreciseDataConnectionState = null;
telephonyManager.registerTelephonyCallback(mSimpleExecutor, this);
}
void awaitDataStateChanged(long timeoutMillis) throws InterruptedException {
try {
mCountDownLatch.await(timeoutMillis, MILLISECONDS);
} finally {
mTelephonyManager.unregisterTelephonyCallback(this);
}
}
@Override
public void onPreciseDataConnectionStateChanged(PreciseDataConnectionState state) {
synchronized (mLock) {
ApnSetting apnSetting = state.getApnSetting();
int dataState = state.getState();
// We should only notify if the following conditions are satisfied:
// 1. The PDCS belongs to a cellular network.
// 2. The APN attached to the PDCS is an internet APN.
// 3. The state is the desired data state.
boolean isInternetApn =
(state.getApnSetting().getApnTypeBitmask() & ApnSetting.TYPE_DEFAULT)
== ApnSetting.TYPE_DEFAULT;
if (isInternetApn
&& state.getTransportType() == AccessNetworkConstants.TRANSPORT_TYPE_WWAN
&& dataState == mDesiredDataState) {
mPreciseDataConnectionState = state;
mCountDownLatch.countDown();
}
}
}
}
}