Merge "[Telephony] Use TelephonyCallback instead of PhoneStateListener part4" into sc-dev
diff --git a/OWNERS b/OWNERS
index 3059d4d..e095b89 100644
--- a/OWNERS
+++ b/OWNERS
@@ -13,3 +13,4 @@
 dbright@google.com
 xiaotonj@google.com
 
+per-file *SimPhonebookProvider* = file:platform/packages/apps/Contacts:/OWNERS
diff --git a/res/values/config.xml b/res/values/config.xml
index 7ce141e..08a84f8 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -310,4 +310,7 @@
     <!-- Whether or not to support device to device communication using RTP and DTMF communication
          transports. -->
     <bool name="config_use_device_to_device_communication">false</bool>
+
+    <!-- Whether or not to show notifications for when bluetooth connection is bad during a call -->
+    <bool name="enable_bluetooth_call_quality_notification">false</bool>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1613ca8..4df6a52 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2176,7 +2176,7 @@
     <!-- details of the message popped up when there is
     bad call quality caused by bluetooth connection-->
     <string name="call_quality_notification_bluetooth_details">
-        Suggestion: Improve Bluetooth connectivity</string>
+        Your bluetooth signal is weak.  Try switching to speakerphone.</string>
     <!-- name of the notification that pops up during
     a phone call when there is bad call quality -->
     <string name="call_quality_notification_name">Call Quality Notification</string>
diff --git a/src/com/android/phone/EmergencyCallbackModeExitDialog.java b/src/com/android/phone/EmergencyCallbackModeExitDialog.java
index 591f435..adba850 100644
--- a/src/com/android/phone/EmergencyCallbackModeExitDialog.java
+++ b/src/com/android/phone/EmergencyCallbackModeExitDialog.java
@@ -16,6 +16,8 @@
 
 package com.android.phone;
 
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -74,7 +76,7 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
+        getWindow().addPrivateFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
         mPhone = PhoneGlobals.getInstance().getPhoneInEcm();
         // Check if phone is in Emergency Callback Mode. If not, exit.
         if (mPhone == null || !mPhone.isInEcm()) {
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 722f97b..24c55cf 100755
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -18,6 +18,8 @@
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
+import static com.android.internal.telephony.PhoneConstants.PHONE_TYPE_CDMA;
+import static com.android.internal.telephony.PhoneConstants.PHONE_TYPE_GSM;
 import static com.android.internal.telephony.PhoneConstants.PHONE_TYPE_IMS;
 import static com.android.internal.telephony.PhoneConstants.SUBSCRIPTION_KEY;
 
@@ -143,6 +145,7 @@
 import com.android.internal.telephony.CommandsInterface;
 import com.android.internal.telephony.DefaultPhoneNotifier;
 import com.android.internal.telephony.GbaManager;
+import com.android.internal.telephony.GsmCdmaPhone;
 import com.android.internal.telephony.HalVersion;
 import com.android.internal.telephony.IBooleanConsumer;
 import com.android.internal.telephony.ICallForwardingInfoCallback;
@@ -4642,7 +4645,7 @@
                         + subId + "' for key:" + key);
                 return ImsConfigImplBase.CONFIG_RESULT_UNKNOWN;
             }
-            return ImsManager.getInstance(mApp, slotId).getConfigInterface().getConfigInt(key);
+            return ImsManager.getInstance(mApp, slotId).getConfigInt(key);
         } catch (com.android.ims.ImsException e) {
             Log.w(LOG_TAG, "getImsProvisioningInt: ImsService is not available for subscription '"
                     + subId + "' for key:" + key);
@@ -4667,7 +4670,7 @@
                         + subId + "' for key:" + key);
                 return ProvisioningManager.STRING_QUERY_RESULT_ERROR_GENERIC;
             }
-            return ImsManager.getInstance(mApp, slotId).getConfigInterface().getConfigString(key);
+            return ImsManager.getInstance(mApp, slotId).getConfigString(key);
         } catch (com.android.ims.ImsException e) {
             Log.w(LOG_TAG, "getImsProvisioningString: ImsService is not available for sub '"
                     + subId + "' for key:" + key);
@@ -4693,10 +4696,10 @@
                         + subId + "' for key:" + key);
                 return ImsConfigImplBase.CONFIG_RESULT_FAILED;
             }
-            return ImsManager.getInstance(mApp, slotId).getConfigInterface().setConfig(key, value);
-        } catch (com.android.ims.ImsException e) {
+            return ImsManager.getInstance(mApp, slotId).setConfig(key, value);
+        } catch (com.android.ims.ImsException | RemoteException e) {
             Log.w(LOG_TAG, "setImsProvisioningInt: ImsService unavailable for sub '" + subId
-                    + "' for key:" + key);
+                    + "' for key:" + key, e);
             return ImsConfigImplBase.CONFIG_RESULT_FAILED;
         } finally {
             Binder.restoreCallingIdentity(identity);
@@ -4719,10 +4722,10 @@
                         + subId + "' for key:" + key);
                 return ImsConfigImplBase.CONFIG_RESULT_FAILED;
             }
-            return ImsManager.getInstance(mApp, slotId).getConfigInterface().setConfig(key, value);
-        } catch (com.android.ims.ImsException e) {
+            return ImsManager.getInstance(mApp, slotId).setConfig(key, value);
+        } catch (com.android.ims.ImsException | RemoteException e) {
             Log.w(LOG_TAG, "setImsProvisioningString: ImsService unavailable for sub '" + subId
-                    + "' for key:" + key);
+                    + "' for key:" + key, e);
             return ImsConfigImplBase.CONFIG_RESULT_FAILED;
         } finally {
             Binder.restoreCallingIdentity(identity);
@@ -6982,15 +6985,6 @@
     }
 
     @Override
-    public void setRadioCapability(RadioAccessFamily[] rafs) {
-        try {
-            ProxyController.getInstance().setRadioCapability(rafs);
-        } catch (RuntimeException e) {
-            Log.w(LOG_TAG, "setRadioCapability: Runtime Exception");
-        }
-    }
-
-    @Override
     public int getRadioAccessFamily(int phoneId, String callingPackage) {
         Phone phone = PhoneFactory.getPhone(phoneId);
         try {
@@ -8699,6 +8693,31 @@
     }
 
     /**
+     * Start emergency callback mode for GsmCdmaPhone for testing.
+     */
+    @Override
+    public void startEmergencyCallbackMode() {
+        TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+                "startEmergencyCallbackMode");
+        enforceModifyPermission();
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            for (Phone phone : PhoneFactory.getPhones()) {
+                Rlog.d(LOG_TAG, "startEmergencyCallbackMode phone type: " + phone.getPhoneType());
+                if (phone != null && ((phone.getPhoneType() == PHONE_TYPE_GSM)
+                        || (phone.getPhoneType() == PHONE_TYPE_CDMA))) {
+                    GsmCdmaPhone gsmCdmaPhone = (GsmCdmaPhone) phone;
+                    gsmCdmaPhone.obtainMessage(
+                            GsmCdmaPhone.EVENT_EMERGENCY_CALLBACK_MODE_ENTER).sendToTarget();
+                    Rlog.d(LOG_TAG, "startEmergencyCallbackMode: triggered");
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
      * Update emergency number list for test mode.
      */
     @Override
@@ -9911,6 +9930,28 @@
     }
 
     /**
+     * Enables or disables the test mode for RCS VoLTE single registration.
+     */
+    @Override
+    public void setRcsSingleRegistrationTestModeEnabled(boolean enabled) {
+        TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+                "setRcsSingleRegistrationTestModeEnabled");
+
+        RcsProvisioningMonitor.getInstance().setTestModeEnabled(enabled);
+    }
+
+    /**
+     * Gets the test mode for RCS VoLTE single registration.
+     */
+    @Override
+    public boolean getRcsSingleRegistrationTestModeEnabled() {
+        TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+                "getRcsSingleRegistrationTestModeEnabled");
+
+        return RcsProvisioningMonitor.getInstance().getTestModeEnabled();
+    }
+
+    /**
      * Overrides the config of RCS VoLTE single registration enabled for the device.
      */
     @Override
@@ -9933,7 +9974,7 @@
     @Override
     public void sendDeviceToDeviceMessage(int message, int value) {
         TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
-                "setCarrierSingleRegistrationEnabledOverride");
+                "sendDeviceToDeviceMessage");
         enforceModifyPermission();
 
         final long identity = Binder.clearCallingIdentity();
@@ -9950,6 +9991,29 @@
         }
     }
 
+    /**
+     * Sets the specified device to device transport active.
+     * @param transport The transport to set active.
+     */
+    @Override
+    public void setActiveDeviceToDeviceTransport(@NonNull String transport) {
+        TelephonyPermissions.enforceShellOnly(Binder.getCallingUid(),
+                "setActiveDeviceToDeviceTransport");
+        enforceModifyPermission();
+
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            TelephonyConnectionService service =
+                    TelecomAccountRegistry.getInstance(null).getTelephonyConnectionService();
+            if (service == null) {
+                Rlog.e(LOG_TAG, "setActiveDeviceToDeviceTransport: not in a call.");
+                return;
+            }
+            service.setActiveDeviceToDeviceTransport(transport);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
 
     /**
      * Gets the config of RCS VoLTE single registration enabled for the device.
diff --git a/src/com/android/phone/RcsProvisioningMonitor.java b/src/com/android/phone/RcsProvisioningMonitor.java
index 77f8a7b..dcf8e92 100644
--- a/src/com/android/phone/RcsProvisioningMonitor.java
+++ b/src/com/android/phone/RcsProvisioningMonitor.java
@@ -39,15 +39,20 @@
 import android.telephony.ims.RcsConfig;
 import android.telephony.ims.aidl.IImsConfig;
 import android.telephony.ims.aidl.IRcsConfigCallback;
-import android.telephony.ims.feature.ImsFeature;
 import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.SparseArray;
 
+import com.android.ims.FeatureConnector;
+import com.android.ims.FeatureUpdates;
+import com.android.ims.RcsFeatureManager;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.util.HandlerExecutor;
 import com.android.internal.util.CollectionUtils;
 import com.android.telephony.Rlog;
 
+import java.util.Arrays;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
@@ -67,6 +72,7 @@
     private static final int EVENT_RECONFIG_REQUEST = 5;
     private static final int EVENT_DEVICE_CONFIG_OVERRIDE = 6;
     private static final int EVENT_CARRIER_CONFIG_OVERRIDE = 7;
+    private static final int EVENT_RESET = 8;
 
     private final PhoneGlobals mPhone;
     private final Handler mHandler;
@@ -77,12 +83,15 @@
     private final HashMap<Integer, Boolean> mCarrierSingleRegistrationEnabledOverride =
             new HashMap<>();
     private String mDmaPackageName;
+    private final SparseArray<RcsFeatureListener> mRcsFeatureListeners = new SparseArray<>();
+    private volatile boolean mTestModeEnabled;
 
     private final CarrierConfigManager mCarrierConfigManager;
     private final DmaChangedListener mDmaChangedListener;
     private final SubscriptionManager mSubscriptionManager;
     private final TelephonyRegistryManager mTelephonyRegistryManager;
     private final RoleManagerAdapter mRoleManager;
+    private FeatureConnectorFactory<RcsFeatureManager> mFeatureFactory;
 
     private static RcsProvisioningMonitor sInstance;
 
@@ -147,6 +156,7 @@
 
         @Override
         public void handleMessage(Message msg) {
+            logv("handleMessage: " + msg);
             switch (msg.what) {
                 case EVENT_SUB_CHANGED:
                     onSubChanged();
@@ -178,6 +188,9 @@
                         onCarrierConfigChange();
                     }
                     break;
+                case EVENT_RESET:
+                    reset();
+                    break;
                 default:
                     loge("Unhandled event " + msg.what);
             }
@@ -188,13 +201,20 @@
         private int mSubId;
         private volatile int mSingleRegistrationCapability;
         private volatile byte[] mConfig;
-        private HashSet<IRcsConfigCallback> mRcsConfigCallbacks;
+        private ArraySet<IRcsConfigCallback> mRcsConfigCallbacks;
+        private IImsConfig mIImsConfig;
+        private boolean mHasReconfigRequest;
 
         RcsProvisioningInfo(int subId, int singleRegistrationCapability, byte[] config) {
             mSubId = subId;
             mSingleRegistrationCapability = singleRegistrationCapability;
             mConfig = config;
-            mRcsConfigCallbacks = new HashSet<>();
+            mRcsConfigCallbacks = new ArraySet<>();
+            registerRcsFeatureListener(this);
+        }
+
+        int getSubId() {
+            return mSubId;
         }
 
         void setSingleRegistrationCapability(int singleRegistrationCapability) {
@@ -206,7 +226,14 @@
         }
 
         void setConfig(byte[] config) {
-            mConfig = config;
+            if (!Arrays.equals(mConfig, config)) {
+                mConfig = config;
+                if (mConfig != null) {
+                    notifyRcsAutoConfigurationReceived();
+                } else {
+                    notifyRcsAutoConfigurationRemoved();
+                }
+            }
         }
 
         byte[] getConfig() {
@@ -214,15 +241,14 @@
         }
 
         boolean addRcsConfigCallback(IRcsConfigCallback cb) {
-            IImsConfig imsConfig = getIImsConfig(mSubId, ImsFeature.FEATURE_RCS);
-            if (imsConfig == null) {
+            if (mIImsConfig == null) {
                 logd("fail to addRcsConfigCallback as imsConfig is null");
                 return false;
             }
 
             synchronized (mRcsConfigCallbacks) {
                 try {
-                    imsConfig.addRcsConfigCallback(cb);
+                    mIImsConfig.addRcsConfigCallback(cb);
                 } catch (RemoteException e) {
                     loge("fail to addRcsConfigCallback due to " + e);
                     return false;
@@ -234,12 +260,11 @@
 
         boolean removeRcsConfigCallback(IRcsConfigCallback cb) {
             boolean result = true;
-            IImsConfig imsConfig = getIImsConfig(mSubId, ImsFeature.FEATURE_RCS);
 
             synchronized (mRcsConfigCallbacks) {
-                if (imsConfig != null) {
+                if (mIImsConfig != null) {
                     try {
-                        imsConfig.removeRcsConfigCallback(cb);
+                        mIImsConfig.removeRcsConfigCallback(cb);
                     } catch (RemoteException e) {
                         loge("fail to removeRcsConfigCallback due to " + e);
                     }
@@ -258,16 +283,89 @@
             return result;
         }
 
+        void triggerRcsReconfiguration() {
+            if (mIImsConfig != null) {
+                try {
+                    logv("triggerRcsReconfiguration for sub:" + mSubId);
+                    mIImsConfig.triggerRcsReconfiguration();
+                    mHasReconfigRequest = false;
+                } catch (RemoteException e) {
+                    loge("triggerRcsReconfiguration failed due to " + e);
+                }
+            } else {
+                logd("triggerRcsReconfiguration failed due to IImsConfig null.");
+                mHasReconfigRequest = true;
+            }
+        }
+
+        void destroy() {
+            unregisterRcsFeatureListener(this);
+            clear();
+            mIImsConfig = null;
+            mRcsConfigCallbacks = null;
+        }
+
         void clear() {
             setConfig(null);
+            clearCallbacks();
+        }
+
+        void onRcsStatusChanged(IImsConfig binder) {
+            logv("onRcsStatusChanged for sub:" + mSubId + ", IImsConfig?" + binder);
+            if (mIImsConfig != binder) {
+                mIImsConfig = binder;
+                if (mIImsConfig != null) {
+                    if (mHasReconfigRequest) {
+                        triggerRcsReconfiguration();
+                    } else {
+                        notifyRcsAutoConfigurationReceived();
+                    }
+                } else {
+                    // clear callbacks if rcs disconnected
+                    clearCallbacks();
+                }
+            }
+        }
+
+        private void notifyRcsAutoConfigurationReceived() {
+            if (mConfig == null) {
+                logd("Rcs config is null for sub : " + mSubId);
+                return;
+            }
+
+            if (mIImsConfig != null) {
+                try {
+                    logv("notifyRcsAutoConfigurationReceived for sub:" + mSubId);
+                    mIImsConfig.notifyRcsAutoConfigurationReceived(mConfig, false);
+                } catch (RemoteException e) {
+                    loge("notifyRcsAutoConfigurationReceived failed due to " + e);
+                }
+            } else {
+                logd("notifyRcsAutoConfigurationReceived failed due to IImsConfig null.");
+            }
+        }
+
+        private void notifyRcsAutoConfigurationRemoved() {
+            if (mIImsConfig != null) {
+                try {
+                    logv("notifyRcsAutoConfigurationRemoved for sub:" + mSubId);
+                    mIImsConfig.notifyRcsAutoConfigurationRemoved();
+                } catch (RemoteException e) {
+                    loge("notifyRcsAutoConfigurationRemoved failed due to " + e);
+                }
+            } else {
+                logd("notifyRcsAutoConfigurationRemoved failed due to IImsConfig null.");
+            }
+        }
+
+        private void clearCallbacks() {
             synchronized (mRcsConfigCallbacks) {
-                IImsConfig imsConfig = getIImsConfig(mSubId, ImsFeature.FEATURE_RCS);
                 Iterator<IRcsConfigCallback> it = mRcsConfigCallbacks.iterator();
                 while (it.hasNext()) {
                     IRcsConfigCallback cb = it.next();
-                    if (imsConfig != null) {
+                    if (mIImsConfig != null) {
                         try {
-                            imsConfig.removeRcsConfigCallback(cb);
+                            mIImsConfig.removeRcsConfigCallback(cb);
                         } catch (RemoteException e) {
                             loge("fail to removeRcsConfigCallback due to " + e);
                         }
@@ -284,7 +382,61 @@
     }
 
     @VisibleForTesting
-    public RcsProvisioningMonitor(PhoneGlobals app, Looper looper, RoleManagerAdapter roleManager) {
+    public interface FeatureConnectorFactory<U extends FeatureUpdates> {
+        /**
+         * @return a {@link FeatureConnector} associated for the given {@link FeatureUpdates}
+         * and slot index.
+         */
+        FeatureConnector<U> create(Context context, int slotIndex,
+                FeatureConnector.Listener<U> listener, Executor executor, String logPrefix);
+    }
+
+    private final class RcsFeatureListener implements FeatureConnector.Listener<RcsFeatureManager> {
+        private final ArraySet<RcsProvisioningInfo> mRcsProvisioningInfos = new ArraySet<>();
+        private RcsFeatureManager mRcsFeatureManager;
+        private FeatureConnector<RcsFeatureManager> mConnector;
+
+        RcsFeatureListener(int slotId) {
+            mConnector = mFeatureFactory.create(
+                    mPhone, slotId, this, new HandlerExecutor(mHandler), TAG);
+            mConnector.connect();
+        }
+
+        void destroy() {
+            mConnector.disconnect();
+            mConnector = null;
+            mRcsFeatureManager = null;
+            mRcsProvisioningInfos.clear();
+        }
+
+        void addRcsProvisioningInfo(RcsProvisioningInfo info) {
+            if (!mRcsProvisioningInfos.contains(info)) {
+                mRcsProvisioningInfos.add(info);
+                info.onRcsStatusChanged(mRcsFeatureManager == null ? null
+                        : mRcsFeatureManager.getConfig());
+            }
+        }
+
+        void removeRcsProvisioningInfo(RcsProvisioningInfo info) {
+            mRcsProvisioningInfos.remove(info);
+        }
+
+        @Override
+        public void connectionReady(RcsFeatureManager manager) {
+            mRcsFeatureManager = manager;
+            mRcsProvisioningInfos.forEach(v -> v.onRcsStatusChanged(manager.getConfig()));
+        }
+
+        @Override
+        public void connectionUnavailable(int reason) {
+            mRcsFeatureManager = null;
+            mRcsProvisioningInfos.forEach(v -> v.onRcsStatusChanged(null));
+        }
+    }
+
+    @VisibleForTesting
+    public RcsProvisioningMonitor(PhoneGlobals app, Looper looper, RoleManagerAdapter roleManager,
+            FeatureConnectorFactory<RcsFeatureManager> factory) {
         mPhone = app;
         mHandler = new MyHandler(looper);
         mCarrierConfigManager = mPhone.getSystemService(CarrierConfigManager.class);
@@ -293,15 +445,9 @@
         mRoleManager = roleManager;
         mDmaPackageName = getDmaPackageName();
         logv("DMA is " + mDmaPackageName);
-        IntentFilter filter = new IntentFilter();
-        filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
-        mPhone.registerReceiver(mReceiver, filter);
-        mTelephonyRegistryManager.addOnSubscriptionsChangedListener(
-                mSubChangedListener, mSubChangedListener.getHandlerExecutor());
         mDmaChangedListener = new DmaChangedListener();
-        mDmaChangedListener.register();
-        //initialize configs for all active sub
-        onSubChanged();
+        mFeatureFactory = factory;
+        init();
     }
 
     /**
@@ -313,7 +459,7 @@
             HandlerThread handlerThread = new HandlerThread(TAG);
             handlerThread.start();
             sInstance = new RcsProvisioningMonitor(app, handlerThread.getLooper(),
-                    new RoleManagerAdapterImpl(app));
+                    new RoleManagerAdapterImpl(app), RcsFeatureManager::getConnector);
         }
         return sInstance;
     }
@@ -325,15 +471,44 @@
         return sInstance;
     }
 
+    private void init() {
+        logd("init.");
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+        mPhone.registerReceiver(mReceiver, filter);
+        mTelephonyRegistryManager.addOnSubscriptionsChangedListener(
+                mSubChangedListener, mSubChangedListener.getHandlerExecutor());
+        mDmaChangedListener.register();
+        //initialize configs for all active sub
+        onSubChanged();
+    }
+
+    private void release() {
+        logd("release.");
+        mDmaChangedListener.unregister();
+        mTelephonyRegistryManager.removeOnSubscriptionsChangedListener(mSubChangedListener);
+        mPhone.unregisterReceiver(mReceiver);
+        for (int i = 0; i < mRcsFeatureListeners.size(); i++) {
+            mRcsFeatureListeners.valueAt(i).destroy();
+        }
+        mRcsFeatureListeners.clear();
+        mRcsProvisioningInfos.forEach((k, v)->v.destroy());
+        mRcsProvisioningInfos.clear();
+        mCarrierSingleRegistrationEnabledOverride.clear();
+    }
+
+    private void reset() {
+        release();
+        init();
+    }
+
     /**
      * destroy the instance
      */
     @VisibleForTesting
     public void destroy() {
         logd("destroy it.");
-        mDmaChangedListener.unregister();
-        mTelephonyRegistryManager.removeOnSubscriptionsChangedListener(mSubChangedListener);
-        mPhone.unregisterReceiver(mReceiver);
+        release();
         mHandler.getLooper().quit();
     }
 
@@ -411,6 +586,27 @@
     }
 
     /**
+     * Enables or disables test mode.
+     *
+     * <p> If test mode is enabled, any rcs config change will not update the database.
+     */
+    public void setTestModeEnabled(boolean enabled) {
+        logv("setTestModeEnabled as " + enabled);
+        if (mTestModeEnabled != enabled) {
+            mTestModeEnabled = enabled;
+            mHandler.sendMessage(mHandler.obtainMessage(EVENT_RESET));
+        }
+    }
+
+
+    /**
+     * Returns whether the test mode is enabled.
+     */
+    public boolean getTestModeEnabled() {
+        return mTestModeEnabled;
+    }
+
+    /**
      * override the device config whether single registration is enabled
      */
     public void overrideDeviceSingleRegistrationEnabled(Boolean enabled) {
@@ -463,61 +659,29 @@
                 v.clear();
                 if (isAcsUsed(k)) {
                     logv("acs used, trigger to re-configure.");
-                    notifyRcsAutoConfigurationRemoved(k);
-                    triggerRcsReconfiguration(k);
+                    updateConfigForSub(k, null, true);
+                    v.triggerRcsReconfiguration();
                 } else {
+                    logv("acs not used, set cached config and notify.");
                     v.setConfig(cachedConfig);
-                    logv("acs not used, notify.");
-                    notifyRcsAutoConfigurationReceived(k, v.getConfig(), false);
                 }
             });
         }
     }
 
-    private void notifyRcsAutoConfigurationReceived(int subId, byte[] config,
-            boolean isCompressed) {
-        if (config == null) {
-            logd("Rcs config is null for sub : " + subId);
-            return;
-        }
-
-        IImsConfig imsConfig = getIImsConfig(subId, ImsFeature.FEATURE_RCS);
-        if (imsConfig != null) {
-            try {
-                imsConfig.notifyRcsAutoConfigurationReceived(config, isCompressed);
-            } catch (RemoteException e) {
-                loge("fail to notify rcs configuration received!");
-            }
-        } else {
-            logd("getIImsConfig returns null.");
+    private void updateConfigForSub(int subId, byte[] config, boolean isCompressed) {
+        logv("updateConfigForSub, subId:" + subId + ", mTestModeEnabled:" + mTestModeEnabled);
+        if (!mTestModeEnabled) {
+            RcsConfig.updateConfigForSub(mPhone, subId, config, isCompressed);
         }
     }
 
-    private void notifyRcsAutoConfigurationRemoved(int subId) {
-        RcsConfig.updateConfigForSub(mPhone, subId, null, true);
-        IImsConfig imsConfig = getIImsConfig(subId, ImsFeature.FEATURE_RCS);
-        if (imsConfig != null) {
-            try {
-                imsConfig.notifyRcsAutoConfigurationRemoved();
-            } catch (RemoteException e) {
-                loge("fail to notify rcs configuration removed!");
-            }
-        } else {
-            logd("getIImsConfig returns null.");
+    private byte[] loadConfigForSub(int subId) {
+        logv("loadConfigForSub, subId:" + subId + ", mTestModeEnabled:" + mTestModeEnabled);
+        if (!mTestModeEnabled) {
+            return RcsConfig.loadRcsConfigForSub(mPhone, subId, false);
         }
-    }
-
-    private void triggerRcsReconfiguration(int subId) {
-        IImsConfig imsConfig = getIImsConfig(subId, ImsFeature.FEATURE_RCS);
-        if (imsConfig != null) {
-            try {
-                imsConfig.triggerRcsReconfiguration();
-            } catch (RemoteException e) {
-                loge("fail to trigger rcs reconfiguration!");
-            }
-        } else {
-            logd("getIImsConfig returns null.");
-        }
+        return null;
     }
 
     private boolean isAcsUsed(int subId) {
@@ -569,26 +733,25 @@
 
     private void onSubChanged() {
         final int[] activeSubs = mSubscriptionManager.getActiveSubscriptionIdList();
-        final HashSet<Integer> subsToBeDeactivated = new HashSet<>(mRcsProvisioningInfos.keySet());
+        final ArraySet<Integer> subsToBeDeactivated =
+                new ArraySet<>(mRcsProvisioningInfos.keySet());
 
         for (int i : activeSubs) {
             subsToBeDeactivated.remove(i);
             if (!mRcsProvisioningInfos.containsKey(i)) {
-                byte[] data = RcsConfig.loadRcsConfigForSub(mPhone, i, false);
+                byte[] data = loadConfigForSub(i);
                 int capability = getSingleRegistrationCapableValue(i);
                 logv("new info is created for sub : " + i + ", single registration capability :"
                         + capability + ", rcs config : " + data);
                 mRcsProvisioningInfos.put(i, new RcsProvisioningInfo(i, capability, data));
-                notifyRcsAutoConfigurationReceived(i, data, false);
                 notifyDmaForSub(i, capability);
             }
         }
 
         subsToBeDeactivated.forEach(i -> {
             RcsProvisioningInfo info = mRcsProvisioningInfos.remove(i);
-            notifyRcsAutoConfigurationRemoved(i);
             if (info != null) {
-                info.clear();
+                info.destroy();
             }
         });
     }
@@ -597,11 +760,8 @@
         logv("onConfigReceived, subId:" + subId + ", config:"
                 + config + ", isCompressed:" + isCompressed);
         RcsProvisioningInfo info = mRcsProvisioningInfos.get(subId);
-        if (info != null) {
-            info.setConfig(isCompressed ? RcsConfig.decompressGzip(config) : config);
-        }
-        RcsConfig.updateConfigForSub(mPhone, subId, config, isCompressed);
-        notifyRcsAutoConfigurationReceived(subId, config, isCompressed);
+        info.setConfig(isCompressed ? RcsConfig.decompressGzip(config) : config);
+        updateConfigForSub(subId, config, isCompressed);
     }
 
     private void onReconfigRequest(int subId) {
@@ -609,9 +769,10 @@
         RcsProvisioningInfo info = mRcsProvisioningInfos.get(subId);
         if (info != null) {
             info.setConfig(null);
+            // clear rcs config stored in db
+            updateConfigForSub(subId, null, true);
+            info.triggerRcsReconfiguration();
         }
-        notifyRcsAutoConfigurationRemoved(subId);
-        triggerRcsReconfiguration(subId);
     }
 
     private void notifyDmaForSub(int subId, int capability) {
@@ -621,13 +782,13 @@
         intent.putExtra(ProvisioningManager.EXTRA_SUBSCRIPTION_ID, subId);
         intent.putExtra(ProvisioningManager.EXTRA_STATUS, capability);
         logv("notify " + intent);
-        // Only send permission to the default sms app if it has the correct permissions.
-        mPhone.sendBroadcast(intent, Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION);
-    }
-
-    private IImsConfig getIImsConfig(int subId, int feature) {
-        return mPhone.getImsResolver().getImsConfig(
-                SubscriptionManager.getSlotIndex(subId), feature);
+        // Only send permission to the default sms app if it has the correct permissions
+        // except test mode enabled
+        if (!mTestModeEnabled) {
+            mPhone.sendBroadcast(intent, Manifest.permission.PERFORM_IMS_SINGLE_REGISTRATION);
+        } else {
+            mPhone.sendBroadcast(intent);
+        }
     }
 
     private String getDmaPackageName() {
@@ -639,6 +800,24 @@
         }
     }
 
+    void registerRcsFeatureListener(RcsProvisioningInfo info) {
+        int slotId = SubscriptionManager.getSlotIndex(info.getSubId());
+        RcsFeatureListener cb = mRcsFeatureListeners.get(slotId);
+        if (cb == null) {
+            cb = new RcsFeatureListener(slotId);
+            mRcsFeatureListeners.put(slotId, cb);
+        }
+        cb.addRcsProvisioningInfo(info);
+    }
+
+    void unregisterRcsFeatureListener(RcsProvisioningInfo info) {
+        int slotId = SubscriptionManager.getSlotIndex(info.getSubId());
+        RcsFeatureListener cb = mRcsFeatureListeners.get(slotId);
+        if (cb != null) {
+            cb.removeRcsProvisioningInfo(info);
+        }
+    }
+
     private static boolean booleanEquals(Boolean val1, Boolean val2) {
         return (val1 == null && val2 == null)
                 || (Boolean.TRUE.equals(val1) && Boolean.TRUE.equals(val2))
diff --git a/src/com/android/phone/SimPhonebookProvider.java b/src/com/android/phone/SimPhonebookProvider.java
index 7a1e93c..6a27130 100644
--- a/src/com/android/phone/SimPhonebookProvider.java
+++ b/src/com/android/phone/SimPhonebookProvider.java
@@ -59,6 +59,7 @@
 import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
@@ -295,6 +296,9 @@
 
     private Cursor queryElementaryFilesItem(PhonebookArgs args, String[] projection) {
         validateProjection(ELEMENTARY_FILES_COLUMNS_SET, projection);
+        if (projection == null) {
+            projection = ELEMENTARY_FILES_ALL_COLUMNS;
+        }
 
         MatrixCursor result = new MatrixCursor(projection);
         try {
@@ -641,8 +645,8 @@
     }
 
     private boolean hasPermissionsForFdnWrite(PhonebookArgs args) {
-        TelephonyManager telephonyManager = getContext().getSystemService(
-                TelephonyManager.class);
+        TelephonyManager telephonyManager = Objects.requireNonNull(
+                getContext().getSystemService(TelephonyManager.class));
         String callingPackage = getCallingPackage();
         int granted = PackageManager.PERMISSION_DENIED;
         if (callingPackage != null) {
@@ -701,7 +705,12 @@
 
         String name = values.getAsString(SimRecords.NAME);
         int length = getEncodedNameLength(name);
-        int maxLength = AdnRecord.getMaxAlphaTagBytes(getRecordSize(getRecordsSizeForEf(args)));
+        int[] recordsSize = getRecordsSizeForEf(args);
+        if (recordsSize == null) {
+            throw new IllegalStateException(
+                    "Failed to get " + ElementaryFiles.NAME_MAX_LENGTH + " from SIM");
+        }
+        int maxLength = AdnRecord.getMaxAlphaTagBytes(getRecordSize(recordsSize));
 
         if (length > maxLength) {
             throw new IllegalArgumentException(SimRecords.NAME + " is too long.");
@@ -740,7 +749,7 @@
 
     private AdnRecord loadRecord(PhonebookArgs args) {
         List<AdnRecord> records = loadRecordsForEf(args);
-        if (args.recordNumber > records.size()) {
+        if (records == null || args.recordNumber > records.size()) {
             return null;
         }
         AdnRecord result = records.get(args.recordNumber - 1);
diff --git a/src/com/android/phone/TelephonyShellCommand.java b/src/com/android/phone/TelephonyShellCommand.java
index 79dfbcf..d395aa2 100644
--- a/src/com/android/phone/TelephonyShellCommand.java
+++ b/src/com/android/phone/TelephonyShellCommand.java
@@ -71,6 +71,7 @@
     private static final String CALL_COMPOSER_SUBCOMMAND = "callcomposer";
     private static final String IMS_SUBCOMMAND = "ims";
     private static final String NUMBER_VERIFICATION_SUBCOMMAND = "numverify";
+    private static final String EMERGENCY_CALLBACK_MODE = "emergency-callback-mode";
     private static final String EMERGENCY_NUMBER_TEST_MODE = "emergency-number-test-mode";
     private static final String END_BLOCK_SUPPRESSION = "end-block-suppression";
     private static final String RESTART_MODEM = "restart-modem";
@@ -83,6 +84,7 @@
 
     private static final String CALL_COMPOSER_TEST_MODE = "test-mode";
     private static final String CALL_COMPOSER_SIMULATE_CALL = "simulate-outgoing-call";
+    private static final String CALL_COMPOSER_USER_SETTING = "user-setting";
 
     private static final String IMS_SET_IMS_SERVICE = "set-ims-service";
     private static final String IMS_GET_IMS_SERVICE = "get-ims-service";
@@ -110,9 +112,12 @@
     private static final String SRC_GET_DEVICE_ENABLED = "get-device-enabled";
     private static final String SRC_SET_CARRIER_ENABLED = "set-carrier-enabled";
     private static final String SRC_GET_CARRIER_ENABLED = "get-carrier-enabled";
+    private static final String SRC_SET_TEST_ENABLED = "set-test-enabled";
+    private static final String SRC_GET_TEST_ENABLED = "get-test-enabled";
 
     private static final String D2D_SUBCOMMAND = "d2d";
     private static final String D2D_SEND = "send";
+    private static final String D2D_TRANSPORT = "transport";
 
     private static final String RCS_UCE_COMMAND = "uce";
     private static final String UCE_GET_EAB_CONTACT = "get-eab-contact";
@@ -193,6 +198,8 @@
                 return handleRcsUceCommand();
             case NUMBER_VERIFICATION_SUBCOMMAND:
                 return handleNumberVerificationCommand();
+            case EMERGENCY_CALLBACK_MODE:
+                return handleEmergencyCallbackModeCommand();
             case EMERGENCY_NUMBER_TEST_MODE:
                 return handleEmergencyNumberTestModeCommand();
             case CARRIER_CONFIG_SUBCOMMAND: {
@@ -271,6 +278,9 @@
                         MESSAGE_DEVICE_BATTERY_STATE));
         pw.println("    Type: " + MESSAGE_DEVICE_NETWORK_COVERAGE + " - "
                 + Communicator.messageToString(MESSAGE_DEVICE_NETWORK_COVERAGE));
+        pw.println("  d2d transport TYPE");
+        pw.println("    Forces the specified D2D transport TYPE to be active.  Use the");
+        pw.println("    short class name of the transport; i.e. DtmfTransport or RtpTransport.");
     }
 
     private void onHelpIms() {
@@ -427,6 +437,11 @@
     private void onHelpSrc() {
         PrintWriter pw = getOutPrintWriter();
         pw.println("RCS VoLTE Single Registration Config Commands:");
+        pw.println("  src set-test-enabled true|false");
+        pw.println("    Sets the test mode enabled for RCS VoLTE single registration.");
+        pw.println("    The value could be true, false, or null(undefined).");
+        pw.println("  src get-test-enabled");
+        pw.println("    Gets the test mode for RCS VoLTE single registration.");
         pw.println("  src set-device-enabled true|false|null");
         pw.println("    Sets the device config for RCS VoLTE single registration to the value.");
         pw.println("    The value could be true, false, or null(undefined).");
@@ -511,6 +526,19 @@
         return 0;
     }
 
+    private int handleEmergencyCallbackModeCommand() {
+        PrintWriter errPw = getErrPrintWriter();
+        try {
+            mInterface.startEmergencyCallbackMode();
+            Log.d(LOG_TAG, "handleEmergencyCallbackModeCommand: triggered");
+        } catch (RemoteException ex) {
+            Log.w(LOG_TAG, "emergency-callback-mode error: " + ex.getMessage());
+            errPw.println("Exception: " + ex.getMessage());
+            return -1;
+        }
+        return 0;
+    }
+
     private int handleEmergencyNumberTestModeCommand() {
         PrintWriter errPw = getErrPrintWriter();
         String opt = getNextOption();
@@ -634,6 +662,9 @@
             case D2D_SEND: {
                 return handleD2dSendCommand();
             }
+            case D2D_TRANSPORT: {
+                return handleD2dTransportCommand();
+            }
         }
 
         return -1;
@@ -641,11 +672,9 @@
 
     private int handleD2dSendCommand() {
         PrintWriter errPw = getErrPrintWriter();
-        String opt;
         int messageType = -1;
         int messageValue = -1;
 
-
         String arg = getNextArg();
         if (arg == null) {
             onHelpD2D();
@@ -681,6 +710,25 @@
         return 0;
     }
 
+    private int handleD2dTransportCommand() {
+        PrintWriter errPw = getErrPrintWriter();
+
+        String arg = getNextArg();
+        if (arg == null) {
+            onHelpD2D();
+            return 0;
+        }
+
+        try {
+            mInterface.setActiveDeviceToDeviceTransport(arg);
+        } catch (RemoteException e) {
+            Log.w(LOG_TAG, "d2d transport error: " + e.getMessage());
+            errPw.println("Exception: " + e.getMessage());
+            return -1;
+        }
+        return 0;
+    }
+
     // ims set-ims-service
     private int handleImsSetServiceCommand() {
         PrintWriter errPw = getErrPrintWriter();
@@ -1628,6 +1676,12 @@
         }
 
         switch (arg) {
+            case SRC_SET_TEST_ENABLED: {
+                return handleSrcSetTestEnabledCommand();
+            }
+            case SRC_GET_TEST_ENABLED: {
+                return handleSrcGetTestEnabledCommand();
+            }
             case SRC_SET_DEVICE_ENABLED: {
                 return handleSrcSetDeviceEnabledCommand();
             }
@@ -1747,6 +1801,40 @@
         return 0;
     }
 
+    private int handleSrcSetTestEnabledCommand() {
+        String enabledStr = getNextArg();
+        if (enabledStr == null) {
+            return -1;
+        }
+
+        try {
+            mInterface.setRcsSingleRegistrationTestModeEnabled(Boolean.parseBoolean(enabledStr));
+            if (VDBG) {
+                Log.v(LOG_TAG, "src set-test-enabled " + enabledStr + ", done");
+            }
+            getOutPrintWriter().println("Done");
+        } catch (NumberFormatException | RemoteException e) {
+            Log.w(LOG_TAG, "src set-test-enabled " + enabledStr + ", error" + e.getMessage());
+            getErrPrintWriter().println("Exception: " + e.getMessage());
+            return -1;
+        }
+        return 0;
+    }
+
+    private int handleSrcGetTestEnabledCommand() {
+        boolean result = false;
+        try {
+            result = mInterface.getRcsSingleRegistrationTestModeEnabled();
+        } catch (RemoteException e) {
+            return -1;
+        }
+        if (VDBG) {
+            Log.v(LOG_TAG, "src get-test-enabled, returned: " + result);
+        }
+        getOutPrintWriter().println(result);
+        return 0;
+    }
+
     private int handleSrcSetDeviceEnabledCommand() {
         String enabledStr = getNextArg();
         if (enabledStr == null) {
@@ -1840,6 +1928,9 @@
         pw.println("  callcomposer simulate-outgoing-call [subId] [UUID]");
         pw.println("    Simulates an outgoing call being placed with the picture ID as");
         pw.println("    the provided UUID. This triggers storage to the call log.");
+        pw.println("  callcomposer user-setting [subId] enable|disable|query");
+        pw.println("    Enables or disables the user setting for call composer, as set by");
+        pw.println("    TelephonyManager#setCallComposerStatus.");
     }
 
     private int handleCallComposerCommand() {
@@ -1883,6 +1974,29 @@
                 }
                 break;
             }
+            case CALL_COMPOSER_USER_SETTING: {
+                try {
+                    int subscriptionId = Integer.valueOf(getNextArg());
+                    String enabledStr = getNextArg();
+                    if (ENABLE.equals(enabledStr)) {
+                        mInterface.setCallComposerStatus(subscriptionId,
+                                TelephonyManager.CALL_COMPOSER_STATUS_ON);
+                    } else if (DISABLE.equals(enabledStr)) {
+                        mInterface.setCallComposerStatus(subscriptionId,
+                                TelephonyManager.CALL_COMPOSER_STATUS_OFF);
+                    } else if (QUERY.equals(enabledStr)) {
+                        getOutPrintWriter().println(mInterface.getCallComposerStatus(subscriptionId)
+                                == TelephonyManager.CALL_COMPOSER_STATUS_ON);
+                    } else {
+                        onHelpCallComposer();
+                        return 1;
+                    }
+                } catch (RemoteException e) {
+                    e.printStackTrace(getOutPrintWriter());
+                    return 1;
+                }
+                break;
+            }
         }
 
         return 0;
diff --git a/src/com/android/phone/callcomposer/CallComposerPictureManager.java b/src/com/android/phone/callcomposer/CallComposerPictureManager.java
index b27a27c..3c9e27e 100644
--- a/src/com/android/phone/callcomposer/CallComposerPictureManager.java
+++ b/src/com/android/phone/callcomposer/CallComposerPictureManager.java
@@ -17,6 +17,7 @@
 package com.android.phone.callcomposer;
 
 import android.content.Context;
+import android.location.Location;
 import android.net.Uri;
 import android.os.OutcomeReceiver;
 import android.os.UserHandle;
@@ -81,6 +82,13 @@
     @VisibleForTesting
     public static boolean sTestMode = false;
     public static final String FAKE_SERVER_URL = "https://example.com/FAKE.png";
+    public static final String FAKE_SUBJECT = "This is a test call subject";
+    public static final Location FAKE_LOCATION = new Location("");
+    static {
+        // Meteor Crater, AZ
+        FAKE_LOCATION.setLatitude(35.027526);
+        FAKE_LOCATION.setLongitude(-111.021696);
+    }
 
     public interface CallLogProxy {
         default void storeCallComposerPictureAsUser(Context context,
diff --git a/src/com/android/services/telephony/CallQualityManager.java b/src/com/android/services/telephony/CallQualityManager.java
index 0e32ddc..262ce88 100644
--- a/src/com/android/services/telephony/CallQualityManager.java
+++ b/src/com/android/services/telephony/CallQualityManager.java
@@ -21,10 +21,12 @@
 import android.app.NotificationManager;
 import android.content.Context;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.telecom.BluetoothCallQualityReport;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.SlidingWindowEventCounter;
 import com.android.phone.R;
 
 /**
@@ -32,16 +34,20 @@
  */
 public class CallQualityManager {
     private static final String TAG = CallQualityManager.class.getCanonicalName();
-    private static final String CALL_QUALITY_REPORT_CHANNEL = "call_quality_report_channel";
 
     /** notification ids */
     public static final int BLUETOOTH_CHOPPY_VOICE_NOTIFICATION_ID = 700;
-
-    public static final String CALL_QUALITY_CHANNEL_ID = "CallQualityNotification";
+    public static final String CALL_QUALITY_CHANNEL_ID = "CallQualityNotificationChannel";
+    public static final long NOTIFICATION_BACKOFF_TIME_MILLIS = 5L * 60 * 1000;
+    public static final int NUM_OCCURRENCES_THRESHOLD = 5;
+    public static final long TIME_WINDOW_MILLIS = 5 * 1000;
 
     private final Context mContext;
     private final NotificationChannel mNotificationChannel;
     private final NotificationManager mNotificationManager;
+    private final SlidingWindowEventCounter mSlidingWindowEventCounter;
+
+    private long mNotificationLastTime;
 
     public CallQualityManager(Context context) {
         mContext = context;
@@ -51,6 +57,11 @@
         mNotificationManager = (NotificationManager)
                 mContext.getSystemService(Context.NOTIFICATION_SERVICE);
         mNotificationManager.createNotificationChannel(mNotificationChannel);
+        //making sure at the start we qualify to show notifications
+        mNotificationLastTime =
+                SystemClock.elapsedRealtime() - NOTIFICATION_BACKOFF_TIME_MILLIS - 1;
+        mSlidingWindowEventCounter =
+                new SlidingWindowEventCounter(TIME_WINDOW_MILLIS, NUM_OCCURRENCES_THRESHOLD);
     }
 
     /**
@@ -80,30 +91,49 @@
     @VisibleForTesting
     public void onChoppyVoice() {
         String title = "Call Quality Improvement";
-        //TODO: update call_quality_bluetooth_enhancement_suggestion with below before submitting:
-//        "Voice is not being transmitted properly via your bluetooth device."
-//                + "To improve, try:\n"
-//                + "1. moving your phone closer to your bluetooth device\n"
-//                + "2. using a different bluetooth device, or your phone's speaker\n";
-        popUpNotification(title,
-                mContext.getText(R.string.call_quality_notification_bluetooth_details));
+        Log.d(TAG, "Bluetooth choppy voice signal received.");
+        if (mSlidingWindowEventCounter.addOccurrence(SystemClock.elapsedRealtime())) {
+            timedNotify(title,
+                    mContext.getText(R.string.call_quality_notification_bluetooth_details));
+        }
     }
 
-    private void popUpNotification(String title, CharSequence details) {
-        int iconId = android.R.drawable.stat_notify_error;
+    // notify user only if you haven't in the last NOTIFICATION_BACKOFF_TIME_MILLIS milliseconds
+    private void timedNotify(String title, CharSequence details) {
+        if (!mContext.getResources().getBoolean(
+                R.bool.enable_bluetooth_call_quality_notification)) {
+            Log.d(TAG, "Bluetooth call quality notifications not enabled.");
+            return;
+        }
+        long now = SystemClock.elapsedRealtime();
+        if (now - mNotificationLastTime > NOTIFICATION_BACKOFF_TIME_MILLIS) {
+            int iconId = android.R.drawable.stat_notify_error;
 
-        Notification notification = new Notification.Builder(mContext)
-                .setSmallIcon(iconId)
-                .setWhen(System.currentTimeMillis())
-                .setAutoCancel(true)
-                .setContentTitle(title)
-                .setContentText(details)
-                .setStyle(new Notification.BigTextStyle().bigText(details))
-                .setAutoCancel(true)
-                .setChannelId(CALL_QUALITY_CHANNEL_ID)
-                .setOnlyAlertOnce(true)
-                .build();
+            Notification notification = new Notification.Builder(mContext)
+                    .setSmallIcon(iconId)
+                    .setWhen(System.currentTimeMillis())
+                    .setAutoCancel(true)
+                    .setContentTitle(title)
+                    .setContentText(details)
+                    .setStyle(new Notification.BigTextStyle().bigText(details))
+                    .setChannelId(CALL_QUALITY_CHANNEL_ID)
+                    .setOnlyAlertOnce(true)
+                    .build();
 
-        mNotificationManager.notify(TAG, BLUETOOTH_CHOPPY_VOICE_NOTIFICATION_ID, notification);
+            mNotificationManager.notify(TAG, BLUETOOTH_CHOPPY_VOICE_NOTIFICATION_ID, notification);
+            mNotificationLastTime = now;
+            Log.d(TAG, "Call quality signal received, showing notification");
+        } else {
+            Log.d(TAG, "Call quality signal received, but not showing notification, "
+                    + "as recently notified in the last "
+                    + NOTIFICATION_BACKOFF_TIME_MILLIS / 1000 + " seconds");
+        }
+    }
+
+    /**
+     * close the notifications that have been emitted during the call
+     */
+    public void clearNotifications() {
+        mNotificationManager.cancel(TAG, BLUETOOTH_CHOPPY_VOICE_NOTIFICATION_ID);
     }
 }
diff --git a/src/com/android/services/telephony/PstnIncomingCallNotifier.java b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
index 235cbce..4191367 100644
--- a/src/com/android/services/telephony/PstnIncomingCallNotifier.java
+++ b/src/com/android/services/telephony/PstnIncomingCallNotifier.java
@@ -301,6 +301,12 @@
                     if (CallComposerPictureManager.sTestMode) {
                         imsCallProfile.setCallExtra(ImsCallProfile.EXTRA_PICTURE_URL,
                                 CallComposerPictureManager.FAKE_SERVER_URL);
+                        imsCallProfile.setCallExtraInt(ImsCallProfile.EXTRA_PRIORITY,
+                                TelecomManager.PRIORITY_URGENT);
+                        imsCallProfile.setCallExtra(ImsCallProfile.EXTRA_CALL_SUBJECT,
+                                CallComposerPictureManager.FAKE_SUBJECT);
+                        imsCallProfile.setCallExtraParcelable(ImsCallProfile.EXTRA_LOCATION,
+                                CallComposerPictureManager.FAKE_LOCATION);
                     }
 
                     extras.putInt(TelecomManager.EXTRA_PRIORITY,
diff --git a/src/com/android/services/telephony/TelephonyConnection.java b/src/com/android/services/telephony/TelephonyConnection.java
index 7a59908..7000733 100755
--- a/src/com/android/services/telephony/TelephonyConnection.java
+++ b/src/com/android/services/telephony/TelephonyConnection.java
@@ -28,6 +28,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Messenger;
 import android.os.PersistableBundle;
 import android.telecom.BluetoothCallQualityReport;
 import android.telecom.CallAudioState;
@@ -95,6 +96,7 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
 
 /**
  * Base class for CDMA and GSM connections.
@@ -133,6 +135,7 @@
     private static final int MSG_ON_CONNECTION_EVENT = 19;
     private static final int MSG_REDIAL_CONNECTION_CHANGED = 20;
     private static final int MSG_REJECT = 21;
+    private static final int MSG_DTMF_DONE = 22;
 
     private static final String JAPAN_COUNTRY_CODE_WITH_PLUS_SIGN = "+81";
     private static final String JAPAN_ISO_COUNTRY_CODE = "JP";
@@ -303,6 +306,9 @@
                     int rejectReason = (int) msg.obj;
                     reject(rejectReason);
                     break;
+                case MSG_DTMF_DONE:
+                    Log.i(this, "MSG_DTMF_DONE");
+                    break;
 
                 case MSG_SET_CALL_RADIO_TECH:
                     int vrat = (int) msg.obj;
@@ -341,6 +347,8 @@
         }
     };
 
+    private final Messenger mHandlerMessenger = new Messenger(mHandler);
+
     /**
      * Handles {@link SuppServiceNotification}s pertinent to Telephony.
      * @param ssn the notification.
@@ -510,7 +518,7 @@
 
         @Override
         public void onStateChanged(android.telecom.Connection c, int state) {
-            mCommunicator.onStateChanged(c, state);
+            mCommunicator.onStateChanged(c.getTelecomCallId(), state);
         }
     }
 
@@ -742,6 +750,15 @@
                     extensionData.size());
             mRtpTransport.onRtpHeaderExtensionsReceived(extensionData);
         }
+
+        @Override
+        public void onReceivedDtmfDigit(char digit) {
+            if (mDtmfTransport == null) {
+                return;
+            }
+            Log.i(this, "onReceivedDtmfDigit: digit=%c", digit);
+            mDtmfTransport.onDtmfReceived(digit);
+        }
     };
 
     private TelephonyConnectionService mTelephonyConnectionService;
@@ -1139,24 +1156,33 @@
     }
 
     @Override
-    public void onCallFilteringCompleted(boolean isBlocked, boolean isInContacts,
-            CallScreeningService.CallResponse callScreeningResponse,
-            boolean isResponseFromSystemDialer) {
+    public void onCallFilteringCompleted(CallFilteringCompletionInfo callFilteringCompletionInfo) {
         // Check what the call screening service has to say, if it's a system dialer.
         boolean isAllowedToDisplayPicture;
+        String callScreeningPackage =
+                callFilteringCompletionInfo.getCallScreeningComponent() == null
+                        ? null
+                        : callFilteringCompletionInfo.getCallScreeningComponent().getPackageName();
+        boolean isResponseFromSystemDialer =
+                Objects.equals(getPhone().getContext()
+                        .getSystemService(TelecomManager.class).getSystemDialerPackage(),
+                        callScreeningPackage);
+        CallScreeningService.CallResponse callScreeningResponse =
+                callFilteringCompletionInfo.getCallResponse();
+
         if (isResponseFromSystemDialer && callScreeningResponse != null
                 && callScreeningResponse.getCallComposerAttachmentsToShow() >= 0) {
             isAllowedToDisplayPicture = (callScreeningResponse.getCallComposerAttachmentsToShow()
                     & CallScreeningService.CallResponse.CALL_COMPOSER_ATTACHMENT_PICTURE) != 0;
         } else {
-            isAllowedToDisplayPicture = isInContacts;
+            isAllowedToDisplayPicture = callFilteringCompletionInfo.isInContacts();
         }
 
         if (isImsConnection()) {
             ImsPhone imsPhone = (getPhone() instanceof ImsPhone) ? (ImsPhone) getPhone() : null;
             if (imsPhone != null
                     && imsPhone.getCallComposerStatus() == TelephonyManager.CALL_COMPOSER_STATUS_ON
-                    && !isBlocked && isAllowedToDisplayPicture) {
+                    && !callFilteringCompletionInfo.isBlocked() && isAllowedToDisplayPicture) {
                 ImsPhoneConnection originalConnection = (ImsPhoneConnection) mOriginalConnection;
                 ImsCallProfile profile = originalConnection.getImsCall().getCallProfile();
                 String serverUrl = CallComposerPictureManager.sTestMode
@@ -2351,7 +2377,7 @@
             }
 
             if (mCommunicator != null) {
-                mCommunicator.onStateChanged(this, getState());
+                mCommunicator.onStateChanged(getTelecomCallId(), getState());
             }
         }
     }
@@ -3144,6 +3170,9 @@
         setDisconnected(disconnectCause);
         notifyDisconnected(disconnectCause);
         notifyStateChanged(getState());
+        if (mCallQualityManager != null) {
+            mCallQualityManager.clearNotifications();
+        }
     }
 
     /**
@@ -3284,13 +3313,18 @@
     private void maybeConfigureDeviceToDeviceCommunication() {
         if (!getPhone().getContext().getResources().getBoolean(
                 R.bool.config_use_device_to_device_communication)) {
-            Log.d(this, "maybeConfigureDeviceToDeviceCommunication: not using D2D.");
+            Log.i(this, "maybeConfigureDeviceToDeviceCommunication: not using D2D.");
             return;
         }
         if (!isImsConnection()) {
-            Log.d(this, "maybeConfigureDeviceToDeviceCommunication: not an IMS connection.");
+            Log.i(this, "maybeConfigureDeviceToDeviceCommunication: not an IMS connection.");
             return;
         }
+        if (mTreatAsEmergencyCall || mIsNetworkIdentifiedEmergencyCall) {
+            Log.i(this, "maybeConfigureDeviceToDeviceCommunication: emergency call; no D2D");
+            return;
+        }
+
         // Implement abstracted out RTP functionality the RTP transport depends on.
         RtpAdapter rtpAdapter = new RtpAdapter() {
             @Override
@@ -3309,8 +3343,11 @@
                 if (!isImsConnection()) {
                     Log.w(TelephonyConnection.this, "sendRtpHeaderExtensions: not an ims conn.");
                 }
-                Log.d(TelephonyConnection.this, "sendRtpHeaderExtensions: sending %d messages",
-                        rtpHeaderExtensions.size());
+
+                Log.i(TelephonyConnection.this, "sendRtpHeaderExtensions: sending: %s",
+                        rtpHeaderExtensions.stream()
+                                .map(r -> r.toString())
+                                .collect(Collectors.joining(",")));
                 ImsPhoneConnection originalConnection =
                         (ImsPhoneConnection) mOriginalConnection;
                 originalConnection.sendRtpHeaderExtensions(rtpHeaderExtensions);
@@ -3322,10 +3359,12 @@
             if (!isImsConnection()) {
                 Log.w(TelephonyConnection.this, "sendDtmf: not an ims conn.");
             }
-            Log.d(TelephonyConnection.this, "sendDtmf: send digit %c", digit);
+            Log.i(TelephonyConnection.this, "sendDtmf: send digit %c", digit);
             ImsPhoneConnection originalConnection =
                     (ImsPhoneConnection) mOriginalConnection;
-            originalConnection.getImsCall().sendDtmf(digit, null /* result msg not needed */);
+            Message dtmfComplete = mHandler.obtainMessage(MSG_DTMF_DONE);
+            dtmfComplete.replyTo = mHandlerMessenger;
+            originalConnection.getImsCall().sendDtmf(digit, dtmfComplete);
         };
         ContentResolver cr = getPhone().getContext().getContentResolver();
         mDtmfTransport = new DtmfTransport(dtmfAdapter, new Timeouts.Adapter(cr),
@@ -3350,14 +3389,13 @@
     @Override
     public void onMessagesReceived(@NonNull Set<Communicator.Message> messages) {
         Log.i(this, "onMessagesReceived: got d2d messages: %s", messages);
-        // TODO: Actually do something WITH the messages.
-
-        // TODO: Remove this prior to launch.
-        // This is just here for debug purposes; send as a connection event so that it
-        // will be output in the Telecom logs.
+        // Send connection events up to Telecom so that we can relay the messages to a valid
+        // CallDiagnosticService which is bound.
         for (Communicator.Message msg : messages) {
-            sendConnectionEvent("D2D_" + Communicator.messageToString(msg.getType())
-                + "_" + Communicator.valueToString(msg.getType(), msg.getValue()), null);
+            Bundle extras = new Bundle();
+            extras.putInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_TYPE, msg.getType());
+            extras.putInt(Connection.EXTRA_DEVICE_TO_DEVICE_MESSAGE_VALUE, msg.getValue());
+            sendConnectionEvent(Connection.EVENT_DEVICE_TO_DEVICE_MESSAGE, extras);
         }
     }
 
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 219c782..0803178 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -2538,6 +2538,26 @@
        });
     }
 
+    /**
+     * Overrides the current D2D transport, forcing the specified one to be active.  Used for test.
+     * @param transport The class simple name of the transport to make active.
+     */
+    public void setActiveDeviceToDeviceTransport(@NonNull String transport) {
+        getAllConnections().stream()
+                .filter(f -> f instanceof TelephonyConnection)
+                .forEach(t -> {
+                    TelephonyConnection tc = (TelephonyConnection) t;
+                    Communicator c = tc.getCommunicator();
+                    if (c == null) {
+                        Log.w(this, "setActiveDeviceToDeviceTransport: D2D not enabled");
+                        return;
+                    }
+                    Log.i(this, "setActiveDeviceToDeviceTransport: callId=%s, set to: %s",
+                            tc.getTelecomCallId(), transport);
+                    c.setTransportActive(transport);
+                });
+    }
+
     private PhoneAccountHandle adjustAccountHandle(Phone phone,
             PhoneAccountHandle origAccountHandle) {
         int origSubId = PhoneUtils.getSubIdForPhoneAccountHandle(origAccountHandle);
diff --git a/src/com/android/services/telephony/rcs/UceControllerManager.java b/src/com/android/services/telephony/rcs/UceControllerManager.java
index 09288f1..20ea17b 100644
--- a/src/com/android/services/telephony/rcs/UceControllerManager.java
+++ b/src/com/android/services/telephony/rcs/UceControllerManager.java
@@ -23,12 +23,12 @@
 import android.telephony.ims.RcsUceAdapter.PublishState;
 import android.telephony.ims.aidl.IRcsUceControllerCallback;
 import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 
 import com.android.ims.RcsFeatureManager;
 import com.android.ims.rcs.uce.UceController;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.IndentingPrintWriter;
 
 import java.io.PrintWriter;
 import java.util.List;
@@ -253,8 +253,7 @@
         IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
         pw.println("UceControllerManager" + "[" + mSlotId + "]:");
         pw.increaseIndent();
-        pw.println("UceController available = " + mUceController != null);
-        //TODO: Add dump for UceController
+        mUceController.dump(pw);
         pw.decreaseIndent();
     }
 }
diff --git a/testapps/TestRcsApp/TestApp/Android.bp b/testapps/TestRcsApp/TestApp/Android.bp
index 2da47dd..4496a8e 100644
--- a/testapps/TestRcsApp/TestApp/Android.bp
+++ b/testapps/TestRcsApp/TestApp/Android.bp
@@ -18,6 +18,7 @@
         "androidx-constraintlayout_constraintlayout",
         "aosp_test_rcs_client_base",
         "androidx.appcompat_appcompat",
+        "libphonenumber-platform"
     ],
 
     certificate: "platform",
diff --git a/testapps/TestRcsApp/TestApp/AndroidManifest.xml b/testapps/TestRcsApp/TestApp/AndroidManifest.xml
index 391d30d..2eea909 100644
--- a/testapps/TestRcsApp/TestApp/AndroidManifest.xml
+++ b/testapps/TestRcsApp/TestApp/AndroidManifest.xml
@@ -19,8 +19,8 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.google.android.sample.rcsclient"
-    android:versionCode="4"
-    android:versionName="1.0.3">
+    android:versionCode="6"
+    android:versionName="1.0.5">
 
     <uses-sdk
         android:minSdkVersion="30"
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java
index 1db4af7..0531209 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/ChatActivity.java
@@ -168,8 +168,10 @@
                 });
             });
         } catch (Exception e) {
-            Log.e(TAG, e.getMessage());
+            Log.e(TAG, "Exception: " + e);
             e.printStackTrace();
+            Toast.makeText(this, getResources().getString(R.string.session_failed),
+                    Toast.LENGTH_SHORT).show();
         }
     }
 
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java
index 9d27fbc..0447d1a 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/ChatManager.java
@@ -91,6 +91,21 @@
         mImsService.setListener((session) -> {
             Log.i(TAG, "onIncomingSession()");
             mContactSessionMap.put(session.getRemoteUri(), session);
+            session.setListener(
+                    // implement onMessageReceived()
+                    (message) -> {
+                        mFixedThreadPool.execute(() -> {
+                            String msg = message.content();
+                            String phoneNumber = getNumberFromUri(
+                                    session.getRemoteUri().toString());
+                            if (TextUtils.isEmpty(phoneNumber)) {
+                                Log.i(TAG, "dest number is empty, uri:"
+                                        + session.getRemoteUri());
+                            } else {
+                                addNewMessage(msg, phoneNumber, SELF);
+                            }
+                        });
+                    });
         });
     }
 
diff --git a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/NumberUtils.java b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/NumberUtils.java
index 72cbf3f..14d3b9c 100644
--- a/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/NumberUtils.java
+++ b/testapps/TestRcsApp/TestApp/src/com/google/android/sample/rcsclient/util/NumberUtils.java
@@ -17,8 +17,12 @@
 package com.google.android.sample.rcsclient.util;
 
 import android.content.Context;
-import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.Phonenumber;
 
 public class NumberUtils {
 
@@ -30,6 +34,13 @@
     public static String formatNumber(Context context, String number) {
         TelephonyManager manager = context.getSystemService(TelephonyManager.class);
         String simCountryIso = manager.getSimCountryIso().toUpperCase();
-        return PhoneNumberUtils.formatNumberToE164(number, simCountryIso);
+        PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+        try {
+            Phonenumber.PhoneNumber phoneNumber = util.parse(number, simCountryIso);
+            return util.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
+        } catch (NumberParseException e) {
+            Log.w("NumberUtils", "formatNumber: could not format " + number + ", error: " + e);
+        }
+        return null;
     }
 }
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/ImsPdnNetworkFetcher.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/ImsPdnNetworkFetcher.java
index 0011011..57bb75a 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/ImsPdnNetworkFetcher.java
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/ImsPdnNetworkFetcher.java
@@ -82,7 +82,7 @@
         return result;
     }
 
-    private ConnectivityManager getConnectivityManager() {
+    ConnectivityManager getConnectivityManager() {
         return context.getSystemService(ConnectivityManager.class);
     }
 }
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpManager.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpManager.java
index 47326bd..81abe89 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpManager.java
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpManager.java
@@ -17,6 +17,7 @@
 package com.android.libraries.rcs.simpleclient.protocol.msrp;
 
 import android.content.Context;
+import android.net.ConnectivityManager;
 import android.net.Network;
 
 import com.google.common.util.concurrent.Futures;
@@ -24,6 +25,7 @@
 import com.google.common.util.concurrent.MoreExecutors;
 
 import java.io.IOException;
+import java.net.InetAddress;
 import java.net.Socket;
 
 /** Provides creating and managing {@link MsrpSession} */
@@ -34,24 +36,27 @@
         imsPdnNetworkFetcher = new ImsPdnNetworkFetcher(context);
     }
 
-    private static MsrpSession createMsrpSession(
-            Network network, String host, int port, MsrpSessionListener listener)
-            throws IOException {
-        Socket socket = network.getSocketFactory().createSocket(host, port);
-        MsrpSession msrpSession = new MsrpSession(socket, listener);
+    private static MsrpSession createMsrpSession(ConnectivityManager manager,
+            Network network, String host, int port, String localIp, int localPort,
+            MsrpSessionListener listener) throws IOException {
+        Socket socket = network.getSocketFactory().createSocket(host, port,
+                InetAddress.getByName(localIp), localPort);
+        MsrpSession msrpSession = new MsrpSession(manager,
+                network, socket, listener);
         Thread thread = new Thread(msrpSession::run);
         thread.start();
         return msrpSession;
     }
 
     public ListenableFuture<MsrpSession> createMsrpSession(
-            String host, int port, MsrpSessionListener listener) {
+            String host, int port, String localIp, int localPort, MsrpSessionListener listener) {
         return Futures.transformAsync(
                 imsPdnNetworkFetcher.getImsPdnNetwork(),
                 network -> {
                     if (network != null) {
                         return Futures.immediateFuture(
-                                createMsrpSession(network, host, port, listener));
+                                createMsrpSession(imsPdnNetworkFetcher.getConnectivityManager(),
+                                        network, host, port, localIp, localPort, listener));
                     } else {
                         return Futures.immediateFailedFuture(
                                 new IllegalStateException("Network is null"));
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSession.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSession.java
index 96ca19c..3f8b986 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSession.java
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/msrp/MsrpSession.java
@@ -19,6 +19,16 @@
 import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Method.SEND;
 import static com.android.libraries.rcs.simpleclient.protocol.msrp.MsrpChunk.Method.UNKNOWN;
 
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.QosCallback;
+import android.net.QosCallbackException;
+import android.net.QosSession;
+import android.net.QosSessionAttributes;
+import android.net.QosSocketInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
 
@@ -26,6 +36,7 @@
 
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -38,6 +49,7 @@
  * Provides MSRP sending and receiving messages ability.
  */
 public class MsrpSession {
+    private final Network network;
     private final Socket socket;
     private final InputStream input;
     private final OutputStream output;
@@ -45,13 +57,51 @@
     private final ConcurrentHashMap<String, MsrpTransaction> transactions =
             new ConcurrentHashMap<>();
     private final MsrpSessionListener listener;
+    private final ConnectivityManager connectivityManager;
+    private final String LOG_TAG = MsrpSession.class.getSimpleName();
 
     /** Creates a new MSRP session on the given listener and the provided streams. */
-    MsrpSession(Socket socket, MsrpSessionListener listener) throws IOException {
+    MsrpSession(ConnectivityManager connectivityManager, Network network, Socket socket,
+            MsrpSessionListener listener) throws IOException {
+        this.connectivityManager = connectivityManager;
+        this.network = network;
         this.socket = socket;
         this.input = socket.getInputStream();
         this.output = socket.getOutputStream();
         this.listener = listener;
+
+        listenForBearer();
+    }
+
+    private final QosCallback qosCallback = new QosCallback() {
+        @Override
+        public void onError(@NonNull QosCallbackException exception) {
+            Log.e(LOG_TAG, "onError: " + exception.toString());
+            super.onError(exception);
+        }
+
+        @Override
+        public void onQosSessionAvailable(@NonNull QosSession session,
+                @NonNull QosSessionAttributes sessionAttributes) {
+            Log.d(LOG_TAG, "onQosSessionAvailable: " + session.toString() + ", "
+                    + sessionAttributes.toString());
+            super.onQosSessionAvailable(session, sessionAttributes);
+        }
+
+        @Override
+        public void onQosSessionLost(@NonNull QosSession session) {
+            Log.e(LOG_TAG, "onQosSessionLost: " + session.toString());
+            super.onQosSessionLost(session);
+        }
+    };
+
+    private void listenForBearer() {
+        try {
+            connectivityManager.registerQosCallback(new QosSocketInfo(network, socket),
+                    qosCallback, MoreExecutors.directExecutor());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
@@ -110,6 +160,7 @@
         if (isOpen.getAndSet(false)) {
             output.flush();
         }
+        connectivityManager.unregisterQosCallback(qosCallback);
         socket.close();
     }
 
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtils.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtils.java
index 7605fb5..13fa53a 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtils.java
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/protocol/sip/SipUtils.java
@@ -54,6 +54,7 @@
 import javax.sip.address.SipURI;
 import javax.sip.address.URI;
 import javax.sip.header.ContactHeader;
+import javax.sip.header.Header;
 import javax.sip.header.HeaderFactory;
 import javax.sip.header.ViaHeader;
 import javax.sip.message.Request;
@@ -262,6 +263,7 @@
 
         Via via = (Via) invite.getTopmostVia().clone();
         via.removeParameter("branch");
+        via.setBranch(Utils.getInstance().generateBranchId());
         request.addHeader(via);
         request.addHeader(
                 sHeaderFactory.createFromHeader(invite.getFrom().getAddress(),
@@ -290,6 +292,27 @@
         if (code == Response.OK) {
             response.setMessageContent(SDP_CONTENT_TYPE, SDP_CONTENT_SUB_TYPE, sdp.encode());
         }
+        response.setToTag(Utils.getInstance().generateTag());
+
+        // Set a Contact header.
+        response.setHeader(generateContactHeader(configuration));
+
+        // Set Conversation-ID and Contribution-ID
+        Header conversationIdHeader = invite.getHeader(CONVERSATION_ID_HEADER_NAME);
+        if (conversationIdHeader != null) {
+            response.setHeader((Header) conversationIdHeader.clone());
+        }
+        Header contributionIdHeader = invite.getHeader(CONTRIBUTION_ID_HEADER_NAME);
+        if (conversationIdHeader != null) {
+            response.setHeader((Header) contributionIdHeader.clone());
+        }
+
+        // Set P-Preferred-Identity
+        List<String> associatedUris = configuration.getAssociatedUris();
+        String preferredUri = Iterables.getFirst(associatedUris,
+                configuration.getPublicUserIdentity());
+        response.setHeader(
+                sHeaderFactory.createHeader(PPreferredIdentityHeader.NAME, preferredUri));
 
         // Set PANI and PLANI if exists
         if (configuration.getPaniHeader() != null) {
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationControllerImpl.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationControllerImpl.java
index ac37110..f1868fc 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationControllerImpl.java
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/registration/RegistrationControllerImpl.java
@@ -47,6 +47,7 @@
 import com.google.common.util.concurrent.SettableFuture;
 
 import java.text.ParseException;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
@@ -152,6 +153,7 @@
                 new DelegateConnectionMessageCallback() {
                     @Override
                     public void onMessageReceived(@NonNull SipMessage message) {
+                        message = repairHeaderSection(message);
                         SipSessionListener listener = sipSessionListener;
                         if (listener != null) {
                             try {
@@ -386,6 +388,9 @@
             String serviceRoutes =
                     configuration.getString(
                             SipDelegateImsConfiguration.KEY_SIP_CONFIG_SERVICE_ROUTE_HEADER_STRING);
+            if (TextUtils.isEmpty(serviceRoutes)) {
+                return Collections.emptyList();
+            }
             return Splitter.on(',').trimResults().splitToList(serviceRoutes);
         }
 
@@ -424,6 +429,23 @@
             return configuration.getInt(
                     SipDelegateImsConfiguration.KEY_SIP_CONFIG_MAX_PAYLOAD_SIZE_ON_UDP_INT, 1500);
         }
+
+        /**
+         * There is a modem issue where "ia:" is returned back instead of "Via:". Fix that locally
+         * for now.
+         * @return A SipMessage with the corrected header section.
+         */
+        private static SipMessage repairHeaderSection(SipMessage message) {
+            String headers = message.getHeaderSection();
+
+            if (headers.startsWith("ia:")) {
+                headers = "V" + headers;
+                Log.i(TAG, "repairHeaderSection: detected malformed via: "
+                        + message.getHeaderSection().substring(0, 10) + "->"
+                        + headers.substring(0, 10));
+            }
+            return new SipMessage(message.getStartLine(), headers, message.getContent());
+        }
     }
 }
 
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/MinimalCpmChatService.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/MinimalCpmChatService.java
index b204de6..4b913f7 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/MinimalCpmChatService.java
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/MinimalCpmChatService.java
@@ -54,7 +54,7 @@
 public class MinimalCpmChatService implements ImsService {
     public static final String CPM_SESSION_TAG =
             "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
-    private static final String TAG = SimpleChatSession.class.getSimpleName();
+    private static final String TAG = MinimalCpmChatService.class.getSimpleName();
     private final Map<String, SimpleChatSession> mTransactions = new HashMap<>();
     private final Map<String, SimpleChatSession> mDialogs = new HashMap<>();
 
@@ -112,7 +112,7 @@
     }
 
     ListenableFuture<Boolean> sendSipRequest(SIPRequest msg, SimpleChatSession session) {
-        Log.i(TAG, "sendSipRequest");
+        Log.i(TAG, "sendSipRequest:\r\n" + msg);
         if (!TextUtils.equals(msg.getMethod(), Request.ACK)) {
             mTransactions.put(msg.getTransactionId(), session);
         }
@@ -126,7 +126,7 @@
     }
 
     ListenableFuture<Boolean> sendSipResponse(SIPResponse msg, SimpleChatSession session) {
-        Log.i(TAG, "sendSipRequest");
+        Log.i(TAG, "sendSipResponse:\r\n" + msg);
         if (TextUtils.equals(msg.getCSeq().getMethod(), Request.BYE)) {
             mDialogs.remove(msg.getDialogId(/* isServer= */ true));
         } else if (TextUtils.equals(msg.getCSeq().getMethod(), Request.INVITE)
@@ -139,6 +139,7 @@
     }
 
     private void handleRequest(SIPRequest request) {
+        Log.i(TAG, "handleRequest:\r\n" + request);
         String dialogId = request.getDialogId(/* isServer= */ true);
         if (mDialogs.containsKey(dialogId)) {
             SimpleChatSession session = mDialogs.get(dialogId);
diff --git a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSession.java b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSession.java
index 6813c91..4cc474c 100644
--- a/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSession.java
+++ b/testapps/TestRcsApp/aosp_test_rcsclient/src/com/android/libraries/rcs/simpleclient/service/chat/SimpleChatSession.java
@@ -167,8 +167,10 @@
         mStartFuture = future;
         mRemoteUri = SipUtils.createUri(telUriContact);
         try {
-            SipSessionConfiguration configuration = mContext.getSipSession().getSessionConfiguration();
-            SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(), false);
+            SipSessionConfiguration configuration =
+                    mContext.getSipSession().getSessionConfiguration();
+            SimpleSdpMessage sdp = SdpUtils.createSdpForMsrp(configuration.getLocalIpAddress(),
+                    false);
             SIPRequest invite =
                     SipUtils.buildInvite(
                             mContext.getSipSession().getSessionConfiguration(),
@@ -229,6 +231,7 @@
         try {
             SIPResponse response = SipUtils.buildInviteResponse(configuration, invite, statusCode,
                     sdp);
+            mLocalSdp = sdp;
             return Futures.transform(
                     mService.sendSipResponse(response, this), result -> null,
                     MoreExecutors.directExecutor());
@@ -402,14 +405,16 @@
     private void startMsrpSession(SimpleSdpMessage remoteSdp) {
         Log.d(TAG, "Start MSRP session: " + remoteSdp);
         if (remoteSdp.getAddress().isPresent() && remoteSdp.getPort().isPresent()) {
+            String localIp = getLocalIp();
             Futures.addCallback(
                     mMsrpManager.createMsrpSession(
-                            remoteSdp.getAddress().get(), remoteSdp.getPort().getAsInt(),
-                            this::receiveMsrpChunk),
+                            remoteSdp.getAddress().get(), remoteSdp.getPort().getAsInt(), localIp,
+                            0 /* localPort */, this::receiveMsrpChunk),
                     new FutureCallback<MsrpSession>() {
                         @Override
                         public void onSuccess(MsrpSession result) {
                             mMsrpSession = result;
+                            sendEmptyPacket();
                             notifySuccess();
                         }
 
@@ -430,6 +435,30 @@
         }
     }
 
+    private void sendEmptyPacket() {
+        MsrpChunk msrpChunk =
+                MsrpChunk.newBuilder()
+                        .method(MsrpChunk.Method.SEND)
+                        .transactionId(MsrpUtils.generateRandomId())
+                        .continuation(Continuation.COMPLETE)
+                        .addHeader(MsrpConstants.HEADER_TO_PATH, mRemoteSdp.getPath().get())
+                        .addHeader(MsrpConstants.HEADER_FROM_PATH, mLocalSdp.getPath().get())
+                        .addHeader(MsrpConstants.HEADER_FAILURE_REPORT,
+                                MsrpConstants.REPORT_VALUE_NO)
+                        .addHeader(MsrpConstants.HEADER_SUCCESS_REPORT,
+                                MsrpConstants.REPORT_VALUE_NO)
+                        .addHeader(MsrpConstants.HEADER_BYTE_RANGE, "1/0-0")
+                        .addHeader(MsrpConstants.HEADER_MESSAGE_ID, MsrpUtils.generateRandomId())
+                        .build();
+
+        mMsrpSession.send(msrpChunk);
+    }
+
+    private String getLocalIp() {
+        SipSessionConfiguration configuration = mContext.getSipSession().getSessionConfiguration();
+        return configuration.getLocalIpAddress();
+    }
+
     private void receiveMsrpChunk(MsrpChunk chunk) {
         Log.d(TAG, "Received msrp= " + chunk + " conversation=" + mConversationId);
 
@@ -441,6 +470,7 @@
 
         String contentType = contentTypeHeader.value();
         if ("message/cpim".equals(contentType)) {
+            Log.d(TAG, "Received CPIM: " + new String(chunk.content(), UTF_8));
             try {
                 SimpleCpimMessage cpim = SimpleCpimMessage.parse(chunk.content());
                 if (mListener != null) {
diff --git a/tests/src/com/android/phone/RcsProvisioningMonitorTest.java b/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
index f8c78d4..f77dd55 100644
--- a/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
+++ b/tests/src/com/android/phone/RcsProvisioningMonitorTest.java
@@ -55,20 +55,21 @@
 import android.telephony.ims.RcsConfig;
 import android.telephony.ims.aidl.IImsConfig;
 import android.telephony.ims.aidl.IRcsConfigCallback;
-import android.telephony.ims.feature.ImsFeature;
 import android.test.mock.MockContentProvider;
 import android.test.mock.MockContentResolver;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.TestableLooper;
 import android.util.Log;
 
+import com.android.ims.FeatureConnector;
+import com.android.ims.RcsFeatureManager;
 import com.android.internal.telephony.ITelephony;
-import com.android.internal.telephony.ims.ImsResolver;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.invocation.InvocationOnMock;
@@ -130,7 +131,13 @@
     @Mock
     private ITelephony.Stub mITelephony;
     @Mock
-    private ImsResolver mImsResolver;
+    private RcsFeatureManager mFeatureManager;
+    @Mock
+    private RcsProvisioningMonitor.FeatureConnectorFactory<RcsFeatureManager> mFeatureFactory;
+    @Mock
+    private FeatureConnector<RcsFeatureManager> mFeatureConnector;
+    @Captor
+    ArgumentCaptor<FeatureConnector.Listener<RcsFeatureManager>> mConnectorListener;
     @Mock
     private IImsConfig.Stub mIImsConfig;
     @Mock
@@ -246,8 +253,7 @@
         when(mCursor.getColumnIndexOrThrow(any())).thenReturn(1);
         when(mCursor.getBlob(anyInt())).thenReturn(
                 RcsConfig.compressGzip(SAMPLE_CONFIG.getBytes()));
-        when(mPhone.getImsResolver()).thenReturn(mImsResolver);
-        when(mImsResolver.getImsConfig(anyInt(), anyInt())).thenReturn(mIImsConfig);
+
         mHandlerThread = new HandlerThread("RcsProvisioningMonitorTest");
         mHandlerThread.start();
     }
@@ -268,8 +274,9 @@
     @Test
     @SmallTest
     public void testInitWithSavedConfig() throws Exception {
-        createMonitor(3);
         ArgumentCaptor<Intent> captorIntent = ArgumentCaptor.forClass(Intent.class);
+        createMonitor(3);
+
         for (int i = 0; i < 3; i++) {
             assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(),
                     mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE + i)));
@@ -279,8 +286,6 @@
         Intent capturedIntent = captorIntent.getAllValues().get(1);
         assertEquals(ProvisioningManager.ACTION_RCS_SINGLE_REGISTRATION_CAPABILITY_UPDATE,
                 capturedIntent.getAction());
-        PhoneGlobals.getInstance().getImsResolver();
-        verify(mPhone, atLeastOnce()).getImsResolver();
         verify(mIImsConfig, times(3)).notifyRcsAutoConfigurationReceived(any(), anyBoolean());
     }
 
@@ -293,6 +298,7 @@
 
         verify(mPhone, times(3)).sendBroadcast(captorIntent.capture(), any());
         Intent capturedIntent = captorIntent.getAllValues().get(1);
+
         assertEquals(ProvisioningManager.ACTION_RCS_SINGLE_REGISTRATION_CAPABILITY_UPDATE,
                 capturedIntent.getAction());
         //Should not notify null config
@@ -304,8 +310,7 @@
     public void testSubInfoChanged() throws Exception {
         createMonitor(3);
         ArgumentCaptor<Intent> captorIntent = ArgumentCaptor.forClass(Intent.class);
-        mExecutor.execute(() -> mSubChangedListener.onSubscriptionsChanged());
-        processAllMessages();
+
         for (int i = 0; i < 3; i++) {
             assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(),
                     mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE + i)));
@@ -354,7 +359,7 @@
         byte[] configCached = mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE);
 
         assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(), configCached));
-        verify(mIImsConfig, never()).notifyRcsAutoConfigurationRemoved();
+        verify(mIImsConfig, times(1)).notifyRcsAutoConfigurationRemoved();
         // The api should be called 2 times, one happens when monitor is initilized,
         // Another happens when DMS is changed.
         verify(mIImsConfig, times(2))
@@ -419,8 +424,6 @@
         mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, SAMPLE_CONFIG.getBytes(), false);
         processAllMessages();
 
-        verify(mImsResolver, atLeastOnce()).getImsConfig(
-                anyInt(), eq(ImsFeature.FEATURE_RCS));
         verify(mIImsConfig, atLeastOnce()).notifyRcsAutoConfigurationReceived(
                 argumentBytes.capture(), eq(false));
         assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(), argumentBytes.getValue()));
@@ -434,8 +437,6 @@
         mRcsProvisioningMonitor.requestReconfig(FAKE_SUB_ID_BASE);
         processAllMessages();
 
-        verify(mImsResolver, atLeastOnce()).getImsConfig(
-                anyInt(), eq(ImsFeature.FEATURE_RCS));
         verify(mIImsConfig, times(1)).notifyRcsAutoConfigurationRemoved();
         verify(mIImsConfig, times(1)).triggerRcsReconfiguration();
     }
@@ -519,19 +520,79 @@
         verify(mCallback, times(1)).onRemoved();
     }
 
-    private void createMonitor(int subCount) {
+    @Test
+    @SmallTest
+    public void testRcsConnectedAndDisconnected() throws Exception {
+        createMonitor(1);
+        mRcsProvisioningMonitor.registerRcsProvisioningChangedCallback(
+                FAKE_SUB_ID_BASE, mCallback);
+
+        verify(mIImsConfig, times(1))
+                .notifyRcsAutoConfigurationReceived(any(), anyBoolean());
+
+        mConnectorListener.getValue().connectionUnavailable(0);
+
+        verify(mCallback, times(1)).onRemoved();
+    }
+
+    @Test
+    @SmallTest
+    public void testTestModeEnabledAndDisabled() throws Exception {
+        when(mCursor.getBlob(anyInt())).thenReturn(null);
+        createMonitor(1);
+
+        verify(mCursor, times(1)).getBlob(anyInt());
+
+        mRcsProvisioningMonitor.setTestModeEnabled(true);
+        processAllMessages();
+
+        //should not query db in test mode
+        verify(mCursor, times(1)).getBlob(anyInt());
+        assertNull(mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, SAMPLE_CONFIG.getBytes(), false);
+        processAllMessages();
+
+        //config cahced in monitor should be updated, but db should not
+        assertNull(mProvider.getContentValues());
+        assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(),
+                mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE)));
+
+        //verify if monitor goes back to normal mode
+        mRcsProvisioningMonitor.setTestModeEnabled(false);
+        processAllMessages();
+
+        verify(mCursor, times(2)).getBlob(anyInt());
+        assertNull(mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE));
+
+        mRcsProvisioningMonitor.updateConfig(FAKE_SUB_ID_BASE, SAMPLE_CONFIG.getBytes(), false);
+        processAllMessages();
+
+        assertTrue(Arrays.equals(SAMPLE_CONFIG.getBytes(),
+                mRcsProvisioningMonitor.getConfig(FAKE_SUB_ID_BASE)));
+        assertTrue(Arrays.equals(RcsConfig.compressGzip(SAMPLE_CONFIG.getBytes()),
+                (byte[]) mProvider.getContentValues().get(SimInfo.COLUMN_RCS_CONFIG)));
+    }
+
+    private void createMonitor(int subCount) throws Exception {
         if (Looper.myLooper() == null) {
             Looper.prepare();
         }
         makeFakeActiveSubIds(subCount);
+        when(mFeatureFactory.create(any(), anyInt(), mConnectorListener.capture(), any(), any()))
+                .thenReturn(mFeatureConnector);
+        when(mFeatureManager.getConfig()).thenReturn(mIImsConfig);
         mRcsProvisioningMonitor = new RcsProvisioningMonitor(mPhone, mHandlerThread.getLooper(),
-                mRoleManager);
+                mRoleManager, mFeatureFactory);
         mHandler = mRcsProvisioningMonitor.getHandler();
         try {
             mLooper = new TestableLooper(mHandler.getLooper());
         } catch (Exception e) {
             logd("Unable to create looper from handler.");
         }
+        mConnectorListener.getValue().connectionReady(mFeatureManager);
+
+        verify(mFeatureConnector, atLeastOnce()).connect();
     }
 
     private void broadcastCarrierConfigChange(int subId) {
diff --git a/tests/src/com/android/phone/SimPhonebookProviderTest.java b/tests/src/com/android/phone/SimPhonebookProviderTest.java
index 8778529..4ab92a7 100644
--- a/tests/src/com/android/phone/SimPhonebookProviderTest.java
+++ b/tests/src/com/android/phone/SimPhonebookProviderTest.java
@@ -118,6 +118,14 @@
 
     @Test
     public void query_entityFiles_returnsCursorWithCorrectProjection() {
+        // Null projection
+        try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, null, null,
+                null)) {
+            assertThat(Objects.requireNonNull(cursor).getColumnNames()).asList()
+                    .containsExactlyElementsIn(
+                            SimPhonebookProvider.ELEMENTARY_FILES_ALL_COLUMNS);
+        }
+
         // Empty projection
         try (Cursor cursor = mResolver.query(ElementaryFiles.CONTENT_URI, new String[0], null,
                 null)) {
@@ -211,18 +219,38 @@
     }
 
     @Test
+    public void query_entityFilesItem_nullProjection_returnsCursorWithCorrectProjection() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        // Null projection
+        try (Cursor cursor = mResolver.query(ElementaryFiles.getItemUri(1, EF_ADN), null, null,
+                null)) {
+            assertThat(Objects.requireNonNull(cursor).getColumnNames()).asList()
+                    .containsExactlyElementsIn(
+                            SimPhonebookProvider.ELEMENTARY_FILES_ALL_COLUMNS);
+        }
+    }
+
+    @Test
     public void query_adnRecords_returnsCursorWithMatchingProjection() {
         setupSimsWithSubscriptionIds(1);
         mIccPhoneBook.makeAllEfsSupported(1);
         Uri contentAdn = SimRecords.getContentUri(1, EF_ADN);
 
+        // Null projection
+        try (Cursor cursor = mResolver.query(contentAdn, null, null, null)) {
+            assertThat(Objects.requireNonNull(cursor).getColumnNames()).asList()
+                    .containsExactlyElementsIn(SimPhonebookProvider.SIM_RECORDS_ALL_COLUMNS);
+        }
+
         // Empty projection
         try (Cursor cursor = mResolver.query(contentAdn, new String[0], null, null)) {
             assertThat(cursor).hasColumnNames();
         }
 
         // Single column
-        try (Cursor cursor = mResolver.query(contentAdn, new String[] {
+        try (Cursor cursor = mResolver.query(contentAdn, new String[]{
                 SimRecords.PHONE_NUMBER
         }, null, null)) {
             assertThat(cursor).hasColumnNames(SimRecords.PHONE_NUMBER);
@@ -531,6 +559,20 @@
     }
 
     @Test
+    public void query_itemUriNullProjection_returnsCursorWithAllColumns() {
+        setupSimsWithSubscriptionIds(1);
+        mIccPhoneBook.makeAllEfsSupported(1);
+
+        try (Cursor cursor = mResolver.query(SimRecords.getItemUri(1, ElementaryFiles.EF_ADN, 1),
+                null, null, null)
+        ) {
+            assertThat(Objects.requireNonNull(
+                    cursor).getColumnNames()).asList().containsExactlyElementsIn(
+                    SimPhonebookProvider.SIM_RECORDS_ALL_COLUMNS);
+        }
+    }
+
+    @Test
     public void query_itemUriEmptyRecord_returnsEmptyCursor() {
         setupSimsWithSubscriptionIds(1);
         mIccPhoneBook.setRecordsSize(1, IccConstants.EF_ADN, 1, 30);