Add CTS to validate data connectivity with carrier ID APNs
- Adds a CTS test that acts as an end to end test that validates that
APNs with carrier ID can establish a data connection when other
identifying columns are absent (MCCMNC, MVNO data/type, numeric).
- This is dependent on changes from ag/22751934
- PARIS needs to be disabled: otherwise, it will insert APNs midtest and
cause it to fail.
Test: atest ApnCarrierIdTest.java
Test: Passed 100x on Cuttlefish: https://android-build.corp.google.com/test_investigate/?invocationId=I71000010157091802&testResultId=TR95528639522097064
Bug: 281705334
Change-Id: I7d436b35d8d4998d8cdf04149b30ee63e2f9b58f
diff --git a/tests/tests/telephony/current/AndroidTest.xml b/tests/tests/telephony/current/AndroidTest.xml
index 8fcbc60..1123345 100644
--- a/tests/tests/telephony/current/AndroidTest.xml
+++ b/tests/tests/telephony/current/AndroidTest.xml
@@ -62,6 +62,9 @@
<option name="run-command" value="pm disable com.android.dialer/com.android.voicemail.impl.OmtpService" />
<option name="teardown-command" value="pm enable com.android.dialer/com.android.voicemail.impl.StatusCheckJobService" />
<option name="teardown-command" value="pm enable com.android.dialer/com.android.voicemail.impl.OmtpService" />
+ <!-- Disable PARIS to prevent unexpected APN insertions -->
+ <option name="run-command" value="pm disable com.google.android.carrier" />
+ <option name="teardown-command" value="pm enable com.google.android.carrier" />
<option name="run-command" value="setprop persist.radio.allow_mock_modem true" />
<option name="teardown-command" value="setprop persist.radio.allow_mock_modem false" />
</target_preparer>
diff --git a/tests/tests/telephony/current/src/android/telephony/cts/ApnCarrierIdTest.java b/tests/tests/telephony/current/src/android/telephony/cts/ApnCarrierIdTest.java
new file mode 100644
index 0000000..6c5623d
--- /dev/null
+++ b/tests/tests/telephony/current/src/android/telephony/cts/ApnCarrierIdTest.java
@@ -0,0 +1,342 @@
+/*
+ * 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();
+ }
+ }
+ }
+ }
+}