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();