Iterate through Phones when routing Emergency Call
am: 0159eeb32c

* commit '0159eeb32c48ecb17856d54f193deb2f04323285':
  Iterate through Phones when routing Emergency Call

Change-Id: I8993a736e2371f45bbfb04f63a75b516271edfbf
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9655756..781bd40 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -655,8 +655,7 @@
         <receiver android:name="com.android.phone.vvm.omtp.sms.OmtpMessageReceiver"
             android:exported="true">
             <intent-filter>
-                <action android:name="android.intent.action.DATA_SMS_RECEIVED" />
-                <data android:scheme="sms" />
+                <action android:name="android.intent.action.VOICEMAIL_SMS_RECEIVED"/>
             </intent-filter>
         </receiver>
         <receiver
diff --git a/proguard.flags b/proguard.flags
index c4af490..41e26a1 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -1 +1,6 @@
+# Keep classes and methods that have the guava @VisibleForTesting annotation
+-keep @**.VisibleForTesting class *
+-keepclassmembers class * {
+@**.VisibleForTesting *;
+}
 -verbose
diff --git a/res/xml/vvm_config.xml b/res/xml/vvm_config.xml
new file mode 100644
index 0000000..57a1050
--- /dev/null
+++ b/res/xml/vvm_config.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<list name="carrier_config_list">
+  <pbundle_as_map>
+    <!-- Test -->
+    <string-array name="mccmnc">
+      <item value="TEST"/>
+    </string-array>
+
+    <int name="vvm_port_number_int" value="20481"/>
+    <string name="vvm_destination_number_string">887</string>
+    <string name="vvm_type_string">vvm_type_omtp</string>
+    <boolean name="vvm_cellular_data_required_bool" value="true"/>
+  </pbundle_as_map>
+
+  <pbundle_as_map>
+    <!-- Orange France -->
+    <string-array name="mccmnc">
+      <item value="20801"/>
+      <item value="20802"/>
+    </string-array>
+
+    <int name="vvm_port_number_int" value="20481"/>
+    <string name="vvm_destination_number_string">21101</string>
+    <string-array name="carrier_vvm_package_name_string_array">
+      <item value="com.orange.vvm"/>
+    </string-array>
+    <string name="vvm_type_string">vvm_type_omtp</string>
+    <boolean name="vvm_cellular_data_required_bool" value="true"/>
+  </pbundle_as_map>
+
+  <pbundle_as_map>
+    <!-- Orange UK -->
+    <string-array name="mccmnc">
+      <item value="23433"/>
+      <item value="23434"/>
+    </string-array>
+
+    <int name="vvm_port_number_int" value="20481"/>
+    <string name="vvm_destination_number_string">881</string>
+    <string name="vvm_type_string">vvm_type_omtp</string>
+  </pbundle_as_map>
+
+  <pbundle_as_map>
+    <!-- T-Mobile USA-->
+    <string-array name="mccmnc">
+      <item value="310160"/>
+      <item value="310200"/>
+      <item value="310210"/>
+      <item value="310220"/>
+      <item value="310230"/>
+      <item value="310240"/>
+      <item value="310250"/>
+      <item value="310260"/>
+      <item value="310270"/>
+      <item value="310300"/>
+      <item value="310310"/>
+      <item value="310490"/>
+      <item value="310530"/>
+      <item value="310590"/>
+      <item value="310640"/>
+      <item value="310660"/>
+      <item value="310800"/>
+    </string-array>
+
+    <int name="vvm_port_number_int" value="1808"/>
+    <int name="vvm_ssl_port_number_int" value="993"/>
+    <string name="vvm_destination_number_string">122</string>
+    <string-array name="carrier_vvm_package_name_string_array">
+      <item value="com.tmobile.vvm.application"/>
+    </string-array>
+    <string name="vvm_type_string">vvm_type_cvvm</string>>
+    <string-array name="vvm_disabled_capabilities_string_array">
+      <!-- b/28717550 -->
+      <item value="AUTH=DIGEST-MD5"/>
+    </string-array>
+  </pbundle_as_map>
+</list>
\ No newline at end of file
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
index 7738b77..acffb79 100644
--- a/src/com/android/phone/PhoneInterfaceManager.java
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -16,6 +16,8 @@
 
 package com.android.phone;
 
+import static com.android.internal.telephony.PhoneConstants.SUBSCRIPTION_KEY;
+
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.content.ComponentName;
@@ -44,13 +46,13 @@
 import android.telephony.CarrierConfigManager;
 import android.telephony.CellInfo;
 import android.telephony.IccOpenLogicalChannelResponse;
+import android.telephony.ModemActivityInfo;
 import android.telephony.NeighboringCellInfo;
 import android.telephony.RadioAccessFamily;
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
-import android.telephony.ModemActivityInfo;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
@@ -67,9 +69,9 @@
 import com.android.internal.telephony.MccTable;
 import com.android.internal.telephony.OperatorInfo;
 import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.telephony.PhoneFactory;
 import com.android.internal.telephony.ProxyController;
-import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.telephony.RILConstants;
 import com.android.internal.telephony.SubscriptionController;
 import com.android.internal.telephony.uicc.IccIoResult;
@@ -79,8 +81,6 @@
 import com.android.internal.util.HexDump;
 import com.android.phone.settings.VoicemailNotificationSettingsUtil;
 
-import static com.android.internal.telephony.PhoneConstants.SUBSCRIPTION_KEY;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -1827,6 +1827,56 @@
         return success;
     }
 
+    @Override
+    public void setVisualVoicemailSmsFilterEnabled(int subId, boolean value) {
+        VisualVoicemailSmsFilterConfig
+                .setVisualVoicemailSmsFilterEnabled(mPhone.getContext(), subId, value);
+    }
+
+    @Override
+    public boolean isVisualVoicemailSmsFilterEnabled(String packageName, int subId) {
+        return VisualVoicemailSmsFilterConfig
+                .isVisualVoicemailSmsFilterEnabled(mPhone.getContext(), packageName, subId);
+    }
+
+    @Override
+    public void setVisualVoicemailSmsFilterClientPrefix(int subId, String prefix) {
+        VisualVoicemailSmsFilterConfig
+                .setVisualVoicemailSmsFilterClientPrefix(mPhone.getContext(), subId, prefix);
+    }
+
+    @Override
+    public String getVisualVoicemailSmsFilterClientPrefix(String packageName, int subId) {
+        return VisualVoicemailSmsFilterConfig
+                .getVisualVoicemailSmsFilterClientPrefix(mPhone.getContext(), packageName, subId);
+    }
+
+    @Override
+    public void setVisualVoicemailSmsFilterOriginatingNumbers(int subId, String[] numbers) {
+        VisualVoicemailSmsFilterConfig
+                .setVisualVoicemailSmsFilterOriginatingNumbers(mPhone.getContext(), subId, numbers);
+    }
+
+    @Override
+    public String[] getVisualVoicemailSmsFilterOriginatingNumbers(String packageName, int subId) {
+        return VisualVoicemailSmsFilterConfig
+                .getVisualVoicemailSmsFilterOriginatingNumbers(mPhone.getContext(), packageName,
+                        subId);
+    }
+
+    @Override
+    public void setVisualVoicemailSmsFilterDestinationPort(int subId, int port) {
+        VisualVoicemailSmsFilterConfig
+                .setVisualVoicemailSmsFilterDestinationPort(mPhone.getContext(), subId, port);
+    }
+
+    @Override
+    public int getVisualVoicemailSmsFilterDestinationPort(String packageName, int subId) {
+        return VisualVoicemailSmsFilterConfig
+                .getVisualVoicemailSmsFilterDestinationPort(mPhone.getContext(), packageName,
+                        subId);
+    }
+
     /**
      * Returns the unread count of voicemails
      */
@@ -3049,4 +3099,62 @@
         return VoicemailNotificationSettingsUtil.isVibrationEnabled(phone);
     }
 
+
+    /**
+     * Make sure either called from same process as self (phone) or IPC caller has read privilege.
+     *
+     * @throws SecurityException if the caller does not have the required permission
+     */
+    private void enforceReadPrivilegedPermission() {
+        mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+                null);
+    }
+
+    /**
+     * Return the application ID for the app type.
+     *
+     * @param subId the subscription ID that this request applies to.
+     * @param appType the uicc app type.
+     * @return Application ID for specificied app type, or null if no uicc.
+     */
+    @Override
+    public String getAidForAppType(int subId, int appType) {
+        enforceReadPrivilegedPermission();
+        Phone phone = getPhone(subId);
+        if (phone == null) {
+            return null;
+        }
+        String aid = null;
+        try {
+            aid = UiccController.getInstance().getUiccCard(phone.getPhoneId())
+                    .getApplicationByType(appType).getAid();
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Not getting aid. Exception ex=" + e);
+        }
+        return aid;
+    }
+
+
+    /**
+     * Return the Electronic Serial Number.
+     *
+     * @param subId the subscription ID that this request applies to.
+     * @return ESN or null if error.
+     */
+    @Override
+    public String getEsn(int subId) {
+        enforceReadPrivilegedPermission();
+        Phone phone = getPhone(subId);
+        if (phone == null) {
+            return null;
+        }
+        String esn = null;
+        try {
+            esn = phone.getEsn();
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Not getting ESN. Exception ex=" + e);
+        }
+        return esn;
+    }
+
 }
diff --git a/src/com/android/phone/VisualVoicemailSmsFilterConfig.java b/src/com/android/phone/VisualVoicemailSmsFilterConfig.java
new file mode 100644
index 0000000..e0139ee
--- /dev/null
+++ b/src/com/android/phone/VisualVoicemailSmsFilterConfig.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.phone;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.telephony.TelephonyManager;
+import android.util.ArraySet;
+
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Stores the config values needed for visual voicemail sms filtering. The values from
+ * OmtpVvmCarrierConfigHelper are stored here during activation instead. These values are read and
+ * written through TelephonyManager.
+ */
+public class VisualVoicemailSmsFilterConfig {
+
+    private static final String VVM_SMS_FILTER_COFIG_SHARED_PREFS_KEY_PREFIX =
+            "vvm_sms_filter_config_";
+    private static final String ENABLED_KEY = "_enabled";
+    private static final String PREFIX_KEY = "_prefix";
+    private static final String ORIGINATING_NUMBERS_KEY = "_originating_numbers";
+    private static final String DESTINATION_PORT_KEY = "_destination_port";
+
+    public static void setVisualVoicemailSmsFilterEnabled(Context context, int subId,
+            boolean value) {
+        setBoolean(context, subId, ENABLED_KEY, value);
+    }
+
+    public static boolean isVisualVoicemailSmsFilterEnabled(Context context, String packageName,
+            int subId) {
+        return getBoolean(context, packageName, subId, ENABLED_KEY);
+    }
+
+    public static void setVisualVoicemailSmsFilterClientPrefix(Context context, int subId,
+            String prefix) {
+        setString(context, subId, PREFIX_KEY, prefix);
+    }
+
+    public static String getVisualVoicemailSmsFilterClientPrefix(Context context,
+            String packageName, int subId) {
+        return getString(context, packageName, subId, PREFIX_KEY);
+    }
+
+    public static void setVisualVoicemailSmsFilterOriginatingNumbers(Context context, int subId,
+            String[] numbers) {
+        ArraySet<String> set = new ArraySet<>();
+        set.addAll(Arrays.asList(numbers));
+        setStringSet(context, subId, ORIGINATING_NUMBERS_KEY, set);
+    }
+
+    public static String[] getVisualVoicemailSmsFilterOriginatingNumbers(Context context,
+            String packageName, int subId) {
+        Set<String> numbers = getStringSet(context, packageName, subId, ORIGINATING_NUMBERS_KEY);
+        return numbers.toArray(new String[numbers.size()]);
+    }
+
+    public static void setVisualVoicemailSmsFilterDestinationPort(Context context, int subId,
+            int port) {
+        setInt(context, subId, DESTINATION_PORT_KEY, port);
+    }
+
+    public static int getVisualVoicemailSmsFilterDestinationPort(Context context,
+            String packageName, int subId) {
+        return getInt(context, packageName, subId, DESTINATION_PORT_KEY,
+                TelephonyManager.VVM_SMS_FILTER_DESTINATION_PORT_ANY);
+    }
+
+    private static int getInt(Context context, String packageName, int subId, String key,
+            int defaultValue) {
+        SharedPreferences prefs = getSharedPreferences(context);
+        return prefs.getInt(makePerPhoneAccountKey(packageName, subId, key), defaultValue);
+    }
+
+    private static void setInt(Context context, int subId, String key, int value) {
+        SharedPreferences.Editor editor = getSharedPreferences(context).edit();
+        editor.putInt(makePerPhoneAccountKey(context.getOpPackageName(), subId, key), value);
+        editor.apply();
+    }
+
+    private static boolean getBoolean(Context context, String packageName, int subId, String key) {
+        SharedPreferences prefs = getSharedPreferences(context);
+        return prefs.getBoolean(makePerPhoneAccountKey(packageName, subId, key), false);
+    }
+
+    private static void setBoolean(Context context, int subId, String key, boolean value) {
+        SharedPreferences.Editor editor = getSharedPreferences(context).edit();
+        editor.putBoolean(makePerPhoneAccountKey(context.getOpPackageName(), subId, key), value);
+        editor.apply();
+    }
+
+    private static String getString(Context context, String packageName, int subId, String key) {
+        SharedPreferences prefs = getSharedPreferences(context);
+        return prefs.getString(makePerPhoneAccountKey(packageName, subId, key), null);
+    }
+
+    private static void setString(Context context, int subId, String key, String value) {
+        SharedPreferences.Editor editor = getSharedPreferences(context).edit();
+        editor.putString(makePerPhoneAccountKey(context.getOpPackageName(), subId, key), value);
+        editor.apply();
+    }
+
+    private static Set<String> getStringSet(Context context, String packageName, int subId,
+            String key) {
+        return getSharedPreferences(context)
+                .getStringSet(makePerPhoneAccountKey(packageName, subId, key), null);
+    }
+
+    private static void setStringSet(Context context, int subId, String key, Set<String> value) {
+        SharedPreferences.Editor editor = getSharedPreferences(context).edit();
+        editor.putStringSet(makePerPhoneAccountKey(context.getOpPackageName(), subId, key), value);
+        editor.apply();
+    }
+
+    private static SharedPreferences getSharedPreferences(Context context) {
+        return PreferenceManager
+                .getDefaultSharedPreferences(context.createDeviceProtectedStorageContext());
+    }
+
+    private static String makePerPhoneAccountKey(String packageName, int subId, String key) {
+        // TODO: make sure subId is persistent enough to serve as a key
+        return VVM_SMS_FILTER_COFIG_SHARED_PREFS_KEY_PREFIX + packageName + "_"
+                + subId + key;
+    }
+}
diff --git a/src/com/android/phone/common/mail/MailTransport.java b/src/com/android/phone/common/mail/MailTransport.java
index f452bab..cc09044 100644
--- a/src/com/android/phone/common/mail/MailTransport.java
+++ b/src/com/android/phone/common/mail/MailTransport.java
@@ -32,7 +32,6 @@
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
-import java.net.SocketAddress;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -66,6 +65,7 @@
     private BufferedOutputStream mOut;
     private final int mFlags;
     private SocketCreator mSocketCreator;
+    private InetSocketAddress mAddress;
 
     public MailTransport(Context context, ImapHelper imapHelper, Network network, String address,
             int port, int flags) {
@@ -126,29 +126,21 @@
         while (socketAddresses.size() > 0) {
             mSocket = createSocket();
             try {
-                InetSocketAddress address = socketAddresses.remove(0);
-                mSocket.connect(address, SOCKET_CONNECT_TIMEOUT);
+                mAddress = socketAddresses.remove(0);
+                mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT);
 
                 if (canTrySslSecurity()) {
-                    /**
-                     * {@link SSLSocket} must connect in its constructor, or create through a
-                     * already connected socket. Since we need to use
-                     * {@link Socket#connect(SocketAddress, int) } to set timeout, we can only
-                     * create it here.
+                    /*
+                    SSLSocket cannot be created with a connection timeout, so instead of doing a
+                    direct SSL connection, we connect with a normal connection and upgrade it into
+                    SSL
                      */
-                    LogUtils.d(TAG, "open: converting to SSL socket");
-                    mSocket = HttpsURLConnection.getDefaultSSLSocketFactory()
-                            .createSocket(mSocket, address.getHostName(), address.getPort(), true);
-                    // After the socket connects to an SSL server, confirm that the hostname is as
-                    // expected
-                    if (!canTrustAllCertificates()) {
-                        verifyHostname(mSocket, mHost);
-                    }
+                    reopenTls();
+                } else {
+                    mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+                    mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+                    mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
                 }
-
-                mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
-                mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
-                mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
                 success = true;
                 return;
             } catch (IOException ioe) {
@@ -205,6 +197,32 @@
     }
 
     /**
+     * Attempts to reopen a normal connection into a TLS connection.
+     */
+    public void reopenTls() throws MessagingException {
+        try {
+            LogUtils.d(TAG, "open: converting to TLS socket");
+            mSocket = HttpsURLConnection.getDefaultSSLSocketFactory()
+                    .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true);
+            // After the socket connects to an SSL server, confirm that the hostname is as
+            // expected
+            if (!canTrustAllCertificates()) {
+                verifyHostname(mSocket, mHost);
+            }
+            mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+            mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+            mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+
+        } catch (SSLException e) {
+            LogUtils.d(TAG, e.toString());
+            throw new CertificateValidationException(e.getMessage(), e);
+        } catch (IOException ioe) {
+            LogUtils.d(TAG, ioe.toString());
+            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+        }
+    }
+
+    /**
      * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
      * service but is not in the public API.
      *
@@ -272,6 +290,10 @@
         mSocket = null;
     }
 
+    public String getHost() {
+        return mHost;
+    }
+
     public InputStream getInputStream() {
         return mIn;
     }
diff --git a/src/com/android/phone/common/mail/store/ImapConnection.java b/src/com/android/phone/common/mail/store/ImapConnection.java
index 9207aa9..914ab10 100644
--- a/src/com/android/phone/common/mail/store/ImapConnection.java
+++ b/src/com/android/phone/common/mail/store/ImapConnection.java
@@ -17,21 +17,26 @@
 
 import android.provider.VoicemailContract.Status;
 import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Base64;
 
 import com.android.phone.common.mail.AuthenticationFailedException;
 import com.android.phone.common.mail.CertificateValidationException;
 import com.android.phone.common.mail.MailTransport;
 import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.store.ImapStore.ImapException;
+import com.android.phone.common.mail.store.imap.DigestMd5Utils;
 import com.android.phone.common.mail.store.imap.ImapConstants;
 import com.android.phone.common.mail.store.imap.ImapResponse;
 import com.android.phone.common.mail.store.imap.ImapResponseParser;
 import com.android.phone.common.mail.store.imap.ImapUtility;
 import com.android.phone.common.mail.utils.LogUtils;
-import com.android.phone.common.mail.store.ImapStore.ImapException;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.net.ssl.SSLException;
@@ -46,6 +51,7 @@
     private ImapStore mImapStore;
     private MailTransport mTransport;
     private ImapResponseParser mParser;
+    private Set<String> mCapabilities = new ArraySet<>();
 
     static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
 
@@ -102,6 +108,22 @@
 
             createParser();
 
+            // The server should greet us with something like
+            // * OK IMAP4rev1 Server
+            // consume the response before doing anything else.
+            ImapResponse response = mParser.readResponse();
+            if (!response.isOk()) {
+                mImapStore.getImapHelper()
+                        .setDataChannelState(Status.DATA_CHANNEL_STATE_SERVER_ERROR);
+                throw new MessagingException(
+                        MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
+                        "Invalid server initial response");
+            }
+
+            queryCapability();
+
+            maybeDoStartTls();
+
             // LOGIN
             doLogin();
         } catch (SSLException e) {
@@ -133,11 +155,30 @@
     }
 
     /**
+     * Attempts to convert the connection into secure connection.
+     */
+    private void maybeDoStartTls() throws IOException, MessagingException {
+        // STARTTLS is required in the OMTP standard but not every implementation support it.
+        // Make sure the server does have this capability
+        if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) {
+            executeSimpleCommand(ImapConstants.STARTTLS);
+            mTransport.reopenTls();
+            createParser();
+            // The cached capabilities should be refreshed after TLS is established.
+            queryCapability();
+        }
+    }
+
+    /**
      * Logs into the IMAP server
      */
     private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
         try {
-            executeSimpleCommand(getLoginPhrase(), true);
+            if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
+                doDigestMd5Auth();
+            } else {
+                executeSimpleCommand(getLoginPhrase(), true);
+            }
         } catch (ImapException ie) {
             LogUtils.d(TAG, "ImapException", ie);
             final String status = ie.getStatus();
@@ -157,6 +198,80 @@
         }
     }
 
+    private void doDigestMd5Auth() throws IOException, MessagingException {
+
+        //  Initiate the authentication.
+        //  The server will issue us a challenge, asking to run MD5 on the nonce with our password
+        //  and other data, including the cnonce we randomly generated.
+        //
+        //  C: a AUTHENTICATE DIGEST-MD5
+        //  S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
+        //             algorithm=md5-sess,charset=utf-8
+        List<ImapResponse> responses = executeSimpleCommand(
+            ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5);
+        String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
+
+        Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge);
+        DigestMd5Utils.Data data = new DigestMd5Utils.Data(mImapStore, mTransport, challenge);
+
+        String response = data.createResponse();
+        //  Respond to the challenge. If the server accepts it, it will reply a response-auth which
+        //  is the MD5 of our password and the cnonce we've provided, to prove the server does know
+        //  the password.
+        //
+        //  C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com",
+        //              nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
+        //              digest-uri="imap/elwood.innosoft.com",
+        //              response=d388dad90d4bbd760a152321f2143af7,qop=auth
+        //  S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd
+
+        responses = executeContinuationResponse(encodeBase64(response), true);
+
+        // Verify response-auth.
+        // If failed verifyResponseAuth() will throw a MessagingException, terminating the
+        // connection
+        String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
+        data.verifyResponseAuth(decodedResponseAuth);
+
+        //  Send a empty response to indicate we've accepted the response-auth
+        //
+        //  C: (empty)
+        //  S: a OK User logged in
+        executeContinuationResponse("", false);
+
+    }
+
+    private static String decodeBase64(String string) {
+        return new String(Base64.decode(string, Base64.DEFAULT));
+    }
+
+    private static String encodeBase64(String string) {
+        return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
+    }
+
+    private void queryCapability() throws IOException, MessagingException {
+        List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY);
+        mCapabilities.clear();
+        Set<String> disabledCapabilities = mImapStore.getImapHelper().getConfig()
+                .getDisabledCapabilities();
+        for (ImapResponse response : responses) {
+            if (response.isTagged()) {
+                continue;
+            }
+            for (int i = 0; i < response.size(); i++) {
+                String capability = response.getStringOrEmpty(i).getString();
+                if (disabledCapabilities != null && !disabledCapabilities.contains(capability)) {
+                    mCapabilities.add(capability);
+                }
+            }
+        }
+
+        LogUtils.d(TAG, "Capabilities: " + mCapabilities.toString());
+    }
+
+    private boolean hasCapability(String capability) {
+        return mCapabilities.contains(capability);
+    }
     /**
      * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
      * set it to {@link #mParser}.
@@ -218,6 +333,12 @@
         return tag;
     }
 
+    List<ImapResponse> executeContinuationResponse(String response, boolean sensitive)
+            throws IOException, MessagingException {
+        mTransport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response));
+        return getCommandResponses();
+    }
+
     /**
      * Read and return all of the responses from the most recent command sent to the server
      *
@@ -231,9 +352,9 @@
         do {
             response = mParser.readResponse();
             responses.add(response);
-        } while (!response.isTagged());
+        } while (!(response.isTagged() || response.isContinuationRequest()));
 
-        if (!response.isOk()) {
+        if (!(response.isOk() || response.isContinuationRequest())) {
             final String toString = response.toString();
             final String status = response.getStatusOrEmpty().getString();
             final String alert = response.getAlertTextOrEmpty().getString();
diff --git a/src/com/android/phone/common/mail/store/imap/DigestMd5Utils.java b/src/com/android/phone/common/mail/store/imap/DigestMd5Utils.java
new file mode 100644
index 0000000..e6376a3
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/DigestMd5Utils.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.phone.common.mail.store.imap;
+
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.phone.common.mail.MailTransport;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.store.ImapStore;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+
+public class DigestMd5Utils {
+
+    private static final String TAG = "DigestMd5Utils";
+
+    private static final String DIGEST_CHARSET = "CHARSET";
+    private static final String DIGEST_USERNAME = "username";
+    private static final String DIGEST_REALM = "realm";
+    private static final String DIGEST_NONCE = "nonce";
+    private static final String DIGEST_NC = "nc";
+    private static final String DIGEST_CNONCE = "cnonce";
+    private static final String DIGEST_URI = "digest-uri";
+    private static final String DIGEST_RESPONSE = "response";
+    private static final String DIGEST_QOP = "qop";
+
+    private static final String RESPONSE_AUTH_HEADER = "rspauth=";
+    private static final String HEX_CHARS = "0123456789abcdef";
+
+    /**
+     * Represents the set of data we need to generate the DIGEST-MD5 response.
+     */
+    public static class Data {
+
+        private static final String CHARSET = "utf-8'";
+
+        public String username;
+        public String password;
+        public String realm;
+        public String nonce;
+        public String nc;
+        public String cnonce;
+        public String digestUri;
+        public String qop;
+
+        @VisibleForTesting
+        Data() {
+            // Do nothing
+        }
+
+        public Data(ImapStore imapStore, MailTransport transport, Map<String, String> challenge) {
+            username = imapStore.getUsername();
+            password = imapStore.getPassword();
+            realm = challenge.getOrDefault(DIGEST_REALM, "");
+            nonce = challenge.get(DIGEST_NONCE);
+            cnonce = createCnonce();
+            nc = "00000001"; // Subsequent Authentication not supported, nounce count always 1.
+            qop = "auth"; // Other config not supported
+            digestUri = "imap/" + transport.getHost();
+        }
+
+        private static String createCnonce() {
+            SecureRandom generator = new SecureRandom();
+
+            // At least 64 bits of entropy is required
+            byte[] rawBytes = new byte[8];
+            generator.nextBytes(rawBytes);
+
+            return Base64.encodeToString(rawBytes, Base64.NO_WRAP);
+        }
+
+        /**
+         * Verify the response-auth returned by the server is correct.
+         */
+        public void verifyResponseAuth(String response)
+                throws MessagingException {
+            if (!response.startsWith(RESPONSE_AUTH_HEADER)) {
+                throw new MessagingException("response-auth expected");
+            }
+            if (!response.substring(RESPONSE_AUTH_HEADER.length())
+                    .equals(DigestMd5Utils.getResponse(this, true))) {
+                throw new MessagingException("invalid response-auth return from the server.");
+            }
+        }
+
+        public String createResponse() {
+            String response = getResponse(this, false);
+            ResponseBuilder builder = new ResponseBuilder();
+            builder
+                    .append(DIGEST_CHARSET, CHARSET)
+                    .appendQuoted(DIGEST_USERNAME, username)
+                    .appendQuoted(DIGEST_REALM, realm)
+                    .appendQuoted(DIGEST_NONCE, nonce)
+                    .append(DIGEST_NC, nc)
+                    .appendQuoted(DIGEST_CNONCE, cnonce)
+                    .appendQuoted(DIGEST_URI, digestUri)
+                    .append(DIGEST_RESPONSE, response)
+                    .append(DIGEST_QOP, qop);
+            return builder.toString();
+        }
+
+        private static class ResponseBuilder {
+
+            private StringBuilder mBuilder = new StringBuilder();
+
+            public ResponseBuilder appendQuoted(String key, String value) {
+                if (mBuilder.length() != 0) {
+                    mBuilder.append(",");
+                }
+                mBuilder.append(key).append("=\"").append(value).append("\"");
+                return this;
+            }
+
+            public ResponseBuilder append(String key, String value) {
+                if (mBuilder.length() != 0) {
+                    mBuilder.append(",");
+                }
+                mBuilder.append(key).append("=").append(value);
+                return this;
+            }
+
+            @Override
+            public String toString() {
+                return mBuilder.toString();
+            }
+        }
+    }
+
+    /*
+        response-value  =
+            toHex( getKeyDigest ( toHex(getMd5(a1)),
+            { nonce-value, ":" nc-value, ":",
+              cnonce-value, ":", qop-value, ":", toHex(getMd5(a2)) }))
+     * @param isResponseAuth is the response the one the server is returning us. response-auth has
+     * different a2 format.
+     */
+    @VisibleForTesting
+    static String getResponse(Data data, boolean isResponseAuth) {
+        StringBuilder a1 = new StringBuilder();
+        a1.append(new String(
+                getMd5(data.username + ":" + data.realm + ":" + data.password),
+                StandardCharsets.ISO_8859_1));
+        a1.append(":").append(data.nonce).append(":").append(data.cnonce);
+
+        StringBuilder a2 = new StringBuilder();
+        if (!isResponseAuth) {
+            a2.append("AUTHENTICATE");
+        }
+        a2.append(":").append(data.digestUri);
+
+        return toHex(getKeyDigest(
+                toHex(getMd5(a1.toString())),
+                data.nonce + ":" + data.nc + ":" + data.cnonce + ":" + data.qop + ":" + toHex(
+                        getMd5(a2.toString()))
+        ));
+    }
+
+    /**
+     * Let getMd5(s) be the 16 octet MD5 hash [RFC 1321] of the octet string s.
+     */
+    private static byte[] getMd5(String s) {
+        try {
+            MessageDigest digester = MessageDigest.getInstance("MD5");
+            digester.update(s.getBytes(StandardCharsets.ISO_8859_1));
+            return digester.digest();
+        } catch (NoSuchAlgorithmException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    /**
+     * Let getKeyDigest(k, s) be getMd5({k, ":", s}), i.e., the 16 octet hash of the string k, a colon and the
+     * string s.
+     */
+    private static byte[] getKeyDigest(String k, String s) {
+        StringBuilder builder = new StringBuilder(k).append(":").append(s);
+        return getMd5(builder.toString());
+    }
+
+    /**
+     * Let toHex(n) be the representation of the 16 octet MD5 hash n as a string of 32 hex digits
+     * (with alphabetic characters always in lower case, since MD5 is case sensitive).
+     */
+    private static String toHex(byte[] n) {
+        StringBuilder result = new StringBuilder();
+        for (byte b : n) {
+            int unsignedByte = b & 0xFF;
+            result.append(HEX_CHARS.charAt(unsignedByte / 16))
+                    .append(HEX_CHARS.charAt(unsignedByte % 16));
+        }
+        return result.toString();
+    }
+
+    public static Map<String, String> parseDigestMessage(String message) throws MessagingException {
+        Map<String, String> result = new DigestMessageParser(message).parse();
+        if (!result.containsKey(DIGEST_NONCE)) {
+            throw new MessagingException("nonce missing from server DIGEST-MD5 challenge");
+        }
+        return result;
+    }
+
+    /**
+     * Parse the key-value pair returned by the server.
+     */
+    private static class DigestMessageParser {
+
+        private final String mMessage;
+        private int mPosition = 0;
+        private Map<String, String> mResult = new ArrayMap<>();
+
+        public DigestMessageParser(String message) {
+            mMessage = message;
+        }
+
+        @Nullable
+        public Map<String, String> parse() {
+            try {
+                while (mPosition < mMessage.length()) {
+                    parsePair();
+                    if (mPosition != mMessage.length()) {
+                        expect(',');
+                    }
+                }
+            } catch (IndexOutOfBoundsException e) {
+                Log.e(TAG, e.toString());
+                return null;
+            }
+            return mResult;
+        }
+
+        private void parsePair() {
+            String key = parseKey();
+            expect('=');
+            String value = parseValue();
+            mResult.put(key, value);
+        }
+
+        private void expect(char c) {
+            if (pop() != c) {
+                throw new IllegalStateException(
+                        "unexpected character " + mMessage.charAt(mPosition));
+            }
+        }
+
+        private char pop() {
+            char result = peek();
+            mPosition++;
+            return result;
+        }
+
+        private char peek() {
+            return mMessage.charAt(mPosition);
+        }
+
+        private void goToNext(char c) {
+            while (peek() != c) {
+                mPosition++;
+            }
+        }
+
+        private String parseKey() {
+            int start = mPosition;
+            goToNext('=');
+            return mMessage.substring(start, mPosition);
+        }
+
+        private String parseValue() {
+            if (peek() == '"') {
+                return parseQuotedValue();
+            } else {
+                return parseUnquotedValue();
+            }
+        }
+
+        private String parseQuotedValue() {
+            expect('"');
+            StringBuilder result = new StringBuilder();
+            while (true) {
+                char c = pop();
+                if (c == '\\') {
+                    result.append(pop());
+                } else if (c == '"') {
+                    break;
+                } else {
+                    result.append(c);
+                }
+            }
+            return result.toString();
+        }
+
+        private String parseUnquotedValue() {
+            StringBuilder result = new StringBuilder();
+            while (true) {
+                char c = pop();
+                if (c == '\\') {
+                    result.append(pop());
+                } else if (c == ',') {
+                    mPosition--;
+                    break;
+                } else {
+                    result.append(c);
+                }
+
+                if (mPosition == mMessage.length()) {
+                    break;
+                }
+            }
+            return result.toString();
+        }
+    }
+}
diff --git a/src/com/android/phone/common/mail/store/imap/ImapConstants.java b/src/com/android/phone/common/mail/store/imap/ImapConstants.java
index 63dda8c..9e6e247 100644
--- a/src/com/android/phone/common/mail/store/imap/ImapConstants.java
+++ b/src/com/android/phone/common/mail/store/imap/ImapConstants.java
@@ -109,4 +109,15 @@
     public static final String EXPIRED = "EXPIRED";
     public static final String AUTHENTICATIONFAILED = "AUTHENTICATIONFAILED";
     public static final String UNAVAILABLE = "UNAVAILABLE";
+
+    /**
+     * capabilities
+     */
+    public static final String CAPABILITY_AUTH_DIGEST_MD5 = "AUTH=DIGEST-MD5";
+    public static final String CAPABILITY_STARTTLS = "STARTTLS";
+
+    /**
+     * authentication
+     */
+    public static final String AUTH_DIGEST_MD5 = "DIGEST-MD5";
 }
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/OmtpConstants.java b/src/com/android/phone/vvm/omtp/OmtpConstants.java
index fa3cb63..5fc0317 100644
--- a/src/com/android/phone/vvm/omtp/OmtpConstants.java
+++ b/src/com/android/phone/vvm/omtp/OmtpConstants.java
@@ -28,9 +28,8 @@
     public static final String SMS_KEY_VALUE_SEPARATOR = "=";
     public static final String SMS_PREFIX_SEPARATOR = ":";
 
-    public static final String CLIENT_PREFIX = "//VVM";
-    public static final String SYNC_SMS_PREFIX = CLIENT_PREFIX + ":SYNC:";
-    public static final String STATUS_SMS_PREFIX = CLIENT_PREFIX + ":STATUS:";
+    public static final String SYNC_SMS_PREFIX = "SYNC";
+    public static final String STATUS_SMS_PREFIX = "STATUS";
 
     // This is the format designated by the OMTP spec.
     public static final String DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm Z";
diff --git a/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java b/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
index 9534a10..df706d5 100644
--- a/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
+++ b/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
@@ -15,6 +15,7 @@
  */
 package com.android.phone.vvm.omtp;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.PersistableBundle;
@@ -23,47 +24,125 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.phone.vvm.omtp.sms.OmtpCvvmMessageSender;
 import com.android.phone.vvm.omtp.sms.OmtpMessageSender;
 import com.android.phone.vvm.omtp.sms.OmtpStandardMessageSender;
 
+import java.util.Arrays;
+import java.util.Set;
+
 /**
- * Handle activation and deactivation of a visual voicemail source. This class is necessary to
- * retrieve carrier vvm configuration details before sending the appropriate texts.
+ * Manages carrier dependent visual voicemail configuration values.
+ * The primary source is the value retrieved from CarrierConfigManager. If CarrierConfigManager does
+ * not provide the config (KEY_VVM_TYPE_STRING is empty, or "hidden" configs), then the value
+ * hardcoded in telephony will be used (in res/xml/vvm_config.xml)
+ *
+ * Hidden configs are new configs that are planned for future APIs, or miscellaneous settings that
+ * may clutter CarrierConfigManager too much.
+ *
+ * The current hidden configs are:
+ * {@link #getSslPort()}
+ * {@link #getDisabledCapabilities()}
  */
 public class OmtpVvmCarrierConfigHelper {
 
     private static final String TAG = "OmtpVvmCarrierCfgHlpr";
-    private Context mContext;
-    private int mSubId;
-    private PersistableBundle mCarrierConfig;
-    private String mVvmType;
+
+    static final String KEY_VVM_TYPE_STRING = CarrierConfigManager.KEY_VVM_TYPE_STRING;
+    static final String KEY_VVM_DESTINATION_NUMBER_STRING =
+            CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING;
+    static final String KEY_VVM_PORT_NUMBER_INT =
+            CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT;
+    static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING =
+            CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING;
+    static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY =
+            "carrier_vvm_package_name_string_array";
+    static final String KEY_VVM_PREFETCH_BOOL =
+            CarrierConfigManager.KEY_VVM_PREFETCH_BOOL;
+    static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL =
+            CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
+
+    /**
+     * @see #getSslPort()
+     */
+    static final String KEY_VVM_SSL_PORT_NUMBER_INT =
+            "vvm_ssl_port_number_int";
+
+    /**
+     * Ban a capability reported by the server from being used. The array of string should be a
+     * subset of the capabilities returned IMAP CAPABILITY command.
+     *
+     * @see #getDisabledCapabilities()
+     */
+    static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY =
+            "vvm_disabled_capabilities_string_array";
+    static final String KEY_VVM_CLIENT_PREFIX_STRING =
+            "vvm_client_prefix_string";
+
+    private final Context mContext;
+    private final int mSubId;
+    private final PersistableBundle mCarrierConfig;
+    private final String mVvmType;
+
+    private final PersistableBundle mTelephonyConfig;
 
     public OmtpVvmCarrierConfigHelper(Context context, int subId) {
         mContext = context;
         mSubId = subId;
         mCarrierConfig = getCarrierConfig();
+
+        TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        mTelephonyConfig = new TelephonyVvmConfigManager(context.getResources())
+                .getConfig(telephonyManager.getNetworkOperator(subId));
+
         mVvmType = getVvmType();
     }
 
-    public String getVvmType() {
-        if (mCarrierConfig == null) {
-            return null;
-        }
-
-        return mCarrierConfig.getString(
-                CarrierConfigManager.KEY_VVM_TYPE_STRING, null);
+    @VisibleForTesting
+    OmtpVvmCarrierConfigHelper(PersistableBundle carrierConfig,
+            PersistableBundle telephonyConfig) {
+        mContext = null;
+        mSubId = 0;
+        mCarrierConfig = carrierConfig;
+        mTelephonyConfig = telephonyConfig;
+        mVvmType = getVvmType();
     }
 
-    public String getCarrierVvmPackageName() {
-        if (mCarrierConfig == null) {
+    @Nullable
+    public String getVvmType() {
+        return (String) getValue(KEY_VVM_TYPE_STRING);
+    }
+
+    @Nullable
+    public Set<String> getCarrierVvmPackageNames() {
+        Set<String> names = getCarrierVvmPackageNames(mCarrierConfig);
+        if (names != null) {
+            return names;
+        }
+        return getCarrierVvmPackageNames(mTelephonyConfig);
+    }
+
+    private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) {
+        if (bundle == null) {
             return null;
         }
-
-        return mCarrierConfig.getString(
-                CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING, null);
+        Set<String> names = new ArraySet<>();
+        if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)) {
+            names.add(bundle.getString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING));
+        }
+        if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)) {
+            names.addAll(Arrays.asList(
+                    bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)));
+        }
+        if (names.isEmpty()) {
+            return null;
+        }
+        return names;
     }
 
     public boolean isOmtpVvmType() {
@@ -76,36 +155,104 @@
      * so by checking if the carrier's voicemail app is installed.
      */
     public boolean isEnabledByDefault() {
-        String packageName = mCarrierConfig.getString(
-                CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING);
-        if (packageName == null) {
+        Set<String> carrierPackages = getCarrierVvmPackageNames();
+        if (carrierPackages == null) {
             return true;
         }
-        try {
-            mContext.getPackageManager().getPackageInfo(packageName, 0);
-            return false;
-        } catch (NameNotFoundException e) {
-            return true;
+        for (String packageName : carrierPackages) {
+            try {
+                mContext.getPackageManager().getPackageInfo(packageName, 0);
+                return false;
+            } catch (NameNotFoundException e) {
+                // Do nothing.
+            }
         }
+        return true;
     }
 
     public boolean isCellularDataRequired() {
-        if (mCarrierConfig == null) {
-            return false;
-        }
-        return mCarrierConfig
-                .getBoolean(CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL);
+        return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL);
     }
 
     public boolean isPrefetchEnabled() {
-        if (mCarrierConfig == null) {
-            return false;
+        return (boolean) getValue(KEY_VVM_PREFETCH_BOOL);
+    }
+
+
+    public int getApplicationPort() {
+        Integer port = (Integer) getValue(KEY_VVM_PORT_NUMBER_INT);
+        if (port != null) {
+            return port;
         }
-        return mCarrierConfig
-                .getBoolean(CarrierConfigManager.KEY_VVM_PREFETCH_BOOL);
+        return 0;
+    }
+
+    @Nullable
+    public String getDestinationNumber() {
+        return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING);
+    }
+
+    /**
+     * Hidden config.
+     *
+     * @return Port to start a SSL IMAP connection directly.
+     *
+     * TODO: make config public and add to CarrierConfigManager
+     */
+    public int getSslPort() {
+        Integer port = (Integer) getValue(KEY_VVM_SSL_PORT_NUMBER_INT);
+        if (port != null) {
+            return port;
+        }
+        return 0;
+    }
+
+    /**
+     * Hidden Config.
+     *
+     * <p>Sometimes the server states it supports a certain feature but we found they have bug on
+     * the server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability
+     * but using it to login will cause subsequent response to be erroneous.
+     *
+     * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined
+     * to have issues and should not be used.
+     */
+    @Nullable
+    public Set<String> getDisabledCapabilities() {
+        Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
+        if (disabledCapabilities != null) {
+            return disabledCapabilities;
+        }
+        return getDisabledCapabilities(mTelephonyConfig);
+    }
+
+    @Nullable
+    private static Set<String> getDisabledCapabilities(@Nullable PersistableBundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+        if (!bundle.containsKey(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)) {
+            return null;
+        }
+        ArraySet<String> result = new ArraySet<String>();
+        result.addAll(
+                Arrays.asList(bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)));
+        return result;
+    }
+
+    public String getClientPrefix() {
+        String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING);
+        if (prefix != null) {
+            return prefix;
+        }
+        return "//VVM";
     }
 
     public void startActivation() {
+        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+        telephonyManager.setVisualVoicemailSmsFilterEnabled(mSubId, true);
+        telephonyManager.setVisualVoicemailSmsFilterClientPrefix(mSubId, getClientPrefix());
+
         OmtpMessageSender messageSender = getMessageSender();
         if (messageSender != null) {
             Log.i(TAG, "Requesting VVM activation for subId: " + mSubId);
@@ -114,6 +261,8 @@
     }
 
     public void startDeactivation() {
+        mContext.getSystemService(TelephonyManager.class)
+                .setVisualVoicemailSmsFilterEnabled(mSubId, false);
         OmtpMessageSender messageSender = getMessageSender();
         if (messageSender != null) {
             Log.i(TAG, "Requesting VVM deactivation for subId: " + mSubId);
@@ -121,6 +270,7 @@
         }
     }
 
+    @Nullable
     private PersistableBundle getCarrierConfig() {
         if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
             Log.w(TAG, "Invalid subscriptionId or subscriptionId not provided in intent.");
@@ -134,19 +284,23 @@
             return null;
         }
 
-        return carrierConfigManager.getConfigForSubId(mSubId);
+        PersistableBundle config = carrierConfigManager.getConfigForSubId(mSubId);
+
+        if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) {
+            Log.w(TAG, "Carrier config missing VVM type, ignoring.");
+            return null;
+        }
+        return config;
     }
 
     private OmtpMessageSender getMessageSender() {
-        if (mCarrierConfig == null) {
+        if (mCarrierConfig == null && mTelephonyConfig == null) {
             Log.w(TAG, "Empty carrier config.");
             return null;
         }
 
-        int applicationPort = mCarrierConfig.getInt(
-                CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT, 0);
-        String destinationNumber = mCarrierConfig.getString(
-                CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING);
+        int applicationPort = getApplicationPort();
+        String destinationNumber = getDestinationNumber();
         if (TextUtils.isEmpty(destinationNumber)) {
             Log.w(TAG, "No destination number for this carrier.");
             return null;
@@ -169,4 +323,22 @@
 
         return messageSender;
     }
+
+    @Nullable
+    private Object getValue(String key) {
+        Object result;
+        if (mCarrierConfig != null) {
+            result = mCarrierConfig.get(key);
+            if (result != null) {
+                return result;
+            }
+        }
+        if (mTelephonyConfig != null) {
+            result = mTelephonyConfig.get(key);
+            if (result != null) {
+                return result;
+            }
+        }
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/TelephonyVvmConfigManager.java b/src/com/android/phone/vvm/omtp/TelephonyVvmConfigManager.java
new file mode 100644
index 0000000..e91481e
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/TelephonyVvmConfigManager.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.phone.vvm.omtp;
+
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.phone.R;
+import com.android.phone.vvm.omtp.utils.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Load and caches telephony vvm config from res/xml/vvm_config.xml
+ */
+public class TelephonyVvmConfigManager {
+
+    private static final String TAG = "TelephonyVvmCfgMgr";
+
+    private static final boolean USE_DEBUG_CONFIG = false; //STOPSHIP if true
+
+    private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
+
+    static final String KEY_MCCMNC = "mccmnc";
+
+    private static Map<String, PersistableBundle> sCachedConfigs;
+
+    private final Map<String, PersistableBundle> mConfigs;
+
+    public TelephonyVvmConfigManager(Resources resources) {
+        if (sCachedConfigs == null) {
+            sCachedConfigs = loadConfigs(resources.getXml(R.xml.vvm_config));
+        }
+        mConfigs = sCachedConfigs;
+    }
+
+    @VisibleForTesting
+    TelephonyVvmConfigManager(XmlPullParser parser) {
+        mConfigs = loadConfigs(parser);
+    }
+
+    @Nullable
+    public PersistableBundle getConfig(String mccMnc) {
+        if (USE_DEBUG_CONFIG) {
+            return mConfigs.get("TEST");
+        }
+        return mConfigs.get(mccMnc);
+    }
+
+    private static Map<String, PersistableBundle> loadConfigs(XmlPullParser parser) {
+        Map<String, PersistableBundle> configs = new ArrayMap<>();
+        try {
+            ArrayList list = readBundleList(parser);
+            for (Object object : list) {
+                if (!(object instanceof PersistableBundle)) {
+                    throw new IllegalArgumentException("PersistableBundle expected, got " + object);
+                }
+                PersistableBundle bundle = (PersistableBundle) object;
+                String[] mccMncs = bundle.getStringArray(KEY_MCCMNC);
+                if (mccMncs == null) {
+                    throw new IllegalArgumentException("MCCMNC is null");
+                }
+                for (String mccMnc : mccMncs) {
+                    configs.put(mccMnc, bundle);
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+        return configs;
+    }
+
+    @Nullable
+    public static ArrayList readBundleList(XmlPullParser in) throws IOException,
+            XmlPullParserException {
+        final int outerDepth = in.getDepth();
+        int event;
+        while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
+                (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+            if (event == XmlPullParser.START_TAG) {
+                final String startTag = in.getName();
+                final String[] tagName = new String[1];
+                in.next();
+                return XmlUtils.readThisListXml(in, startTag, tagName,
+                        new MyReadMapCallback(), false);
+            }
+        }
+        return null;
+    }
+
+    public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
+            XmlPullParserException {
+        final int outerDepth = in.getDepth();
+        final String startTag = in.getName();
+        final String[] tagName = new String[1];
+        int event;
+        while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
+                (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+            if (event == XmlPullParser.START_TAG) {
+                ArrayMap<String, ?> map =
+                        XmlUtils.readThisArrayMapXml(in, startTag, tagName,
+                                new MyReadMapCallback());
+                PersistableBundle result = new PersistableBundle();
+                for (Entry<String, ?> entry : map.entrySet()) {
+                    Object value = entry.getValue();
+                    if (value instanceof Integer) {
+                        result.putInt(entry.getKey(), (int) value);
+                    } else if (value instanceof Boolean) {
+                        result.putBoolean(entry.getKey(), (boolean) value);
+                    } else if (value instanceof String) {
+                        result.putString(entry.getKey(), (String) value);
+                    } else if (value instanceof String[]) {
+                        result.putStringArray(entry.getKey(), (String[]) value);
+                    } else if (value instanceof PersistableBundle) {
+                        result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
+                    }
+                }
+                return result;
+            }
+        }
+        return PersistableBundle.EMPTY;
+    }
+
+    static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
+
+        @Override
+        public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+                throws XmlPullParserException, IOException {
+            if (TAG_PERSISTABLEMAP.equals(tag)) {
+                return restoreFromXml(in);
+            }
+            throw new XmlPullParserException("Unknown tag=" + tag);
+        }
+    }
+}
diff --git a/src/com/android/phone/vvm/omtp/VvmPackageInstallReceiver.java b/src/com/android/phone/vvm/omtp/VvmPackageInstallReceiver.java
index 0c4eb62..1767c6b 100644
--- a/src/com/android/phone/vvm/omtp/VvmPackageInstallReceiver.java
+++ b/src/com/android/phone/vvm/omtp/VvmPackageInstallReceiver.java
@@ -52,7 +52,10 @@
 
             OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(
                     context, PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount));
-            if (packageName.equals(carrierConfigHelper.getCarrierVvmPackageName())) {
+            if (carrierConfigHelper.getCarrierVvmPackageNames() == null) {
+                continue;
+            }
+            if (carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) {
                 VisualVoicemailSettingsUtil.setVisualVoicemailEnabled(
                         context, phoneAccount, false, false);
                 OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount);
diff --git a/src/com/android/phone/vvm/omtp/imap/ImapHelper.java b/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
index 2c10377..404c771 100644
--- a/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
+++ b/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
@@ -25,7 +25,6 @@
 import android.provider.VoicemailContract.Status;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.Voicemail;
-import android.telephony.TelephonyManager;
 import android.util.Base64;
 import android.util.Log;
 
@@ -80,10 +79,14 @@
     private int mQuotaOccupied;
     private int mQuotaTotal;
 
+    private final OmtpVvmCarrierConfigHelper mConfig;
+
     public ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network) {
         mContext = context;
         mPhoneAccount = phoneAccount;
         mNetwork = network;
+        mConfig = new OmtpVvmCarrierConfigHelper(context,
+                PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount));
         try {
             TempDirectory.setTempDirectory(context);
 
@@ -98,11 +101,9 @@
                             OmtpConstants.IMAP_PORT, phoneAccount));
             int auth = ImapStore.FLAG_NONE;
 
-            OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(context,
-                    PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount));
-            if (TelephonyManager.VVM_TYPE_CVVM.equals(carrierConfigHelper.getVvmType())) {
-                // TODO: move these into the carrier config app
-                port = 993;
+            int sslPort = mConfig.getSslPort();
+            if (sslPort != 0) {
+                port = sslPort;
                 auth = ImapStore.FLAG_SSL;
             }
 
@@ -142,6 +143,10 @@
         return info.isRoaming();
     }
 
+    public OmtpVvmCarrierConfigHelper getConfig() {
+        return mConfig;
+    }
+
     /** The caller thread will block until the method returns. */
     public boolean markMessagesAsRead(List<Voicemail> voicemails) {
         return setFlags(voicemails, Flag.SEEN);
diff --git a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
index 9ac37a4..67ffef7 100644
--- a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
+++ b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
@@ -20,15 +20,14 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.UserManager;
-import android.provider.Telephony;
 import android.provider.VoicemailContract;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.Voicemail;
-import android.telephony.SmsMessage;
+import android.telephony.SubscriptionManager;
 import android.util.Log;
 
-import com.android.internal.telephony.PhoneConstants;
 import com.android.phone.PhoneGlobals;
 import com.android.phone.PhoneUtils;
 import com.android.phone.settings.VisualVoicemailSettingsUtil;
@@ -57,7 +56,8 @@
 
         mContext = context;
         mPhoneAccount = PhoneUtils.makePstnPhoneAccountHandle(
-                intent.getExtras().getInt(PhoneConstants.PHONE_KEY));
+                SubscriptionManager.getPhoneId(
+                        intent.getExtras().getInt(VoicemailContract.EXTRA_VOICEMAIL_SMS_SUBID)));
 
         if (mPhoneAccount == null) {
             Log.w(TAG, "Received message for null phone account");
@@ -69,41 +69,26 @@
             return;
         }
 
-        SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
+        String eventType = intent.getExtras()
+                .getString(VoicemailContract.EXTRA_VOICEMAIL_SMS_PREFIX);
+        Bundle data = intent.getExtras().getBundle(VoicemailContract.EXTRA_VOICEMAIL_SMS_FIELDS);
 
-        if (messages == null) {
-            Log.w(TAG, "Message does not exist in the intent.");
-            return;
+        if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+            SyncMessage message = new SyncMessage(data);
+
+            Log.v(TAG, "Received SYNC sms for " + mPhoneAccount.getId() +
+                    " with event " + message.getSyncTriggerEvent());
+            LocalLogHelper.log(TAG, "Received SYNC sms for " + mPhoneAccount.getId() +
+                    " with event " + message.getSyncTriggerEvent());
+            processSync(message);
+        } else if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+            Log.v(TAG, "Received STATUS sms for " + mPhoneAccount.getId());
+            LocalLogHelper.log(TAG, "Received Status sms for " + mPhoneAccount.getId());
+            StatusMessage message = new StatusMessage(data);
+            updateSource(message);
+        } else {
+            Log.e(TAG, "Unknown prefix: " + eventType);
         }
-
-        StringBuilder messageBody = new StringBuilder();
-
-        for (int i = 0; i < messages.length; i++) {
-            if (messages[i].mWrappedSmsMessage != null) {
-                messageBody.append(messages[i].getMessageBody());
-            }
-        }
-
-        WrappedMessageData messageData = OmtpSmsParser.parse(messageBody.toString());
-        if (messageData != null) {
-            if (messageData.getPrefix() == OmtpConstants.SYNC_SMS_PREFIX) {
-                SyncMessage message = new SyncMessage(messageData);
-
-                Log.v(TAG, "Received SYNC sms for " + mPhoneAccount.getId() +
-                        " with event " + message.getSyncTriggerEvent());
-                LocalLogHelper.log(TAG, "Received SYNC sms for " + mPhoneAccount.getId() +
-                        " with event " + message.getSyncTriggerEvent());
-                processSync(message);
-            } else if (messageData.getPrefix() == OmtpConstants.STATUS_SMS_PREFIX) {
-                Log.v(TAG, "Received STATUS sms for " + mPhoneAccount.getId());
-                LocalLogHelper.log(TAG, "Received Status sms for " + mPhoneAccount.getId());
-                StatusMessage message = new StatusMessage(messageData);
-                updateSource(message);
-            } else {
-                Log.e(TAG, "This should never have happened");
-            }
-        }
-        // Let this fall through: this is not a message we're interested in.
     }
 
     /**
diff --git a/src/com/android/phone/vvm/omtp/sms/OmtpSmsParser.java b/src/com/android/phone/vvm/omtp/sms/OmtpSmsParser.java
deleted file mode 100644
index 54a2a02..0000000
--- a/src/com/android/phone/vvm/omtp/sms/OmtpSmsParser.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.phone.vvm.omtp.sms;
-
-import android.util.ArrayMap;
-import android.util.Log;
-
-import com.android.phone.vvm.omtp.OmtpConstants;
-
-import java.util.Map;
-
-/**
- * OMTP SMS parser interface, for parsing SYNC and STATUS SMS sent by OMTP visual voicemail server.
- */
-public class OmtpSmsParser {
-    private static String TAG = "OmtpSmsParser";
-    /**
-     * Parses the supplied SMS body and returns back a structured OMTP message.
-     * Returns null if unable to parse the SMS body.
-     */
-    public static WrappedMessageData parse(String smsBody) {
-        if (smsBody == null) {
-            return null;
-        }
-
-        WrappedMessageData messageData = null;
-        if (smsBody.startsWith(OmtpConstants.SYNC_SMS_PREFIX)) {
-            messageData = new WrappedMessageData(OmtpConstants.SYNC_SMS_PREFIX,
-                    parseSmsBody(smsBody.substring(OmtpConstants.SYNC_SMS_PREFIX.length())));
-            // Check for a mandatory field.
-            String triggerEvent = messageData.extractString(OmtpConstants.SYNC_TRIGGER_EVENT);
-            if (triggerEvent == null) {
-                Log.e(TAG, "Missing mandatory field: " + OmtpConstants.SYNC_TRIGGER_EVENT);
-                return null;
-            }
-        } else if (smsBody.startsWith(OmtpConstants.STATUS_SMS_PREFIX)) {
-            messageData = new WrappedMessageData(OmtpConstants.STATUS_SMS_PREFIX,
-                    parseSmsBody(smsBody.substring(OmtpConstants.STATUS_SMS_PREFIX.length())));
-        }
-
-        return messageData;
-    }
-
-    /**
-     * Converts a String of key/value pairs into a Map object. The WrappedMessageData object
-     * contains helper functions to retrieve the values.
-     *
-     * e.g. "//VVM:STATUS:st=R;rc=0;srv=1;dn=1;ipt=1;spt=0;u=eg@example.com;pw=1"
-     * => "WrappedMessageData [mFields={st=R, ipt=1, srv=1, dn=1, u=eg@example.com, pw=1, rc=0}]"
-     *
-     * @param message The sms string with the prefix removed.
-     * @return A WrappedMessageData object containing the map.
-     */
-    private static Map<String, String> parseSmsBody(String message) {
-        Map<String, String> keyValues = new ArrayMap<String, String>();
-        String[] entries = message.split(OmtpConstants.SMS_FIELD_SEPARATOR);
-        for (String entry : entries) {
-            String[] keyValue = entry.split(OmtpConstants.SMS_KEY_VALUE_SEPARATOR);
-            if (keyValue.length != 2) {
-                continue;
-            }
-            keyValues.put(keyValue[0].trim(), keyValue[1].trim());
-        }
-
-        return keyValues;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/sms/StatusMessage.java b/src/com/android/phone/vvm/omtp/sms/StatusMessage.java
index 7e4faac..4d8c815 100644
--- a/src/com/android/phone/vvm/omtp/sms/StatusMessage.java
+++ b/src/com/android/phone/vvm/omtp/sms/StatusMessage.java
@@ -15,6 +15,7 @@
  */
 package com.android.phone.vvm.omtp.sms;
 
+import android.os.Bundle;
 import android.telecom.Log;
 
 import com.android.phone.vvm.omtp.OmtpConstants;
@@ -59,20 +60,20 @@
                 + ", mSmtpPassword=" + Log.pii(mSmtpPassword) + "]";
     }
 
-    public StatusMessage(WrappedMessageData wrappedData) {
-        mProvisioningStatus = wrappedData.extractString(OmtpConstants.PROVISIONING_STATUS);
-        mStatusReturnCode = wrappedData.extractString(OmtpConstants.RETURN_CODE);
-        mSubscriptionUrl = wrappedData.extractString(OmtpConstants.SUBSCRIPTION_URL);
-        mServerAddress = wrappedData.extractString(OmtpConstants.SERVER_ADDRESS);
-        mTuiAccessNumber = wrappedData.extractString(OmtpConstants.TUI_ACCESS_NUMBER);
-        mClientSmsDestinationNumber = wrappedData.extractString(
+    public StatusMessage(Bundle wrappedData) {
+        mProvisioningStatus = wrappedData.getString(OmtpConstants.PROVISIONING_STATUS);
+        mStatusReturnCode = wrappedData.getString(OmtpConstants.RETURN_CODE);
+        mSubscriptionUrl = wrappedData.getString(OmtpConstants.SUBSCRIPTION_URL);
+        mServerAddress = wrappedData.getString(OmtpConstants.SERVER_ADDRESS);
+        mTuiAccessNumber = wrappedData.getString(OmtpConstants.TUI_ACCESS_NUMBER);
+        mClientSmsDestinationNumber = wrappedData.getString(
                 OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER);
-        mImapPort = wrappedData.extractString(OmtpConstants.IMAP_PORT);
-        mImapUserName = wrappedData.extractString(OmtpConstants.IMAP_USER_NAME);
-        mImapPassword = wrappedData.extractString(OmtpConstants.IMAP_PASSWORD);
-        mSmtpPort = wrappedData.extractString(OmtpConstants.SMTP_PORT);
-        mSmtpUserName = wrappedData.extractString(OmtpConstants.SMTP_USER_NAME);
-        mSmtpPassword = wrappedData.extractString(OmtpConstants.SMTP_PASSWORD);
+        mImapPort = wrappedData.getString(OmtpConstants.IMAP_PORT);
+        mImapUserName = wrappedData.getString(OmtpConstants.IMAP_USER_NAME);
+        mImapPassword = wrappedData.getString(OmtpConstants.IMAP_PASSWORD);
+        mSmtpPort = wrappedData.getString(OmtpConstants.SMTP_PORT);
+        mSmtpUserName = wrappedData.getString(OmtpConstants.SMTP_USER_NAME);
+        mSmtpPassword = wrappedData.getString(OmtpConstants.SMTP_PASSWORD);
     }
 
     /**
diff --git a/src/com/android/phone/vvm/omtp/sms/SyncMessage.java b/src/com/android/phone/vvm/omtp/sms/SyncMessage.java
index 6829981..1e565da 100644
--- a/src/com/android/phone/vvm/omtp/sms/SyncMessage.java
+++ b/src/com/android/phone/vvm/omtp/sms/SyncMessage.java
@@ -15,8 +15,14 @@
  */
 package com.android.phone.vvm.omtp.sms;
 
+import android.os.Bundle;
+
 import com.android.phone.vvm.omtp.OmtpConstants;
 
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
 /**
  * Structured data representation of an OMTP SYNC message.
  *
@@ -49,16 +55,25 @@
                 + ", mMsgTimeMillis=" + mMsgTimeMillis + "]";
     }
 
-    public SyncMessage(WrappedMessageData wrappedData) {
-        mSyncTriggerEvent = wrappedData.extractString(OmtpConstants.SYNC_TRIGGER_EVENT);
-        mMessageId = wrappedData.extractString(OmtpConstants.MESSAGE_UID);
-        mMessageLength = wrappedData.extractInteger(OmtpConstants.MESSAGE_LENGTH);
-        mContentType = wrappedData.extractString(OmtpConstants.CONTENT_TYPE);
-        mSender = wrappedData.extractString(OmtpConstants.SENDER);
-        mNewMessageCount = wrappedData.extractInteger(OmtpConstants.NUM_MESSAGE_COUNT);
-        mMsgTimeMillis = wrappedData.extractTime(OmtpConstants.TIME);
+    public SyncMessage(Bundle wrappedData) {
+        mSyncTriggerEvent = wrappedData.getString(OmtpConstants.SYNC_TRIGGER_EVENT);
+        mMessageId = wrappedData.getString(OmtpConstants.MESSAGE_UID);
+        mMessageLength = Integer.parseInt(wrappedData.getString(OmtpConstants.MESSAGE_LENGTH));
+        mContentType = wrappedData.getString(OmtpConstants.CONTENT_TYPE);
+        mSender = wrappedData.getString(OmtpConstants.SENDER);
+        mNewMessageCount = Integer.parseInt(wrappedData.getString(OmtpConstants.NUM_MESSAGE_COUNT));
+        mMsgTimeMillis = parseTime(wrappedData.getString(OmtpConstants.TIME));
     }
 
+    static Long parseTime(String value) {
+        try {
+            return new SimpleDateFormat(
+                    OmtpConstants.DATE_TIME_FORMAT, Locale.US)
+                    .parse(value).getTime();
+        } catch (ParseException e) {
+            return 0L;
+        }
+    }
     /**
      * @return the event that triggered the sync message. This is a mandatory field and must always
      * be set.
diff --git a/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java b/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java
deleted file mode 100644
index b4c86d4..0000000
--- a/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.phone.vvm.omtp.sms;
-
-import android.text.TextUtils;
-import android.util.ArrayMap;
-import android.util.Log;
-
-import com.android.phone.vvm.omtp.OmtpConstants;
-
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Locale;
-import java.util.Map;
-
-/**
- * Class wrapping the raw OMTP message data, internally represented as as map of all key-value pairs
- * found in the SMS body.
- * <p>
- * Provides convenience methods to extract parse fields of different types.
- * <p>
- * All the methods return null if either the field was not present or it could not be parsed.
- */
-public class WrappedMessageData {
-    private final String TAG = "WrappedMessageData";
-    private final String mPrefix;
-    private final Map<String, String> mFields;
-
-    @Override
-    public String toString() {
-        return "WrappedMessageData [mFields=" + mFields + "]";
-    }
-
-    WrappedMessageData(String prefix, Map<String, String> keyValues) {
-        mPrefix = prefix;
-        mFields = new ArrayMap<String, String>();
-        mFields.putAll(keyValues);
-    }
-
-    /**
-     * @return The String prefix of the message, designating whether this is the message data of a
-     * STATUS or SYNC sms.
-     */
-    String getPrefix() {
-        return mPrefix;
-    }
-
-    /**
-     * Extracts the requested field from underlying data and returns the String value as is.
-     *
-     * @param field The requested field.
-     * @return the parsed string value, or null if the field was not present or not valid.
-     */
-    String extractString(final String field) {
-        String value = mFields.get(field);
-        if (value == null) {
-            return null;
-        }
-
-        String[] possibleValues = OmtpConstants.possibleValuesMap.get(field);
-        if (possibleValues == null) {
-            return value;
-        }
-        for (int i = 0; i < possibleValues.length; i++) {
-            if (TextUtils.equals(value, possibleValues[i])) {
-                return value;
-            }
-        }
-        Log.e(TAG, "extractString - value \"" + value +
-                "\" of field \"" + field + "\" is not allowed.");
-        return null;
-    }
-
-    /**
-     * Extracts the requested field from underlying data and parses it as an {@link Integer}.
-     *
-     * @param field The requested field.
-     * @return the parsed integer value, or null if the field was not present.
-     */
-    Integer extractInteger(final String field) {
-        String value = mFields.get(field);
-        if (value == null) {
-            return null;
-        }
-
-        try {
-            return Integer.decode(value);
-        } catch (NumberFormatException e) {
-            Log.e(TAG, "extractInteger - could not parse integer: " + value);
-            return null;
-        }
-    }
-
-    /**
-     * Extracts the requested field from underlying data and parses it as a date/time represented in
-     * {@link OmtpConstants#DATE_TIME_FORMAT} format.
-     *
-     * @param field The requested field.
-     * @return the parsed string value, or null if the field was not present.
-     */
-    Long extractTime(final String field) {
-        String value = mFields.get(field);
-        if (value == null) {
-            return null;
-        }
-
-        try {
-            return new SimpleDateFormat(
-                    OmtpConstants.DATE_TIME_FORMAT, Locale.US).parse(value).getTime();
-        } catch (ParseException e) {
-            Log.e(TAG, "extractTime - could not parse time: " + value);
-            return null;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/utils/XmlUtils.java b/src/com/android/phone/vvm/omtp/utils/XmlUtils.java
new file mode 100644
index 0000000..4eeb5ce
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/utils/XmlUtils.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.phone.vvm.omtp.utils;
+
+import android.util.ArrayMap;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class XmlUtils {
+
+    public static final ArrayMap<String, ?> readThisArrayMapXml(XmlPullParser parser, String endTag,
+            String[] name, ReadMapCallback callback)
+            throws XmlPullParserException, java.io.IOException {
+        ArrayMap<String, Object> map = new ArrayMap<>();
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == XmlPullParser.START_TAG) {
+                Object val = readThisValueXml(parser, name, callback, true);
+                map.put(name[0], val);
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return map;
+                }
+                throw new XmlPullParserException(
+                        "Expected " + endTag + " end tag at: " + parser.getName());
+            }
+            eventType = parser.next();
+        } while (eventType != XmlPullParser.END_DOCUMENT);
+
+        throw new XmlPullParserException(
+                "Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read an ArrayList object from an XmlPullParser.  The XML data could previously have been
+     * generated by writeListXml().  The XmlPullParser must be positioned <em>after</em> the tag
+     * that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "list".
+     * @param name An array of one string, used to return the name attribute of the list's tag.
+     * @return HashMap The newly generated list.
+     */
+    public static final ArrayList readThisListXml(XmlPullParser parser, String endTag,
+            String[] name, ReadMapCallback callback, boolean arrayMap)
+            throws XmlPullParserException, java.io.IOException {
+        ArrayList list = new ArrayList();
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == XmlPullParser.START_TAG) {
+                Object val = readThisValueXml(parser, name, callback, arrayMap);
+                list.add(val);
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return list;
+                }
+                throw new XmlPullParserException(
+                        "Expected " + endTag + " end tag at: " + parser.getName());
+            }
+            eventType = parser.next();
+        } while (eventType != XmlPullParser.END_DOCUMENT);
+
+        throw new XmlPullParserException(
+                "Document ended before " + endTag + " end tag");
+    }
+
+    /**
+     * Read a String[] object from an XmlPullParser.  The XML data could previously have been
+     * generated by writeStringArrayXml().  The XmlPullParser must be positioned <em>after</em> the
+     * tag that begins the list.
+     *
+     * @param parser The XmlPullParser from which to read the list data.
+     * @param endTag Name of the tag that will end the list, usually "string-array".
+     * @param name An array of one string, used to return the name attribute of the list's tag.
+     * @return Returns a newly generated String[].
+     */
+    public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag,
+            String[] name) throws XmlPullParserException, java.io.IOException {
+
+        parser.next();
+
+        List<String> array = new ArrayList<>();
+
+        int eventType = parser.getEventType();
+        do {
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                    try {
+                        array.add(parser.getAttributeValue(null, "value"));
+                    } catch (NullPointerException e) {
+                        throw new XmlPullParserException("Need value attribute in item");
+                    } catch (NumberFormatException e) {
+                        throw new XmlPullParserException("Not a number in value attribute in item");
+                    }
+                } else {
+                    throw new XmlPullParserException("Expected item tag at: " + parser.getName());
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals(endTag)) {
+                    return array.toArray(new String[0]);
+                } else if (parser.getName().equals("item")) {
+
+                } else {
+                    throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
+                            parser.getName());
+                }
+            }
+            eventType = parser.next();
+        } while (eventType != XmlPullParser.END_DOCUMENT);
+
+        throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+    }
+
+    private static Object readThisValueXml(XmlPullParser parser, String[] name,
+            ReadMapCallback callback, boolean arrayMap)
+            throws XmlPullParserException, java.io.IOException {
+        final String valueName = parser.getAttributeValue(null, "name");
+        final String tagName = parser.getName();
+
+        Object res;
+
+        if (tagName.equals("null")) {
+            res = null;
+        } else if (tagName.equals("string")) {
+            String value = "";
+            int eventType;
+            while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
+                if (eventType == XmlPullParser.END_TAG) {
+                    if (parser.getName().equals("string")) {
+                        name[0] = valueName;
+                        return value;
+                    }
+                    throw new XmlPullParserException(
+                            "Unexpected end tag in <string>: " + parser.getName());
+                } else if (eventType == XmlPullParser.TEXT) {
+                    value += parser.getText();
+                } else if (eventType == XmlPullParser.START_TAG) {
+                    throw new XmlPullParserException(
+                            "Unexpected start tag in <string>: " + parser.getName());
+                }
+            }
+            throw new XmlPullParserException(
+                    "Unexpected end of document in <string>");
+        } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) {
+            // all work already done by readThisPrimitiveValueXml
+        } else if (tagName.equals("string-array")) {
+            res = readThisStringArrayXml(parser, "string-array", name);
+            name[0] = valueName;
+            return res;
+        } else if (tagName.equals("list")) {
+            parser.next();
+            res = readThisListXml(parser, "list", name, callback, arrayMap);
+            name[0] = valueName;
+            return res;
+        } else if (callback != null) {
+            res = callback.readThisUnknownObjectXml(parser, tagName);
+            name[0] = valueName;
+            return res;
+        } else {
+            throw new XmlPullParserException("Unknown tag: " + tagName);
+        }
+
+        // Skip through to end tag.
+        int eventType;
+        while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals(tagName)) {
+                    name[0] = valueName;
+                    return res;
+                }
+                throw new XmlPullParserException(
+                        "Unexpected end tag in <" + tagName + ">: " + parser.getName());
+            } else if (eventType == XmlPullParser.TEXT) {
+                throw new XmlPullParserException(
+                        "Unexpected text in <" + tagName + ">: " + parser.getName());
+            } else if (eventType == XmlPullParser.START_TAG) {
+                throw new XmlPullParserException(
+                        "Unexpected start tag in <" + tagName + ">: " + parser.getName());
+            }
+        }
+        throw new XmlPullParserException(
+                "Unexpected end of document in <" + tagName + ">");
+    }
+
+    private static final Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName)
+            throws XmlPullParserException, java.io.IOException {
+        try {
+            if (tagName.equals("int")) {
+                return Integer.parseInt(parser.getAttributeValue(null, "value"));
+            } else if (tagName.equals("long")) {
+                return Long.valueOf(parser.getAttributeValue(null, "value"));
+            } else if (tagName.equals("float")) {
+                return Float.valueOf(parser.getAttributeValue(null, "value"));
+            } else if (tagName.equals("double")) {
+                return Double.valueOf(parser.getAttributeValue(null, "value"));
+            } else if (tagName.equals("boolean")) {
+                return Boolean.valueOf(parser.getAttributeValue(null, "value"));
+            } else {
+                return null;
+            }
+        } catch (NullPointerException e) {
+            throw new XmlPullParserException("Need value attribute in <" + tagName + ">");
+        } catch (NumberFormatException e) {
+            throw new XmlPullParserException(
+                    "Not a number in value attribute in <" + tagName + ">");
+        }
+    }
+
+    public interface ReadMapCallback {
+
+        /**
+         * Called from readThisMapXml when a START_TAG is not recognized. The input stream is
+         * positioned within the start tag so that attributes can be read using in.getAttribute.
+         *
+         * @param in the XML input stream
+         * @param tag the START_TAG that was not recognized.
+         * @return the Object parsed from the stream which will be put into the map.
+         * @throws XmlPullParserException if the START_TAG is not recognized.
+         * @throws IOException on XmlPullParser serialization errors.
+         */
+        Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+                throws XmlPullParserException, IOException;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/services/telephony/CdmaConnection.java b/src/com/android/services/telephony/CdmaConnection.java
index bfb0d23..8020996 100644
--- a/src/com/android/services/telephony/CdmaConnection.java
+++ b/src/com/android/services/telephony/CdmaConnection.java
@@ -26,6 +26,7 @@
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.CallStateException;
 import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.imsphone.ImsPhoneConnection;
 import com.android.internal.telephony.Phone;
 import com.android.phone.settings.SettingsConstants;
 
@@ -84,7 +85,9 @@
         mAllowMute = allowMute;
         mIsOutgoing = isOutgoing;
         mIsCallWaiting = connection != null && connection.getState() == Call.State.WAITING;
-        if (mIsCallWaiting) {
+        boolean isImsCall = getOriginalConnection() instanceof ImsPhoneConnection;
+        // Start call waiting timer for CDMA waiting call.
+        if (mIsCallWaiting && !isImsCall) {
             startCallWaitingTimer();
         }
     }
diff --git a/src/com/android/services/telephony/TelecomAccountRegistry.java b/src/com/android/services/telephony/TelecomAccountRegistry.java
index 3ed8356..19b1d8a 100644
--- a/src/com/android/services/telephony/TelecomAccountRegistry.java
+++ b/src/com/android/services/telephony/TelecomAccountRegistry.java
@@ -71,6 +71,7 @@
         private boolean mIsVideoPresenceSupported;
         private boolean mIsVideoPauseSupported;
         private boolean mIsMergeCallSupported;
+        private boolean mIsVideoConferencingSupported;
 
         AccountEntry(Phone phone, boolean isEmergency, boolean isDummy) {
             mPhone = phone;
@@ -189,6 +190,7 @@
                 instantLetteringExtras = getPhoneAccountExtras();
             }
             mIsMergeCallSupported = isCarrierMergeCallSupported();
+            mIsVideoConferencingSupported = isCarrierVideoConferencingSupported();
 
             if (isEmergency && mContext.getResources().getBoolean(
                     R.bool.config_emergency_account_emergency_calls_only)) {
@@ -244,7 +246,8 @@
             // Check if IMS video pause is supported.
             PersistableBundle b =
                     PhoneGlobals.getInstance().getCarrierConfigForSubId(mPhone.getSubId());
-            return b.getBoolean(CarrierConfigManager.KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL);
+            return b != null &&
+                    b.getBoolean(CarrierConfigManager.KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL);
         }
 
         /**
@@ -256,7 +259,8 @@
         private boolean isCarrierVideoPresenceSupported() {
             PersistableBundle b =
                     PhoneGlobals.getInstance().getCarrierConfigForSubId(mPhone.getSubId());
-            return b.getBoolean(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL);
+            return b != null &&
+                    b.getBoolean(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL);
         }
 
         /**
@@ -267,7 +271,8 @@
         private boolean isCarrierInstantLetteringSupported() {
             PersistableBundle b =
                     PhoneGlobals.getInstance().getCarrierConfigForSubId(mPhone.getSubId());
-            return b.getBoolean(CarrierConfigManager.KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL);
+            return b != null &&
+                    b.getBoolean(CarrierConfigManager.KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL);
         }
 
         /**
@@ -278,7 +283,8 @@
         private boolean isCarrierMergeCallSupported() {
             PersistableBundle b =
                     PhoneGlobals.getInstance().getCarrierConfigForSubId(mPhone.getSubId());
-            return b.getBoolean(CarrierConfigManager.KEY_SUPPORT_CONFERENCE_CALL_BOOL);
+            return b != null &&
+                    b.getBoolean(CarrierConfigManager.KEY_SUPPORT_CONFERENCE_CALL_BOOL);
         }
 
         /**
@@ -289,7 +295,20 @@
         private boolean isCarrierEmergencyVideoCallsAllowed() {
             PersistableBundle b =
                     PhoneGlobals.getInstance().getCarrierConfigForSubId(mPhone.getSubId());
-            return b.getBoolean(CarrierConfigManager.KEY_ALLOW_EMERGENCY_VIDEO_CALLS_BOOL);
+            return b != null &&
+                    b.getBoolean(CarrierConfigManager.KEY_ALLOW_EMERGENCY_VIDEO_CALLS_BOOL);
+        }
+
+        /**
+         * Determines from carrier config whether video conferencing is supported.
+         *
+         * @return {@code true} if video conferencing is supported, {@code false} otherwise.
+         */
+        private boolean isCarrierVideoConferencingSupported() {
+            PersistableBundle b =
+                    PhoneGlobals.getInstance().getCarrierConfigForSubId(mPhone.getSubId());
+            return b != null &&
+                    b.getBoolean(CarrierConfigManager.KEY_SUPPORT_VIDEO_CONFERENCE_CALL_BOOL);
         }
 
         /**
@@ -339,6 +358,14 @@
         public boolean isMergeCallSupported() {
             return mIsMergeCallSupported;
         }
+
+        /**
+         * Indicates whether this account supports video conferencing.
+         * @return {@code true} if the account supports video conferencing, {@code false} otherwise.
+         */
+        public boolean isVideoConferencingSupported() {
+            return mIsVideoConferencingSupported;
+        }
     }
 
     private OnSubscriptionsChangedListener mOnSubscriptionsChangedListener =
@@ -430,6 +457,22 @@
     }
 
     /**
+     * Determines if the {@link AccountEntry} associated with a {@link PhoneAccountHandle} supports
+     * video conferencing.
+     *
+     * @param handle The {@link PhoneAccountHandle}.
+     * @return {@code True} if video conferencing is supported.
+     */
+    boolean isVideoConferencingSupported(PhoneAccountHandle handle) {
+        for (AccountEntry entry : mAccounts) {
+            if (entry.getPhoneAccountHandle().equals(handle)) {
+                return entry.isVideoConferencingSupported();
+            }
+        }
+        return false;
+    }
+
+    /**
      * @return Reference to the {@code TelecomAccountRegistry}'s subscription manager.
      */
     SubscriptionManager getSubscriptionManager() {
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index 0af667a..a4434dd 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -296,7 +296,7 @@
 
         final TelephonyConnection connection =
                 createConnectionFor(phone, null, true /* isOutgoing */, request.getAccountHandle(),
-                        request.getTelecomCallId(), request.getAddress());
+                        request.getTelecomCallId(), request.getAddress(), request.getVideoState());
         if (connection == null) {
             return Connection.createFailedConnection(
                     DisconnectCauseUtil.toTelecomDisconnectCause(
@@ -378,10 +378,15 @@
             return Connection.createCanceledConnection();
         }
 
+        // We should rely on the originalConnection to get the video state.  The request coming
+        // from Telecom does not know the video state of the incoming call.
+        int videoState = originalConnection != null ? originalConnection.getVideoState() :
+                VideoProfile.STATE_AUDIO_ONLY;
+
         Connection connection =
                 createConnectionFor(phone, originalConnection, false /* isOutgoing */,
                         request.getAccountHandle(), request.getTelecomCallId(),
-                        request.getAddress());
+                        request.getAddress(), videoState);
         if (connection == null) {
             return Connection.createCanceledConnection();
         } else {
@@ -478,11 +483,16 @@
             return Connection.createCanceledConnection();
         }
 
+        // We should rely on the originalConnection to get the video state.  The request coming
+        // from Telecom does not know the video state of the unknown call.
+        int videoState = unknownConnection != null ? unknownConnection.getVideoState() :
+                VideoProfile.STATE_AUDIO_ONLY;
+
         TelephonyConnection connection =
                 createConnectionFor(phone, unknownConnection,
                         !unknownConnection.isIncoming() /* isOutgoing */,
                         request.getAccountHandle(), request.getTelecomCallId(),
-                        request.getAddress());
+                        request.getAddress(), videoState);
 
         if (connection == null) {
             return Connection.createCanceledConnection();
@@ -546,7 +556,8 @@
             boolean isOutgoing,
             PhoneAccountHandle phoneAccountHandle,
             String telecomCallId,
-            Uri address) {
+            Uri address,
+            int videoState) {
         TelephonyConnection returnConnection = null;
         int phoneType = phone.getPhoneType();
         if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
@@ -564,9 +575,13 @@
                             phoneAccountHandle));
             boolean isEmergencyCall = (address != null && PhoneNumberUtils.isEmergencyNumber(
                     address.getSchemeSpecificPart()));
-            returnConnection.setConferenceSupported(!isEmergencyCall
-                    && TelecomAccountRegistry.getInstance(this).isMergeCallSupported(
-                            phoneAccountHandle));
+            boolean isVideoCall = VideoProfile.isVideo(videoState);
+            boolean isConferencingSupported = TelecomAccountRegistry.getInstance(this)
+                    .isMergeCallSupported(phoneAccountHandle);
+            boolean isVideoConferencingSupported = TelecomAccountRegistry.getInstance(this)
+                    .isVideoConferencingSupported(phoneAccountHandle);
+            returnConnection.setConferenceSupported(!isEmergencyCall && isConferencingSupported
+                    && (!isVideoCall || (isVideoCall && isVideoConferencingSupported)));
         }
         return returnConnection;
     }
diff --git a/tests/src/com/android/phone/common/mail/store/imap/DigestMd5UtilsTest.java b/tests/src/com/android/phone/common/mail/store/imap/DigestMd5UtilsTest.java
new file mode 100644
index 0000000..5534632
--- /dev/null
+++ b/tests/src/com/android/phone/common/mail/store/imap/DigestMd5UtilsTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.phone.common.mail.store.imap;
+
+import junit.framework.TestCase;
+
+public class DigestMd5UtilsTest extends TestCase {
+
+    public void testGetResponse() {
+        // Example data from RFC 2831.4
+        DigestMd5Utils.Data data = new DigestMd5Utils.Data();
+        data.username = "chris";
+        data.password = "secret";
+        data.realm = "elwood.innosoft.com";
+        data.nonce = "OA6MG9tEQGm2hh";
+        data.cnonce = "OA6MHXh6VqTrRk";
+        data.nc = "00000001";
+        data.qop = "auth";
+        data.digestUri = "imap/elwood.innosoft.com";
+        String response = DigestMd5Utils.getResponse(data, false);
+        assertEquals("d388dad90d4bbd760a152321f2143af7", response);
+    }
+
+    public void testGetResponse_ResponseAuth() {
+        // Example data from RFC 2831.4
+        DigestMd5Utils.Data data = new DigestMd5Utils.Data();
+        data.username = "chris";
+        data.password = "secret";
+        data.realm = "elwood.innosoft.com";
+        data.nonce = "OA6MG9tEQGm2hh";
+        data.cnonce = "OA6MHXh6VqTrRk";
+        data.nc = "00000001";
+        data.qop = "auth";
+        data.digestUri = "imap/elwood.innosoft.com";
+        String response = DigestMd5Utils.getResponse(data, true);
+        assertEquals("ea40f60335c427b5527b84dbabcdfffd", response);
+    }
+
+}
diff --git a/tests/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelperTest.java b/tests/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelperTest.java
new file mode 100644
index 0000000..63c7f60
--- /dev/null
+++ b/tests/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelperTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+
+package com.android.phone.vvm.omtp;
+
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_CARRIER_VVM_PACKAGE_NAME_STRING;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_DESTINATION_NUMBER_STRING;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_PORT_NUMBER_INT;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_PREFETCH_BOOL;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_SSL_PORT_NUMBER_INT;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_TYPE_STRING;
+
+import android.os.PersistableBundle;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class OmtpVvmCarrierConfigHelperTest extends TestCase {
+
+    private static final String CARRIER_TYPE = "omtp.carrier";
+    private static final String CARRIER_PACKAGE_NAME = "omtp.carrier.package";
+    private static final boolean CARRIER_CELLULAR_REQUIRED = false;
+    private static final boolean CARRIER_PREFETCH = true;
+    private static final String CARRIER_DESTINATION_NUMBER = "123";
+    private static final int CARRIER_APPLICATION_PORT = 456;
+    private static final int DEFAULT_SSL_PORT = 0;
+    private static final Set<String> DEFAULT_DISABLED_CAPABILITIES = null;
+
+    private static final String TELEPHONY_TYPE = "omtp.telephony";
+    private static final String[] TELEPHONY_PACKAGE_NAMES = {"omtp.telephony.package"};
+    private static final boolean TELEPHONY_CELLULAR_REQUIRED = true;
+    private static final boolean TELEPHONY_PREFETCH = false;
+    private static final String TELEPHONY_DESTINATION_NUMBER = "321";
+    private static final int TELEPHONY_APPLICATION_PORT = 654;
+    private static final int TELEPHONY_SSL_PORT = 997;
+    private static final String[] TELEPHONY_DISABLED_CAPABILITIES = {"foo"};
+
+    private OmtpVvmCarrierConfigHelper mHelper;
+
+    public void testCarrierConfig() {
+        mHelper = new OmtpVvmCarrierConfigHelper(createCarrierConfig(), null);
+        verifyCarrierConfig();
+        verifyDefaultExtraConfig();
+    }
+
+    public void testTelephonyConfig() {
+        mHelper = new OmtpVvmCarrierConfigHelper(null, createTelephonyConfig());
+        verifyTelephonyConfig();
+        verifyTelephonyExtraConfig();
+    }
+
+    public void testMixedConfig() {
+        mHelper = new OmtpVvmCarrierConfigHelper(createCarrierConfig(), createTelephonyConfig());
+        verifyCarrierConfig();
+        verifyTelephonyExtraConfig();
+    }
+
+    private PersistableBundle createCarrierConfig() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(KEY_VVM_TYPE_STRING, CARRIER_TYPE);
+        bundle.putString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING,
+                CARRIER_PACKAGE_NAME);
+        bundle.putBoolean(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL,
+                CARRIER_CELLULAR_REQUIRED);
+        bundle.putBoolean(KEY_VVM_PREFETCH_BOOL,
+                CARRIER_PREFETCH);
+        bundle.putString(KEY_VVM_DESTINATION_NUMBER_STRING,
+                CARRIER_DESTINATION_NUMBER);
+        bundle.putInt(KEY_VVM_PORT_NUMBER_INT, CARRIER_APPLICATION_PORT);
+        return bundle;
+    }
+
+    private void verifyCarrierConfig() {
+        assertEquals(CARRIER_TYPE, mHelper.getVvmType());
+        assertEquals(new HashSet<>(Arrays.asList(CARRIER_PACKAGE_NAME)),
+                mHelper.getCarrierVvmPackageNames());
+        assertEquals(CARRIER_CELLULAR_REQUIRED, mHelper.isCellularDataRequired());
+        assertEquals(CARRIER_PREFETCH, mHelper.isPrefetchEnabled());
+        assertEquals(CARRIER_APPLICATION_PORT, mHelper.getApplicationPort());
+        assertEquals(CARRIER_DESTINATION_NUMBER, mHelper.getDestinationNumber());
+    }
+
+
+    private void verifyDefaultExtraConfig() {
+        assertEquals(DEFAULT_SSL_PORT, mHelper.getSslPort());
+        assertEquals(DEFAULT_DISABLED_CAPABILITIES, mHelper.getDisabledCapabilities());
+    }
+
+
+    private PersistableBundle createTelephonyConfig() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(KEY_VVM_TYPE_STRING, TELEPHONY_TYPE);
+        bundle.putStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY,
+                TELEPHONY_PACKAGE_NAMES);
+        bundle.putBoolean(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL,
+                TELEPHONY_CELLULAR_REQUIRED);
+        bundle.putBoolean(KEY_VVM_PREFETCH_BOOL,
+                TELEPHONY_PREFETCH);
+        bundle.putString(KEY_VVM_DESTINATION_NUMBER_STRING,
+                TELEPHONY_DESTINATION_NUMBER);
+        bundle.putInt(KEY_VVM_PORT_NUMBER_INT, TELEPHONY_APPLICATION_PORT);
+        bundle.putInt(KEY_VVM_SSL_PORT_NUMBER_INT, TELEPHONY_SSL_PORT);
+        bundle.putStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY,
+                TELEPHONY_DISABLED_CAPABILITIES);
+        return bundle;
+    }
+
+    private void verifyTelephonyConfig() {
+        assertEquals(TELEPHONY_TYPE, mHelper.getVvmType());
+        assertEquals(new HashSet<>(Arrays.asList(TELEPHONY_PACKAGE_NAMES)),
+                mHelper.getCarrierVvmPackageNames());
+        assertEquals(TELEPHONY_CELLULAR_REQUIRED, mHelper.isCellularDataRequired());
+        assertEquals(TELEPHONY_PREFETCH, mHelper.isPrefetchEnabled());
+        assertEquals(TELEPHONY_APPLICATION_PORT, mHelper.getApplicationPort());
+        assertEquals(TELEPHONY_DESTINATION_NUMBER, mHelper.getDestinationNumber());
+    }
+
+    private void verifyTelephonyExtraConfig() {
+        assertEquals(TELEPHONY_SSL_PORT, mHelper.getSslPort());
+        assertEquals(new HashSet<>(Arrays.asList(TELEPHONY_DISABLED_CAPABILITIES)),
+                mHelper.getDisabledCapabilities());
+    }
+}
diff --git a/tests/src/com/android/phone/vvm/omtp/TelephonyVvmConfigManagerTest.java b/tests/src/com/android/phone/vvm/omtp/TelephonyVvmConfigManagerTest.java
new file mode 100644
index 0000000..8e7a0da
--- /dev/null
+++ b/tests/src/com/android/phone/vvm/omtp/TelephonyVvmConfigManagerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.phone.vvm.omtp;
+
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_DESTINATION_NUMBER_STRING;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_PORT_NUMBER_INT;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_PREFETCH_BOOL;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_SSL_PORT_NUMBER_INT;
+import static com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper.KEY_VVM_TYPE_STRING;
+
+import android.os.PersistableBundle;
+
+import junit.framework.TestCase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.StringReader;
+import java.util.Arrays;
+
+public class TelephonyVvmConfigManagerTest extends TestCase {
+
+    private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+            + "<list name=\"carrier_config_list\">\n";
+    private static final String XML_FOOTER = "</list>";
+
+    private static final String CARRIER = "  <pbundle_as_map>\n"
+            + "    <string-array name=\"mccmnc\">\n"
+            + "      <item value=\"12345\"/>\n"
+            + "      <item value=\"67890\"/>\n"
+            + "    </string-array>\n"
+            + "    <int name=\"vvm_port_number_int\" value=\"54321\"/>\n"
+            + "    <string name=\"vvm_destination_number_string\">11111</string>\n"
+            + "    <string-array name=\"carrier_vvm_package_name_string_array\">\n"
+            + "      <item value=\"com.android.phone\"/>\n"
+            + "    </string-array>\n"
+            + "    <string name=\"vvm_type_string\">vvm_type_omtp</string>\n"
+            + "    <boolean name=\"vvm_cellular_data_required\" value=\"true\"/>\n"
+            + "    <boolean name=\"vvm_prefetch\" value=\"true\"/>\n"
+            + "    <int name=\"vvm_ssl_port_number_int\" value=\"997\"/>\n"
+            + "    <string-array name=\"vvm_disabled_capabilities_string_array\">\n"
+            + "      <item value =\"foo\"/>\n"
+            + "      <item value =\"bar\"/>\n"
+            + "    </string-array>\n"
+            + "  </pbundle_as_map>\n";
+
+    private static final String CARRIER_EMPTY = "<pbundle_as_map></pbundle_as_map>\n";
+
+
+    public void testLoadConfigFromXml() {
+        TelephonyVvmConfigManager manager = createManager(XML_HEADER + CARRIER + XML_FOOTER);
+        verifyCarrier(manager.getConfig("12345"));
+        verifyCarrier(manager.getConfig("67890"));
+    }
+
+    public void testLoadConfigFromXml_Multiple() {
+        TelephonyVvmConfigManager manager =
+                createManager(XML_HEADER + CARRIER + CARRIER + XML_FOOTER);
+        verifyCarrier(manager.getConfig("12345"));
+        verifyCarrier(manager.getConfig("67890"));
+    }
+
+    public void testLoadConfigFromXml_Empty() {
+        createManager(XML_HEADER + CARRIER_EMPTY + XML_FOOTER);
+    }
+
+
+    private void verifyCarrier(PersistableBundle config) {
+        assertTrue(Arrays.equals(new String[]{"12345", "67890"},
+                config.getStringArray(TelephonyVvmConfigManager.KEY_MCCMNC)));
+        assertEquals(54321, config.getInt(KEY_VVM_PORT_NUMBER_INT));
+        assertEquals("11111", config.getString(KEY_VVM_DESTINATION_NUMBER_STRING));
+        assertTrue(Arrays.equals(new String[]{"com.android.phone"},
+                config.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)));
+        assertEquals("vvm_type_omtp", config.getString(KEY_VVM_TYPE_STRING));
+        assertEquals(true, config.getBoolean(KEY_VVM_PREFETCH_BOOL));
+        assertEquals(true, config.getBoolean(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL));
+        assertEquals(997, config.getInt(KEY_VVM_SSL_PORT_NUMBER_INT));
+        assertTrue(Arrays.equals(new String[]{"foo", "bar"},
+                config.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)));
+    }
+
+    private TelephonyVvmConfigManager createManager(String xml) {
+        try {
+            XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+            parser.setInput(new StringReader(xml));
+            return new TelephonyVvmConfigManager(parser);
+        } catch (XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}