Hook CarrierConfig 5G Capability to setN1ModeEnabled HAL
For devices that support the setN1Mode HAL API, hook it up
to the KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY to allow
carriers to specify whether SA is allowed.
Bug: 302033535
Test: telephony sanity
Test: manually verified
Test: atest GsmCdmaPhoneTest#testNrCapabilityChanged
Change-Id: Ibff0796c3fa3690355677dbd809062cd71db6534
diff --git a/src/java/com/android/internal/telephony/GsmCdmaPhone.java b/src/java/com/android/internal/telephony/GsmCdmaPhone.java
index 5bcfb64..0cba06b 100644
--- a/src/java/com/android/internal/telephony/GsmCdmaPhone.java
+++ b/src/java/com/android/internal/telephony/GsmCdmaPhone.java
@@ -297,6 +297,16 @@
private final SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionsChangedListener;
private final CallWaitingController mCallWaitingController;
+ // Set via Carrier Config
+ private boolean mIsN1ModeAllowedByCarrier = true;
+ // Set via a call to the method on Phone; the only caller is IMS, and all of this code will
+ // need to be updated to a voting mechanism (...enabled for reason...) if additional callers
+ // are desired.
+ private boolean mIsN1ModeAllowedByIms = true;
+ // If this value is null, then the modem value is unknown. If a caller explicitly sets the
+ // N1 mode, this value will be initialized before any attempt to set the value in the modem.
+ private Boolean mModemN1Mode = null;
+
// Constructors
public GsmCdmaPhone(Context context, CommandsInterface ci, PhoneNotifier notifier, int phoneId,
@@ -2356,6 +2366,75 @@
mSsOverCdmaSupported = b.getBoolean(CarrierConfigManager.KEY_SUPPORT_SS_OVER_CDMA_BOOL);
}
+ /**
+ * Enables or disables N1 mode (access to 5G core network) in accordance with
+ * 3GPP TS 24.501 4.9.
+ *
+ * <p> To prevent redundant calls down to the modem and to support a mechanism whereby
+ * N1 mode is only on if both IMS and carrier config believe that it should be on, this
+ * method will first sync the value from the modem prior to possibly setting it. In addition
+ * N1 mode will not be set to enabled unless both IMS and Carrier want it, since the use
+ * cases require all entities to agree lest it default to disabled.
+ *
+ * @param enable {@code true} to enable N1 mode, {@code false} to disable N1 mode.
+ * @param result Callback message to receive the result or null.
+ */
+ @Override
+ public void setN1ModeEnabled(boolean enable, @Nullable Message result) {
+ if (mFeatureFlags.enableCarrierConfigN1Control()) {
+ // This might be called by IMS on another thread, so to avoid the requirement to
+ // lock, post it through the handler.
+ post(() -> {
+ mIsN1ModeAllowedByIms = enable;
+ if (mModemN1Mode == null) {
+ mCi.isN1ModeEnabled(obtainMessage(EVENT_GET_N1_MODE_ENABLED_DONE, result));
+ } else {
+ maybeUpdateModemN1Mode(result);
+ }
+ });
+ } else {
+ super.setN1ModeEnabled(enable, result);
+ }
+ }
+
+ /** Only called on the handler thread. */
+ private void maybeUpdateModemN1Mode(@Nullable Message result) {
+ final boolean wantN1Enabled = mIsN1ModeAllowedByCarrier && mIsN1ModeAllowedByIms;
+
+ logd("N1 Mode: isModemN1Enabled=" + mModemN1Mode + ", wantN1Enabled=" + wantN1Enabled);
+
+ // mModemN1Mode is never null here
+ if (mModemN1Mode != wantN1Enabled) {
+ // Assume success pending a response, which avoids multiple concurrent requests
+ // going down to the modem. If it fails, that is addressed in the response.
+ mModemN1Mode = wantN1Enabled;
+ super.setN1ModeEnabled(
+ wantN1Enabled, obtainMessage(EVENT_SET_N1_MODE_ENABLED_DONE, result));
+ } else if (result != null) {
+ AsyncResult.forMessage(result);
+ result.sendToTarget();
+ }
+ }
+
+ /** Only called on the handler thread. */
+ private void updateCarrierN1ModeSupported(@Nullable PersistableBundle b) {
+ if (!mFeatureFlags.enableCarrierConfigN1Control()) return;
+
+ if (b == null) return;
+
+
+ final int[] supportedNrModes = b.getIntArray(
+ CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY);
+
+ mIsN1ModeAllowedByCarrier = ArrayUtils.contains(
+ supportedNrModes, CarrierConfigManager.CARRIER_NR_AVAILABILITY_SA);
+ if (mModemN1Mode == null) {
+ mCi.isN1ModeEnabled(obtainMessage(EVENT_GET_N1_MODE_ENABLED_DONE));
+ } else {
+ maybeUpdateModemN1Mode(null);
+ }
+ }
+
@Override
public boolean useSsOverIms(Message onComplete) {
boolean isUtEnabled = isUtEnabled();
@@ -3228,6 +3307,7 @@
updateNrSettingsAfterCarrierConfigChanged(b);
updateVoNrSettings(b);
updateSsOverCdmaSupported(b);
+ updateCarrierN1ModeSupported(b);
loadAllowedNetworksFromSubscriptionDatabase();
// Obtain new radio capabilities from the modem, since some are SIM-dependent
mCi.getRadioCapability(obtainMessage(EVENT_GET_RADIO_CAPABILITY));
@@ -3554,6 +3634,41 @@
}
}
break;
+
+ case EVENT_GET_N1_MODE_ENABLED_DONE:
+ logd("EVENT_GET_N1_MODE_ENABLED_DONE");
+ ar = (AsyncResult) msg.obj;
+ if (ar == null || ar.exception != null
+ || ar.result == null || !(ar.result instanceof Boolean)) {
+ Rlog.e(LOG_TAG, "Failed to Retrieve N1 Mode", ar.exception);
+ if (ar != null && ar.userObj instanceof Message) {
+ // original requester's message is stashed in the userObj
+ final Message rsp = (Message) ar.userObj;
+ AsyncResult.forMessage(rsp, null, ar.exception);
+ rsp.sendToTarget();
+ }
+ break;
+ }
+
+ mModemN1Mode = (Boolean) ar.result;
+ maybeUpdateModemN1Mode((Message) ar.userObj);
+ break;
+
+ case EVENT_SET_N1_MODE_ENABLED_DONE:
+ logd("EVENT_SET_N1_MODE_ENABLED_DONE");
+ ar = (AsyncResult) msg.obj;
+ if (ar == null || ar.exception != null) {
+ Rlog.e(LOG_TAG, "Failed to Set N1 Mode", ar.exception);
+ // Set failed, so we have no idea at this point.
+ mModemN1Mode = null;
+ }
+ if (ar != null && ar.userObj instanceof Message) {
+ // original requester's message is stashed in the userObj
+ final Message rsp = (Message) ar.userObj;
+ AsyncResult.forMessage(rsp, null, ar.exception);
+ rsp.sendToTarget();
+ }
+ break;
default:
super.handleMessage(msg);
}
diff --git a/src/java/com/android/internal/telephony/Phone.java b/src/java/com/android/internal/telephony/Phone.java
index ea15af6..f4f756f 100644
--- a/src/java/com/android/internal/telephony/Phone.java
+++ b/src/java/com/android/internal/telephony/Phone.java
@@ -252,8 +252,10 @@
protected static final int EVENT_SET_NULL_CIPHER_AND_INTEGRITY_DONE = 66;
protected static final int EVENT_GET_DEVICE_IMEI_DONE = 67;
protected static final int EVENT_TRIGGER_NOTIFY_ANBR = 68;
+ protected static final int EVENT_GET_N1_MODE_ENABLED_DONE = 69;
+ protected static final int EVENT_SET_N1_MODE_ENABLED_DONE = 70;
- protected static final int EVENT_LAST = EVENT_TRIGGER_NOTIFY_ANBR;
+ protected static final int EVENT_LAST = EVENT_SET_N1_MODE_ENABLED_DONE;
// For shared prefs.
private static final String GSM_ROAMING_LIST_OVERRIDE_PREFIX = "gsm_roaming_list_";
@@ -868,7 +870,7 @@
}
break;
default:
- throw new RuntimeException("unexpected event not handled");
+ throw new RuntimeException("unexpected event not handled, msgId=" + msg.what);
}
}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/GsmCdmaPhoneTest.java b/tests/telephonytests/src/com/android/internal/telephony/GsmCdmaPhoneTest.java
index 1143190..5aa474e 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/GsmCdmaPhoneTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/GsmCdmaPhoneTest.java
@@ -173,6 +173,8 @@
mMockCi = Mockito.mock(CommandsInterface.class);
adnRecordCache = Mockito.mock(AdnRecordCache.class);
mDomainSelectionResolver = Mockito.mock(DomainSelectionResolver.class);
+ mFeatureFlags = Mockito.mock(FeatureFlags.class);
+
doReturn(false).when(mSST).isDeviceShuttingDown();
doReturn(true).when(mImsManager).isVolteEnabledByPlatform();
doReturn(false).when(mDomainSelectionResolver).isDomainSelectionSupported();
@@ -1482,6 +1484,143 @@
assertEquals(captor.getValue().what, Phone.EVENT_GET_RADIO_CAPABILITY);
}
+ @Test
+ public void testNrCapabilityChanged_firstRequest_noChangeNeeded() {
+ when(mFeatureFlags.enableCarrierConfigN1Control()).thenReturn(true);
+
+ mPhoneUT.mCi = mMockCi;
+ PersistableBundle bundle = mContextFixture.getCarrierConfigBundle();
+ bundle.putIntArray(CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY,
+ new int[]{
+ CarrierConfigManager.CARRIER_NR_AVAILABILITY_NSA,
+ CarrierConfigManager.CARRIER_NR_AVAILABILITY_SA});
+
+ mPhoneUT.sendMessage(mPhoneUT.obtainMessage(Phone.EVENT_CARRIER_CONFIG_CHANGED));
+ processAllMessages();
+
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockCi, times(1)).isN1ModeEnabled(messageCaptor.capture());
+ AsyncResult.forMessage(messageCaptor.getValue(), Boolean.TRUE, null);
+ messageCaptor.getValue().sendToTarget();
+ processAllMessages();
+
+ verify(mMockCi, never()).setN1ModeEnabled(anyBoolean(), any());
+ }
+
+ @Test
+ public void testNrCapabilityChanged_firstRequest_needsChange() {
+ when(mFeatureFlags.enableCarrierConfigN1Control()).thenReturn(true);
+
+ mPhoneUT.mCi = mMockCi;
+ PersistableBundle bundle = mContextFixture.getCarrierConfigBundle();
+ bundle.putIntArray(CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY,
+ new int[]{
+ CarrierConfigManager.CARRIER_NR_AVAILABILITY_NSA,
+ CarrierConfigManager.CARRIER_NR_AVAILABILITY_SA});
+
+ mPhoneUT.sendMessage(mPhoneUT.obtainMessage(Phone.EVENT_CARRIER_CONFIG_CHANGED));
+ processAllMessages();
+
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockCi, times(1)).isN1ModeEnabled(messageCaptor.capture());
+ AsyncResult.forMessage(messageCaptor.getValue(), Boolean.FALSE, null);
+ messageCaptor.getValue().sendToTarget();
+ processAllMessages();
+
+ verify(mMockCi, times(1)).setN1ModeEnabled(eq(true), messageCaptor.capture());
+ AsyncResult.forMessage(messageCaptor.getValue(), Boolean.TRUE, null);
+ messageCaptor.getValue().sendToTarget();
+ processAllMessages();
+ }
+
+ @Test
+ public void testNrCapabilityChanged_CarrierConfigChanges() {
+ when(mFeatureFlags.enableCarrierConfigN1Control()).thenReturn(true);
+
+ // Initialize the inner cache and set the modem to N1 mode = enabled/true
+ testNrCapabilityChanged_firstRequest_needsChange();
+
+ PersistableBundle bundle = mContextFixture.getCarrierConfigBundle();
+ // Remove SA support and send an additional carrier config change
+ bundle.putIntArray(
+ CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY,
+ new int[]{CarrierConfigManager.CARRIER_NR_AVAILABILITY_NSA});
+
+ mPhoneUT.sendMessage(mPhoneUT.obtainMessage(Phone.EVENT_CARRIER_CONFIG_CHANGED));
+ processAllMessages();
+
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockCi, times(1)).isN1ModeEnabled(any()); // not called again
+ verify(mMockCi, times(1)).setN1ModeEnabled(eq(false), messageCaptor.capture());
+ AsyncResult.forMessage(messageCaptor.getValue(), null, null);
+ messageCaptor.getValue().sendToTarget();
+ processAllMessages();
+ }
+
+ @Test
+ public void testNrCapabilityChanged_CarrierConfigChanges_ErrorResponse() {
+ when(mFeatureFlags.enableCarrierConfigN1Control()).thenReturn(true);
+
+ mPhoneUT.mCi = mMockCi;
+ for (int i = 0; i < 2; i++) {
+ PersistableBundle bundle = mContextFixture.getCarrierConfigBundle();
+ bundle.putIntArray(CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY,
+ new int[]{
+ CarrierConfigManager.CARRIER_NR_AVAILABILITY_NSA,
+ CarrierConfigManager.CARRIER_NR_AVAILABILITY_SA});
+
+ mPhoneUT.sendMessage(mPhoneUT.obtainMessage(Phone.EVENT_CARRIER_CONFIG_CHANGED));
+ processAllMessages();
+
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockCi, times(i + 1)).isN1ModeEnabled(messageCaptor.capture());
+ AsyncResult.forMessage(messageCaptor.getValue(), null, new RuntimeException());
+ messageCaptor.getValue().sendToTarget();
+ processAllMessages();
+
+ verify(mMockCi, never()).setN1ModeEnabled(anyBoolean(), any());
+ }
+ }
+
+ @Test
+ public void testNrCapabilityChanged_firstRequest_ImsChanges() {
+ when(mFeatureFlags.enableCarrierConfigN1Control()).thenReturn(true);
+
+ mPhoneUT.mCi = mMockCi;
+ Message passthroughMessage = mTestHandler.obtainMessage(0xC0FFEE);
+
+ mPhoneUT.setN1ModeEnabled(false, passthroughMessage);
+ processAllMessages();
+
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockCi, times(1)).isN1ModeEnabled(messageCaptor.capture());
+ assertEquals(messageCaptor.getValue().obj, passthroughMessage);
+ AsyncResult.forMessage(messageCaptor.getValue(), Boolean.TRUE, null);
+ messageCaptor.getValue().sendToTarget();
+ processAllMessages();
+
+ verify(mMockCi, times(1)).setN1ModeEnabled(eq(false), messageCaptor.capture());
+ assertEquals(messageCaptor.getValue().obj, passthroughMessage);
+ AsyncResult.forMessage(messageCaptor.getValue(), null, null);
+ messageCaptor.getValue().sendToTarget();
+ processAllMessages();
+
+ // Verify the return message was received
+ ArgumentCaptor<Message> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mTestHandler, times(1)).sendMessageAtTime(messageArgumentCaptor.capture(),
+ anyLong());
+ assertEquals(messageArgumentCaptor.getValue(), passthroughMessage);
+
+ mPhoneUT.setN1ModeEnabled(true, null);
+ processAllMessages();
+
+ verify(mMockCi, times(1)).isN1ModeEnabled(any()); // not called again
+ verify(mMockCi, times(1)).setN1ModeEnabled(eq(true), messageCaptor.capture());
+ AsyncResult.forMessage(messageCaptor.getValue(), null, null);
+ messageCaptor.getValue().sendToTarget();
+ processAllMessages();
+ }
+
private void setupForWpsCallTest() throws Exception {
mSST.mSS = mServiceState;
doReturn(ServiceState.STATE_IN_SERVICE).when(mServiceState).getState();