Merge "Telephony plumbing for SIM call manager voice status"
diff --git a/src/java/com/android/internal/telephony/GsmCdmaPhone.java b/src/java/com/android/internal/telephony/GsmCdmaPhone.java
index ca6b37a..a247ff1 100644
--- a/src/java/com/android/internal/telephony/GsmCdmaPhone.java
+++ b/src/java/com/android/internal/telephony/GsmCdmaPhone.java
@@ -264,6 +264,9 @@
 
     private int mRilVersion;
     private boolean mBroadcastEmergencyCallStateChanges = false;
+    private @ServiceState.RegState int mTelecomVoiceServiceStateOverride =
+            ServiceState.STATE_OUT_OF_SERVICE;
+
     private CarrierKeyDownloadManager mCDM;
     private CarrierInfoManager mCIM;
 
@@ -600,19 +603,19 @@
     @Override
     @NonNull
     public ServiceState getServiceState() {
-        if (mSST == null || mSST.mSS.getState() != ServiceState.STATE_IN_SERVICE) {
-            if (mImsPhone != null) {
-                return mergeServiceStates((mSST == null)
-                                ? new ServiceState() : mSST.getServiceState(),
-                        mImsPhone.getServiceState());
-            }
-        }
+        ServiceState baseSs = mSST != null ? mSST.getServiceState() : new ServiceState();
+        ServiceState imsSs = mImsPhone != null ? mImsPhone.getServiceState() : new ServiceState();
+        return mergeVoiceServiceStates(baseSs, imsSs, mTelecomVoiceServiceStateOverride);
+    }
 
-        if (mSST != null) {
-            return mSST.getServiceState();
-        } else {
-            // avoid potential NPE in EmergencyCallHelper during Phone switch
-            return new ServiceState();
+    @Override
+    public void setVoiceServiceStateOverride(boolean hasService) {
+        int newOverride =
+                hasService ? ServiceState.STATE_IN_SERVICE : ServiceState.STATE_OUT_OF_SERVICE;
+        boolean changed = newOverride != mTelecomVoiceServiceStateOverride;
+        mTelecomVoiceServiceStateOverride = newOverride;
+        if (changed && mSST != null) {
+            mSST.onTelecomVoiceServiceStateOverrideChanged();
         }
     }
 
@@ -1093,28 +1096,48 @@
     }
 
     /**
-     * ImsService reports "IN_SERVICE" for its voice registration state even if the device
-     * has lost the physical link to the tower. This helper method merges the IMS and modem
-     * ServiceState, only overriding the voice registration state when we are registered to IMS. In
-     * this case the voice registration state may be "OUT_OF_SERVICE", so override the voice
-     * registration state with the data registration state.
+     * Amends {@code baseSs} if its voice registration state is {@code OUT_OF_SERVICE}.
+     *
+     * <p>Even if the device has lost the CS link to the tower, there are two potential additional
+     * sources of voice capability not directly saved inside ServiceStateTracker:
+     *
+     * <ul>
+     *   <li>IMS voice registration state ({@code imsSs}) - if this is {@code IN_SERVICE} for voice,
+     *       we substite {@code baseSs#getDataRegState} as the final voice service state (ImsService
+     *       reports {@code IN_SERVICE} for its voice registration state even if the device has lost
+     *       the physical link to the tower)
+     *   <li>OTT voice capability provided through telecom ({@code telecomSs}) - if this is {@code
+     *       IN_SERVICE}, we directly substitute it as the final voice service state
+     * </ul>
      */
-    private ServiceState mergeServiceStates(ServiceState baseSs, ServiceState imsSs) {
-        // No need to merge states if the baseSs is IN_SERVICE.
+    private static ServiceState mergeVoiceServiceStates(
+            ServiceState baseSs, ServiceState imsSs, @ServiceState.RegState int telecomSs) {
         if (baseSs.getState() == ServiceState.STATE_IN_SERVICE) {
+            // No need to merge states if the baseSs is IN_SERVICE.
             return baseSs;
         }
-        // "IN_SERVICE" in this case means IMS is registered.
-        if (imsSs.getState() != ServiceState.STATE_IN_SERVICE) {
+        // If any of the following additional sources are IN_SERVICE, we use that since voice calls
+        // can be routed through something other than the CS link.
+        @ServiceState.RegState int finalVoiceSs = ServiceState.STATE_OUT_OF_SERVICE;
+        if (telecomSs == ServiceState.STATE_IN_SERVICE) {
+            // If telecom reports there's a PhoneAccount that can provide voice service
+            // (CAPABILITY_VOICE_CALLING_AVAILABLE), then we trust that info as it may account for
+            // external possibilities like wi-fi calling provided by the SIM call manager app. Note
+            // that CAPABILITY_PLACE_EMERGENCY_CALLS is handled separately.
+            finalVoiceSs = telecomSs;
+        } else if (imsSs.getState() == ServiceState.STATE_IN_SERVICE) {
+            // Voice override for IMS case. In this case, voice registration is OUT_OF_SERVICE, but
+            // IMS is available, so use data registration state as a basis for determining
+            // whether or not the physical link is available.
+            finalVoiceSs = baseSs.getDataRegistrationState();
+        }
+        if (finalVoiceSs != ServiceState.STATE_IN_SERVICE) {
+            // None of the additional sources provide a usable route, and they only use IN/OUT.
             return baseSs;
         }
-
         ServiceState newSs = new ServiceState(baseSs);
-        // Voice override for IMS case. In this case, voice registration is OUT_OF_SERVICE, but
-        // IMS is available, so use data registration state as a basis for determining
-        // whether or not the physical link is available.
-        newSs.setVoiceRegState(baseSs.getDataRegistrationState());
-        newSs.setEmergencyOnly(false); // only get here if voice is IN_SERVICE
+        newSs.setVoiceRegState(finalVoiceSs);
+        newSs.setEmergencyOnly(false); // Must be IN_SERVICE if we're here
         return newSs;
     }
 
@@ -4263,6 +4286,10 @@
         }
         pw.println(" isCspPlmnEnabled()=" + isCspPlmnEnabled());
         pw.println(" mManualNetworkSelectionPlmn=" + mManualNetworkSelectionPlmn);
+        pw.println(
+                " mTelecomVoiceServiceStateOverride=" + mTelecomVoiceServiceStateOverride + "("
+                        + ServiceState.rilServiceStateToString(mTelecomVoiceServiceStateOverride)
+                        + ")");
         pw.flush();
     }
 
diff --git a/src/java/com/android/internal/telephony/Phone.java b/src/java/com/android/internal/telephony/Phone.java
index f8f0037..e05e861 100644
--- a/src/java/com/android/internal/telephony/Phone.java
+++ b/src/java/com/android/internal/telephony/Phone.java
@@ -1867,6 +1867,15 @@
     }
 
     /**
+     * Override to merge into {@link #getServiceState} when telecom has registered a SIM call
+     * manager that supports over-the-top SIM-based calling (e.g. carrier-provided wi-fi calling
+     * implementation).
+     *
+     * @param hasService Whether or not the SIM call manager currently provides over-the-top voice
+     */
+    public void setVoiceServiceStateOverride(boolean hasService) {}
+
+    /**
      * Check whether the radio is off for thermal reason.
      *
      * @return {@code true} only if thermal mitigation is one of the reason for which radio is off.
@@ -4062,19 +4071,6 @@
     }
 
     /**
-     * Return the service state of mImsPhone if it is STATE_IN_SERVICE
-     * otherwise return the current voice service state
-     */
-    public int getVoicePhoneServiceState() {
-        Phone imsPhone = mImsPhone;
-        if (imsPhone != null
-                && imsPhone.getServiceState().getState() == ServiceState.STATE_IN_SERVICE) {
-            return ServiceState.STATE_IN_SERVICE;
-        }
-        return getServiceState().getState();
-    }
-
-    /**
      * Override the service provider name and the operator name for the current ICCID.
      */
     public boolean setOperatorBrandOverride(String brand) {
diff --git a/src/java/com/android/internal/telephony/ServiceStateTracker.java b/src/java/com/android/internal/telephony/ServiceStateTracker.java
index c01d7bd..a8b1fe1 100755
--- a/src/java/com/android/internal/telephony/ServiceStateTracker.java
+++ b/src/java/com/android/internal/telephony/ServiceStateTracker.java
@@ -286,6 +286,9 @@
     private static final int EVENT_POWER_OFF_RADIO_IMS_DEREG_TIMEOUT   = 62;
     protected static final int EVENT_RESET_LAST_KNOWN_CELL_IDENTITY    = 63;
     private static final int EVENT_REGISTER_DATA_NETWORK_EXISTING_CHANGED = 64;
+    // Telecom has un/registered a PhoneAccount that provides OTT voice calling capability, e.g.
+    // wi-fi calling.
+    protected static final int EVENT_TELECOM_VOICE_SERVICE_STATE_OVERRIDE_CHANGED = 65;
 
     /**
      * The current service state.
@@ -1735,6 +1738,15 @@
                 break;
             }
 
+            case EVENT_TELECOM_VOICE_SERVICE_STATE_OVERRIDE_CHANGED:
+                if (DBG) log("EVENT_TELECOM_VOICE_SERVICE_STATE_OVERRIDE_CHANGED");
+                // Similar to IMS, OTT voice state will only affect the merged service state if the
+                // CS voice service state of GsmCdma phone is not STATE_IN_SERVICE.
+                if (mSS.getState() != ServiceState.STATE_IN_SERVICE) {
+                    mPhone.notifyServiceStateChanged(mPhone.getServiceState());
+                }
+                break;
+
             default:
                 log("Unhandled message with number: " + msg.what);
                 break;
@@ -5288,6 +5300,11 @@
         }
     }
 
+    /** Called when telecom has reported a voice service state change. */
+    public void onTelecomVoiceServiceStateOverrideChanged() {
+        sendMessage(obtainMessage(EVENT_TELECOM_VOICE_SERVICE_STATE_OVERRIDE_CHANGED));
+    }
+
     private void dumpCellInfoList(PrintWriter pw) {
         pw.print(" mLastCellInfoList={");
         if(mLastCellInfoList != null) {
diff --git a/tests/telephonytests/src/com/android/internal/telephony/GsmCdmaPhoneTest.java b/tests/telephonytests/src/com/android/internal/telephony/GsmCdmaPhoneTest.java
index d00c3e1..b16db37 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/GsmCdmaPhoneTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/GsmCdmaPhoneTest.java
@@ -1606,4 +1606,37 @@
 
         mPhoneUT.setImsPhone(mImsPhone);
     }
+
+    @Test
+    public void testSetVoiceServiceStateOverride() throws Exception {
+        // Start with CS voice and IMS both OOS, no override
+        ServiceState csServiceState = new ServiceState();
+        csServiceState.setStateOutOfService();
+        csServiceState.setDataRegState(ServiceState.STATE_IN_SERVICE);
+        doReturn(csServiceState).when(mSST).getServiceState();
+        ServiceState imsServiceState = new ServiceState();
+        imsServiceState.setStateOutOfService();
+        doReturn(imsServiceState).when(mImsPhone).getServiceState();
+        replaceInstance(Phone.class, "mImsPhone", mPhoneUT, mImsPhone);
+
+        assertEquals(ServiceState.STATE_OUT_OF_SERVICE, mPhoneUT.getServiceState().getState());
+
+        // Now set the telecom override
+        mPhoneUT.setVoiceServiceStateOverride(true);
+        verify(mSST).onTelecomVoiceServiceStateOverrideChanged();
+        assertEquals(ServiceState.STATE_IN_SERVICE, mPhoneUT.getServiceState().getState());
+
+        // IMS and telecom voice are treated as equivalent for merging purposes
+        imsServiceState.setVoiceRegState(ServiceState.STATE_IN_SERVICE);
+        assertEquals(ServiceState.STATE_IN_SERVICE, mPhoneUT.getServiceState().getState());
+
+        // Clearing the telecom override still lets IMS override separately
+        mPhoneUT.setVoiceServiceStateOverride(false);
+        verify(mSST, times(2)).onTelecomVoiceServiceStateOverrideChanged();
+        assertEquals(ServiceState.STATE_IN_SERVICE, mPhoneUT.getServiceState().getState());
+
+        // And now removing the IMS IN_SERVICE results in the base OOS showing through
+        imsServiceState.setStateOutOfService();
+        assertEquals(ServiceState.STATE_OUT_OF_SERVICE, mPhoneUT.getServiceState().getState());
+    }
 }