Update Dialer source from latest green build.

* Refactor voicemail component
* Add new enriched calling components

Test: treehugger, manual aosp testing

Change-Id: I521a0f86327d4b42e14d93927c7d613044ed5942
diff --git a/java/com/android/voicemail/VoicemailClient.java b/java/com/android/voicemail/VoicemailClient.java
new file mode 100644
index 0000000..b237f65
--- /dev/null
+++ b/java/com/android/voicemail/VoicemailClient.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 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.voicemail;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import java.util.List;
+
+/** Public interface for the voicemail module */
+public interface VoicemailClient {
+
+  /**
+   * Broadcast to tell the client to upload local database changes to the server. Since the dialer
+   * UI and the client are in the same package, the {@link
+   * android.content.Intent#ACTION_PROVIDER_CHANGED} will always be a self-change even if the UI is
+   * external to the client.
+   */
+  String ACTION_UPLOAD = "com.android.voicemailomtp.VoicemailClient.ACTION_UPLOAD";
+
+  /**
+   * Appends the selection to ignore voicemails from non-active OMTP voicemail package. In OC there
+   * can be multiple packages handling OMTP voicemails which represents the same source of truth.
+   * These packages should mark their voicemails as {@link Voicemails#IS_OMTP_VOICEMAIL} and only
+   * the voicemails from {@link TelephonyManager#getVisualVoicemailPackageName()} should be shown.
+   * For example, the user synced voicemails with DialerA, and then switched to DialerB, voicemails
+   * from DialerA should be ignored as they are no longer current. Voicemails from {@link
+   * #OMTP_VOICEMAIL_BLACKLIST} will also be ignored as they are voicemail source only valid pre-OC.
+   */
+  void appendOmtpVoicemailSelectionClause(
+      Context context, StringBuilder where, List<String> selectionArgs);
+  /**
+   * @return the class name of the {@link android.preference.PreferenceFragment} for voicemail
+   *     settings, or {@code null} if dialer cannot control voicemail settings. Always return {@code
+   *     null} before OC.
+   */
+  @Nullable
+  String getSettingsFragment();
+
+  boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle);
+
+  void setVoicemailArchiveEnabled(
+      Context context, PhoneAccountHandle phoneAccountHandle, boolean value);
+}
diff --git a/java/com/android/voicemail/VoicemailComponent.java b/java/com/android/voicemail/VoicemailComponent.java
new file mode 100644
index 0000000..6dd6f9d
--- /dev/null
+++ b/java/com/android/voicemail/VoicemailComponent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 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.voicemail;
+
+import android.content.Context;
+import dagger.Subcomponent;
+import com.android.voicemail.impl.VoicemailClientImpl;
+
+/** Subcomponent that can be used to access the voicemail implementation. */
+public class VoicemailComponent {
+    private static VoicemailComponent instance;
+    private VoicemailClientImpl voicemailClient;
+
+  public VoicemailClient getVoicemailClient() {
+    if (voicemailClient == null) {
+        voicemailClient = new VoicemailClientImpl();
+    }
+    return voicemailClient;
+  }
+
+  public static VoicemailComponent get(Context context) {
+    if (instance == null) {
+        instance = new VoicemailComponent();
+    }
+    return instance;
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    VoicemailComponent voicemailComponent();
+  }
+}
diff --git a/java/com/android/voicemail/impl/ActivationTask.java b/java/com/android/voicemail/impl/ActivationTask.java
new file mode 100644
index 0000000..c471611
--- /dev/null
+++ b/java/com/android/voicemail/impl/ActivationTask.java
@@ -0,0 +1,298 @@
+/*
+ * 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.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sms.StatusSmsFetcher;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Task to activate the visual voicemail service. A request to activate VVM will be sent to the
+ * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If
+ * the user is not provisioned provisioning will be attempted. Activation happens when the phone
+ * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier
+ * spontaneously sent a STATUS SMS.
+ */
+@TargetApi(VERSION_CODES.O)
+public class ActivationTask extends BaseTask {
+
+  private static final String TAG = "VvmActivationTask";
+
+  private static final int RETRY_TIMES = 4;
+  private static final int RETRY_INTERVAL_MILLIS = 5_000;
+
+  private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle";
+
+  @Nullable private static DeviceProvisionedObserver sDeviceProvisionedObserver;
+
+  private final RetryPolicy mRetryPolicy;
+
+  private Bundle mMessageData;
+
+  public ActivationTask() {
+    super(TASK_ACTIVATION);
+    mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
+    addPolicy(mRetryPolicy);
+  }
+
+  /** Has the user gone through the setup wizard yet. */
+  private static boolean isDeviceProvisioned(Context context) {
+    return Settings.Global.getInt(
+            context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0)
+        == 1;
+  }
+
+  /**
+   * @param messageData The optional bundle from {@link android.provider.VoicemailContract#
+   *     EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task
+   *     will request a status SMS itself.
+   */
+  public static void start(
+      Context context, PhoneAccountHandle phoneAccountHandle, @Nullable Bundle messageData) {
+    if (!isDeviceProvisioned(context)) {
+      VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing");
+      // Activation might need information such as system language to be set, so wait until
+      // the setup wizard is finished. The data bundle from the SMS will be re-requested upon
+      // activation.
+      queueActivationAfterProvisioned(context, phoneAccountHandle);
+      return;
+    }
+
+    Intent intent = BaseTask.createIntent(context, ActivationTask.class, phoneAccountHandle);
+    if (messageData != null) {
+      intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData);
+    }
+    context.startService(intent);
+  }
+
+  @Override
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+    mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE);
+  }
+
+  @Override
+  public Intent createRestartIntent() {
+    Intent intent = super.createRestartIntent();
+    // mMessageData is discarded, request a fresh STATUS SMS for retries.
+    return intent;
+  }
+
+  @Override
+  @WorkerThread
+  public void onExecuteInBackgroundThread() {
+    Assert.isNotMainThread();
+
+    PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+    if (phoneAccountHandle == null) {
+      // This should never happen
+      VvmLog.e(TAG, "null PhoneAccountHandle");
+      return;
+    }
+
+    OmtpVvmCarrierConfigHelper helper =
+        new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
+    if (!helper.isValid()) {
+      VvmLog.i(TAG, "VVM not supported on phoneAccountHandle " + phoneAccountHandle);
+      VvmAccountManager.removeAccount(getContext(), phoneAccountHandle);
+      return;
+    }
+
+    // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm
+    // content provider URI which we will use.  On some occasions, setting that URI will
+    // fail, so we will perform a few attempts to ensure that the vvm content provider has
+    // a good chance of being started up.
+    if (!VoicemailStatus.edit(getContext(), phoneAccountHandle)
+        .setType(helper.getVvmType())
+        .apply()) {
+      VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType());
+      fail();
+    }
+    VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType());
+
+    if (VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle)) {
+      VvmLog.i(TAG, "Account is already activated");
+      return;
+    }
+    helper.handleEvent(
+        VoicemailStatus.edit(getContext(), phoneAccountHandle), OmtpEvents.CONFIG_ACTIVATING);
+
+    if (!hasSignal(getContext(), phoneAccountHandle)) {
+      VvmLog.i(TAG, "Service lost during activation, aborting");
+      // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING
+      // event.
+      helper.handleEvent(
+          VoicemailStatus.edit(getContext(), phoneAccountHandle),
+          OmtpEvents.NOTIFICATION_SERVICE_LOST);
+      // Don't retry, a new activation will be started after the signal returned.
+      return;
+    }
+
+    helper.activateSmsFilter();
+    VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor();
+
+    VisualVoicemailProtocol protocol = helper.getProtocol();
+
+    Bundle data;
+    if (mMessageData != null) {
+      // The content of STATUS SMS is provided to launch this task, no need to request it
+      // again.
+      data = mMessageData;
+    } else {
+      try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(), phoneAccountHandle)) {
+        protocol.startActivation(helper, fetcher.getSentIntent());
+        // Both the fetcher and OmtpMessageReceiver will be triggered, but
+        // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be
+        // rejected because the task is still running.
+        data = fetcher.get();
+      } catch (TimeoutException e) {
+        // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS
+        // handleEvent() will do the logging.
+        helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT);
+        fail();
+        return;
+      } catch (CancellationException e) {
+        VvmLog.e(TAG, "Unable to send status request SMS");
+        fail();
+        return;
+      } catch (InterruptedException | ExecutionException | IOException e) {
+        VvmLog.e(TAG, "can't get future STATUS SMS", e);
+        fail();
+        return;
+      }
+    }
+
+    StatusMessage message = new StatusMessage(data);
+    VvmLog.d(
+        TAG,
+        "STATUS SMS received: st="
+            + message.getProvisioningStatus()
+            + ", rc="
+            + message.getReturnCode());
+
+    if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) {
+      VvmLog.d(TAG, "subscriber ready, no activation required");
+      updateSource(getContext(), phoneAccountHandle, status, message);
+    } else {
+      if (helper.supportsProvisioning()) {
+        VvmLog.i(TAG, "Subscriber not ready, start provisioning");
+        helper.startProvisioning(this, phoneAccountHandle, status, message, data);
+
+      } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) {
+        VvmLog.i(TAG, "Subscriber new but provisioning is not supported");
+        // Ignore the non-ready state and attempt to use the provided info as is.
+        // This is probably caused by not completing the new user tutorial.
+        updateSource(getContext(), phoneAccountHandle, status, message);
+      } else {
+        VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported");
+        helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
+      }
+    }
+  }
+
+  public static void updateSource(
+      Context context,
+      PhoneAccountHandle phone,
+      VoicemailStatus.Editor status,
+      StatusMessage message) {
+
+    if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) {
+      OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phone);
+      helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS);
+
+      // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
+      VvmAccountManager.addAccount(context, phone, message);
+
+      SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC);
+    } else {
+      VvmLog.e(TAG, "Visual voicemail not available for subscriber.");
+    }
+  }
+
+  private static boolean hasSignal(Context context, PhoneAccountHandle phoneAccountHandle) {
+    TelephonyManager telephonyManager =
+        context
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(phoneAccountHandle);
+    return telephonyManager.getServiceState().getState() == ServiceState.STATE_IN_SERVICE;
+  }
+
+  private static void queueActivationAfterProvisioned(
+      Context context, PhoneAccountHandle phoneAccountHandle) {
+    if (sDeviceProvisionedObserver == null) {
+      sDeviceProvisionedObserver = new DeviceProvisionedObserver(context);
+      context
+          .getContentResolver()
+          .registerContentObserver(
+              Settings.Global.getUriFor(Global.DEVICE_PROVISIONED),
+              false,
+              sDeviceProvisionedObserver);
+    }
+    sDeviceProvisionedObserver.addPhoneAcountHandle(phoneAccountHandle);
+  }
+
+  private static class DeviceProvisionedObserver extends ContentObserver {
+
+    private final Context mContext;
+    private final Set<PhoneAccountHandle> mPhoneAccountHandles = new HashSet<>();
+
+    private DeviceProvisionedObserver(Context context) {
+      super(null);
+      mContext = context;
+    }
+
+    public void addPhoneAcountHandle(PhoneAccountHandle phoneAccountHandle) {
+      mPhoneAccountHandles.add(phoneAccountHandle);
+    }
+
+    @Override
+    public void onChange(boolean selfChange) {
+      if (isDeviceProvisioned(mContext)) {
+        VvmLog.i(TAG, "device provisioned, resuming activation");
+        for (PhoneAccountHandle phoneAccountHandle : mPhoneAccountHandles) {
+          start(mContext, phoneAccountHandle, null);
+        }
+        mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver);
+        sDeviceProvisionedObserver = null;
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/AndroidManifest.xml b/java/com/android/voicemail/impl/AndroidManifest.xml
new file mode 100644
index 0000000..0d90d59
--- /dev/null
+++ b/java/com/android/voicemail/impl/AndroidManifest.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+  package="com.android.voicemailomtp"
+  >
+
+  <application
+    android:allowBackup="false"
+    android:supportsRtl="true"
+    android:usesCleartextTraffic="true"
+    android:defaultToDeviceProtectedStorage="true"
+    android:directBootAware="true">
+
+    <!-- Causes the "Voicemail" item under "Calls" setting to be hidden. The voicemail module will
+      be handling the settings. Has no effect before OC where dialer cannot provide voicemail
+      settings-->
+    <meta-data android:name="android.telephony.HIDE_VOICEMAIL_SETTINGS_MENU" android:value="true"/>
+
+    <receiver
+      android:name="com.android.voicemail.impl.sms.OmtpMessageReceiver"
+      android:exported="false"
+      androidprv:systemUserOnly="true">
+      <intent-filter>
+        <action android:name="com.android.vociemailomtp.sms.sms_received"/>
+      </intent-filter>
+    </receiver>
+
+    <receiver android:name="com.android.voicemail.impl.VoicemailClientReceiver"
+      android:exported="false">
+      <intent-filter>
+        <action android:name="com.android.voicemailomtp.VoicemailClient.ACTION_UPLOAD"/>
+      </intent-filter>
+    </receiver>
+
+    <receiver
+      android:name="com.android.voicemail.impl.fetch.FetchVoicemailReceiver"
+      android:exported="true"
+      android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
+      androidprv:systemUserOnly="true">
+      <intent-filter>
+        <action android:name="android.intent.action.FETCH_VOICEMAIL"/>
+        <data
+          android:scheme="content"
+          android:host="com.android.voicemail"
+          android:mimeType="vnd.android.cursor.item/voicemail"/>
+      </intent-filter>
+    </receiver>
+    <receiver
+      android:name="com.android.voicemail.impl.sync.OmtpVvmSyncReceiver"
+      android:exported="true"
+      android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
+      androidprv:systemUserOnly="true">
+      <intent-filter>
+        <action android:name="android.provider.action.SYNC_VOICEMAIL"/>
+      </intent-filter>
+    </receiver>
+    <receiver
+      android:name="com.android.voicemail.impl.sync.VoicemailProviderChangeReceiver"
+      android:exported="true">
+      <intent-filter>
+        <action android:name="android.intent.action.PROVIDER_CHANGED"/>
+        <data
+          android:scheme="content"
+          android:host="com.android.voicemail"
+          android:mimeType="vnd.android.cursor.dir/voicemails"/>
+      </intent-filter>
+    </receiver>
+
+    <service
+      android:name="com.android.voicemail.impl.scheduling.TaskSchedulerService"
+      android:exported="false"/>
+
+    <service
+      android:name="com.android.voicemail.impl.OmtpService"
+      android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
+      android:exported="true">
+      <intent-filter>
+        <action android:name="android.telephony.VisualVoicemailService"/>
+      </intent-filter>
+    </service>
+
+    <activity
+      android:name="com.android.voicemail.impl.settings.VoicemailChangePinActivity"
+      android:exported="false"
+      android:windowSoftInputMode="stateVisible|adjustResize">
+    </activity>
+  </application>
+</manifest>
diff --git a/java/com/android/voicemail/impl/Assert.java b/java/com/android/voicemail/impl/Assert.java
new file mode 100644
index 0000000..fe06372
--- /dev/null
+++ b/java/com/android/voicemail/impl/Assert.java
@@ -0,0 +1,57 @@
+/*
+ * 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.voicemail.impl;
+
+import android.os.Looper;
+
+/** Assertions which will result in program termination. */
+public class Assert {
+
+  private static Boolean sIsMainThreadForTest;
+
+  public static void isTrue(boolean condition) {
+    if (!condition) {
+      throw new AssertionError("Expected condition to be true");
+    }
+  }
+
+  public static void isMainThread() {
+    if (sIsMainThreadForTest != null) {
+      isTrue(sIsMainThreadForTest);
+      return;
+    }
+    isTrue(Looper.getMainLooper().equals(Looper.myLooper()));
+  }
+
+  public static void isNotMainThread() {
+    if (sIsMainThreadForTest != null) {
+      isTrue(!sIsMainThreadForTest);
+      return;
+    }
+    isTrue(!Looper.getMainLooper().equals(Looper.myLooper()));
+  }
+
+  public static void fail() {
+    throw new AssertionError("Fail");
+  }
+
+  /** Override the main thread status for tests. Set to null to revert to normal behavior */
+  @NeededForTesting
+  public static void setIsMainThreadForTesting(Boolean isMainThread) {
+    sIsMainThreadForTest = isMainThread;
+  }
+}
diff --git a/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java b/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java
new file mode 100644
index 0000000..13aaf05
--- /dev/null
+++ b/java/com/android/voicemail/impl/DefaultOmtpEventHandler.java
@@ -0,0 +1,193 @@
+/*
+ * 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.voicemail.impl;
+
+import android.content.Context;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import com.android.voicemail.impl.OmtpEvents.Type;
+
+public class DefaultOmtpEventHandler {
+
+  private static final String TAG = "DefErrorCodeHandler";
+
+  public static void handleEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    switch (event.getType()) {
+      case Type.CONFIGURATION:
+        handleConfigurationEvent(context, status, event);
+        break;
+      case Type.DATA_CHANNEL:
+        handleDataChannelEvent(context, status, event);
+        break;
+      case Type.NOTIFICATION_CHANNEL:
+        handleNotificationChannelEvent(context, config, status, event);
+        break;
+      case Type.OTHER:
+        handleOtherEvent(context, status, event);
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
+    }
+  }
+
+  private static void handleConfigurationEvent(
+      Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case CONFIG_DEFAULT_PIN_REPLACED:
+      case CONFIG_REQUEST_STATUS_SUCCESS:
+      case CONFIG_PIN_SET:
+        status
+            .setConfigurationState(VoicemailContract.Status.CONFIGURATION_STATE_OK)
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case CONFIG_ACTIVATING:
+        // Wipe all errors from the last activation. All errors shown should be new errors
+        // for this activation.
+        status
+            .setConfigurationState(Status.CONFIGURATION_STATE_CONFIGURING)
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case CONFIG_ACTIVATING_SUBSEQUENT:
+        status
+            .setConfigurationState(Status.CONFIGURATION_STATE_OK)
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case CONFIG_SERVICE_NOT_AVAILABLE:
+        status.setConfigurationState(Status.CONFIGURATION_STATE_FAILED).apply();
+        break;
+      case CONFIG_STATUS_SMS_TIME_OUT:
+        status.setConfigurationState(Status.CONFIGURATION_STATE_FAILED).apply();
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid configuration event " + event);
+    }
+  }
+
+  private static void handleDataChannelEvent(
+      Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case DATA_IMAP_OPERATION_STARTED:
+      case DATA_IMAP_OPERATION_COMPLETED:
+        status.setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
+        break;
+
+      case DATA_NO_CONNECTION:
+        status.setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION).apply();
+        break;
+
+      case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
+        status
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED)
+            .apply();
+        break;
+      case DATA_INVALID_PORT:
+        status
+            .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
+            .apply();
+        break;
+      case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
+        status
+            .setDataChannelState(
+                VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR)
+            .apply();
+        break;
+      case DATA_SSL_INVALID_HOST_NAME:
+      case DATA_CANNOT_ESTABLISH_SSL_SESSION:
+      case DATA_IOE_ON_OPEN:
+      case DATA_GENERIC_IMAP_IOE:
+        status
+            .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR)
+            .apply();
+        break;
+      case DATA_BAD_IMAP_CREDENTIAL:
+      case DATA_AUTH_UNKNOWN_USER:
+      case DATA_AUTH_UNKNOWN_DEVICE:
+      case DATA_AUTH_INVALID_PASSWORD:
+      case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
+      case DATA_AUTH_SERVICE_NOT_PROVISIONED:
+      case DATA_AUTH_SERVICE_NOT_ACTIVATED:
+      case DATA_AUTH_USER_IS_BLOCKED:
+        status
+            .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
+            .apply();
+        break;
+
+      case DATA_REJECTED_SERVER_RESPONSE:
+      case DATA_INVALID_INITIAL_SERVER_RESPONSE:
+      case DATA_MAILBOX_OPEN_FAILED:
+      case DATA_SSL_EXCEPTION:
+      case DATA_ALL_SOCKET_CONNECTION_FAILED:
+        status
+            .setDataChannelState(VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_ERROR)
+            .apply();
+        break;
+
+      default:
+        VvmLog.wtf(TAG, "invalid data channel event " + event);
+    }
+  }
+
+  private static void handleNotificationChannelEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    switch (event) {
+      case NOTIFICATION_IN_SERVICE:
+        status
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            // Clear the error state. A sync should follow signal return so any error
+            // will be reposted.
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case NOTIFICATION_SERVICE_LOST:
+        status.setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+        if (config.isCellularDataRequired()) {
+          status.setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED);
+        }
+        status.apply();
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid notification channel event " + event);
+    }
+  }
+
+  private static void handleOtherEvent(
+      Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case OTHER_SOURCE_REMOVED:
+        status
+            .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+            .apply();
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid other event " + event);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/NeededForTesting.java b/java/com/android/voicemail/impl/NeededForTesting.java
new file mode 100644
index 0000000..70e7383
--- /dev/null
+++ b/java/com/android/voicemail/impl/NeededForTesting.java
@@ -0,0 +1,23 @@
+/*
+ * 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.voicemail.impl;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+public @interface NeededForTesting {}
diff --git a/java/com/android/voicemail/impl/OmtpConstants.java b/java/com/android/voicemail/impl/OmtpConstants.java
new file mode 100644
index 0000000..599d0d5
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpConstants.java
@@ -0,0 +1,239 @@
+/*
+ * 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.voicemail.impl;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Wrapper class to hold relevant OMTP constants as defined in the OMTP spec.
+ *
+ * <p>In essence this is a programmatic representation of the relevant portions of OMTP spec.
+ */
+public class OmtpConstants {
+  public static final String SMS_FIELD_SEPARATOR = ";";
+  public static final String SMS_KEY_VALUE_SEPARATOR = "=";
+  public static final String SMS_PREFIX_SEPARATOR = ":";
+
+  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";
+
+  /** OMTP protocol versions. */
+  public static final String PROTOCOL_VERSION1_1 = "11";
+
+  public static final String PROTOCOL_VERSION1_2 = "12";
+  public static final String PROTOCOL_VERSION1_3 = "13";
+
+  ///////////////////////// Client/Mobile originated SMS //////////////////////
+
+  /** Mobile Originated requests */
+  public static final String ACTIVATE_REQUEST = "Activate";
+
+  public static final String DEACTIVATE_REQUEST = "Deactivate";
+  public static final String STATUS_REQUEST = "Status";
+
+  /** fields that can be present in a Mobile Originated OMTP SMS */
+  public static final String CLIENT_TYPE = "ct";
+
+  public static final String APPLICATION_PORT = "pt";
+  public static final String PROTOCOL_VERSION = "pv";
+
+  //////////////////////////////// Sync SMS fields ////////////////////////////
+
+  /**
+   * Sync SMS fields.
+   *
+   * <p>Each string constant is the field's key in the SMS body which is used by the parser to
+   * identify the field's value, if present, in the SMS body.
+   */
+
+  /** The event that triggered this SYNC SMS. See {@link OmtpConstants#SYNC_TRIGGER_EVENT_VALUES} */
+  public static final String SYNC_TRIGGER_EVENT = "ev";
+
+  public static final String MESSAGE_UID = "id";
+  public static final String MESSAGE_LENGTH = "l";
+  public static final String NUM_MESSAGE_COUNT = "c";
+  /** See {@link OmtpConstants#CONTENT_TYPE_VALUES} */
+  public static final String CONTENT_TYPE = "t";
+
+  public static final String SENDER = "s";
+  public static final String TIME = "dt";
+
+  /**
+   * SYNC message trigger events.
+   *
+   * <p>These are the possible values of {@link OmtpConstants#SYNC_TRIGGER_EVENT}.
+   */
+  public static final String NEW_MESSAGE = "NM";
+
+  public static final String MAILBOX_UPDATE = "MBU";
+  public static final String GREETINGS_UPDATE = "GU";
+
+  public static final String[] SYNC_TRIGGER_EVENT_VALUES = {
+    NEW_MESSAGE, MAILBOX_UPDATE, GREETINGS_UPDATE
+  };
+
+  /**
+   * Content types supported by OMTP VVM.
+   *
+   * <p>These are the possible values of {@link OmtpConstants#CONTENT_TYPE}.
+   */
+  public static final String VOICE = "v";
+
+  public static final String VIDEO = "o";
+  public static final String FAX = "f";
+  /** Voice message deposited by an external application */
+  public static final String INFOTAINMENT = "i";
+  /** Empty Call Capture - i.e. voicemail with no voice message. */
+  public static final String ECC = "e";
+
+  public static final String[] CONTENT_TYPE_VALUES = {VOICE, VIDEO, FAX, INFOTAINMENT, ECC};
+
+  ////////////////////////////// Status SMS fields ////////////////////////////
+
+  /**
+   * Status SMS fields.
+   *
+   * <p>Each string constant is the field's key in the SMS body which is used by the parser to
+   * identify the field's value, if present, in the SMS body.
+   */
+  /** See {@link OmtpConstants#PROVISIONING_STATUS_VALUES} */
+  public static final String PROVISIONING_STATUS = "st";
+  /** See {@link OmtpConstants#RETURN_CODE_VALUES} */
+  public static final String RETURN_CODE = "rc";
+  /** URL to send users to for activation VVM */
+  public static final String SUBSCRIPTION_URL = "rs";
+  /** IMAP4/SMTP server IP address or fully qualified domain name */
+  public static final String SERVER_ADDRESS = "srv";
+  /** Phone number to access voicemails through Telephony User Interface */
+  public static final String TUI_ACCESS_NUMBER = "tui";
+
+  public static final String TUI_PASSWORD_LENGTH = "pw_len";
+  /** Number to send client origination SMS */
+  public static final String CLIENT_SMS_DESTINATION_NUMBER = "dn";
+
+  public static final String IMAP_PORT = "ipt";
+  public static final String IMAP_USER_NAME = "u";
+  public static final String IMAP_PASSWORD = "pw";
+  public static final String SMTP_PORT = "spt";
+  public static final String SMTP_USER_NAME = "smtp_u";
+  public static final String SMTP_PASSWORD = "smtp_pw";
+
+  /**
+   * User provisioning status values.
+   *
+   * <p>Referred by {@link OmtpConstants#PROVISIONING_STATUS}.
+   */
+  public static final String SUBSCRIBER_NEW = "N";
+
+  public static final String SUBSCRIBER_READY = "R";
+  public static final String SUBSCRIBER_PROVISIONED = "P";
+  public static final String SUBSCRIBER_UNKNOWN = "U";
+  public static final String SUBSCRIBER_BLOCKED = "B";
+
+  public static final String[] PROVISIONING_STATUS_VALUES = {
+    SUBSCRIBER_NEW, SUBSCRIBER_READY, SUBSCRIBER_PROVISIONED, SUBSCRIBER_UNKNOWN, SUBSCRIBER_BLOCKED
+  };
+
+  /**
+   * The return code included in a status message.
+   *
+   * <p>These are the possible values of {@link OmtpConstants#RETURN_CODE}.
+   */
+  public static final String SUCCESS = "0";
+
+  public static final String SYSTEM_ERROR = "1";
+  public static final String SUBSCRIBER_ERROR = "2";
+  public static final String MAILBOX_UNKNOWN = "3";
+  public static final String VVM_NOT_ACTIVATED = "4";
+  public static final String VVM_NOT_PROVISIONED = "5";
+  public static final String VVM_CLIENT_UKNOWN = "6";
+  public static final String VVM_MAILBOX_NOT_INITIALIZED = "7";
+
+  public static final String[] RETURN_CODE_VALUES = {
+    SUCCESS,
+    SYSTEM_ERROR,
+    SUBSCRIBER_ERROR,
+    MAILBOX_UNKNOWN,
+    VVM_NOT_ACTIVATED,
+    VVM_NOT_PROVISIONED,
+    VVM_CLIENT_UKNOWN,
+    VVM_MAILBOX_NOT_INITIALIZED,
+  };
+
+  /** IMAP command extensions */
+
+  /**
+   * OMTP spec v1.3 2.3.1 Change password request syntax
+   *
+   * <p>This changes the PIN to access the Telephone User Interface, the traditional voicemail
+   * system.
+   */
+  public static final String IMAP_CHANGE_TUI_PWD_FORMAT = "XCHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+
+  /**
+   * OMTP spec v1.3 2.4.1 Change languate request syntax
+   *
+   * <p>This changes the language in the Telephone User Interface.
+   */
+  public static final String IMAP_CHANGE_VM_LANG_FORMAT = "XCHANGE_VM_LANG LANG=%1$s";
+
+  /**
+   * OMTP spec v1.3 2.5.1 Close NUT Request syntax
+   *
+   * <p>This disables the new user tutorial, the message played to new users calling in the
+   * Telephone User Interface.
+   */
+  public static final String IMAP_CLOSE_NUT = "XCLOSE_NUT";
+
+  /** Possible NO responses for CHANGE_TUI_PWD */
+  public static final String RESPONSE_CHANGE_PIN_TOO_SHORT = "password too short";
+
+  public static final String RESPONSE_CHANGE_PIN_TOO_LONG = "password too long";
+  public static final String RESPONSE_CHANGE_PIN_TOO_WEAK = "password too weak";
+  public static final String RESPONSE_CHANGE_PIN_MISMATCH = "old password mismatch";
+  public static final String RESPONSE_CHANGE_PIN_INVALID_CHARACTER =
+      "password contains invalid characters";
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(
+    value = {
+      CHANGE_PIN_SUCCESS,
+      CHANGE_PIN_TOO_SHORT,
+      CHANGE_PIN_TOO_LONG,
+      CHANGE_PIN_TOO_WEAK,
+      CHANGE_PIN_MISMATCH,
+      CHANGE_PIN_INVALID_CHARACTER,
+      CHANGE_PIN_SYSTEM_ERROR
+    }
+  )
+  public @interface ChangePinResult {}
+
+  public static final int CHANGE_PIN_SUCCESS = 0;
+  public static final int CHANGE_PIN_TOO_SHORT = 1;
+  public static final int CHANGE_PIN_TOO_LONG = 2;
+  public static final int CHANGE_PIN_TOO_WEAK = 3;
+  public static final int CHANGE_PIN_MISMATCH = 4;
+  public static final int CHANGE_PIN_INVALID_CHARACTER = 5;
+  public static final int CHANGE_PIN_SYSTEM_ERROR = 6;
+
+  /** Indicates the client is Google visual voicemail version 1.0. */
+  public static final String CLIENT_TYPE_GOOGLE_10 = "google.vvm.10";
+}
diff --git a/java/com/android/voicemail/impl/OmtpEvents.java b/java/com/android/voicemail/impl/OmtpEvents.java
new file mode 100644
index 0000000..6807edc
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpEvents.java
@@ -0,0 +1,152 @@
+/*
+ * 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.voicemail.impl;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Events internal to the OMTP client. These should be translated into {@link
+ * android.provider.VoicemailContract.Status} error codes before writing into the voicemail status
+ * table.
+ */
+public enum OmtpEvents {
+
+  // Configuration State
+  CONFIG_REQUEST_STATUS_SUCCESS(Type.CONFIGURATION, true),
+
+  CONFIG_PIN_SET(Type.CONFIGURATION, true),
+  // The voicemail PIN is replaced with a generated PIN, user should change it.
+  CONFIG_DEFAULT_PIN_REPLACED(Type.CONFIGURATION, true),
+  CONFIG_ACTIVATING(Type.CONFIGURATION, true),
+  // There are already activation records, this is only a book-keeping activation.
+  CONFIG_ACTIVATING_SUBSEQUENT(Type.CONFIGURATION, true),
+  CONFIG_STATUS_SMS_TIME_OUT(Type.CONFIGURATION),
+  CONFIG_SERVICE_NOT_AVAILABLE(Type.CONFIGURATION),
+
+  // Data channel State
+
+  // A new sync has started, old errors in data channel should be cleared.
+  DATA_IMAP_OPERATION_STARTED(Type.DATA_CHANNEL, true),
+  // Successfully downloaded/uploaded data from the server, which means the data channel is clear.
+  DATA_IMAP_OPERATION_COMPLETED(Type.DATA_CHANNEL, true),
+  // The port provided in the STATUS SMS is invalid.
+  DATA_INVALID_PORT(Type.DATA_CHANNEL),
+  // No connection to the internet, and the carrier requires cellular data
+  DATA_NO_CONNECTION_CELLULAR_REQUIRED(Type.DATA_CHANNEL),
+  // No connection to the internet.
+  DATA_NO_CONNECTION(Type.DATA_CHANNEL),
+  // Address lookup for the server hostname failed. DNS error?
+  DATA_CANNOT_RESOLVE_HOST_ON_NETWORK(Type.DATA_CHANNEL),
+  // All destination address that resolves to the server hostname are rejected or timed out
+  DATA_ALL_SOCKET_CONNECTION_FAILED(Type.DATA_CHANNEL),
+  // Failed to establish SSL with the server, either with a direct SSL connection or by
+  // STARTTLS command
+  DATA_CANNOT_ESTABLISH_SSL_SESSION(Type.DATA_CHANNEL),
+  // Identity of the server cannot be verified.
+  DATA_SSL_INVALID_HOST_NAME(Type.DATA_CHANNEL),
+  // The server rejected our username/password
+  DATA_BAD_IMAP_CREDENTIAL(Type.DATA_CHANNEL),
+
+  DATA_AUTH_UNKNOWN_USER(Type.DATA_CHANNEL),
+  DATA_AUTH_UNKNOWN_DEVICE(Type.DATA_CHANNEL),
+  DATA_AUTH_INVALID_PASSWORD(Type.DATA_CHANNEL),
+  DATA_AUTH_MAILBOX_NOT_INITIALIZED(Type.DATA_CHANNEL),
+  DATA_AUTH_SERVICE_NOT_PROVISIONED(Type.DATA_CHANNEL),
+  DATA_AUTH_SERVICE_NOT_ACTIVATED(Type.DATA_CHANNEL),
+  DATA_AUTH_USER_IS_BLOCKED(Type.DATA_CHANNEL),
+
+  // A command to the server didn't result with an "OK" or continuation request
+  DATA_REJECTED_SERVER_RESPONSE(Type.DATA_CHANNEL),
+  // The server did not greet us with a "OK", possibly not a IMAP server.
+  DATA_INVALID_INITIAL_SERVER_RESPONSE(Type.DATA_CHANNEL),
+  // An IOException occurred while trying to open an ImapConnection
+  // TODO: reduce scope
+  DATA_IOE_ON_OPEN(Type.DATA_CHANNEL),
+  // The SELECT command on a mailbox is rejected
+  DATA_MAILBOX_OPEN_FAILED(Type.DATA_CHANNEL),
+  // An IOException has occurred
+  // TODO: reduce scope
+  DATA_GENERIC_IMAP_IOE(Type.DATA_CHANNEL),
+  // An SslException has occurred while opening an ImapConnection
+  // TODO: reduce scope
+  DATA_SSL_EXCEPTION(Type.DATA_CHANNEL),
+
+  // Notification Channel
+
+  // Cell signal restored, can received VVM SMSs
+  NOTIFICATION_IN_SERVICE(Type.NOTIFICATION_CHANNEL, true),
+  // Cell signal lost, cannot received VVM SMSs
+  NOTIFICATION_SERVICE_LOST(Type.NOTIFICATION_CHANNEL, false),
+
+  // Other
+  OTHER_SOURCE_REMOVED(Type.OTHER, false),
+
+  // VVM3
+  VVM3_NEW_USER_SETUP_FAILED,
+  // Table 4. client internal error handling
+  VVM3_VMG_DNS_FAILURE,
+  VVM3_SPG_DNS_FAILURE,
+  VVM3_VMG_CONNECTION_FAILED,
+  VVM3_SPG_CONNECTION_FAILED,
+  VVM3_VMG_TIMEOUT,
+  VVM3_STATUS_SMS_TIMEOUT,
+
+  VVM3_SUBSCRIBER_PROVISIONED,
+  VVM3_SUBSCRIBER_BLOCKED,
+  VVM3_SUBSCRIBER_UNKNOWN;
+
+  public static class Type {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({CONFIGURATION, DATA_CHANNEL, NOTIFICATION_CHANNEL, OTHER})
+    public @interface Values {}
+
+    public static final int CONFIGURATION = 1;
+    public static final int DATA_CHANNEL = 2;
+    public static final int NOTIFICATION_CHANNEL = 3;
+    public static final int OTHER = 4;
+  }
+
+  private final int mType;
+  private final boolean mIsSuccess;
+
+  OmtpEvents(int type, boolean isSuccess) {
+    mType = type;
+    mIsSuccess = isSuccess;
+  }
+
+  OmtpEvents(int type) {
+    mType = type;
+    mIsSuccess = false;
+  }
+
+  OmtpEvents() {
+    mType = Type.OTHER;
+    mIsSuccess = false;
+  }
+
+  @Type.Values
+  public int getType() {
+    return mType;
+  }
+
+  public boolean isSuccess() {
+    return mIsSuccess;
+  }
+}
diff --git a/java/com/android/voicemail/impl/OmtpService.java b/java/com/android/voicemail/impl/OmtpService.java
new file mode 100644
index 0000000..dfbd4cf
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpService.java
@@ -0,0 +1,63 @@
+/*
+ * 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.voicemail.impl;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailService;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+public class OmtpService extends VisualVoicemailService {
+
+  private static final String TAG = "VvmOmtpService";
+
+  public static final String ACTION_SMS_RECEIVED = "com.android.vociemailomtp.sms.sms_received";
+
+  public static final String EXTRA_VOICEMAIL_SMS = "extra_voicemail_sms";
+
+  @Override
+  public void onCellServiceConnected(
+      VisualVoicemailTask task, final PhoneAccountHandle phoneAccountHandle) {
+    VvmLog.i(TAG, "onCellServiceConnected");
+    ActivationTask.start(OmtpService.this, phoneAccountHandle, null);
+    task.finish();
+  }
+
+  @Override
+  public void onSmsReceived(VisualVoicemailTask task, final VisualVoicemailSms sms) {
+    VvmLog.i(TAG, "onSmsReceived");
+    Intent intent = new Intent(ACTION_SMS_RECEIVED);
+    intent.setPackage(getPackageName());
+    intent.putExtra(EXTRA_VOICEMAIL_SMS, sms);
+    sendBroadcast(intent);
+    task.finish();
+  }
+
+  @Override
+  public void onSimRemoved(
+      final VisualVoicemailTask task, final PhoneAccountHandle phoneAccountHandle) {
+    VvmLog.i(TAG, "onSimRemoved");
+    VvmAccountManager.removeAccount(this, phoneAccountHandle);
+    task.finish();
+  }
+
+  @Override
+  public void onStopped(VisualVoicemailTask task) {
+    VvmLog.i(TAG, "onStopped");
+  }
+}
diff --git a/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
new file mode 100644
index 0000000..0296d20
--- /dev/null
+++ b/java/com/android/voicemail/impl/OmtpVvmCarrierConfigHelper.java
@@ -0,0 +1,444 @@
+/*
+ * 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.voicemail.impl;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.telephony.VisualVoicemailService;
+import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocolFactory;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * 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)
+ *
+ * <p>Hidden configs are new configs that are planned for future APIs, or miscellaneous settings
+ * that may clutter CarrierConfigManager too much.
+ *
+ * <p>The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()}
+ */
+public class OmtpVvmCarrierConfigHelper {
+
+  private static final String TAG = "OmtpVvmCarrierCfgHlpr";
+
+  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";
+
+  /** @see #isLegacyModeEnabled() */
+  static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = "vvm_legacy_mode_enabled_bool";
+
+  /**
+   * 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 PersistableBundle mCarrierConfig;
+  private final String mVvmType;
+  private final VisualVoicemailProtocol mProtocol;
+  private final PersistableBundle mTelephonyConfig;
+
+  private PhoneAccountHandle mPhoneAccountHandle;
+
+  public OmtpVvmCarrierConfigHelper(Context context, PhoneAccountHandle handle) {
+    mContext = context;
+    mPhoneAccountHandle = handle;
+    TelephonyManager telephonyManager =
+        context
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(mPhoneAccountHandle);
+    if (telephonyManager == null) {
+      VvmLog.e(TAG, "PhoneAccountHandle is invalid");
+      mCarrierConfig = null;
+      mTelephonyConfig = null;
+      mVvmType = null;
+      mProtocol = null;
+      return;
+    }
+
+    mCarrierConfig = getCarrierConfig(telephonyManager);
+    mTelephonyConfig =
+        new TelephonyVvmConfigManager(context.getResources())
+            .getConfig(telephonyManager.getSimOperator());
+
+    mVvmType = getVvmType();
+    mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
+  }
+
+  @VisibleForTesting
+  OmtpVvmCarrierConfigHelper(
+      Context context, PersistableBundle carrierConfig, PersistableBundle telephonyConfig) {
+    mContext = context;
+    mCarrierConfig = carrierConfig;
+    mTelephonyConfig = telephonyConfig;
+    mVvmType = getVvmType();
+    mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
+  }
+
+  public Context getContext() {
+    return mContext;
+  }
+
+  @Nullable
+  public PhoneAccountHandle getPhoneAccountHandle() {
+    return mPhoneAccountHandle;
+  }
+
+  /**
+   * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a
+   * known protocol.
+   */
+  public boolean isValid() {
+    return mProtocol != null;
+  }
+
+  @Nullable
+  public String getVvmType() {
+    return (String) getValue(KEY_VVM_TYPE_STRING);
+  }
+
+  @Nullable
+  public VisualVoicemailProtocol getProtocol() {
+    return mProtocol;
+  }
+
+  /** @returns arbitrary String stored in the config file. Used for protocol specific values. */
+  @Nullable
+  public String getString(String key) {
+    Assert.checkArgument(isValid());
+    return (String) getValue(key);
+  }
+
+  @Nullable
+  public Set<String> getCarrierVvmPackageNames() {
+    Assert.checkArgument(isValid());
+    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;
+    }
+    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)) {
+      String[] vvmPackages = bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY);
+      if (vvmPackages != null && vvmPackages.length > 0) {
+        Collections.addAll(names, vvmPackages);
+      }
+    }
+    if (names.isEmpty()) {
+      return null;
+    }
+    return names;
+  }
+
+  /**
+   * For checking upon sim insertion whether visual voicemail should be enabled. This method does so
+   * by checking if the carrier's voicemail app is installed.
+   */
+  public boolean isEnabledByDefault() {
+    if (!isValid()) {
+      return false;
+    }
+
+    Set<String> carrierPackages = getCarrierVvmPackageNames();
+    if (carrierPackages == null) {
+      return true;
+    }
+    for (String packageName : carrierPackages) {
+      try {
+        mContext.getPackageManager().getPackageInfo(packageName, 0);
+        return false;
+      } catch (NameNotFoundException e) {
+        // Do nothing.
+      }
+    }
+    return true;
+  }
+
+  public boolean isCellularDataRequired() {
+    Assert.checkArgument(isValid());
+    return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false);
+  }
+
+  public boolean isPrefetchEnabled() {
+    Assert.checkArgument(isValid());
+    return (boolean) getValue(KEY_VVM_PREFETCH_BOOL, true);
+  }
+
+  public int getApplicationPort() {
+    Assert.checkArgument(isValid());
+    return (int) getValue(KEY_VVM_PORT_NUMBER_INT, 0);
+  }
+
+  @Nullable
+  public String getDestinationNumber() {
+    Assert.checkArgument(isValid());
+    return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING);
+  }
+
+  /**
+   * @return Port to start a SSL IMAP connection directly.
+   */
+  public int getSslPort() {
+    Assert.checkArgument(isValid());
+    return (int) getValue(KEY_VVM_SSL_PORT_NUMBER_INT, 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() {
+    Assert.checkArgument(isValid());
+    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;
+    }
+    String[] disabledCapabilities =
+        bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY);
+    if (disabledCapabilities != null && disabledCapabilities.length > 0) {
+      ArraySet<String> result = new ArraySet<>();
+      Collections.addAll(result, disabledCapabilities);
+      return result;
+    }
+    return null;
+  }
+
+  public String getClientPrefix() {
+    Assert.checkArgument(isValid());
+    String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING);
+    if (prefix != null) {
+      return prefix;
+    }
+    return "//VVM";
+  }
+
+  /**
+   * Should legacy mode be used when the OMTP VVM client is disabled?
+   *
+   * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on
+   * the client side all network operations are disabled. SMSs are still monitored so a new message
+   * SYNC SMS will be translated to show a message waiting indicator, like traditional voicemails.
+   *
+   * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to
+   * function without the data cost.
+   */
+  public boolean isLegacyModeEnabled() {
+    Assert.checkArgument(isValid());
+    return (boolean) getValue(KEY_VVM_LEGACY_MODE_ENABLED_BOOL, false);
+  }
+
+  public void startActivation() {
+    Assert.checkArgument(isValid());
+    PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+    if (phoneAccountHandle == null) {
+      // This should never happen
+      // Error logged in getPhoneAccountHandle().
+      return;
+    }
+
+    if (mVvmType == null || mVvmType.isEmpty()) {
+      // The VVM type is invalid; we should never have gotten here in the first place since
+      // this is loaded initially in the constructor, and callers should check isValid()
+      // before trying to start activation anyways.
+      VvmLog.e(TAG, "startActivation : vvmType is null or empty for account " + phoneAccountHandle);
+      return;
+    }
+
+    if (mProtocol != null) {
+      ActivationTask.start(mContext, mPhoneAccountHandle, null);
+    }
+  }
+
+  public void activateSmsFilter() {
+    Assert.checkArgument(isValid());
+    VisualVoicemailService.setSmsFilterSettings(
+        mContext,
+        getPhoneAccountHandle(),
+        new VisualVoicemailSmsFilterSettings.Builder().setClientPrefix(getClientPrefix()).build());
+  }
+
+  public void startDeactivation() {
+    Assert.checkArgument(isValid());
+    if (!isLegacyModeEnabled()) {
+      // SMS should still be filtered in legacy mode
+      VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(), null);
+    }
+    if (mProtocol != null) {
+      mProtocol.startDeactivation(this);
+    }
+    VvmAccountManager.removeAccount(mContext, getPhoneAccountHandle());
+  }
+
+  public boolean supportsProvisioning() {
+    Assert.checkArgument(isValid());
+    return mProtocol.supportsProvisioning();
+  }
+
+  public void startProvisioning(
+      ActivationTask task,
+      PhoneAccountHandle phone,
+      VoicemailStatus.Editor status,
+      StatusMessage message,
+      Bundle data) {
+    Assert.checkArgument(isValid());
+    mProtocol.startProvisioning(task, phone, this, status, message, data);
+  }
+
+  public void requestStatus(@Nullable PendingIntent sentIntent) {
+    Assert.checkArgument(isValid());
+    mProtocol.requestStatus(this, sentIntent);
+  }
+
+  public void handleEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+    Assert.checkArgument(isValid());
+    VvmLog.i(TAG, "OmtpEvent:" + event);
+    mProtocol.handleEvent(mContext, this, status, event);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder("OmtpVvmCarrierConfigHelper [");
+    builder
+        .append("phoneAccountHandle: ")
+        .append(mPhoneAccountHandle)
+        .append(", carrierConfig: ")
+        .append(mCarrierConfig != null)
+        .append(", telephonyConfig: ")
+        .append(mTelephonyConfig != null)
+        .append(", type: ")
+        .append(getVvmType())
+        .append(", destinationNumber: ")
+        .append(getDestinationNumber())
+        .append(", applicationPort: ")
+        .append(getApplicationPort())
+        .append(", sslPort: ")
+        .append(getSslPort())
+        .append(", isEnabledByDefault: ")
+        .append(isEnabledByDefault())
+        .append(", isCellularDataRequired: ")
+        .append(isCellularDataRequired())
+        .append(", isPrefetchEnabled: ")
+        .append(isPrefetchEnabled())
+        .append(", isLegacyModeEnabled: ")
+        .append(isLegacyModeEnabled())
+        .append("]");
+    return builder.toString();
+  }
+
+  @Nullable
+  private PersistableBundle getCarrierConfig(@NonNull TelephonyManager telephonyManager) {
+    CarrierConfigManager carrierConfigManager =
+        (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+    if (carrierConfigManager == null) {
+      VvmLog.w(TAG, "No carrier config service found.");
+      return null;
+    }
+
+    PersistableBundle config = telephonyManager.getCarrierConfig();
+
+    if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) {
+      return null;
+    }
+    return config;
+  }
+
+  @Nullable
+  private Object getValue(String key) {
+    return getValue(key, null);
+  }
+
+  @Nullable
+  private Object getValue(String key, Object defaultValue) {
+    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 defaultValue;
+  }
+}
diff --git a/java/com/android/voicemail/impl/SubscriptionInfoHelper.java b/java/com/android/voicemail/impl/SubscriptionInfoHelper.java
new file mode 100644
index 0000000..d8a8423
--- /dev/null
+++ b/java/com/android/voicemail/impl/SubscriptionInfoHelper.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.voicemail.impl;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+/**
+ * Helper for manipulating intents or components with subscription-related information.
+ *
+ * <p>In settings, subscription ids and labels are passed along to indicate that settings are being
+ * changed for particular subscriptions. This helper provides functions for helping extract this
+ * info and perform common operations using this info.
+ */
+public class SubscriptionInfoHelper {
+  public static final int NO_SUB_ID = -1;
+
+  // Extra on intent containing the id of a subscription.
+  public static final String SUB_ID_EXTRA =
+      "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionId";
+  // Extra on intent containing the label of a subscription.
+  private static final String SUB_LABEL_EXTRA =
+      "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionLabel";
+
+  private static Context mContext;
+
+  private static int mSubId = NO_SUB_ID;
+  private static String mSubLabel;
+
+  /** Instantiates the helper, by extracting the subscription id and label from the intent. */
+  public SubscriptionInfoHelper(Context context, Intent intent) {
+    mContext = context;
+    mSubId = intent.getIntExtra(SUB_ID_EXTRA, NO_SUB_ID);
+    mSubLabel = intent.getStringExtra(SUB_LABEL_EXTRA);
+  }
+
+  /**
+   * Sets the action bar title to the string specified by the given resource id, formatting it with
+   * the subscription label. This assumes the resource string is formattable with a string-type
+   * specifier.
+   *
+   * <p>If the subscription label does not exists, leave the existing title.
+   */
+  public void setActionBarTitle(ActionBar actionBar, Resources res, int resId) {
+    if (actionBar == null || TextUtils.isEmpty(mSubLabel)) {
+      return;
+    }
+
+    String title = String.format(res.getString(resId), mSubLabel);
+    actionBar.setTitle(title);
+  }
+
+  public int getSubId() {
+    return mSubId;
+  }
+}
diff --git a/java/com/android/voicemail/impl/TelephonyManagerStub.java b/java/com/android/voicemail/impl/TelephonyManagerStub.java
new file mode 100644
index 0000000..4762e90
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyManagerStub.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 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.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+
+/**
+ * Temporary stub for public APIs that should be added into telephony manager.
+ *
+ * <p>TODO(b/32637799) remove this.
+ */
+@TargetApi(VERSION_CODES.O)
+public class TelephonyManagerStub {
+
+  public static void showVoicemailNotification(int voicemailCount) {}
+
+  /**
+   * Dismisses the message waiting (voicemail) indicator.
+   *
+   * @param subId the subscription id we should dismiss the notification for.
+   */
+  public static void clearMwiIndicator(int subId) {}
+
+  public static void setShouldCheckVisualVoicemailConfigurationForMwi(int subId, boolean enabled) {}
+}
diff --git a/java/com/android/voicemail/impl/TelephonyMangerCompat.java b/java/com/android/voicemail/impl/TelephonyMangerCompat.java
new file mode 100644
index 0000000..353cd69
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyMangerCompat.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 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.voicemail.impl;
+
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import java.lang.reflect.Method;
+
+/** Handles {@link TelephonyManager} API changes in experimental SDK */
+public class TelephonyMangerCompat {
+
+  private static final String GET_VISUAL_VOICEMAIL_PACKGE_NAME = "getVisualVoicemailPackageName";
+
+  /**
+   * Changed from getVisualVoicemailPackageName(PhoneAccountHandle) to
+   * getVisualVoicemailPackageName()
+   */
+  public static String getVisualVoicemailPackageName(TelephonyManager telephonyManager) {
+    try {
+      Method method = TelephonyManager.class.getMethod(GET_VISUAL_VOICEMAIL_PACKGE_NAME);
+      try {
+        return (String) method.invoke(telephonyManager);
+      } catch (ReflectiveOperationException e) {
+        throw new RuntimeException(e);
+      }
+    } catch (NoSuchMethodException e) {
+      // Do nothing, try the next version.
+    }
+
+    try {
+      Method method =
+          TelephonyManager.class.getMethod(
+              GET_VISUAL_VOICEMAIL_PACKGE_NAME, PhoneAccountHandle.class);
+      try {
+        return (String) method.invoke(telephonyManager, (Object) null);
+      } catch (ReflectiveOperationException e) {
+        throw new RuntimeException(e);
+      }
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java b/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java
new file mode 100644
index 0000000..04012c9
--- /dev/null
+++ b/java/com/android/voicemail/impl/TelephonyVvmConfigManager.java
@@ -0,0 +1,150 @@
+/*
+ * 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.voicemail.impl;
+
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import com.android.voicemail.impl.utils.XmlUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** 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;
+
+  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/java/com/android/voicemail/impl/VisualVoicemailPreferences.java b/java/com/android/voicemail/impl/VisualVoicemailPreferences.java
new file mode 100644
index 0000000..72506eb
--- /dev/null
+++ b/java/com/android/voicemail/impl/VisualVoicemailPreferences.java
@@ -0,0 +1,37 @@
+/*
+ * 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.voicemail.impl;
+
+import android.content.Context;
+import android.preference.PreferenceManager;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.PerAccountSharedPreferences;
+
+/**
+ * Save visual voicemail values in shared preferences to be retrieved later. Because a voicemail
+ * source is tied 1:1 to a phone account, the phone account handle is used in the key for each
+ * voicemail source and the associated data.
+ */
+public class VisualVoicemailPreferences extends PerAccountSharedPreferences {
+
+  public VisualVoicemailPreferences(Context context, PhoneAccountHandle phoneAccountHandle) {
+    super(
+        context,
+        phoneAccountHandle,
+        PreferenceManager.getDefaultSharedPreferences(context),
+        "visual_voicemail_");
+  }
+}
diff --git a/java/com/android/voicemail/impl/Voicemail.java b/java/com/android/voicemail/impl/Voicemail.java
new file mode 100644
index 0000000..f98d56f
--- /dev/null
+++ b/java/com/android/voicemail/impl/Voicemail.java
@@ -0,0 +1,341 @@
+/*
+ * 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.voicemail.impl;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+
+/** Represents a single voicemail stored in the voicemail content provider. */
+public class Voicemail implements Parcelable {
+
+  private final Long mTimestamp;
+  private final String mNumber;
+  private final PhoneAccountHandle mPhoneAccount;
+  private final Long mId;
+  private final Long mDuration;
+  private final String mSource;
+  private final String mProviderData;
+  private final Uri mUri;
+  private final Boolean mIsRead;
+  private final Boolean mHasContent;
+  private final String mTranscription;
+
+  private Voicemail(
+      Long timestamp,
+      String number,
+      PhoneAccountHandle phoneAccountHandle,
+      Long id,
+      Long duration,
+      String source,
+      String providerData,
+      Uri uri,
+      Boolean isRead,
+      Boolean hasContent,
+      String transcription) {
+    mTimestamp = timestamp;
+    mNumber = number;
+    mPhoneAccount = phoneAccountHandle;
+    mId = id;
+    mDuration = duration;
+    mSource = source;
+    mProviderData = providerData;
+    mUri = uri;
+    mIsRead = isRead;
+    mHasContent = hasContent;
+    mTranscription = transcription;
+  }
+
+  /**
+   * Create a {@link Builder} for a new {@link Voicemail} to be inserted.
+   *
+   * <p>The number and the timestamp are mandatory for insertion.
+   */
+  public static Builder createForInsertion(long timestamp, String number) {
+    return new Builder().setNumber(number).setTimestamp(timestamp);
+  }
+
+  /**
+   * Create a {@link Builder} for a {@link Voicemail} to be updated (or deleted).
+   *
+   * <p>The id and source data fields are mandatory for update - id is necessary for updating the
+   * database and source data is necessary for updating the server.
+   */
+  public static Builder createForUpdate(long id, String sourceData) {
+    return new Builder().setId(id).setSourceData(sourceData);
+  }
+
+  /**
+   * Builder pattern for creating a {@link Voicemail}. The builder must be created with the {@link
+   * #createForInsertion(long, String)} method.
+   *
+   * <p>This class is <b>not thread safe</b>
+   */
+  public static class Builder {
+
+    private Long mBuilderTimestamp;
+    private String mBuilderNumber;
+    private PhoneAccountHandle mBuilderPhoneAccount;
+    private Long mBuilderId;
+    private Long mBuilderDuration;
+    private String mBuilderSourcePackage;
+    private String mBuilderSourceData;
+    private Uri mBuilderUri;
+    private Boolean mBuilderIsRead;
+    private boolean mBuilderHasContent;
+    private String mBuilderTranscription;
+
+    /** You should use the correct factory method to construct a builder. */
+    private Builder() {}
+
+    public Builder setNumber(String number) {
+      mBuilderNumber = number;
+      return this;
+    }
+
+    public Builder setTimestamp(long timestamp) {
+      mBuilderTimestamp = timestamp;
+      return this;
+    }
+
+    public Builder setPhoneAccount(PhoneAccountHandle phoneAccount) {
+      mBuilderPhoneAccount = phoneAccount;
+      return this;
+    }
+
+    public Builder setId(long id) {
+      mBuilderId = id;
+      return this;
+    }
+
+    public Builder setDuration(long duration) {
+      mBuilderDuration = duration;
+      return this;
+    }
+
+    public Builder setSourcePackage(String sourcePackage) {
+      mBuilderSourcePackage = sourcePackage;
+      return this;
+    }
+
+    public Builder setSourceData(String sourceData) {
+      mBuilderSourceData = sourceData;
+      return this;
+    }
+
+    public Builder setUri(Uri uri) {
+      mBuilderUri = uri;
+      return this;
+    }
+
+    public Builder setIsRead(boolean isRead) {
+      mBuilderIsRead = isRead;
+      return this;
+    }
+
+    public Builder setHasContent(boolean hasContent) {
+      mBuilderHasContent = hasContent;
+      return this;
+    }
+
+    public Builder setTranscription(String transcription) {
+      mBuilderTranscription = transcription;
+      return this;
+    }
+
+    public Voicemail build() {
+      mBuilderId = mBuilderId == null ? -1 : mBuilderId;
+      mBuilderTimestamp = mBuilderTimestamp == null ? 0 : mBuilderTimestamp;
+      mBuilderDuration = mBuilderDuration == null ? 0 : mBuilderDuration;
+      mBuilderIsRead = mBuilderIsRead == null ? false : mBuilderIsRead;
+      return new Voicemail(
+          mBuilderTimestamp,
+          mBuilderNumber,
+          mBuilderPhoneAccount,
+          mBuilderId,
+          mBuilderDuration,
+          mBuilderSourcePackage,
+          mBuilderSourceData,
+          mBuilderUri,
+          mBuilderIsRead,
+          mBuilderHasContent,
+          mBuilderTranscription);
+    }
+  }
+
+  /**
+   * The identifier of the voicemail in the content provider.
+   *
+   * <p>This may be missing in the case of a new {@link Voicemail} that we plan to insert into the
+   * content provider, since until it has been inserted we don't know what id it should have. If
+   * none is specified, we return -1.
+   */
+  public long getId() {
+    return mId;
+  }
+
+  /** The number of the person leaving the voicemail, empty string if unknown, null if not set. */
+  public String getNumber() {
+    return mNumber;
+  }
+
+  /** The phone account associated with the voicemail, null if not set. */
+  public PhoneAccountHandle getPhoneAccount() {
+    return mPhoneAccount;
+  }
+
+  /** The timestamp the voicemail was received, in millis since the epoch, zero if not set. */
+  public long getTimestampMillis() {
+    return mTimestamp;
+  }
+
+  /** Gets the duration of the voicemail in millis, or zero if the field is not set. */
+  public long getDuration() {
+    return mDuration;
+  }
+
+  /**
+   * Returns the package name of the source that added this voicemail, or null if this field is not
+   * set.
+   */
+  public String getSourcePackage() {
+    return mSource;
+  }
+
+  /**
+   * Returns the application-specific data type stored with the voicemail, or null if this field is
+   * not set.
+   *
+   * <p>Source data is typically used as an identifier to uniquely identify the voicemail against
+   * the voicemail server. This is likely to be something like the IMAP UID, or some other
+   * server-generated identifying string.
+   */
+  public String getSourceData() {
+    return mProviderData;
+  }
+
+  /**
+   * Gets the Uri that can be used to refer to this voicemail, and to make it play.
+   *
+   * <p>Returns null if we don't know the Uri.
+   */
+  public Uri getUri() {
+    return mUri;
+  }
+
+  /**
+   * Tells us if the voicemail message has been marked as read.
+   *
+   * <p>Always returns false if this field has not been set, i.e. if hasRead() returns false.
+   */
+  public boolean isRead() {
+    return mIsRead;
+  }
+
+  /** Tells us if there is content stored at the Uri. */
+  public boolean hasContent() {
+    return mHasContent;
+  }
+
+  /** Returns the text transcription of this voicemail, or null if this field is not set. */
+  public String getTranscription() {
+    return mTranscription;
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeLong(mTimestamp);
+    writeCharSequence(dest, mNumber);
+    if (mPhoneAccount == null) {
+      dest.writeInt(0);
+    } else {
+      dest.writeInt(1);
+      mPhoneAccount.writeToParcel(dest, flags);
+    }
+    dest.writeLong(mId);
+    dest.writeLong(mDuration);
+    writeCharSequence(dest, mSource);
+    writeCharSequence(dest, mProviderData);
+    if (mUri == null) {
+      dest.writeInt(0);
+    } else {
+      dest.writeInt(1);
+      mUri.writeToParcel(dest, flags);
+    }
+    if (mIsRead) {
+      dest.writeInt(1);
+    } else {
+      dest.writeInt(0);
+    }
+    if (mHasContent) {
+      dest.writeInt(1);
+    } else {
+      dest.writeInt(0);
+    }
+    writeCharSequence(dest, mTranscription);
+  }
+
+  public static final Creator<Voicemail> CREATOR =
+      new Creator<Voicemail>() {
+        @Override
+        public Voicemail createFromParcel(Parcel in) {
+          return new Voicemail(in);
+        }
+
+        @Override
+        public Voicemail[] newArray(int size) {
+          return new Voicemail[size];
+        }
+      };
+
+  private Voicemail(Parcel in) {
+    mTimestamp = in.readLong();
+    mNumber = (String) readCharSequence(in);
+    if (in.readInt() > 0) {
+      mPhoneAccount = PhoneAccountHandle.CREATOR.createFromParcel(in);
+    } else {
+      mPhoneAccount = null;
+    }
+    mId = in.readLong();
+    mDuration = in.readLong();
+    mSource = (String) readCharSequence(in);
+    mProviderData = (String) readCharSequence(in);
+    if (in.readInt() > 0) {
+      mUri = Uri.CREATOR.createFromParcel(in);
+    } else {
+      mUri = null;
+    }
+    mIsRead = in.readInt() > 0 ? true : false;
+    mHasContent = in.readInt() > 0 ? true : false;
+    mTranscription = (String) readCharSequence(in);
+  }
+
+  private static CharSequence readCharSequence(Parcel in) {
+    return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+  }
+
+  public static void writeCharSequence(Parcel dest, CharSequence val) {
+    TextUtils.writeToParcel(val, dest, 0);
+  }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailClientImpl.java b/java/com/android/voicemail/impl/VoicemailClientImpl.java
new file mode 100644
index 0000000..1ad12ae
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailClientImpl.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.voicemail.impl;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.settings.VoicemailSettingsFragment;
+import java.util.List;
+import javax.inject.Inject;
+
+/**
+ * {@link VoicemailClient} to be used when the voicemail module is activated. May only be used above
+ * O.
+ */
+public class VoicemailClientImpl implements VoicemailClient {
+
+  /**
+   * List of legacy OMTP voicemail packages that should be ignored. It could never be the active VVM
+   * package anymore. For example, voicemails in OC will no longer be handled by telephony, but
+   * legacy voicemails might still exist in the database due to upgrading from NYC. Dialer will
+   * fetch these voicemails again so it should be ignored.
+   */
+  private static final String[] OMTP_VOICEMAIL_BLACKLIST = {"com.android.phone"};
+
+  @Inject
+  public VoicemailClientImpl() {
+    Assert.checkArgument(BuildCompat.isAtLeastO());
+  }
+
+  @Nullable
+  @Override
+  public String getSettingsFragment() {
+    return VoicemailSettingsFragment.class.getName();
+  }
+
+  @Override
+  public boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle);
+  }
+
+  @Override
+  public void setVoicemailArchiveEnabled(
+      Context context, PhoneAccountHandle phoneAccountHandle, boolean value) {
+    VisualVoicemailSettingsUtil.setArchiveEnabled(context, phoneAccountHandle, value);
+  }
+
+  @TargetApi(VERSION_CODES.O)
+  @Override
+  public void appendOmtpVoicemailSelectionClause(
+      Context context, StringBuilder where, List<String> selectionArgs) {
+    TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+    String omtpSource = TelephonyMangerCompat.getVisualVoicemailPackageName(telephonyManager);
+    where.append(
+        "AND ("
+            + "("
+            + Voicemails.IS_OMTP_VOICEMAIL
+            + " != 1)"
+            + "OR "
+            + "("
+            + Voicemails.SOURCE_PACKAGE
+            + " = ? )"
+            + ")");
+    selectionArgs.add(omtpSource);
+
+    for (String blacklistedPackage : OMTP_VOICEMAIL_BLACKLIST) {
+      where.append("AND (" + Voicemails.SOURCE_PACKAGE + "!= ?)");
+      selectionArgs.add(blacklistedPackage);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailClientReceiver.java b/java/com/android/voicemail/impl/VoicemailClientReceiver.java
new file mode 100644
index 0000000..49a55a4
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailClientReceiver.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 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.voicemail.impl;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.impl.sync.UploadTask;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Receiver for broadcasts in {@link VoicemailClient#ACTION_UPLOAD} */
+public class VoicemailClientReceiver extends BroadcastReceiver {
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    switch (intent.getAction()) {
+      case VoicemailClient.ACTION_UPLOAD:
+        doUpload(context);
+        break;
+      default:
+        Assert.fail("Unexpected action " + intent.getAction());
+        break;
+    }
+  }
+
+  /** Upload local database changes to the server. */
+  private static void doUpload(Context context) {
+    LogUtil.i("VoicemailClientReceiver.onReceive", "ACTION_UPLOAD received");
+    for (PhoneAccountHandle phoneAccountHandle : VvmAccountManager.getActiveAccounts(context)) {
+      UploadTask.start(context, phoneAccountHandle);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VoicemailModule.java b/java/com/android/voicemail/impl/VoicemailModule.java
new file mode 100644
index 0000000..c3e5714
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailModule.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 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.voicemail.impl;
+
+import android.support.v4.os.BuildCompat;
+import com.android.voicemail.VoicemailClient;
+import com.android.voicemail.stub.StubVoicemailClient;
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** This module provides an instance of the voicemail client. */
+@Module
+public final class VoicemailModule {
+
+  @Provides
+  @Singleton
+  static VoicemailClient provideVoicemailClient() {
+    if (BuildCompat.isAtLeastO()) {
+      return new VoicemailClientImpl();
+    } else {
+      return new StubVoicemailClient();
+    }
+  }
+
+  private VoicemailModule() {}
+}
diff --git a/java/com/android/voicemail/impl/VoicemailStatus.java b/java/com/android/voicemail/impl/VoicemailStatus.java
new file mode 100644
index 0000000..ec1ab4e
--- /dev/null
+++ b/java/com/android/voicemail/impl/VoicemailStatus.java
@@ -0,0 +1,160 @@
+/*
+ * 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.voicemail.impl;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+
+public class VoicemailStatus {
+
+  private static final String TAG = "VvmStatus";
+
+  public static class Editor {
+
+    private final Context mContext;
+    @Nullable private final PhoneAccountHandle mPhoneAccountHandle;
+
+    private ContentValues mValues = new ContentValues();
+
+    private Editor(Context context, PhoneAccountHandle phoneAccountHandle) {
+      mContext = context;
+      mPhoneAccountHandle = phoneAccountHandle;
+      if (mPhoneAccountHandle == null) {
+        VvmLog.w(
+            TAG,
+            "VoicemailStatus.Editor created with null phone account, status will"
+                + " not be written");
+      }
+    }
+
+    @Nullable
+    public PhoneAccountHandle getPhoneAccountHandle() {
+      return mPhoneAccountHandle;
+    }
+
+    public Editor setType(String type) {
+      mValues.put(Status.SOURCE_TYPE, type);
+      return this;
+    }
+
+    public Editor setConfigurationState(int configurationState) {
+      mValues.put(Status.CONFIGURATION_STATE, configurationState);
+      return this;
+    }
+
+    public Editor setDataChannelState(int dataChannelState) {
+      mValues.put(Status.DATA_CHANNEL_STATE, dataChannelState);
+      return this;
+    }
+
+    public Editor setNotificationChannelState(int notificationChannelState) {
+      mValues.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState);
+      return this;
+    }
+
+    public Editor setQuota(int occupied, int total) {
+      if (occupied == VoicemailContract.Status.QUOTA_UNAVAILABLE
+          && total == VoicemailContract.Status.QUOTA_UNAVAILABLE) {
+        return this;
+      }
+
+      mValues.put(Status.QUOTA_OCCUPIED, occupied);
+      mValues.put(Status.QUOTA_TOTAL, total);
+      return this;
+    }
+
+    /**
+     * Apply the changes to the {@link VoicemailStatus} {@link #Editor}.
+     *
+     * @return {@code true} if the changes were successfully applied, {@code false} otherwise.
+     */
+    public boolean apply() {
+      if (mPhoneAccountHandle == null) {
+        return false;
+      }
+      mValues.put(
+          Status.PHONE_ACCOUNT_COMPONENT_NAME,
+          mPhoneAccountHandle.getComponentName().flattenToString());
+      mValues.put(Status.PHONE_ACCOUNT_ID, mPhoneAccountHandle.getId());
+      ContentResolver contentResolver = mContext.getContentResolver();
+      Uri statusUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
+      try {
+        contentResolver.insert(statusUri, mValues);
+      } catch (IllegalArgumentException iae) {
+        VvmLog.e(TAG, "apply :: failed to insert content resolver ", iae);
+        mValues.clear();
+        return false;
+      }
+      mValues.clear();
+      return true;
+    }
+
+    public ContentValues getValues() {
+      return mValues;
+    }
+  }
+
+  /**
+   * A voicemail status editor that the decision of whether to actually write to the database can be
+   * deferred. This object will be passed around as a usual {@link Editor}, but {@link #apply()}
+   * doesn't do anything. If later the creator of this object decides any status changes written to
+   * it should be committed, {@link #deferredApply()} should be called.
+   */
+  public static class DeferredEditor extends Editor {
+
+    private DeferredEditor(Context context, PhoneAccountHandle phoneAccountHandle) {
+      super(context, phoneAccountHandle);
+    }
+
+    @Override
+    public boolean apply() {
+      // Do nothing
+      return true;
+    }
+
+    public void deferredApply() {
+      super.apply();
+    }
+  }
+
+  public static Editor edit(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return new Editor(context, phoneAccountHandle);
+  }
+
+  /**
+   * Reset the status to the "disabled" state, which the UI should not show anything for this
+   * phoneAccountHandle.
+   */
+  public static void disable(Context context, PhoneAccountHandle phoneAccountHandle) {
+    edit(context, phoneAccountHandle)
+        .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
+        .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+        .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
+        .apply();
+  }
+
+  public static DeferredEditor deferredEdit(
+      Context context, PhoneAccountHandle phoneAccountHandle) {
+    return new DeferredEditor(context, phoneAccountHandle);
+  }
+}
diff --git a/java/com/android/voicemail/impl/VvmLog.java b/java/com/android/voicemail/impl/VvmLog.java
new file mode 100644
index 0000000..595207f
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmLog.java
@@ -0,0 +1,177 @@
+/*
+ * 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.voicemail.impl;
+
+import com.android.dialer.common.LogUtil;
+import com.android.voicemail.impl.utils.IndentingPrintWriter;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import java.util.Calendar;
+import java.util.Deque;
+import java.util.Iterator;
+
+/** Helper methods for adding to OMTP visual voicemail local logs. */
+public class VvmLog {
+
+  private static final int MAX_OMTP_VVM_LOGS = 100;
+
+  private static final LocalLog sLocalLog = new LocalLog(MAX_OMTP_VVM_LOGS);
+
+  public static void log(String tag, String log) {
+    sLocalLog.log(tag + ": " + log);
+  }
+
+  public static void dump(FileDescriptor fd, PrintWriter printwriter, String[] args) {
+    IndentingPrintWriter indentingPrintWriter = new IndentingPrintWriter(printwriter, "  ");
+    indentingPrintWriter.increaseIndent();
+    sLocalLog.dump(fd, indentingPrintWriter, args);
+    indentingPrintWriter.decreaseIndent();
+  }
+
+  public static void e(String tag, String log) {
+    log(tag, log);
+    LogUtil.e(tag, log);
+  }
+
+  public static void e(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.e(tag, log, e);
+  }
+
+  public static void w(String tag, String log) {
+    log(tag, log);
+    LogUtil.w(tag, log);
+  }
+
+  public static void w(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.w(tag, log, e);
+  }
+
+  public static void i(String tag, String log) {
+    log(tag, log);
+    LogUtil.i(tag, log);
+  }
+
+  public static void i(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.i(tag, log, e);
+  }
+
+  public static void d(String tag, String log) {
+    log(tag, log);
+    LogUtil.d(tag, log);
+  }
+
+  public static void d(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.d(tag, log, e);
+  }
+
+  public static void v(String tag, String log) {
+    log(tag, log);
+    LogUtil.v(tag, log);
+  }
+
+  public static void v(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.v(tag, log, e);
+  }
+
+  public static void wtf(String tag, String log) {
+    log(tag, log);
+    LogUtil.e(tag, log);
+  }
+
+  public static void wtf(String tag, String log, Throwable e) {
+    log(tag, log + " " + e);
+    LogUtil.e(tag, log, e);
+  }
+
+  /**
+   * Redact personally identifiable information for production users. If we are running in verbose
+   * mode, return the original string, otherwise return a SHA-1 hash of the input string.
+   */
+  public static String pii(Object pii) {
+    if (pii == null) {
+      return String.valueOf(pii);
+    }
+    return "[PII]";
+  }
+
+  public static class LocalLog {
+
+    private final Deque<String> mLog;
+    private final int mMaxLines;
+
+    public LocalLog(int maxLines) {
+      mMaxLines = Math.max(0, maxLines);
+      mLog = new ArrayDeque<>(mMaxLines);
+    }
+
+    public void log(String msg) {
+      if (mMaxLines <= 0) {
+        return;
+      }
+      Calendar c = Calendar.getInstance();
+      c.setTimeInMillis(System.currentTimeMillis());
+      append(String.format("%tm-%td %tH:%tM:%tS.%tL - %s", c, c, c, c, c, c, msg));
+    }
+
+    private synchronized void append(String logLine) {
+      while (mLog.size() >= mMaxLines) {
+        mLog.remove();
+      }
+      mLog.add(logLine);
+    }
+
+    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+      Iterator<String> itr = mLog.iterator();
+      while (itr.hasNext()) {
+        pw.println(itr.next());
+      }
+    }
+
+    public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+      Iterator<String> itr = mLog.descendingIterator();
+      while (itr.hasNext()) {
+        pw.println(itr.next());
+      }
+    }
+
+    public static class ReadOnlyLocalLog {
+
+      private final LocalLog mLog;
+
+      ReadOnlyLocalLog(LocalLog log) {
+        mLog = log;
+      }
+
+      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mLog.dump(fd, pw, args);
+      }
+
+      public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        mLog.reverseDump(fd, pw, args);
+      }
+    }
+
+    public ReadOnlyLocalLog readOnlyLocalLog() {
+      return new ReadOnlyLocalLog(this);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java b/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java
new file mode 100644
index 0000000..c5650b3
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmPackageInstallReceiver.java
@@ -0,0 +1,65 @@
+/*
+ * 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.voicemail.impl;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+
+/**
+ * When a new package is installed, check if it matches any of the vvm carrier apps of the currently
+ * enabled dialer vvm sources.
+ */
+public class VvmPackageInstallReceiver extends BroadcastReceiver {
+
+  private static final String TAG = "VvmPkgInstallReceiver";
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    if (intent.getData() == null) {
+      return;
+    }
+
+    String packageName = intent.getData().getSchemeSpecificPart();
+    if (packageName == null) {
+      return;
+    }
+
+    for (PhoneAccountHandle phoneAccount :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+      if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) {
+        // Skip the check if this voicemail source's setting is overridden by the user.
+        continue;
+      }
+
+      OmtpVvmCarrierConfigHelper carrierConfigHelper =
+          new OmtpVvmCarrierConfigHelper(context, phoneAccount);
+      if (carrierConfigHelper.getCarrierVvmPackageNames() == null) {
+        continue;
+      }
+      if (carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) {
+        // Force deactivate the client. The user can re-enable it in the settings.
+        // There is no need to update the settings for deactivation. At this point, if the
+        // default value is used it should be false because a carrier package is present.
+        VvmLog.i(TAG, "Carrier VVM package installed, disabling system VVM client");
+        VisualVoicemailSettingsUtil.setEnabled(context, phoneAccount, false);
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/VvmPhoneStateListener.java b/java/com/android/voicemail/impl/VvmPhoneStateListener.java
new file mode 100644
index 0000000..48b7204
--- /dev/null
+++ b/java/com/android/voicemail/impl/VvmPhoneStateListener.java
@@ -0,0 +1,104 @@
+/*
+ * 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.voicemail.impl;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VoicemailStatusQueryHelper;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/**
+ * Check if service is lost and indicate this in the voicemail status. TODO(b/35125657): Not used
+ * for now, restore it.
+ */
+public class VvmPhoneStateListener extends PhoneStateListener {
+
+  private static final String TAG = "VvmPhoneStateListener";
+
+  private PhoneAccountHandle mPhoneAccount;
+  private Context mContext;
+  private int mPreviousState = -1;
+
+  public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) {
+    // TODO: b/32637799 too much trouble to call super constructor through reflection,
+    // just use non-phoneAccountHandle version for now.
+    super();
+    mContext = context;
+    mPhoneAccount = accountHandle;
+  }
+
+  @Override
+  public void onServiceStateChanged(ServiceState serviceState) {
+    if (mPhoneAccount == null) {
+      VvmLog.e(
+          TAG,
+          "onServiceStateChanged on phoneAccount "
+              + mPhoneAccount
+              + " with invalid phoneAccountHandle, ignoring");
+      return;
+    }
+
+    int state = serviceState.getState();
+    if (state == mPreviousState
+        || (state != ServiceState.STATE_IN_SERVICE
+            && mPreviousState != ServiceState.STATE_IN_SERVICE)) {
+      // Only interested in state changes or transitioning into or out of "in service".
+      // Otherwise just quit.
+      mPreviousState = state;
+      return;
+    }
+
+    OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, mPhoneAccount);
+
+    if (state == ServiceState.STATE_IN_SERVICE) {
+      VoicemailStatusQueryHelper voicemailStatusQueryHelper =
+          new VoicemailStatusQueryHelper(mContext);
+      if (voicemailStatusQueryHelper.isVoicemailSourceConfigured(mPhoneAccount)) {
+        if (!voicemailStatusQueryHelper.isNotificationsChannelActive(mPhoneAccount)) {
+          VvmLog.v(TAG, "Notifications channel is active for " + mPhoneAccount);
+          helper.handleEvent(
+              VoicemailStatus.edit(mContext, mPhoneAccount), OmtpEvents.NOTIFICATION_IN_SERVICE);
+        }
+      }
+
+      if (VvmAccountManager.isAccountActivated(mContext, mPhoneAccount)) {
+        VvmLog.v(TAG, "Signal returned: requesting resync for " + mPhoneAccount);
+        // If the source is already registered, run a full sync in case something was missed
+        // while signal was down.
+        SyncTask.start(mContext, mPhoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
+      } else {
+        VvmLog.v(TAG, "Signal returned: reattempting activation for " + mPhoneAccount);
+        // Otherwise initiate an activation because this means that an OMTP source was
+        // recognized but either the activation text was not successfully sent or a response
+        // was not received.
+        helper.startActivation();
+      }
+    } else {
+      VvmLog.v(TAG, "Notifications channel is inactive for " + mPhoneAccount);
+
+      if (!VvmAccountManager.isAccountActivated(mContext, mPhoneAccount)) {
+        return;
+      }
+      helper.handleEvent(
+          VoicemailStatus.edit(mContext, mPhoneAccount), OmtpEvents.NOTIFICATION_SERVICE_LOST);
+    }
+    mPreviousState = state;
+  }
+}
diff --git a/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java b/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java
new file mode 100644
index 0000000..07e8008
--- /dev/null
+++ b/java/com/android/voicemail/impl/fetch/FetchVoicemailReceiver.java
@@ -0,0 +1,218 @@
+/*
+ * 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.voicemail.impl.fetch;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+import com.android.voicemail.impl.sync.VvmNetworkRequestCallback;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** handles {@link VoicemailContract#ACTION_FETCH_VOICEMAIL} */
+@TargetApi(VERSION_CODES.O)
+public class FetchVoicemailReceiver extends BroadcastReceiver {
+
+  private static final String TAG = "FetchVoicemailReceiver";
+
+  static final String[] PROJECTION =
+      new String[] {
+        Voicemails.SOURCE_DATA, // 0
+        Voicemails.PHONE_ACCOUNT_ID, // 1
+        Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, // 2
+      };
+
+  public static final int SOURCE_DATA = 0;
+  public static final int PHONE_ACCOUNT_ID = 1;
+  public static final int PHONE_ACCOUNT_COMPONENT_NAME = 2;
+
+  // Number of retries
+  private static final int NETWORK_RETRY_COUNT = 3;
+
+  private ContentResolver mContentResolver;
+  private Uri mUri;
+  private VvmNetworkRequestCallback mNetworkCallback;
+  private Context mContext;
+  private String mUid;
+  private PhoneAccountHandle mPhoneAccount;
+  private int mRetryCount = NETWORK_RETRY_COUNT;
+
+  @Override
+  public void onReceive(final Context context, Intent intent) {
+    if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {
+      VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received");
+      mContext = context;
+      mContentResolver = context.getContentResolver();
+      mUri = intent.getData();
+
+      if (mUri == null) {
+        VvmLog.w(TAG, VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data");
+        return;
+      }
+
+      if (!context
+          .getPackageName()
+          .equals(mUri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) {
+        // Ignore if the fetch request is for a voicemail not from this package.
+        VvmLog.e(TAG, "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName());
+        return;
+      }
+
+      Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null);
+      if (cursor == null) {
+        VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null");
+        return;
+      }
+      try {
+        if (cursor.moveToFirst()) {
+          mUid = cursor.getString(SOURCE_DATA);
+          String accountId = cursor.getString(PHONE_ACCOUNT_ID);
+          if (TextUtils.isEmpty(accountId)) {
+            TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+            accountId = telephonyManager.getSimSerialNumber();
+
+            if (TextUtils.isEmpty(accountId)) {
+              VvmLog.e(TAG, "Account null and no default sim found.");
+              return;
+            }
+          }
+
+          mPhoneAccount =
+              new PhoneAccountHandle(
+                  ComponentName.unflattenFromString(cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)),
+                  cursor.getString(PHONE_ACCOUNT_ID));
+          if (!VvmAccountManager.isAccountActivated(context, mPhoneAccount)) {
+            mPhoneAccount = getAccountFromMarshmallowAccount(context, mPhoneAccount);
+            if (mPhoneAccount == null) {
+              VvmLog.w(TAG, "Account not registered - cannot retrieve message.");
+              return;
+            }
+            VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle");
+          }
+          VvmLog.i(TAG, "Requesting network to fetch voicemail");
+          mNetworkCallback = new fetchVoicemailNetworkRequestCallback(context, mPhoneAccount);
+          mNetworkCallback.requestNetwork();
+        }
+      } finally {
+        cursor.close();
+      }
+    }
+  }
+
+  /**
+   * In ag/930496 the format of PhoneAccountHandle has changed between Marshmallow and Nougat. This
+   * method attempts to search the account from the old database in registered sources using the old
+   * format. There's a chance of M phone account collisions on multi-SIM devices, but visual
+   * voicemail is not supported on M multi-SIM.
+   */
+  @Nullable
+  private static PhoneAccountHandle getAccountFromMarshmallowAccount(
+      Context context, PhoneAccountHandle oldAccount) {
+    if (!BuildCompat.isAtLeastN()) {
+      return null;
+    }
+    for (PhoneAccountHandle handle :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+      if (getIccSerialNumberFromFullIccSerialNumber(handle.getId()).equals(oldAccount.getId())) {
+        return handle;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * getIccSerialNumber() is used for ID before N, and getFullIccSerialNumber() after.
+   * getIccSerialNumber() stops at the first hex char.
+   */
+  @NonNull
+  private static String getIccSerialNumberFromFullIccSerialNumber(@NonNull String id) {
+    for (int i = 0; i < id.length(); i++) {
+      if (!Character.isDigit(id.charAt(i))) {
+        return id.substring(0, i);
+      }
+    }
+    return id;
+  }
+
+  private class fetchVoicemailNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+    public fetchVoicemailNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount) {
+      super(context, phoneAccount, VoicemailStatus.edit(context, phoneAccount));
+    }
+
+    @Override
+    public void onAvailable(final Network network) {
+      super.onAvailable(network);
+      fetchVoicemail(network, getVoicemailStatusEditor());
+    }
+  }
+
+  private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) {
+    Executor executor = Executors.newCachedThreadPool();
+    executor.execute(
+        new Runnable() {
+          @Override
+          public void run() {
+            try {
+              while (mRetryCount > 0) {
+                VvmLog.i(TAG, "fetching voicemail, retry count=" + mRetryCount);
+                try (ImapHelper imapHelper =
+                    new ImapHelper(mContext, mPhoneAccount, network, status)) {
+                  boolean success =
+                      imapHelper.fetchVoicemailPayload(
+                          new VoicemailFetchedCallback(mContext, mUri, mPhoneAccount), mUid);
+                  if (!success && mRetryCount > 0) {
+                    VvmLog.i(TAG, "fetch voicemail failed, retrying");
+                    mRetryCount--;
+                  } else {
+                    return;
+                  }
+                } catch (InitializingException e) {
+                  VvmLog.w(TAG, "Can't retrieve Imap credentials ", e);
+                  return;
+                }
+              }
+            } finally {
+              if (mNetworkCallback != null) {
+                mNetworkCallback.releaseNetwork();
+              }
+            }
+          }
+        });
+  }
+}
diff --git a/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java
new file mode 100644
index 0000000..f386fce
--- /dev/null
+++ b/java/com/android/voicemail/impl/fetch/VoicemailFetchedCallback.java
@@ -0,0 +1,102 @@
+/*
+ * 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.voicemail.impl.fetch;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.VoicemailPayload;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Callback for when a voicemail payload is fetched. It copies the returned stream to the data file
+ * corresponding to the voicemail.
+ */
+public class VoicemailFetchedCallback {
+  private static final String TAG = "VoicemailFetchedCallback";
+
+  private final Context mContext;
+  private final ContentResolver mContentResolver;
+  private final Uri mUri;
+  private final PhoneAccountHandle mPhoneAccountHandle;
+
+  public VoicemailFetchedCallback(Context context, Uri uri, PhoneAccountHandle phoneAccountHandle) {
+    mContext = context;
+    mContentResolver = context.getContentResolver();
+    mUri = uri;
+    mPhoneAccountHandle = phoneAccountHandle;
+  }
+
+  /**
+   * Saves the voicemail payload data into the voicemail provider then sets the "has_content" bit of
+   * the voicemail to "1".
+   *
+   * @param voicemailPayload The object containing the content data for the voicemail
+   */
+  public void setVoicemailContent(@Nullable VoicemailPayload voicemailPayload) {
+    if (voicemailPayload == null) {
+      VvmLog.i(TAG, "Payload not found, message has unsupported format");
+      ContentValues values = new ContentValues();
+      values.put(
+          Voicemails.TRANSCRIPTION,
+          mContext.getString(
+              R.string.vvm_unsupported_message_format,
+              mContext
+                  .getSystemService(TelecomManager.class)
+                  .getVoiceMailNumber(mPhoneAccountHandle)));
+      updateVoicemail(values);
+      return;
+    }
+
+    VvmLog.d(TAG, String.format("Writing new voicemail content: %s", mUri));
+    OutputStream outputStream = null;
+
+    try {
+      outputStream = mContentResolver.openOutputStream(mUri);
+      byte[] inputBytes = voicemailPayload.getBytes();
+      if (inputBytes != null) {
+        outputStream.write(inputBytes);
+      }
+    } catch (IOException e) {
+      VvmLog.w(TAG, String.format("File not found for %s", mUri));
+      return;
+    } finally {
+      IOUtils.closeQuietly(outputStream);
+    }
+
+    // Update mime_type & has_content after we are done with file update.
+    ContentValues values = new ContentValues();
+    values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType());
+    values.put(Voicemails.HAS_CONTENT, true);
+    updateVoicemail(values);
+  }
+
+  private void updateVoicemail(ContentValues values) {
+    int updatedCount = mContentResolver.update(mUri, values, null, null);
+    if (updatedCount != 1) {
+      VvmLog.e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/imap/ImapHelper.java b/java/com/android/voicemail/impl/imap/ImapHelper.java
new file mode 100644
index 0000000..6aa4158
--- /dev/null
+++ b/java/com/android/voicemail/impl/imap/ImapHelper.java
@@ -0,0 +1,693 @@
+/*
+ * 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.voicemail.impl.imap;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.util.Base64;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpConstants.ChangePinResult;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VoicemailStatus.Editor;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
+import com.android.voicemail.impl.mail.Address;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.FetchProfile;
+import com.android.voicemail.impl.mail.Flag;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.TempDirectory;
+import com.android.voicemail.impl.mail.internet.MimeMessage;
+import com.android.voicemail.impl.mail.store.ImapConnection;
+import com.android.voicemail.impl.mail.store.ImapFolder;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import org.apache.commons.io.IOUtils;
+
+/** A helper interface to abstract commands sent across IMAP interface for a given account. */
+public class ImapHelper implements Closeable {
+
+  private static final String TAG = "ImapHelper";
+
+  private ImapFolder mFolder;
+  private ImapStore mImapStore;
+
+  private final Context mContext;
+  private final PhoneAccountHandle mPhoneAccount;
+  private final Network mNetwork;
+  private final Editor mStatus;
+
+  VisualVoicemailPreferences mPrefs;
+  private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_";
+  private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_";
+
+  private int mQuotaOccupied;
+  private int mQuotaTotal;
+
+  private final OmtpVvmCarrierConfigHelper mConfig;
+
+  /** InitializingException */
+  public static class InitializingException extends Exception {
+
+    public InitializingException(String message) {
+      super(message);
+    }
+  }
+
+  public ImapHelper(
+      Context context,
+      PhoneAccountHandle phoneAccount,
+      Network network,
+      Editor status)
+      throws InitializingException {
+    this(
+        context,
+        new OmtpVvmCarrierConfigHelper(context, phoneAccount),
+        phoneAccount,
+        network,
+        status);
+  }
+
+  public ImapHelper(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      PhoneAccountHandle phoneAccount,
+      Network network,
+      Editor status)
+      throws InitializingException {
+    mContext = context;
+    mPhoneAccount = phoneAccount;
+    mNetwork = network;
+    mStatus = status;
+    mConfig = config;
+    mPrefs = new VisualVoicemailPreferences(context, phoneAccount);
+
+    try {
+      TempDirectory.setTempDirectory(context);
+
+      String username = mPrefs.getString(OmtpConstants.IMAP_USER_NAME, null);
+      String password = mPrefs.getString(OmtpConstants.IMAP_PASSWORD, null);
+      String serverName = mPrefs.getString(OmtpConstants.SERVER_ADDRESS, null);
+      int port = Integer.parseInt(mPrefs.getString(OmtpConstants.IMAP_PORT, null));
+      int auth = ImapStore.FLAG_NONE;
+
+      int sslPort = mConfig.getSslPort();
+      if (sslPort != 0) {
+        port = sslPort;
+        auth = ImapStore.FLAG_SSL;
+      }
+
+      mImapStore =
+          new ImapStore(context, this, username, password, port, serverName, auth, network);
+    } catch (NumberFormatException e) {
+      handleEvent(OmtpEvents.DATA_INVALID_PORT);
+      LogUtils.w(TAG, "Could not parse port number");
+      throw new InitializingException("cannot initialize ImapHelper:" + e.toString());
+    }
+
+    mQuotaOccupied =
+        mPrefs.getInt(PREF_KEY_QUOTA_OCCUPIED, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+    mQuotaTotal = mPrefs.getInt(PREF_KEY_QUOTA_TOTAL, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+  }
+
+  @Override
+  public void close() {
+    mImapStore.closeConnection();
+  }
+
+  public boolean isRoaming() {
+    ConnectivityManager connectivityManager =
+        (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+    NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork);
+    if (info == null) {
+      return false;
+    }
+    return info.isRoaming();
+  }
+
+  public OmtpVvmCarrierConfigHelper getConfig() {
+    return mConfig;
+  }
+
+  public ImapConnection connect() {
+    return mImapStore.getConnection();
+  }
+
+  /** The caller thread will block until the method returns. */
+  public boolean markMessagesAsRead(List<Voicemail> voicemails) {
+    return setFlags(voicemails, Flag.SEEN);
+  }
+
+  /** The caller thread will block until the method returns. */
+  public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
+    return setFlags(voicemails, Flag.DELETED);
+  }
+
+  public void handleEvent(OmtpEvents event) {
+    mConfig.handleEvent(mStatus, event);
+  }
+
+  /**
+   * Set flags on the server for a given set of voicemails.
+   *
+   * @param voicemails The voicemails to set flags for.
+   * @param flags The flags to set on the voicemails.
+   * @return {@code true} if the operation completes successfully, {@code false} otherwise.
+   */
+  private boolean setFlags(List<Voicemail> voicemails, String... flags) {
+    if (voicemails.size() == 0) {
+      return false;
+    }
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder != null) {
+        mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
+        return true;
+      }
+      return false;
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging exception");
+      return false;
+    } finally {
+      closeImapFolder();
+    }
+  }
+
+  /**
+   * Fetch a list of voicemails from the server.
+   *
+   * @return A list of voicemail objects containing data about voicemails stored on the server.
+   */
+  public List<Voicemail> fetchAllVoicemails() {
+    List<Voicemail> result = new ArrayList<Voicemail>();
+    Message[] messages;
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder == null) {
+        // This means we were unable to successfully open the folder.
+        return null;
+      }
+
+      // This method retrieves lightweight messages containing only the uid of the message.
+      messages = mFolder.getMessages(null);
+
+      for (Message message : messages) {
+        // Get the voicemail details (message structure).
+        MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
+        if (messageStructureWrapper != null) {
+          result.add(getVoicemailFromMessageStructure(messageStructureWrapper));
+        }
+      }
+      return result;
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging Exception");
+      return null;
+    } finally {
+      closeImapFolder();
+    }
+  }
+
+  /**
+   * Extract voicemail details from the message structure. Also fetch transcription if a
+   * transcription exists.
+   */
+  private Voicemail getVoicemailFromMessageStructure(
+      MessageStructureWrapper messageStructureWrapper) throws MessagingException {
+    Message messageDetails = messageStructureWrapper.messageStructure;
+
+    TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
+    if (messageStructureWrapper.transcriptionBodyPart != null) {
+      FetchProfile fetchProfile = new FetchProfile();
+      fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
+
+      mFolder.fetch(new Message[] {messageDetails}, fetchProfile, listener);
+    }
+
+    // Found an audio attachment, this is a valid voicemail.
+    long time = messageDetails.getSentDate().getTime();
+    String number = getNumber(messageDetails.getFrom());
+    boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN);
+    return Voicemail.createForInsertion(time, number)
+        .setPhoneAccount(mPhoneAccount)
+        .setSourcePackage(mContext.getPackageName())
+        .setSourceData(messageDetails.getUid())
+        .setIsRead(isRead)
+        .setTranscription(listener.getVoicemailTranscription())
+        .build();
+  }
+
+  /**
+   * The "from" field of a visual voicemail IMAP message is the number of the caller who left the
+   * message. Extract this number from the list of "from" addresses.
+   *
+   * @param fromAddresses A list of addresses that comprise the "from" line.
+   * @return The number of the voicemail sender.
+   */
+  private String getNumber(Address[] fromAddresses) {
+    if (fromAddresses != null && fromAddresses.length > 0) {
+      if (fromAddresses.length != 1) {
+        LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
+      }
+      String sender = fromAddresses[0].getAddress();
+      int atPos = sender.indexOf('@');
+      if (atPos != -1) {
+        // Strip domain part of the address.
+        sender = sender.substring(0, atPos);
+      }
+      return sender;
+    }
+    return null;
+  }
+
+  /**
+   * Fetches the structure of the given message and returns a wrapper containing the message
+   * structure and the transcription structure (if applicable).
+   *
+   * @throws MessagingException if fetching the structure of the message fails
+   */
+  private MessageStructureWrapper fetchMessageStructure(Message message) throws MessagingException {
+    LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
+
+    MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
+
+    FetchProfile fetchProfile = new FetchProfile();
+    fetchProfile.addAll(
+        Arrays.asList(
+            FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE, FetchProfile.Item.STRUCTURE));
+
+    // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
+    // message is successfully retrieved.
+    mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+    return listener.getMessageStructure();
+  }
+
+  public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder == null) {
+        // This means we were unable to successfully open the folder.
+        return false;
+      }
+      Message message = mFolder.getMessage(uid);
+      if (message == null) {
+        return false;
+      }
+      VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
+      callback.setVoicemailContent(voicemailPayload);
+      return true;
+    } catch (MessagingException e) {
+    } finally {
+      closeImapFolder();
+    }
+    return false;
+  }
+
+  /**
+   * Fetches the body of the given message and returns the parsed voicemail payload.
+   *
+   * @throws MessagingException if fetching the body of the message fails
+   */
+  private VoicemailPayload fetchVoicemailPayload(Message message) throws MessagingException {
+    LogUtils.d(TAG, "Fetching message body for " + message.getUid());
+
+    MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
+
+    FetchProfile fetchProfile = new FetchProfile();
+    fetchProfile.add(FetchProfile.Item.BODY);
+
+    mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+    return listener.getVoicemailPayload();
+  }
+
+  public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder == null) {
+        // This means we were unable to successfully open the folder.
+        return false;
+      }
+
+      Message message = mFolder.getMessage(uid);
+      if (message == null) {
+        return false;
+      }
+
+      MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
+      if (messageStructureWrapper != null) {
+        TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
+        if (messageStructureWrapper.transcriptionBodyPart != null) {
+          FetchProfile fetchProfile = new FetchProfile();
+          fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
+
+          // This method is called synchronously so the transcription will be populated
+          // in the listener once the next method is called.
+          mFolder.fetch(new Message[] {message}, fetchProfile, listener);
+          callback.setVoicemailTranscription(listener.getVoicemailTranscription());
+        }
+      }
+      return true;
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging Exception");
+      return false;
+    } finally {
+      closeImapFolder();
+    }
+  }
+
+  @ChangePinResult
+  public int changePin(String oldPin, String newPin) throws MessagingException {
+    ImapConnection connection = mImapStore.getConnection();
+    try {
+      String command =
+          getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT);
+      connection.sendCommand(String.format(Locale.US, command, newPin, oldPin), true);
+      return getChangePinResultFromImapResponse(connection.readResponse());
+    } catch (IOException ioe) {
+      VvmLog.e(TAG, "changePin: ", ioe);
+      return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
+    } finally {
+      connection.destroyResponses();
+    }
+  }
+
+  public void changeVoicemailTuiLanguage(String languageCode) throws MessagingException {
+    ImapConnection connection = mImapStore.getConnection();
+    try {
+      String command =
+          getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT);
+      connection.sendCommand(String.format(Locale.US, command, languageCode), true);
+    } catch (IOException ioe) {
+      LogUtils.e(TAG, ioe.toString());
+    } finally {
+      connection.destroyResponses();
+    }
+  }
+
+  public void closeNewUserTutorial() throws MessagingException {
+    ImapConnection connection = mImapStore.getConnection();
+    try {
+      String command = getConfig().getProtocol().getCommand(OmtpConstants.IMAP_CLOSE_NUT);
+      connection.executeSimpleCommand(command, false);
+    } catch (IOException ioe) {
+      throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString());
+    } finally {
+      connection.destroyResponses();
+    }
+  }
+
+  @ChangePinResult
+  private static int getChangePinResultFromImapResponse(ImapResponse response)
+      throws MessagingException {
+    if (!response.isTagged()) {
+      throw new MessagingException(MessagingException.SERVER_ERROR, "tagged response expected");
+    }
+    if (!response.isOk()) {
+      String message = response.getStringOrEmpty(1).getString();
+      LogUtils.d(TAG, "change PIN failed: " + message);
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_TOO_SHORT;
+      }
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_TOO_LONG;
+      }
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_TOO_WEAK;
+      }
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_MISMATCH;
+      }
+      if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
+        return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER;
+      }
+      return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
+    }
+    LogUtils.d(TAG, "change PIN succeeded");
+    return OmtpConstants.CHANGE_PIN_SUCCESS;
+  }
+
+  public void updateQuota() {
+    try {
+      mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+      if (mFolder == null) {
+        // This means we were unable to successfully open the folder.
+        return;
+      }
+      updateQuota(mFolder);
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging Exception");
+    } finally {
+      closeImapFolder();
+    }
+  }
+
+  public int getOccuupiedQuota() {
+    return mQuotaOccupied;
+  }
+
+  public int getTotalQuota() {
+    return mQuotaTotal;
+  }
+
+  private void updateQuota(ImapFolder folder) throws MessagingException {
+    setQuota(folder.getQuota());
+  }
+
+  private void setQuota(ImapFolder.Quota quota) {
+    if (quota == null) {
+      return;
+    }
+    if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) {
+      VvmLog.v(TAG, "Quota hasn't changed");
+      return;
+    }
+    mQuotaOccupied = quota.occupied;
+    mQuotaTotal = quota.total;
+    VoicemailStatus.edit(mContext, mPhoneAccount).setQuota(mQuotaOccupied, mQuotaTotal).apply();
+    mPrefs
+        .edit()
+        .putInt(PREF_KEY_QUOTA_OCCUPIED, mQuotaOccupied)
+        .putInt(PREF_KEY_QUOTA_TOTAL, mQuotaTotal)
+        .apply();
+    VvmLog.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal);
+  }
+
+  /**
+   * A wrapper to hold a message with its header details and the structure for transcriptions (so
+   * they can be fetched in the future).
+   */
+  public static class MessageStructureWrapper {
+
+    public Message messageStructure;
+    public BodyPart transcriptionBodyPart;
+
+    public MessageStructureWrapper() {}
+  }
+
+  /** Listener for the message structure being fetched. */
+  private final class MessageStructureFetchedListener
+      implements ImapFolder.MessageRetrievalListener {
+
+    private MessageStructureWrapper mMessageStructure;
+
+    public MessageStructureFetchedListener() {}
+
+    public MessageStructureWrapper getMessageStructure() {
+      return mMessageStructure;
+    }
+
+    @Override
+    public void messageRetrieved(Message message) {
+      LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
+      LogUtils.d(TAG, "Message retrieved: " + message);
+      try {
+        mMessageStructure = getMessageOrNull(message);
+        if (mMessageStructure == null) {
+          LogUtils.d(TAG, "This voicemail does not have an attachment...");
+          return;
+        }
+      } catch (MessagingException e) {
+        LogUtils.e(TAG, e, "Messaging Exception");
+        closeImapFolder();
+      }
+    }
+
+    /**
+     * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
+     *
+     * @param message The IMAP message.
+     * @return The MessageStructureWrapper object corresponding to an IMAP message and
+     *     transcription.
+     */
+    private MessageStructureWrapper getMessageOrNull(Message message) throws MessagingException {
+      if (!message.getMimeType().startsWith("multipart/")) {
+        LogUtils.w(TAG, "Ignored non multi-part message");
+        return null;
+      }
+
+      MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
+
+      Multipart multipart = (Multipart) message.getBody();
+      for (int i = 0; i < multipart.getCount(); ++i) {
+        BodyPart bodyPart = multipart.getBodyPart(i);
+        String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+        LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
+
+        if (bodyPartMimeType.startsWith("audio/")) {
+          messageStructureWrapper.messageStructure = message;
+        } else if (bodyPartMimeType.startsWith("text/")) {
+          messageStructureWrapper.transcriptionBodyPart = bodyPart;
+        } else {
+          VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType);
+        }
+      }
+
+      if (messageStructureWrapper.messageStructure != null) {
+        return messageStructureWrapper;
+      }
+
+      // No attachment found, this is not a voicemail.
+      return null;
+    }
+  }
+
+  /** Listener for the message body being fetched. */
+  private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
+
+    private VoicemailPayload mVoicemailPayload;
+
+    /** Returns the fetch voicemail payload. */
+    public VoicemailPayload getVoicemailPayload() {
+      return mVoicemailPayload;
+    }
+
+    @Override
+    public void messageRetrieved(Message message) {
+      LogUtils.d(TAG, "Fetched message body for " + message.getUid());
+      LogUtils.d(TAG, "Message retrieved: " + message);
+      try {
+        mVoicemailPayload = getVoicemailPayloadFromMessage(message);
+      } catch (MessagingException e) {
+        LogUtils.e(TAG, "Messaging Exception:", e);
+      } catch (IOException e) {
+        LogUtils.e(TAG, "IO Exception:", e);
+      }
+    }
+
+    private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
+        throws MessagingException, IOException {
+      Multipart multipart = (Multipart) message.getBody();
+      List<String> mimeTypes = new ArrayList<>();
+      for (int i = 0; i < multipart.getCount(); ++i) {
+        BodyPart bodyPart = multipart.getBodyPart(i);
+        String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+        mimeTypes.add(bodyPartMimeType);
+        if (bodyPartMimeType.startsWith("audio/")) {
+          byte[] bytes = getDataFromBody(bodyPart.getBody());
+          LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
+          return new VoicemailPayload(bodyPartMimeType, bytes);
+        }
+      }
+      LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes);
+      return null;
+    }
+  }
+
+  /** Listener for the transcription being fetched. */
+  private final class TranscriptionFetchedListener implements ImapFolder.MessageRetrievalListener {
+
+    private String mVoicemailTranscription;
+
+    /** Returns the fetched voicemail transcription. */
+    public String getVoicemailTranscription() {
+      return mVoicemailTranscription;
+    }
+
+    @Override
+    public void messageRetrieved(Message message) {
+      LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
+      try {
+        mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
+      } catch (MessagingException e) {
+        LogUtils.e(TAG, "Messaging Exception:", e);
+      } catch (IOException e) {
+        LogUtils.e(TAG, "IO Exception:", e);
+      }
+    }
+  }
+
+  private ImapFolder openImapFolder(String modeReadWrite) {
+    try {
+      if (mImapStore == null) {
+        return null;
+      }
+      ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
+      folder.open(modeReadWrite);
+      return folder;
+    } catch (MessagingException e) {
+      LogUtils.e(TAG, e, "Messaging Exception");
+    }
+    return null;
+  }
+
+  private Message[] convertToImapMessages(List<Voicemail> voicemails) {
+    Message[] messages = new Message[voicemails.size()];
+    for (int i = 0; i < voicemails.size(); ++i) {
+      messages[i] = new MimeMessage();
+      messages[i].setUid(voicemails.get(i).getSourceData());
+    }
+    return messages;
+  }
+
+  private void closeImapFolder() {
+    if (mFolder != null) {
+      mFolder.close(true);
+    }
+  }
+
+  private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
+    try {
+      body.writeTo(bufferedOut);
+      return Base64.decode(out.toByteArray(), Base64.DEFAULT);
+    } finally {
+      IOUtils.closeQuietly(bufferedOut);
+      IOUtils.closeQuietly(out);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/imap/VoicemailPayload.java b/java/com/android/voicemail/impl/imap/VoicemailPayload.java
new file mode 100644
index 0000000..69befb4
--- /dev/null
+++ b/java/com/android/voicemail/impl/imap/VoicemailPayload.java
@@ -0,0 +1,36 @@
+/*
+ * 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.voicemail.impl.imap;
+
+/** The payload for a voicemail, usually audio data. */
+public class VoicemailPayload {
+  private final String mMimeType;
+  private final byte[] mBytes;
+
+  public VoicemailPayload(String mimeType, byte[] bytes) {
+    mMimeType = mimeType;
+    mBytes = bytes;
+  }
+
+  public byte[] getBytes() {
+    return mBytes;
+  }
+
+  public String getMimeType() {
+    return mMimeType;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Address.java b/java/com/android/voicemail/impl/mail/Address.java
new file mode 100644
index 0000000..3a7a866
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Address.java
@@ -0,0 +1,520 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+
+/**
+ * This class represent email address.
+ *
+ * <p>RFC822 email address may have following format. "name" <address> (comment) "name" <address>
+ * name <address> address Name and comment part should be MIME/base64 encoded in header if
+ * necessary.
+ */
+public class Address implements Parcelable {
+  public static final String ADDRESS_DELIMETER = ",";
+  /** Address part, in the form local_part@domain_part. No surrounding angle brackets. */
+  private String mAddress;
+
+  /**
+   * Name part. No surrounding double quote, and no MIME/base64 encoding. This must be null if
+   * Address has no name part.
+   */
+  private String mPersonal;
+
+  /**
+   * When personal is set, it will return the first token of the personal string. Otherwise, it will
+   * return the e-mail address up to the '@' sign.
+   */
+  private String mSimplifiedName;
+
+  // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
+  private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
+  // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
+  private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
+  // Regex that matches escaped character '\\([\\"])'
+  private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
+
+  // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
+  // TODO: Fix this to better constrain comments.
+  /** Regex for the local part of an email address. */
+  private static final String LOCAL_PART = "[^@]+";
+  /** Regex for each part of the domain part, i.e. the thing between the dots. */
+  private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
+  /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
+  private static final String DOMAIN_PART = "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
+
+  /** Pattern to check if an email address is valid. */
+  private static final Pattern EMAIL_ADDRESS =
+      Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
+
+  private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
+
+  // delimiters are chars that do not appear in an email address, used by fromHeader
+  private static final char LIST_DELIMITER_EMAIL = '\1';
+  private static final char LIST_DELIMITER_PERSONAL = '\2';
+
+  private static final String LOG_TAG = "Email Address";
+
+  @VisibleForTesting
+  public Address(String address) {
+    setAddress(address);
+  }
+
+  public Address(String address, String personal) {
+    setPersonal(personal);
+    setAddress(address);
+  }
+
+  /**
+   * Returns a simplified string for this e-mail address. When a name is known, it will return the
+   * first token of that name. Otherwise, it will return the e-mail address up to the '@' sign.
+   */
+  public String getSimplifiedName() {
+    if (mSimplifiedName == null) {
+      if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
+        int atSign = mAddress.indexOf('@');
+        mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
+      } else if (!TextUtils.isEmpty(mPersonal)) {
+
+        // TODO: use Contacts' NameSplitter for more reliable first-name extraction
+
+        int end = mPersonal.indexOf(' ');
+        while (end > 0 && mPersonal.charAt(end - 1) == ',') {
+          end--;
+        }
+        mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);
+
+      } else {
+        LogUtils.w(LOG_TAG, "Unable to get a simplified name");
+        mSimplifiedName = "";
+      }
+    }
+    return mSimplifiedName;
+  }
+
+  public static synchronized Address getEmailAddress(String rawAddress) {
+    if (TextUtils.isEmpty(rawAddress)) {
+      return null;
+    }
+    String name, address;
+    final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
+    if (tokens.length > 0) {
+      final String tokenizedName = tokens[0].getName();
+      name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString() : "";
+      address = Html.fromHtml(tokens[0].getAddress()).toString();
+    } else {
+      name = "";
+      address = rawAddress == null ? "" : Html.fromHtml(rawAddress).toString();
+    }
+    return new Address(address, name);
+  }
+
+  public String getAddress() {
+    return mAddress;
+  }
+
+  public void setAddress(String address) {
+    mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
+  }
+
+  /**
+   * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
+   *
+   * @return Name part of email address. Returns null if it is omitted.
+   */
+  public String getPersonal() {
+    return mPersonal;
+  }
+
+  /**
+   * Set personal part from UTF-16 string. Optional surrounding double quote will be removed. It
+   * will be also unquoted and MIME/base64 decoded.
+   *
+   * @param personal name part of email address as UTF-16 string. Null is acceptable.
+   */
+  public void setPersonal(String personal) {
+    mPersonal = decodeAddressPersonal(personal);
+  }
+
+  /**
+   * Decodes name from UTF-16 string. Optional surrounding double quote will be removed. It will be
+   * also unquoted and MIME/base64 decoded.
+   *
+   * @param personal name part of email address as UTF-16 string. Null is acceptable.
+   */
+  public static String decodeAddressPersonal(String personal) {
+    if (personal != null) {
+      personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
+      personal = UNQUOTE.matcher(personal).replaceAll("$1");
+      personal = DecoderUtil.decodeEncodedWords(personal);
+      if (personal.length() == 0) {
+        personal = null;
+      }
+    }
+    return personal;
+  }
+
+  /**
+   * This method is used to check that all the addresses that the user entered in a list (e.g. To:)
+   * are valid, so that none is dropped.
+   */
+  @VisibleForTesting
+  public static boolean isAllValid(String addressList) {
+    // This code mimics the parse() method below.
+    // I don't know how to better avoid the code-duplication.
+    if (addressList != null && addressList.length() > 0) {
+      Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+      for (int i = 0, length = tokens.length; i < length; ++i) {
+        Rfc822Token token = tokens[i];
+        String address = token.getAddress();
+        if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Parse a comma-delimited list of addresses in RFC822 format and return an array of Address
+   * objects.
+   *
+   * @param addressList Address list in comma-delimited string.
+   * @return An array of 0 or more Addresses.
+   */
+  public static Address[] parse(String addressList) {
+    if (addressList == null || addressList.length() == 0) {
+      return EMPTY_ADDRESS_ARRAY;
+    }
+    Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+    ArrayList<Address> addresses = new ArrayList<Address>();
+    for (int i = 0, length = tokens.length; i < length; ++i) {
+      Rfc822Token token = tokens[i];
+      String address = token.getAddress();
+      if (!TextUtils.isEmpty(address)) {
+        if (isValidAddress(address)) {
+          String name = token.getName();
+          if (TextUtils.isEmpty(name)) {
+            name = null;
+          }
+          addresses.add(new Address(address, name));
+        }
+      }
+    }
+    return addresses.toArray(new Address[addresses.size()]);
+  }
+
+  /** Checks whether a string email address is valid. E.g. name@domain.com is valid. */
+  @VisibleForTesting
+  static boolean isValidAddress(final String address) {
+    return EMAIL_ADDRESS.matcher(address).find();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof Address) {
+      // It seems that the spec says that the "user" part is case-sensitive,
+      // while the domain part in case-insesitive.
+      // So foo@yahoo.com and Foo@yahoo.com are different.
+      // This may seem non-intuitive from the user POV, so we
+      // may re-consider it if it creates UI trouble.
+      // A problem case is "replyAll" sending to both
+      // a@b.c and to A@b.c, which turn out to be the same on the server.
+      // Leave unchanged for now (i.e. case-sensitive).
+      return getAddress().equals(((Address) o).getAddress());
+    }
+    return super.equals(o);
+  }
+
+  @Override
+  public int hashCode() {
+    return getAddress().hashCode();
+  }
+
+  /**
+   * Get human readable address string. Do not use this for email header.
+   *
+   * @return Human readable address string. Not quoted and not encoded.
+   */
+  @Override
+  public String toString() {
+    if (mPersonal != null && !mPersonal.equals(mAddress)) {
+      if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
+        return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
+      } else {
+        return mPersonal + " <" + mAddress + ">";
+      }
+    } else {
+      return mAddress;
+    }
+  }
+
+  /**
+   * Ensures that the given string starts and ends with the double quote character. The string is
+   * not modified in any way except to add the double quote character to start and end if it's not
+   * already there.
+   *
+   * <p>sample -> "sample" "sample" -> "sample" ""sample"" -> "sample" "sample"" -> "sample"
+   * sa"mp"le -> "sa"mp"le" "sa"mp"le" -> "sa"mp"le" (empty string) -> "" " -> ""
+   */
+  private static String ensureQuotedString(String s) {
+    if (s == null) {
+      return null;
+    }
+    if (!s.matches("^\".*\"$")) {
+      return "\"" + s + "\"";
+    } else {
+      return s;
+    }
+  }
+
+  /**
+   * Get human readable comma-delimited address string.
+   *
+   * @param addresses Address array
+   * @return Human readable comma-delimited address string.
+   */
+  @VisibleForTesting
+  public static String toString(Address[] addresses) {
+    return toString(addresses, ADDRESS_DELIMETER);
+  }
+
+  /**
+   * Get human readable address strings joined with the specified separator.
+   *
+   * @param addresses Address array
+   * @param separator Separator
+   * @return Human readable comma-delimited address string.
+   */
+  public static String toString(Address[] addresses, String separator) {
+    if (addresses == null || addresses.length == 0) {
+      return null;
+    }
+    if (addresses.length == 1) {
+      return addresses[0].toString();
+    }
+    StringBuilder sb = new StringBuilder(addresses[0].toString());
+    for (int i = 1; i < addresses.length; i++) {
+      sb.append(separator);
+      // TODO: investigate why this .trim() is needed.
+      sb.append(addresses[i].toString().trim());
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Get RFC822/MIME compatible address string.
+   *
+   * @return RFC822/MIME compatible address string. It may be surrounded by double quote or quoted
+   *     and MIME/base64 encoded if necessary.
+   */
+  public String toHeader() {
+    if (mPersonal != null) {
+      return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
+    } else {
+      return mAddress;
+    }
+  }
+
+  /**
+   * Get RFC822/MIME compatible comma-delimited address string.
+   *
+   * @param addresses Address array
+   * @return RFC822/MIME compatible comma-delimited address string. it may be surrounded by double
+   *     quoted or quoted and MIME/base64 encoded if necessary.
+   */
+  public static String toHeader(Address[] addresses) {
+    if (addresses == null || addresses.length == 0) {
+      return null;
+    }
+    if (addresses.length == 1) {
+      return addresses[0].toHeader();
+    }
+    StringBuilder sb = new StringBuilder(addresses[0].toHeader());
+    for (int i = 1; i < addresses.length; i++) {
+      // We need space character to be able to fold line.
+      sb.append(", ");
+      sb.append(addresses[i].toHeader());
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Get Human friendly address string.
+   *
+   * @return the personal part of this Address, or the address part if the personal part is not
+   *     available
+   */
+  @VisibleForTesting
+  public String toFriendly() {
+    if (mPersonal != null && mPersonal.length() > 0) {
+      return mPersonal;
+    } else {
+      return mAddress;
+    }
+  }
+
+  /**
+   * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
+   * details on the per-address conversion).
+   *
+   * @param addresses Array of Address[] values
+   * @return A comma-delimited string listing all of the addresses supplied. Null if source was null
+   *     or empty.
+   */
+  @VisibleForTesting
+  public static String toFriendly(Address[] addresses) {
+    if (addresses == null || addresses.length == 0) {
+      return null;
+    }
+    if (addresses.length == 1) {
+      return addresses[0].toFriendly();
+    }
+    StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
+    for (int i = 1; i < addresses.length; i++) {
+      sb.append(", ");
+      sb.append(addresses[i].toFriendly());
+    }
+    return sb.toString();
+  }
+
+  /** Returns exactly the same result as Address.toString(Address.fromHeader(addressList)). */
+  @VisibleForTesting
+  public static String fromHeaderToString(String addressList) {
+    return toString(fromHeader(addressList));
+  }
+
+  /** Returns exactly the same result as Address.toHeader(Address.parse(addressList)). */
+  @VisibleForTesting
+  public static String parseToHeader(String addressList) {
+    return Address.toHeader(Address.parse(addressList));
+  }
+
+  /**
+   * Returns null if the addressList has 0 addresses, otherwise returns the first address. The same
+   * as Address.fromHeader(addressList)[0] for non-empty list. This is an utility method that offers
+   * some performance optimization opportunities.
+   */
+  @VisibleForTesting
+  public static Address firstAddress(String addressList) {
+    Address[] array = fromHeader(addressList);
+    return array.length > 0 ? array[0] : null;
+  }
+
+  /**
+   * This method exists to convert an address list formatted in a deprecated legacy format to the
+   * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy
+   * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format.
+   *
+   * <p>This implementation is brute-force, and could be replaced with a more efficient version if
+   * desired.
+   */
+  public static String reformatToHeader(String addressList) {
+    return toHeader(fromHeader(addressList));
+  }
+
+  /**
+   * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format
+   * @return array of addresses parsed from <code>addressList</code>
+   */
+  @VisibleForTesting
+  public static Address[] fromHeader(String addressList) {
+    if (addressList == null || addressList.length() == 0) {
+      return EMPTY_ADDRESS_ARRAY;
+    }
+    // IF we're CSV, just parse
+    if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1)
+        && (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
+      return Address.parse(addressList);
+    }
+    // Otherwise, do backward-compatible unpack
+    ArrayList<Address> addresses = new ArrayList<Address>();
+    int length = addressList.length();
+    int pairStartIndex = 0;
+    int pairEndIndex;
+
+    /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
+       is used, not for every email address; i.e. not for every iteration of the while().
+       This reduces the theoretical complexity from quadratic to linear,
+       and provides some speed-up in practice by removing redundant scans of the string.
+    */
+    int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
+
+    while (pairStartIndex < length) {
+      pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
+      if (pairEndIndex == -1) {
+        pairEndIndex = length;
+      }
+      Address address;
+      if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
+        // in this case the DELIMITER_PERSONAL is in a future pair,
+        // so don't use personal, and don't update addressEndIndex
+        address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
+      } else {
+        address =
+            new Address(
+                addressList.substring(pairStartIndex, addressEndIndex),
+                addressList.substring(addressEndIndex + 1, pairEndIndex));
+        // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
+        addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
+      }
+      addresses.add(address);
+      pairStartIndex = pairEndIndex + 1;
+    }
+    return addresses.toArray(new Address[addresses.size()]);
+  }
+
+  public static final Creator<Address> CREATOR =
+      new Creator<Address>() {
+        @Override
+        public Address createFromParcel(Parcel parcel) {
+          return new Address(parcel);
+        }
+
+        @Override
+        public Address[] newArray(int size) {
+          return new Address[size];
+        }
+      };
+
+  public Address(Parcel in) {
+    setPersonal(in.readString());
+    setAddress(in.readString());
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel out, int flags) {
+    out.writeString(mPersonal);
+    out.writeString(mAddress);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/AuthenticationFailedException.java b/java/com/android/voicemail/impl/mail/AuthenticationFailedException.java
new file mode 100644
index 0000000..c9fa087
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/AuthenticationFailedException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.voicemail.impl.mail;
+
+public class AuthenticationFailedException extends MessagingException {
+  public static final long serialVersionUID = -1;
+
+  public AuthenticationFailedException(String message) {
+    super(MessagingException.AUTHENTICATION_FAILED, message);
+  }
+
+  public AuthenticationFailedException(int exceptionType, String message) {
+    super(exceptionType, message);
+  }
+
+  public AuthenticationFailedException(String message, Throwable throwable) {
+    super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Base64Body.java b/java/com/android/voicemail/impl/mail/Base64Body.java
new file mode 100644
index 0000000..def94db
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Base64Body.java
@@ -0,0 +1,61 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+public class Base64Body implements Body {
+  private final InputStream mSource;
+  // Because we consume the input stream, we can only write out once
+  private boolean mAlreadyWritten;
+
+  public Base64Body(InputStream source) {
+    mSource = source;
+  }
+
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    return mSource;
+  }
+
+  /**
+   * This method consumes the input stream, so can only be called once
+   *
+   * @param out Stream to write to
+   * @throws IllegalStateException If called more than once
+   * @throws IOException
+   * @throws MessagingException
+   */
+  @Override
+  public void writeTo(OutputStream out)
+      throws IllegalStateException, IOException, MessagingException {
+    if (mAlreadyWritten) {
+      throw new IllegalStateException("Base64Body can only be written once");
+    }
+    mAlreadyWritten = true;
+    try {
+      final Base64OutputStream b64out = new Base64OutputStream(out, Base64.DEFAULT);
+      IOUtils.copyLarge(mSource, b64out);
+    } finally {
+      mSource.close();
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Body.java b/java/com/android/voicemail/impl/mail/Body.java
new file mode 100644
index 0000000..3ad81bc
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Body.java
@@ -0,0 +1,26 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public interface Body {
+  public InputStream getInputStream() throws MessagingException;
+
+  public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/java/com/android/voicemail/impl/mail/BodyPart.java b/java/com/android/voicemail/impl/mail/BodyPart.java
new file mode 100644
index 0000000..3d15d4b
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/BodyPart.java
@@ -0,0 +1,24 @@
+/*
+ * 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.voicemail.impl.mail;
+
+public abstract class BodyPart implements Part {
+  protected Multipart mParent;
+
+  public Multipart getParent() {
+    return mParent;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/CertificateValidationException.java b/java/com/android/voicemail/impl/mail/CertificateValidationException.java
new file mode 100644
index 0000000..6f3bb2f
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/CertificateValidationException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.voicemail.impl.mail;
+
+public class CertificateValidationException extends MessagingException {
+  public static final long serialVersionUID = -1;
+
+  public CertificateValidationException(String message) {
+    super(CERTIFICATE_VALIDATION_ERROR, message);
+  }
+
+  public CertificateValidationException(String message, Throwable throwable) {
+    super(CERTIFICATE_VALIDATION_ERROR, message, throwable);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/FetchProfile.java b/java/com/android/voicemail/impl/mail/FetchProfile.java
new file mode 100644
index 0000000..28a7080
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/FetchProfile.java
@@ -0,0 +1,79 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import java.util.ArrayList;
+
+/**
+ *
+ *
+ * <pre>
+ * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
+ * FetchProfile can contain the following objects:
+ *      FetchProfile.Item:      Described below.
+ *      Message:                Indicates that the body of the entire message should be fetched.
+ *                              Synonymous with FetchProfile.Item.BODY.
+ *      Part:                   Indicates that the given Part should be fetched. The provider
+ *                              is expected have previously created the given BodyPart and stored
+ *                              any information it needs to download the content.
+ * </pre>
+ */
+public class FetchProfile extends ArrayList<Fetchable> {
+  /**
+   * Default items available for pre-fetching. It should be expected that any item fetched by using
+   * these items could potentially include all of the previous items.
+   */
+  public enum Item implements Fetchable {
+    /** Download the flags of the message. */
+    FLAGS,
+
+    /**
+     * Download the envelope of the message. This should include at minimum the size and the
+     * following headers: date, subject, from, content-type, to, cc
+     */
+    ENVELOPE,
+
+    /**
+     * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE and may map
+     * to other providers. The provider should, if possible, fill in a properly formatted MIME
+     * structure in the message without actually downloading any message data. If the provider is
+     * not capable of this operation it should specifically set the body of the message to null so
+     * that upper levels can detect that a full body download is needed.
+     */
+    STRUCTURE,
+
+    /**
+     * A sane portion of the entire message, cut off at a provider determined limit. This should
+     * generally be around 50kB.
+     */
+    BODY_SANE,
+
+    /** The entire message. */
+    BODY,
+  }
+
+  /**
+   * @return the first {@link Part} in this collection, or null if it doesn't contain {@link Part}.
+   */
+  public Part getFirstPart() {
+    for (Fetchable o : this) {
+      if (o instanceof Part) {
+        return (Part) o;
+      }
+    }
+    return null;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Fetchable.java b/java/com/android/voicemail/impl/mail/Fetchable.java
new file mode 100644
index 0000000..237ef69
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Fetchable.java
@@ -0,0 +1,22 @@
+/*
+ * 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.voicemail.impl.mail;
+
+/**
+ * Interface for classes that can be added to {@link FetchProfile}. i.e. {@link Part} and its
+ * subclasses, and {@link FetchProfile.Item}.
+ */
+public interface Fetchable {}
diff --git a/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java b/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java
new file mode 100644
index 0000000..bd3c164
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/FixedLengthInputStream.java
@@ -0,0 +1,79 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that stops allowing reads after the given length has been read. This is
+ * used to allow a client to read directly from an underlying protocol stream without reading past
+ * where the protocol handler intended the client to read.
+ */
+public class FixedLengthInputStream extends InputStream {
+  private final InputStream mIn;
+  private final int mLength;
+  private int mCount;
+
+  public FixedLengthInputStream(InputStream in, int length) {
+    this.mIn = in;
+    this.mLength = length;
+  }
+
+  @Override
+  public int available() throws IOException {
+    return mLength - mCount;
+  }
+
+  @Override
+  public int read() throws IOException {
+    if (mCount < mLength) {
+      mCount++;
+      return mIn.read();
+    } else {
+      return -1;
+    }
+  }
+
+  @Override
+  public int read(byte[] b, int offset, int length) throws IOException {
+    if (mCount < mLength) {
+      int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
+      if (d == -1) {
+        return -1;
+      } else {
+        mCount += d;
+        return d;
+      }
+    } else {
+      return -1;
+    }
+  }
+
+  @Override
+  public int read(byte[] b) throws IOException {
+    return read(b, 0, b.length);
+  }
+
+  public int getLength() {
+    return mLength;
+  }
+
+  @Override
+  public String toString() {
+    return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Flag.java b/java/com/android/voicemail/impl/mail/Flag.java
new file mode 100644
index 0000000..72b5c1f
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Flag.java
@@ -0,0 +1,27 @@
+/*
+ * 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.voicemail.impl.mail;
+
+/** Flags that can be applied to Messages. */
+public class Flag {
+  // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
+  public static final String DELETED = "deleted";
+  public static final String SEEN = "seen";
+  public static final String ANSWERED = "answered";
+  public static final String FLAGGED = "flagged";
+  public static final String DRAFT = "draft";
+  public static final String RECENT = "recent";
+}
diff --git a/java/com/android/voicemail/impl/mail/MailTransport.java b/java/com/android/voicemail/impl/mail/MailTransport.java
new file mode 100644
index 0000000..3df36d5
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MailTransport.java
@@ -0,0 +1,343 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import android.content.Context;
+import android.net.Network;
+import android.support.annotation.VisibleForTesting;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+/** Make connection and perform operations on mail server by reading and writing lines. */
+public class MailTransport {
+  private static final String TAG = "MailTransport";
+
+  // TODO protected eventually
+  /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
+  /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
+
+  private static final HostnameVerifier HOSTNAME_VERIFIER =
+      HttpsURLConnection.getDefaultHostnameVerifier();
+
+  private final Context mContext;
+  private final ImapHelper mImapHelper;
+  private final Network mNetwork;
+  private final String mHost;
+  private final int mPort;
+  private Socket mSocket;
+  private BufferedInputStream mIn;
+  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) {
+    mContext = context;
+    mImapHelper = imapHelper;
+    mNetwork = network;
+    mHost = address;
+    mPort = port;
+    mFlags = flags;
+  }
+
+  /**
+   * Returns a new transport, using the current transport as a model. The new transport is
+   * configured identically, but not opened or connected in any way.
+   */
+  @Override
+  public MailTransport clone() {
+    return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags);
+  }
+
+  public boolean canTrySslSecurity() {
+    return (mFlags & ImapStore.FLAG_SSL) != 0;
+  }
+
+  public boolean canTrustAllCertificates() {
+    return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
+  }
+
+  /**
+   * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt an
+   * SSL connection if indicated.
+   */
+  public void open() throws MessagingException {
+    LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
+
+    List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>();
+
+    if (mNetwork == null) {
+      socketAddresses.add(new InetSocketAddress(mHost, mPort));
+    } else {
+      try {
+        InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
+        if (inetAddresses.length == 0) {
+          throw new MessagingException(
+              MessagingException.IOERROR,
+              "Host name " + mHost + "cannot be resolved on designated network");
+        }
+        for (int i = 0; i < inetAddresses.length; i++) {
+          socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort));
+        }
+      } catch (IOException ioe) {
+        LogUtils.d(TAG, ioe.toString());
+        mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK);
+        throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+      }
+    }
+
+    boolean success = false;
+    while (socketAddresses.size() > 0) {
+      mSocket = createSocket();
+      try {
+        mAddress = socketAddresses.remove(0);
+        mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT);
+
+        if (canTrySslSecurity()) {
+          /*
+          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
+           */
+          reopenTls();
+        } else {
+          mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+          mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+          mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+        }
+        success = true;
+        return;
+      } catch (IOException ioe) {
+        LogUtils.d(TAG, ioe.toString());
+        if (socketAddresses.size() == 0) {
+          // Only throw an error when there are no more sockets to try.
+          mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED);
+          throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+        }
+      } finally {
+        if (!success) {
+          try {
+            mSocket.close();
+            mSocket = null;
+          } catch (IOException ioe) {
+            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+          }
+        }
+      }
+    }
+  }
+
+  // For testing. We need something that can replace the behavior of "new Socket()"
+  @VisibleForTesting
+  interface SocketCreator {
+
+    Socket createSocket() throws MessagingException;
+  }
+
+  @VisibleForTesting
+  void setSocketCreator(SocketCreator creator) {
+    mSocketCreator = creator;
+  }
+
+  protected Socket createSocket() throws MessagingException {
+    if (mSocketCreator != null) {
+      return mSocketCreator.createSocket();
+    }
+
+    if (mNetwork == null) {
+      LogUtils.v(TAG, "createSocket: network not specified");
+      return new Socket();
+    }
+
+    try {
+      LogUtils.v(TAG, "createSocket: network specified");
+      return mNetwork.getSocketFactory().createSocket();
+    } catch (IOException ioe) {
+      LogUtils.d(TAG, ioe.toString());
+      throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+    }
+  }
+
+  /** 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.
+   *
+   * <p>Verify the hostname of the certificate used by the other end of a connected socket. It is
+   * harmless to call this method redundantly if the hostname has already been verified.
+   *
+   * <p>Wildcard certificates are allowed to verify any matching hostname, so "foo.bar.example.com"
+   * is verified if the peer has a certificate for "*.example.com".
+   *
+   * @param socket An SSL socket which has been connected to a server
+   * @param hostname The expected hostname of the remote server
+   * @throws IOException if something goes wrong handshaking with the server
+   * @throws SSLPeerUnverifiedException if the server cannot prove its identity
+   */
+  private void verifyHostname(Socket socket, String hostname) throws IOException {
+    // The code at the start of OpenSSLSocketImpl.startHandshake()
+    // ensures that the call is idempotent, so we can safely call it.
+    SSLSocket ssl = (SSLSocket) socket;
+    ssl.startHandshake();
+
+    SSLSession session = ssl.getSession();
+    if (session == null) {
+      mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION);
+      throw new SSLException("Cannot verify SSL socket without session");
+    }
+    // TODO: Instead of reporting the name of the server we think we're connecting to,
+    // we should be reporting the bad name in the certificate.  Unfortunately this is buried
+    // in the verifier code and is not available in the verifier API, and extracting the
+    // CN & alts is beyond the scope of this patch.
+    if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
+      mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME);
+      throw new SSLPeerUnverifiedException(
+          "Certificate hostname not useable for server: " + session.getPeerPrincipal());
+    }
+  }
+
+  public boolean isOpen() {
+    return (mIn != null
+        && mOut != null
+        && mSocket != null
+        && mSocket.isConnected()
+        && !mSocket.isClosed());
+  }
+
+  /** Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. */
+  public void close() {
+    try {
+      mIn.close();
+    } catch (Exception e) {
+      // May fail if the connection is already closed.
+    }
+    try {
+      mOut.close();
+    } catch (Exception e) {
+      // May fail if the connection is already closed.
+    }
+    try {
+      mSocket.close();
+    } catch (Exception e) {
+      // May fail if the connection is already closed.
+    }
+    mIn = null;
+    mOut = null;
+    mSocket = null;
+  }
+
+  public String getHost() {
+    return mHost;
+  }
+
+  public InputStream getInputStream() {
+    return mIn;
+  }
+
+  public OutputStream getOutputStream() {
+    return mOut;
+  }
+
+  /** Writes a single line to the server using \r\n termination. */
+  public void writeLine(String s, String sensitiveReplacement) throws IOException {
+    if (sensitiveReplacement != null) {
+      LogUtils.d(TAG, ">>> " + sensitiveReplacement);
+    } else {
+      LogUtils.d(TAG, ">>> " + s);
+    }
+
+    OutputStream out = getOutputStream();
+    out.write(s.getBytes());
+    out.write('\r');
+    out.write('\n');
+    out.flush();
+  }
+
+  /**
+   * Reads a single line from the server, using either \r\n or \n as the delimiter. The delimiter
+   * char(s) are not included in the result.
+   */
+  public String readLine(boolean loggable) throws IOException {
+    StringBuffer sb = new StringBuffer();
+    InputStream in = getInputStream();
+    int d;
+    while ((d = in.read()) != -1) {
+      if (((char) d) == '\r') {
+        continue;
+      } else if (((char) d) == '\n') {
+        break;
+      } else {
+        sb.append((char) d);
+      }
+    }
+    if (d == -1) {
+      LogUtils.d(TAG, "End of stream reached while trying to read line.");
+    }
+    String ret = sb.toString();
+    if (loggable) {
+      LogUtils.d(TAG, "<<< " + ret);
+    }
+    return ret;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/MeetingInfo.java b/java/com/android/voicemail/impl/mail/MeetingInfo.java
new file mode 100644
index 0000000..9fe953d
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MeetingInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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.voicemail.impl.mail;
+
+public class MeetingInfo {
+  // Predefined tags; others can be added
+  public static final String MEETING_DTSTAMP = "DTSTAMP";
+  public static final String MEETING_UID = "UID";
+  public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
+  public static final String MEETING_DTSTART = "DTSTART";
+  public static final String MEETING_DTEND = "DTEND";
+  public static final String MEETING_TITLE = "TITLE";
+  public static final String MEETING_LOCATION = "LOC";
+  public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
+  public static final String MEETING_ALL_DAY = "ALLDAY";
+}
diff --git a/java/com/android/voicemail/impl/mail/Message.java b/java/com/android/voicemail/impl/mail/Message.java
new file mode 100644
index 0000000..aea5d3e
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Message.java
@@ -0,0 +1,146 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import android.support.annotation.VisibleForTesting;
+import java.util.Date;
+import java.util.HashSet;
+
+public abstract class Message implements Part, Body {
+  public static final Message[] EMPTY_ARRAY = new Message[0];
+
+  public static final String RECIPIENT_TYPE_TO = "to";
+  public static final String RECIPIENT_TYPE_CC = "cc";
+  public static final String RECIPIENT_TYPE_BCC = "bcc";
+
+  public enum RecipientType {
+    TO,
+    CC,
+    BCC,
+  }
+
+  protected String mUid;
+
+  private HashSet<String> mFlags = null;
+
+  protected Date mInternalDate;
+
+  public String getUid() {
+    return mUid;
+  }
+
+  public void setUid(String uid) {
+    this.mUid = uid;
+  }
+
+  public abstract String getSubject() throws MessagingException;
+
+  public abstract void setSubject(String subject) throws MessagingException;
+
+  public Date getInternalDate() {
+    return mInternalDate;
+  }
+
+  public void setInternalDate(Date internalDate) {
+    this.mInternalDate = internalDate;
+  }
+
+  public abstract Date getReceivedDate() throws MessagingException;
+
+  public abstract Date getSentDate() throws MessagingException;
+
+  public abstract void setSentDate(Date sentDate) throws MessagingException;
+
+  public abstract Address[] getRecipients(String type) throws MessagingException;
+
+  public abstract void setRecipients(String type, Address[] addresses) throws MessagingException;
+
+  public void setRecipient(String type, Address address) throws MessagingException {
+    setRecipients(type, new Address[] {address});
+  }
+
+  public abstract Address[] getFrom() throws MessagingException;
+
+  public abstract void setFrom(Address from) throws MessagingException;
+
+  public abstract Address[] getReplyTo() throws MessagingException;
+
+  public abstract void setReplyTo(Address[] from) throws MessagingException;
+
+  // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
+  public abstract void setMessageId(String messageId) throws MessagingException;
+
+  public abstract String getMessageId() throws MessagingException;
+
+  @Override
+  public boolean isMimeType(String mimeType) throws MessagingException {
+    return getContentType().startsWith(mimeType);
+  }
+
+  private HashSet<String> getFlagSet() {
+    if (mFlags == null) {
+      mFlags = new HashSet<String>();
+    }
+    return mFlags;
+  }
+
+  /*
+   * TODO Refactor Flags at some point to be able to store user defined flags.
+   */
+  public String[] getFlags() {
+    return getFlagSet().toArray(new String[] {});
+  }
+
+  /**
+   * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses. Only
+   * used for testing.
+   */
+  @VisibleForTesting
+  private final void setFlagDirectlyForTest(String flag, boolean set) throws MessagingException {
+    if (set) {
+      getFlagSet().add(flag);
+    } else {
+      getFlagSet().remove(flag);
+    }
+  }
+
+  public void setFlag(String flag, boolean set) throws MessagingException {
+    setFlagDirectlyForTest(flag, set);
+  }
+
+  /**
+   * This method calls setFlag(String, boolean)
+   *
+   * @param flags
+   * @param set
+   */
+  public void setFlags(String[] flags, boolean set) throws MessagingException {
+    for (String flag : flags) {
+      setFlag(flag, set);
+    }
+  }
+
+  public boolean isSet(String flag) {
+    return getFlagSet().contains(flag);
+  }
+
+  public abstract void saveChanges() throws MessagingException;
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + ':' + mUid;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/MessageDateComparator.java b/java/com/android/voicemail/impl/mail/MessageDateComparator.java
new file mode 100644
index 0000000..89231f6
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MessageDateComparator.java
@@ -0,0 +1,35 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import java.util.Comparator;
+
+public class MessageDateComparator implements Comparator<Message> {
+  @Override
+  public int compare(Message o1, Message o2) {
+    try {
+      if (o1.getSentDate() == null) {
+        return 1;
+      } else if (o2.getSentDate() == null) {
+        return -1;
+      } else {
+        return o2.getSentDate().compareTo(o1.getSentDate());
+      }
+    } catch (Exception e) {
+      return 0;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/MessagingException.java b/java/com/android/voicemail/impl/mail/MessagingException.java
new file mode 100644
index 0000000..c1e3051
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/MessagingException.java
@@ -0,0 +1,143 @@
+/*
+ * 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.voicemail.impl.mail;
+
+/**
+ * This exception is used for most types of failures that occur during server interactions.
+ *
+ * <p>Data passed through this exception should be considered non-localized. Any strings should
+ * either be internal-only (for debugging) or server-generated.
+ *
+ * <p>TO DO: Does it make sense to further collapse AuthenticationFailedException and
+ * CertificateValidationException and any others into this?
+ */
+public class MessagingException extends Exception {
+  public static final long serialVersionUID = -1;
+
+  public static final int NO_ERROR = -1;
+  /** Any exception that does not specify a specific issue */
+  public static final int UNSPECIFIED_EXCEPTION = 0;
+  /** Connection or IO errors */
+  public static final int IOERROR = 1;
+  /** The configuration requested TLS but the server did not support it. */
+  public static final int TLS_REQUIRED = 2;
+  /** Authentication is required but the server did not support it. */
+  public static final int AUTH_REQUIRED = 3;
+  /** General security failures */
+  public static final int GENERAL_SECURITY = 4;
+  /** Authentication failed */
+  public static final int AUTHENTICATION_FAILED = 5;
+  /** Attempt to create duplicate account */
+  public static final int DUPLICATE_ACCOUNT = 6;
+  /** Required security policies reported - advisory only */
+  public static final int SECURITY_POLICIES_REQUIRED = 7;
+  /** Required security policies not supported */
+  public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
+  /** The protocol (or protocol version) isn't supported */
+  public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
+  /** The server's SSL certificate couldn't be validated */
+  public static final int CERTIFICATE_VALIDATION_ERROR = 10;
+  /** Authentication failed during autodiscover */
+  public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
+  /** Autodiscover completed with a result (non-error) */
+  public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
+  /** Ambiguous failure; server error or bad credentials */
+  public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
+  /** The server refused access */
+  public static final int ACCESS_DENIED = 14;
+  /** The server refused access */
+  public static final int ATTACHMENT_NOT_FOUND = 15;
+  /** A client SSL certificate is required for connections to the server */
+  public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
+  /** The client SSL certificate specified is invalid */
+  public static final int CLIENT_CERTIFICATE_ERROR = 17;
+  /** The server indicates it does not support OAuth authentication */
+  public static final int OAUTH_NOT_SUPPORTED = 18;
+  /** The server indicates it experienced an internal error */
+  public static final int SERVER_ERROR = 19;
+
+  protected int mExceptionType;
+  // Exception type-specific data
+  protected Object mExceptionData;
+
+  public MessagingException(String message, Throwable throwable) {
+    this(UNSPECIFIED_EXCEPTION, message, throwable);
+  }
+
+  public MessagingException(int exceptionType, String message, Throwable throwable) {
+    super(message, throwable);
+    mExceptionType = exceptionType;
+    mExceptionData = null;
+  }
+
+  /**
+   * Constructs a MessagingException with an exceptionType and a null message.
+   *
+   * @param exceptionType The exception type to set for this exception.
+   */
+  public MessagingException(int exceptionType) {
+    this(exceptionType, null, null);
+  }
+
+  /**
+   * Constructs a MessagingException with a message.
+   *
+   * @param message the message for this exception
+   */
+  public MessagingException(String message) {
+    this(UNSPECIFIED_EXCEPTION, message, null);
+  }
+
+  /**
+   * Constructs a MessagingException with an exceptionType and a message.
+   *
+   * @param exceptionType The exception type to set for this exception.
+   */
+  public MessagingException(int exceptionType, String message) {
+    this(exceptionType, message, null);
+  }
+
+  /**
+   * Constructs a MessagingException with an exceptionType, a message, and data
+   *
+   * @param exceptionType The exception type to set for this exception.
+   * @param message the message for the exception (or null)
+   * @param data exception-type specific data for the exception (or null)
+   */
+  public MessagingException(int exceptionType, String message, Object data) {
+    super(message);
+    mExceptionType = exceptionType;
+    mExceptionData = data;
+  }
+
+  /**
+   * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set.
+   *
+   * @return Returns the exception type.
+   */
+  public int getExceptionType() {
+    return mExceptionType;
+  }
+  /**
+   * Return the exception data. Will be null if not explicitly set.
+   *
+   * @return Returns the exception data.
+   */
+  public Object getExceptionData() {
+    return mExceptionData;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Multipart.java b/java/com/android/voicemail/impl/mail/Multipart.java
new file mode 100644
index 0000000..e8d5046
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Multipart.java
@@ -0,0 +1,62 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import java.util.ArrayList;
+
+public abstract class Multipart implements Body {
+  protected Part mParent;
+
+  protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
+
+  protected String mContentType;
+
+  public void addBodyPart(BodyPart part) throws MessagingException {
+    mParts.add(part);
+  }
+
+  public void addBodyPart(BodyPart part, int index) throws MessagingException {
+    mParts.add(index, part);
+  }
+
+  public BodyPart getBodyPart(int index) throws MessagingException {
+    return mParts.get(index);
+  }
+
+  public String getContentType() throws MessagingException {
+    return mContentType;
+  }
+
+  public int getCount() throws MessagingException {
+    return mParts.size();
+  }
+
+  public boolean removeBodyPart(BodyPart part) throws MessagingException {
+    return mParts.remove(part);
+  }
+
+  public void removeBodyPart(int index) throws MessagingException {
+    mParts.remove(index);
+  }
+
+  public Part getParent() throws MessagingException {
+    return mParent;
+  }
+
+  public void setParent(Part parent) throws MessagingException {
+    this.mParent = parent;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/PackedString.java b/java/com/android/voicemail/impl/mail/PackedString.java
new file mode 100644
index 0000000..701dab6
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/PackedString.java
@@ -0,0 +1,172 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import android.util.ArrayMap;
+import java.util.Map;
+
+/**
+ * A utility class for creating and modifying Strings that are tagged and packed together.
+ *
+ * <p>Uses non-printable (control chars) for internal delimiters; Intended for regular displayable
+ * strings only, so please use base64 or other encoding if you need to hide any binary data here.
+ *
+ * <p>Binary compatible with Address.pack() format, which should migrate to use this code.
+ */
+public class PackedString {
+
+  /**
+   * Packing format is: element : [ value ] or [ value TAG-DELIMITER tag ] packed-string : [ element
+   * ] [ ELEMENT-DELIMITER [ element ] ]*
+   */
+  private static final char DELIMITER_ELEMENT = '\1';
+
+  private static final char DELIMITER_TAG = '\2';
+
+  private String mString;
+  private ArrayMap<String, String> mExploded;
+  private static final ArrayMap<String, String> EMPTY_MAP = new ArrayMap<String, String>();
+
+  /**
+   * Create a packed string using an already-packed string (e.g. from database)
+   *
+   * @param string packed string
+   */
+  public PackedString(String string) {
+    mString = string;
+    mExploded = null;
+  }
+
+  /**
+   * Get the value referred to by a given tag. If the tag does not exist, return null.
+   *
+   * @param tag identifier of string of interest
+   * @return returns value, or null if no string is found
+   */
+  public String get(String tag) {
+    if (mExploded == null) {
+      mExploded = explode(mString);
+    }
+    return mExploded.get(tag);
+  }
+
+  /**
+   * Return a map of all of the values referred to by a given tag. This is a shallow copy, don't
+   * edit the values.
+   *
+   * @return a map of the values in the packed string
+   */
+  public Map<String, String> unpack() {
+    if (mExploded == null) {
+      mExploded = explode(mString);
+    }
+    return new ArrayMap<String, String>(mExploded);
+  }
+
+  /** Read out all values into a map. */
+  private static ArrayMap<String, String> explode(String packed) {
+    if (packed == null || packed.length() == 0) {
+      return EMPTY_MAP;
+    }
+    ArrayMap<String, String> map = new ArrayMap<String, String>();
+
+    int length = packed.length();
+    int elementStartIndex = 0;
+    int elementEndIndex = 0;
+    int tagEndIndex = packed.indexOf(DELIMITER_TAG);
+
+    while (elementStartIndex < length) {
+      elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
+      if (elementEndIndex == -1) {
+        elementEndIndex = length;
+      }
+      String tag;
+      String value;
+      if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
+        // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
+        // so synthesize a positional tag for the value, and don't update tagEndIndex
+        value = packed.substring(elementStartIndex, elementEndIndex);
+        tag = Integer.toString(map.size());
+      } else {
+        value = packed.substring(elementStartIndex, tagEndIndex);
+        tag = packed.substring(tagEndIndex + 1, elementEndIndex);
+        // scan forward for next tag, if any
+        tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
+      }
+      map.put(tag, value);
+      elementStartIndex = elementEndIndex + 1;
+    }
+
+    return map;
+  }
+
+  /**
+   * Builder class for creating PackedString values. Can also be used for editing existing
+   * PackedString representations.
+   */
+  public static class Builder {
+    ArrayMap<String, String> mMap;
+
+    /** Create a builder that's empty (for filling) */
+    public Builder() {
+      mMap = new ArrayMap<String, String>();
+    }
+
+    /** Create a builder using the values of an existing PackedString (for editing). */
+    public Builder(String packed) {
+      mMap = explode(packed);
+    }
+
+    /**
+     * Add a tagged value
+     *
+     * @param tag identifier of string of interest
+     * @param value the value to record in this position. null to delete entry.
+     */
+    public void put(String tag, String value) {
+      if (value == null) {
+        mMap.remove(tag);
+      } else {
+        mMap.put(tag, value);
+      }
+    }
+
+    /**
+     * Get the value referred to by a given tag. If the tag does not exist, return null.
+     *
+     * @param tag identifier of string of interest
+     * @return returns value, or null if no string is found
+     */
+    public String get(String tag) {
+      return mMap.get(tag);
+    }
+
+    /** Pack the values and return a single, encoded string */
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+      for (Map.Entry<String, String> entry : mMap.entrySet()) {
+        if (sb.length() > 0) {
+          sb.append(DELIMITER_ELEMENT);
+        }
+        sb.append(entry.getValue());
+        sb.append(DELIMITER_TAG);
+        sb.append(entry.getKey());
+      }
+      return sb.toString();
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/Part.java b/java/com/android/voicemail/impl/mail/Part.java
new file mode 100644
index 0000000..3be5c57
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/Part.java
@@ -0,0 +1,51 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface Part extends Fetchable {
+  public void addHeader(String name, String value) throws MessagingException;
+
+  public void removeHeader(String name) throws MessagingException;
+
+  public void setHeader(String name, String value) throws MessagingException;
+
+  public Body getBody() throws MessagingException;
+
+  public String getContentType() throws MessagingException;
+
+  public String getDisposition() throws MessagingException;
+
+  public String getContentId() throws MessagingException;
+
+  public String[] getHeader(String name) throws MessagingException;
+
+  public void setExtendedHeader(String name, String value) throws MessagingException;
+
+  public String getExtendedHeader(String name) throws MessagingException;
+
+  public int getSize() throws MessagingException;
+
+  public boolean isMimeType(String mimeType) throws MessagingException;
+
+  public String getMimeType() throws MessagingException;
+
+  public void setBody(Body body) throws MessagingException;
+
+  public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/java/com/android/voicemail/impl/mail/PeekableInputStream.java b/java/com/android/voicemail/impl/mail/PeekableInputStream.java
new file mode 100644
index 0000000..08f867f
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/PeekableInputStream.java
@@ -0,0 +1,81 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that allows single byte "peeks" without consuming the byte. The client of
+ * this stream can call peek() to see the next available byte in the stream and a subsequent read
+ * will still return the peeked byte.
+ */
+public class PeekableInputStream extends InputStream {
+  private final InputStream mIn;
+  private boolean mPeeked;
+  private int mPeekedByte;
+
+  public PeekableInputStream(InputStream in) {
+    this.mIn = in;
+  }
+
+  @Override
+  public int read() throws IOException {
+    if (!mPeeked) {
+      return mIn.read();
+    } else {
+      mPeeked = false;
+      return mPeekedByte;
+    }
+  }
+
+  public int peek() throws IOException {
+    if (!mPeeked) {
+      mPeekedByte = read();
+      mPeeked = true;
+    }
+    return mPeekedByte;
+  }
+
+  @Override
+  public int read(byte[] b, int offset, int length) throws IOException {
+    if (!mPeeked) {
+      return mIn.read(b, offset, length);
+    } else {
+      b[0] = (byte) mPeekedByte;
+      mPeeked = false;
+      int r = mIn.read(b, offset + 1, length - 1);
+      if (r == -1) {
+        return 1;
+      } else {
+        return r + 1;
+      }
+    }
+  }
+
+  @Override
+  public int read(byte[] b) throws IOException {
+    return read(b, 0, b.length);
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
+        mIn.toString(), mPeeked, mPeekedByte);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/TempDirectory.java b/java/com/android/voicemail/impl/mail/TempDirectory.java
new file mode 100644
index 0000000..42adbeb
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/TempDirectory.java
@@ -0,0 +1,40 @@
+/*
+ * 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.voicemail.impl.mail;
+
+import android.content.Context;
+import java.io.File;
+
+/**
+ * TempDirectory caches the directory used for caching file. It is set up during application
+ * initialization.
+ */
+public class TempDirectory {
+  private static File sTempDirectory = null;
+
+  public static void setTempDirectory(Context context) {
+    sTempDirectory = context.getCacheDir();
+  }
+
+  public static File getTempDirectory() {
+    if (sTempDirectory == null) {
+      throw new RuntimeException(
+          "TempDirectory not set.  "
+              + "If in a unit test, call Email.setTempDirectory(context) in setUp().");
+    }
+    return sTempDirectory;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java b/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java
new file mode 100644
index 0000000..753b70f
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/BinaryTempFileBody.java
@@ -0,0 +1,87 @@
+/*
+ * 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.voicemail.impl.mail.internet;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.TempDirectory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows the
+ * user to write to the temp file. After the write the body is available via getInputStream and
+ * writeTo one time. After writeTo is called, or the InputStream returned from getInputStream is
+ * closed the file is deleted and the Body should be considered disposed of.
+ */
+public class BinaryTempFileBody implements Body {
+  private File mFile;
+
+  /**
+   * An alternate way to put data into a BinaryTempFileBody is to simply supply an already- created
+   * file. Note that this file will be deleted after it is read.
+   *
+   * @param filePath The file containing the data to be stored on disk temporarily
+   */
+  public void setFile(String filePath) {
+    mFile = new File(filePath);
+  }
+
+  public OutputStream getOutputStream() throws IOException {
+    mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
+    mFile.deleteOnExit();
+    return new FileOutputStream(mFile);
+  }
+
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    try {
+      return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+    } catch (IOException ioe) {
+      throw new MessagingException("Unable to open body", ioe);
+    }
+  }
+
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    InputStream in = getInputStream();
+    Base64OutputStream base64Out = new Base64OutputStream(out, Base64.CRLF | Base64.NO_CLOSE);
+    IOUtils.copy(in, base64Out);
+    base64Out.close();
+    mFile.delete();
+    in.close();
+  }
+
+  class BinaryTempFileBodyInputStream extends FilterInputStream {
+    public BinaryTempFileBodyInputStream(InputStream in) {
+      super(in);
+    }
+
+    @Override
+    public void close() throws IOException {
+      super.close();
+      mFile.delete();
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java b/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java
new file mode 100644
index 0000000..2add76c
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeBodyPart.java
@@ -0,0 +1,200 @@
+/*
+ * 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.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
+
+/** TODO this is a close approximation of Message, need to update along with Message. */
+public class MimeBodyPart extends BodyPart {
+  protected MimeHeader mHeader = new MimeHeader();
+  protected MimeHeader mExtendedHeader;
+  protected Body mBody;
+  protected int mSize;
+
+  // regex that matches content id surrounded by "<>" optionally.
+  private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+  // regex that matches end of line.
+  private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+  public MimeBodyPart() throws MessagingException {
+    this(null);
+  }
+
+  public MimeBodyPart(Body body) throws MessagingException {
+    this(body, null);
+  }
+
+  public MimeBodyPart(Body body, String mimeType) throws MessagingException {
+    if (mimeType != null) {
+      setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
+    }
+    setBody(body);
+  }
+
+  protected String getFirstHeader(String name) throws MessagingException {
+    return mHeader.getFirstHeader(name);
+  }
+
+  @Override
+  public void addHeader(String name, String value) throws MessagingException {
+    mHeader.addHeader(name, value);
+  }
+
+  @Override
+  public void setHeader(String name, String value) throws MessagingException {
+    mHeader.setHeader(name, value);
+  }
+
+  @Override
+  public String[] getHeader(String name) throws MessagingException {
+    return mHeader.getHeader(name);
+  }
+
+  @Override
+  public void removeHeader(String name) throws MessagingException {
+    mHeader.removeHeader(name);
+  }
+
+  @Override
+  public Body getBody() throws MessagingException {
+    return mBody;
+  }
+
+  @Override
+  public void setBody(Body body) throws MessagingException {
+    this.mBody = body;
+    if (body instanceof Multipart) {
+      Multipart multipart =
+          ((Multipart) body);
+      multipart.setParent(this);
+      setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+    } else if (body instanceof TextBody) {
+      String contentType = String.format("%s;\n charset=utf-8", getMimeType());
+      String name = MimeUtility.getHeaderParameter(getContentType(), "name");
+      if (name != null) {
+        contentType += String.format(";\n name=\"%s\"", name);
+      }
+      setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+      setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+    }
+  }
+
+  @Override
+  public String getContentType() throws MessagingException {
+    String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+    if (contentType == null) {
+      return "text/plain";
+    } else {
+      return contentType;
+    }
+  }
+
+  @Override
+  public String getDisposition() throws MessagingException {
+    String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+    if (contentDisposition == null) {
+      return null;
+    } else {
+      return contentDisposition;
+    }
+  }
+
+  @Override
+  public String getContentId() throws MessagingException {
+    String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+    if (contentId == null) {
+      return null;
+    } else {
+      // remove optionally surrounding brackets.
+      return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+    }
+  }
+
+  @Override
+  public String getMimeType() throws MessagingException {
+    return MimeUtility.getHeaderParameter(getContentType(), null);
+  }
+
+  @Override
+  public boolean isMimeType(String mimeType) throws MessagingException {
+    return getMimeType().equals(mimeType);
+  }
+
+  public void setSize(int size) {
+    this.mSize = size;
+  }
+
+  @Override
+  public int getSize() throws MessagingException {
+    return mSize;
+  }
+
+  /**
+   * Set extended header
+   *
+   * @param name Extended header name
+   * @param value header value - flattened by removing CR-NL if any remove header if value is null
+   * @throws MessagingException
+   */
+  @Override
+  public void setExtendedHeader(String name, String value) throws MessagingException {
+    if (value == null) {
+      if (mExtendedHeader != null) {
+        mExtendedHeader.removeHeader(name);
+      }
+      return;
+    }
+    if (mExtendedHeader == null) {
+      mExtendedHeader = new MimeHeader();
+    }
+    mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+  }
+
+  /**
+   * Get extended header
+   *
+   * @param name Extended header name
+   * @return header value - null if header does not exist
+   * @throws MessagingException
+   */
+  @Override
+  public String getExtendedHeader(String name) throws MessagingException {
+    if (mExtendedHeader == null) {
+      return null;
+    }
+    return mExtendedHeader.getFirstHeader(name);
+  }
+
+  /** Write the MimeMessage out in MIME format. */
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+    mHeader.writeTo(out);
+    writer.write("\r\n");
+    writer.flush();
+    if (mBody != null) {
+      mBody.writeTo(out);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeHeader.java b/java/com/android/voicemail/impl/mail/internet/MimeHeader.java
new file mode 100644
index 0000000..d41cdb3
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeHeader.java
@@ -0,0 +1,158 @@
+/*
+ * 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.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.MessagingException;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+public class MimeHeader {
+  /**
+   * Application specific header that contains Store specific information about an attachment. In
+   * IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later retrieve the
+   * attachment at will from the server. The info is recorded from this header on
+   * LocalStore.appendMessage and is put back into the MIME data by LocalStore.fetch.
+   */
+  public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA =
+      "X-Android-Attachment-StoreData";
+
+  public static final String HEADER_CONTENT_TYPE = "Content-Type";
+  public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+  public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+  public static final String HEADER_CONTENT_ID = "Content-ID";
+
+  /** Fields that should be omitted when writing the header using writeTo() */
+  private static final String[] WRITE_OMIT_FIELDS = {
+    //        HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
+    //        HEADER_ANDROID_ATTACHMENT_ID,
+    HEADER_ANDROID_ATTACHMENT_STORE_DATA
+  };
+
+  protected final ArrayList<Field> mFields = new ArrayList<Field>();
+
+  public void clear() {
+    mFields.clear();
+  }
+
+  public String getFirstHeader(String name) throws MessagingException {
+    String[] header = getHeader(name);
+    if (header == null) {
+      return null;
+    }
+    return header[0];
+  }
+
+  public void addHeader(String name, String value) throws MessagingException {
+    mFields.add(new Field(name, value));
+  }
+
+  public void setHeader(String name, String value) throws MessagingException {
+    if (name == null || value == null) {
+      return;
+    }
+    removeHeader(name);
+    addHeader(name, value);
+  }
+
+  public String[] getHeader(String name) throws MessagingException {
+    ArrayList<String> values = new ArrayList<String>();
+    for (Field field : mFields) {
+      if (field.name.equalsIgnoreCase(name)) {
+        values.add(field.value);
+      }
+    }
+    if (values.size() == 0) {
+      return null;
+    }
+    return values.toArray(new String[] {});
+  }
+
+  public void removeHeader(String name) throws MessagingException {
+    ArrayList<Field> removeFields = new ArrayList<Field>();
+    for (Field field : mFields) {
+      if (field.name.equalsIgnoreCase(name)) {
+        removeFields.add(field);
+      }
+    }
+    mFields.removeAll(removeFields);
+  }
+
+  /**
+   * Write header into String
+   *
+   * @return CR-NL separated header string except the headers in writeOmitFields null if header is
+   *     empty
+   */
+  public String writeToString() {
+    if (mFields.size() == 0) {
+      return null;
+    }
+    StringBuilder builder = new StringBuilder();
+    for (Field field : mFields) {
+      if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+        builder.append(field.name + ": " + field.value + "\r\n");
+      }
+    }
+    return builder.toString();
+  }
+
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+    for (Field field : mFields) {
+      if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+        writer.write(field.name + ": " + field.value + "\r\n");
+      }
+    }
+    writer.flush();
+  }
+
+  private static class Field {
+    final String name;
+    final String value;
+
+    public Field(String name, String value) {
+      this.name = name;
+      this.value = value;
+    }
+
+    @Override
+    public String toString() {
+      return name + "=" + value;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return (mFields == null) ? null : mFields.toString();
+  }
+
+  public static final boolean arrayContains(Object[] a, Object o) {
+    int index = arrayIndex(a, o);
+    return (index >= 0);
+  }
+
+  public static final int arrayIndex(Object[] a, Object o) {
+    for (int i = 0, count = a.length; i < count; i++) {
+      if (a[i].equals(o)) {
+        return i;
+      }
+    }
+    return -1;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeMessage.java b/java/com/android/voicemail/impl/mail/internet/MimeMessage.java
new file mode 100644
index 0000000..dfb7d7c
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeMessage.java
@@ -0,0 +1,676 @@
+/*
+ * 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.voicemail.impl.mail.internet;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.mail.Address;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.Part;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.regex.Pattern;
+import org.apache.james.mime4j.BodyDescriptor;
+import org.apache.james.mime4j.ContentHandler;
+import org.apache.james.mime4j.EOLConvertingInputStream;
+import org.apache.james.mime4j.MimeStreamParser;
+import org.apache.james.mime4j.field.DateTimeField;
+import org.apache.james.mime4j.field.Field;
+
+/**
+ * An implementation of Message that stores all of its metadata in RFC 822 and RFC 2045 style
+ * headers.
+ *
+ * <p>NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed.
+ * It would be better to simply do it explicitly on local creation of new outgoing messages.
+ */
+public class MimeMessage extends Message {
+  private MimeHeader mHeader;
+  private MimeHeader mExtendedHeader;
+
+  // NOTE:  The fields here are transcribed out of headers, and values stored here will supersede
+  // the values found in the headers.  Use caution to prevent any out-of-phase errors.  In
+  // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
+  private Address[] mFrom;
+  private Address[] mTo;
+  private Address[] mCc;
+  private Address[] mBcc;
+  private Address[] mReplyTo;
+  private Date mSentDate;
+  private Body mBody;
+  protected int mSize;
+  private boolean mInhibitLocalMessageId = false;
+  private boolean mComplete = true;
+
+  // Shared random source for generating local message-id values
+  private static final java.util.Random sRandom = new java.util.Random();
+
+  // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+  // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+  // This conversion is used when generating outgoing MIME messages. Incoming MIME date
+  // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
+  // localization code.
+  private static final SimpleDateFormat DATE_FORMAT =
+      new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+  // regex that matches content id surrounded by "<>" optionally.
+  private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+  // regex that matches end of line.
+  private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+  public MimeMessage() {
+    mHeader = null;
+  }
+
+  /**
+   * Generate a local message id. This is only used when none has been assigned, and is installed
+   * lazily. Any remote (typically server-assigned) message id takes precedence.
+   *
+   * @return a long, locally-generated message-ID value
+   */
+  private static String generateMessageId() {
+    final StringBuilder sb = new StringBuilder();
+    sb.append("<");
+    for (int i = 0; i < 24; i++) {
+      // We'll use a 5-bit range (0..31)
+      final int value = sRandom.nextInt() & 31;
+      final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
+      sb.append(c);
+    }
+    sb.append(".");
+    sb.append(Long.toString(System.currentTimeMillis()));
+    sb.append("@email.android.com>");
+    return sb.toString();
+  }
+
+  /**
+   * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
+   *
+   * @param in InputStream providing message content
+   * @throws IOException
+   * @throws MessagingException
+   */
+  public MimeMessage(InputStream in) throws IOException, MessagingException {
+    parse(in);
+  }
+
+  private MimeStreamParser init() {
+    // Before parsing the input stream, clear all local fields that may be superceded by
+    // the new incoming message.
+    getMimeHeaders().clear();
+    mInhibitLocalMessageId = true;
+    mFrom = null;
+    mTo = null;
+    mCc = null;
+    mBcc = null;
+    mReplyTo = null;
+    mSentDate = null;
+    mBody = null;
+
+    final MimeStreamParser parser = new MimeStreamParser();
+    parser.setContentHandler(new MimeMessageBuilder());
+    return parser;
+  }
+
+  protected void parse(InputStream in) throws IOException, MessagingException {
+    final MimeStreamParser parser = init();
+    parser.parse(new EOLConvertingInputStream(in));
+    mComplete = !parser.getPrematureEof();
+  }
+
+  public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
+      throws IOException, MessagingException {
+    final MimeStreamParser parser = init();
+    parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
+    mComplete = !parser.getPrematureEof();
+  }
+
+  /**
+   * Return the internal mHeader value, with very lazy initialization. The goal is to save memory by
+   * not creating the headers until needed.
+   */
+  private MimeHeader getMimeHeaders() {
+    if (mHeader == null) {
+      mHeader = new MimeHeader();
+    }
+    return mHeader;
+  }
+
+  @Override
+  public Date getReceivedDate() throws MessagingException {
+    return null;
+  }
+
+  @Override
+  public Date getSentDate() throws MessagingException {
+    if (mSentDate == null) {
+      try {
+        DateTimeField field =
+            (DateTimeField)
+                Field.parse("Date: " + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
+        mSentDate = field.getDate();
+        // TODO: We should make it more clear what exceptions can be thrown here,
+        // and whether they reflect a normal or error condition.
+      } catch (Exception e) {
+        LogUtils.v(LogUtils.TAG, "Message missing Date header");
+      }
+    }
+    if (mSentDate == null) {
+      // If we still don't have a date, fall back to "Delivery-date"
+      try {
+        DateTimeField field =
+            (DateTimeField)
+                Field.parse(
+                    "Date: " + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
+        mSentDate = field.getDate();
+        // TODO: We should make it more clear what exceptions can be thrown here,
+        // and whether they reflect a normal or error condition.
+      } catch (Exception e) {
+        LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
+      }
+    }
+    return mSentDate;
+  }
+
+  @Override
+  public void setSentDate(Date sentDate) throws MessagingException {
+    setHeader("Date", DATE_FORMAT.format(sentDate));
+    this.mSentDate = sentDate;
+  }
+
+  @Override
+  public String getContentType() throws MessagingException {
+    final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+    if (contentType == null) {
+      return "text/plain";
+    } else {
+      return contentType;
+    }
+  }
+
+  @Override
+  public String getDisposition() throws MessagingException {
+    return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+  }
+
+  @Override
+  public String getContentId() throws MessagingException {
+    final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+    if (contentId == null) {
+      return null;
+    } else {
+      // remove optionally surrounding brackets.
+      return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+    }
+  }
+
+  public boolean isComplete() {
+    return mComplete;
+  }
+
+  @Override
+  public String getMimeType() throws MessagingException {
+    return MimeUtility.getHeaderParameter(getContentType(), null);
+  }
+
+  @Override
+  public int getSize() throws MessagingException {
+    return mSize;
+  }
+
+  /**
+   * Returns a list of the given recipient type from this message. If no addresses are found the
+   * method returns an empty array.
+   */
+  @Override
+  public Address[] getRecipients(String type) throws MessagingException {
+    if (type == RECIPIENT_TYPE_TO) {
+      if (mTo == null) {
+        mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+      }
+      return mTo;
+    } else if (type == RECIPIENT_TYPE_CC) {
+      if (mCc == null) {
+        mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+      }
+      return mCc;
+    } else if (type == RECIPIENT_TYPE_BCC) {
+      if (mBcc == null) {
+        mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+      }
+      return mBcc;
+    } else {
+      throw new MessagingException("Unrecognized recipient type.");
+    }
+  }
+
+  @Override
+  public void setRecipients(String type, Address[] addresses) throws MessagingException {
+    final int toLength = 4; // "To: "
+    final int ccLength = 4; // "Cc: "
+    final int bccLength = 5; // "Bcc: "
+    if (type == RECIPIENT_TYPE_TO) {
+      if (addresses == null || addresses.length == 0) {
+        removeHeader("To");
+        this.mTo = null;
+      } else {
+        setHeader("To", MimeUtility.fold(Address.toHeader(addresses), toLength));
+        this.mTo = addresses;
+      }
+    } else if (type == RECIPIENT_TYPE_CC) {
+      if (addresses == null || addresses.length == 0) {
+        removeHeader("CC");
+        this.mCc = null;
+      } else {
+        setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), ccLength));
+        this.mCc = addresses;
+      }
+    } else if (type == RECIPIENT_TYPE_BCC) {
+      if (addresses == null || addresses.length == 0) {
+        removeHeader("BCC");
+        this.mBcc = null;
+      } else {
+        setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), bccLength));
+        this.mBcc = addresses;
+      }
+    } else {
+      throw new MessagingException("Unrecognized recipient type.");
+    }
+  }
+
+  /** Returns the unfolded, decoded value of the Subject header. */
+  @Override
+  public String getSubject() throws MessagingException {
+    return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
+  }
+
+  @Override
+  public void setSubject(String subject) throws MessagingException {
+    final int headerNameLength = 9; // "Subject: "
+    setHeader("Subject", MimeUtility.foldAndEncode2(subject, headerNameLength));
+  }
+
+  @Override
+  public Address[] getFrom() throws MessagingException {
+    if (mFrom == null) {
+      String list = MimeUtility.unfold(getFirstHeader("From"));
+      if (list == null || list.length() == 0) {
+        list = MimeUtility.unfold(getFirstHeader("Sender"));
+      }
+      mFrom = Address.parse(list);
+    }
+    return mFrom;
+  }
+
+  @Override
+  public void setFrom(Address from) throws MessagingException {
+    final int fromLength = 6; // "From: "
+    if (from != null) {
+      setHeader("From", MimeUtility.fold(from.toHeader(), fromLength));
+      this.mFrom = new Address[] {from};
+    } else {
+      this.mFrom = null;
+    }
+  }
+
+  @Override
+  public Address[] getReplyTo() throws MessagingException {
+    if (mReplyTo == null) {
+      mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
+    }
+    return mReplyTo;
+  }
+
+  @Override
+  public void setReplyTo(Address[] replyTo) throws MessagingException {
+    final int replyToLength = 10; // "Reply-to: "
+    if (replyTo == null || replyTo.length == 0) {
+      removeHeader("Reply-to");
+      mReplyTo = null;
+    } else {
+      setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), replyToLength));
+      mReplyTo = replyTo;
+    }
+  }
+
+  /**
+   * Set the mime "Message-ID" header
+   *
+   * @param messageId the new Message-ID value
+   * @throws MessagingException
+   */
+  @Override
+  public void setMessageId(String messageId) throws MessagingException {
+    setHeader("Message-ID", messageId);
+  }
+
+  /**
+   * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated random
+   * ID, if the value has not previously been set. Local generation can be inhibited/ overridden by
+   * explicitly clearing the headers, removing the message-id header, etc.
+   *
+   * @return the Message-ID header string, or null if explicitly has been set to null
+   */
+  @Override
+  public String getMessageId() throws MessagingException {
+    String messageId = getFirstHeader("Message-ID");
+    if (messageId == null && !mInhibitLocalMessageId) {
+      messageId = generateMessageId();
+      setMessageId(messageId);
+    }
+    return messageId;
+  }
+
+  @Override
+  public void saveChanges() throws MessagingException {
+    throw new MessagingException("saveChanges not yet implemented");
+  }
+
+  @Override
+  public Body getBody() throws MessagingException {
+    return mBody;
+  }
+
+  @Override
+  public void setBody(Body body) throws MessagingException {
+    this.mBody = body;
+    if (body instanceof Multipart) {
+      final Multipart multipart = ((Multipart) body);
+      multipart.setParent(this);
+      setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+      setHeader("MIME-Version", "1.0");
+    } else if (body instanceof TextBody) {
+      setHeader(
+          MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", getMimeType()));
+      setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+    }
+  }
+
+  protected String getFirstHeader(String name) throws MessagingException {
+    return getMimeHeaders().getFirstHeader(name);
+  }
+
+  @Override
+  public void addHeader(String name, String value) throws MessagingException {
+    getMimeHeaders().addHeader(name, value);
+  }
+
+  @Override
+  public void setHeader(String name, String value) throws MessagingException {
+    getMimeHeaders().setHeader(name, value);
+  }
+
+  @Override
+  public String[] getHeader(String name) throws MessagingException {
+    return getMimeHeaders().getHeader(name);
+  }
+
+  @Override
+  public void removeHeader(String name) throws MessagingException {
+    getMimeHeaders().removeHeader(name);
+    if ("Message-ID".equalsIgnoreCase(name)) {
+      mInhibitLocalMessageId = true;
+    }
+  }
+
+  /**
+   * Set extended header
+   *
+   * @param name Extended header name
+   * @param value header value - flattened by removing CR-NL if any remove header if value is null
+   * @throws MessagingException
+   */
+  @Override
+  public void setExtendedHeader(String name, String value) throws MessagingException {
+    if (value == null) {
+      if (mExtendedHeader != null) {
+        mExtendedHeader.removeHeader(name);
+      }
+      return;
+    }
+    if (mExtendedHeader == null) {
+      mExtendedHeader = new MimeHeader();
+    }
+    mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+  }
+
+  /**
+   * Get extended header
+   *
+   * @param name Extended header name
+   * @return header value - null if header does not exist
+   * @throws MessagingException
+   */
+  @Override
+  public String getExtendedHeader(String name) throws MessagingException {
+    if (mExtendedHeader == null) {
+      return null;
+    }
+    return mExtendedHeader.getFirstHeader(name);
+  }
+
+  /**
+   * Set entire extended headers from String
+   *
+   * @param headers Extended header and its value - "CR-NL-separated pairs if null or empty, remove
+   *     entire extended headers
+   * @throws MessagingException
+   */
+  public void setExtendedHeaders(String headers) throws MessagingException {
+    if (TextUtils.isEmpty(headers)) {
+      mExtendedHeader = null;
+    } else {
+      mExtendedHeader = new MimeHeader();
+      for (final String header : END_OF_LINE.split(headers)) {
+        final String[] tokens = header.split(":", 2);
+        if (tokens.length != 2) {
+          throw new MessagingException("Illegal extended headers: " + headers);
+        }
+        mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
+      }
+    }
+  }
+
+  /**
+   * Get entire extended headers as String
+   *
+   * @return "CR-NL-separated extended headers - null if extended header does not exist
+   */
+  public String getExtendedHeaders() {
+    if (mExtendedHeader != null) {
+      return mExtendedHeader.writeToString();
+    }
+    return null;
+  }
+
+  /**
+   * Write message header and body to output stream
+   *
+   * @param out Output steam to write message header and body.
+   */
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+    // Force creation of local message-id
+    getMessageId();
+    getMimeHeaders().writeTo(out);
+    // mExtendedHeader will not be write out to external output stream,
+    // because it is intended to internal use.
+    writer.write("\r\n");
+    writer.flush();
+    if (mBody != null) {
+      mBody.writeTo(out);
+    }
+  }
+
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    return null;
+  }
+
+  class MimeMessageBuilder implements ContentHandler {
+    private final Stack<Object> stack = new Stack<Object>();
+
+    public MimeMessageBuilder() {}
+
+    private void expect(Class<?> c) {
+      if (!c.isInstance(stack.peek())) {
+        throw new IllegalStateException(
+            "Internal stack error: "
+                + "Expected '"
+                + c.getName()
+                + "' found '"
+                + stack.peek().getClass().getName()
+                + "'");
+      }
+    }
+
+    @Override
+    public void startMessage() {
+      if (stack.isEmpty()) {
+        stack.push(MimeMessage.this);
+      } else {
+        expect(Part.class);
+        try {
+          final MimeMessage m = new MimeMessage();
+          ((Part) stack.peek()).setBody(m);
+          stack.push(m);
+        } catch (MessagingException me) {
+          throw new Error(me);
+        }
+      }
+    }
+
+    @Override
+    public void endMessage() {
+      expect(MimeMessage.class);
+      stack.pop();
+    }
+
+    @Override
+    public void startHeader() {
+      expect(Part.class);
+    }
+
+    @Override
+    public void field(String fieldData) {
+      expect(Part.class);
+      try {
+        final String[] tokens = fieldData.split(":", 2);
+        ((Part) stack.peek()).addHeader(tokens[0], tokens[1].trim());
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void endHeader() {
+      expect(Part.class);
+    }
+
+    @Override
+    public void startMultipart(BodyDescriptor bd) {
+      expect(Part.class);
+
+      final Part e = (Part) stack.peek();
+      try {
+        final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+        e.setBody(multiPart);
+        stack.push(multiPart);
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void body(BodyDescriptor bd, InputStream in) throws IOException {
+      expect(Part.class);
+      final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+      try {
+        ((Part) stack.peek()).setBody(body);
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void endMultipart() {
+      stack.pop();
+    }
+
+    @Override
+    public void startBodyPart() {
+      expect(MimeMultipart.class);
+
+      try {
+        final MimeBodyPart bodyPart = new MimeBodyPart();
+        ((MimeMultipart) stack.peek()).addBodyPart(bodyPart);
+        stack.push(bodyPart);
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void endBodyPart() {
+      expect(BodyPart.class);
+      stack.pop();
+    }
+
+    @Override
+    public void epilogue(InputStream is) throws IOException {
+      expect(MimeMultipart.class);
+      final StringBuilder sb = new StringBuilder();
+      int b;
+      while ((b = is.read()) != -1) {
+        sb.append((char) b);
+      }
+      // TODO: why is this commented out?
+      // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+    }
+
+    @Override
+    public void preamble(InputStream is) throws IOException {
+      expect(MimeMultipart.class);
+      final StringBuilder sb = new StringBuilder();
+      int b;
+      while ((b = is.read()) != -1) {
+        sb.append((char) b);
+      }
+      try {
+        ((MimeMultipart) stack.peek()).setPreamble(sb.toString());
+      } catch (MessagingException me) {
+        throw new Error(me);
+      }
+    }
+
+    @Override
+    public void raw(InputStream is) throws IOException {
+      throw new UnsupportedOperationException("Not supported");
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java b/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java
new file mode 100644
index 0000000..87b88b5
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeMultipart.java
@@ -0,0 +1,113 @@
+/*
+ * 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.voicemail.impl.mail.internet;
+
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class MimeMultipart extends Multipart {
+  protected String mPreamble;
+
+  protected String mContentType;
+
+  protected String mBoundary;
+
+  protected String mSubType;
+
+  public MimeMultipart() throws MessagingException {
+    mBoundary = generateBoundary();
+    setSubType("mixed");
+  }
+
+  public MimeMultipart(String contentType) throws MessagingException {
+    this.mContentType = contentType;
+    try {
+      mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
+      mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+      if (mBoundary == null) {
+        throw new MessagingException("MultiPart does not contain boundary: " + contentType);
+      }
+    } catch (Exception e) {
+      throw new MessagingException(
+          "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+              + contentType
+              + ")",
+          e);
+    }
+  }
+
+  public String generateBoundary() {
+    StringBuffer sb = new StringBuffer();
+    sb.append("----");
+    for (int i = 0; i < 30; i++) {
+      sb.append(Integer.toString((int) (Math.random() * 35), 36));
+    }
+    return sb.toString().toUpperCase();
+  }
+
+  public String getPreamble() throws MessagingException {
+    return mPreamble;
+  }
+
+  public void setPreamble(String preamble) throws MessagingException {
+    this.mPreamble = preamble;
+  }
+
+  @Override
+  public String getContentType() throws MessagingException {
+    return mContentType;
+  }
+
+  public void setSubType(String subType) throws MessagingException {
+    this.mSubType = subType;
+    mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+  }
+
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+
+    if (mPreamble != null) {
+      writer.write(mPreamble + "\r\n");
+    }
+
+    for (int i = 0, count = mParts.size(); i < count; i++) {
+      BodyPart bodyPart = mParts.get(i);
+      writer.write("--" + mBoundary + "\r\n");
+      writer.flush();
+      bodyPart.writeTo(out);
+      writer.write("\r\n");
+    }
+
+    writer.write("--" + mBoundary + "--\r\n");
+    writer.flush();
+  }
+
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    return null;
+  }
+
+  public String getSubTypeForTest() {
+    return mSubType;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/MimeUtility.java b/java/com/android/voicemail/impl/mail/internet/MimeUtility.java
new file mode 100644
index 0000000..9984602
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/MimeUtility.java
@@ -0,0 +1,400 @@
+/*
+ * 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.voicemail.impl.mail.internet;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.BodyPart;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Multipart;
+import com.android.voicemail.impl.mail.Part;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+import org.apache.james.mime4j.util.CharsetUtil;
+
+public class MimeUtility {
+  private static final String LOG_TAG = "Email";
+
+  public static final String MIME_TYPE_RFC822 = "message/rfc822";
+  private static final Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
+
+  /**
+   * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string object whenever
+   * possible.
+   */
+  public static String unfold(String s) {
+    if (s == null) {
+      return null;
+    }
+    Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
+    if (patternMatcher.find()) {
+      patternMatcher.reset();
+      s = patternMatcher.replaceAll("");
+    }
+    return s;
+  }
+
+  public static String decode(String s) {
+    if (s == null) {
+      return null;
+    }
+    return DecoderUtil.decodeEncodedWords(s);
+  }
+
+  public static String unfoldAndDecode(String s) {
+    return decode(unfold(s));
+  }
+
+  // TODO implement proper foldAndEncode
+  // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
+  // duplication of encoding.
+  public static String foldAndEncode(String s) {
+    return s;
+  }
+
+  /**
+   * INTERIM version of foldAndEncode that will be used only by Subject: headers. This is safer than
+   * implementing foldAndEncode() (see above) and risking unknown damage to other headers.
+   *
+   * <p>TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
+   *
+   * @param s original string to encode and fold
+   * @param usedCharacters number of characters already used up by header name
+   * @return the String ready to be transmitted
+   */
+  public static String foldAndEncode2(String s, int usedCharacters) {
+    // james.mime4j.codec.EncoderUtil.java
+    // encode:  encodeIfNecessary(text, usage, numUsedInHeaderName)
+    // Usage.TEXT_TOKENlooks like the right thing for subjects
+    // use WORD_ENTITY for address/names
+
+    String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN, usedCharacters);
+
+    return fold(encoded, usedCharacters);
+  }
+
+  /**
+   * INTERIM: From newer version of org.apache.james (but we don't want to import the entire
+   * MimeUtil class).
+   *
+   * <p>Splits the specified string into a multiple-line representation with lines no longer than 76
+   * characters (because the line might contain encoded words; see <a
+   * href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a> section 2). If the string contains
+   * non-whitespace sequences longer than 76 characters a line break is inserted at the whitespace
+   * character following the sequence resulting in a line longer than 76 characters.
+   *
+   * @param s string to split.
+   * @param usedCharacters number of characters already used up. Usually the number of characters
+   *     for header field name plus colon and one space.
+   * @return a multiple-line representation of the given string.
+   */
+  public static String fold(String s, int usedCharacters) {
+    final int maxCharacters = 76;
+
+    final int length = s.length();
+    if (usedCharacters + length <= maxCharacters) {
+      return s;
+    }
+
+    StringBuilder sb = new StringBuilder();
+
+    int lastLineBreak = -usedCharacters;
+    int wspIdx = indexOfWsp(s, 0);
+    while (true) {
+      if (wspIdx == length) {
+        sb.append(s.substring(Math.max(0, lastLineBreak)));
+        return sb.toString();
+      }
+
+      int nextWspIdx = indexOfWsp(s, wspIdx + 1);
+
+      if (nextWspIdx - lastLineBreak > maxCharacters) {
+        sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
+        sb.append("\r\n");
+        lastLineBreak = wspIdx;
+      }
+
+      wspIdx = nextWspIdx;
+    }
+  }
+
+  /**
+   * INTERIM: From newer version of org.apache.james (but we don't want to import the entire
+   * MimeUtil class).
+   *
+   * <p>Search for whitespace.
+   */
+  private static int indexOfWsp(String s, int fromIndex) {
+    final int len = s.length();
+    for (int index = fromIndex; index < len; index++) {
+      char c = s.charAt(index);
+      if (c == ' ' || c == '\t') {
+        return index;
+      }
+    }
+    return len;
+  }
+
+  /**
+   * Returns the named parameter of a header field. If name is null the first parameter is returned,
+   * or if there are no additional parameters in the field the entire field is returned. Otherwise
+   * the named parameter is searched for in a case insensitive fashion and returned. If the
+   * parameter cannot be found the method returns null.
+   *
+   * <p>TODO: quite inefficient with the inner trimming & splitting. TODO: Also has a latent bug:
+   * uses "startsWith" to match the name, which can false-positive. TODO: The doc says that for a
+   * null name you get the first param, but you get the header. Should probably just fix the doc,
+   * but if other code assumes that behavior, fix the code. TODO: Need to decode %-escaped strings,
+   * as in: filename="ab%22d". ('+' -> ' ' conversion too? check RFC)
+   *
+   * @param header
+   * @param name
+   * @return the entire header (if name=null), the found parameter, or null
+   */
+  public static String getHeaderParameter(String header, String name) {
+    if (header == null) {
+      return null;
+    }
+    String[] parts = unfold(header).split(";");
+    if (name == null) {
+      return parts[0].trim();
+    }
+    String lowerCaseName = name.toLowerCase();
+    for (String part : parts) {
+      if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
+        String[] parameterParts = part.split("=", 2);
+        if (parameterParts.length < 2) {
+          return null;
+        }
+        String parameter = parameterParts[1].trim();
+        if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
+          return parameter.substring(1, parameter.length() - 1);
+        } else {
+          return parameter;
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Reads the Part's body and returns a String based on any charset conversion that needed to be
+   * done.
+   *
+   * @param part The part containing a body
+   * @return a String containing the converted text in the body, or null if there was no text or an
+   *     error during conversion.
+   */
+  public static String getTextFromPart(Part part) {
+    try {
+      if (part != null && part.getBody() != null) {
+        InputStream in = part.getBody().getInputStream();
+        String mimeType = part.getMimeType();
+        if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
+          /*
+           * Now we read the part into a buffer for further processing. Because
+           * the stream is now wrapped we'll remove any transfer encoding at this point.
+           */
+          ByteArrayOutputStream out = new ByteArrayOutputStream();
+          IOUtils.copy(in, out);
+          in.close();
+          in = null; // we want all of our memory back, and close might not release
+
+          /*
+           * We've got a text part, so let's see if it needs to be processed further.
+           */
+          String charset = getHeaderParameter(part.getContentType(), "charset");
+          if (charset != null) {
+            /*
+             * See if there is conversion from the MIME charset to the Java one.
+             */
+            charset = CharsetUtil.toJavaCharset(charset);
+          }
+          /*
+           * No encoding, so use us-ascii, which is the standard.
+           */
+          if (charset == null) {
+            charset = "ASCII";
+          }
+          /*
+           * Convert and return as new String
+           */
+          String result = out.toString(charset);
+          out.close();
+          return result;
+        }
+      }
+
+    } catch (OutOfMemoryError oom) {
+      /*
+       * If we are not able to process the body there's nothing we can do about it. Return
+       * null and let the upper layers handle the missing content.
+       */
+      VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString());
+    } catch (Exception e) {
+      /*
+       * If we are not able to process the body there's nothing we can do about it. Return
+       * null and let the upper layers handle the missing content.
+       */
+      VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + e.toString());
+    }
+    return null;
+  }
+
+  /**
+   * Returns true if the given mimeType matches the matchAgainst specification. The comparison
+   * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
+   *
+   * @param mimeType A MIME type to check.
+   * @param matchAgainst A MIME type to check against. May include wildcards.
+   * @return true if the mimeType matches
+   */
+  public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
+    Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"), Pattern.CASE_INSENSITIVE);
+    return p.matcher(mimeType).matches();
+  }
+
+  /**
+   * Returns true if the given mimeType matches any of the matchAgainst specifications. The
+   * comparison ignores case and the matchAgainst strings may include "*" for a wildcard (e.g.
+   * "image/*").
+   *
+   * @param mimeType A MIME type to check.
+   * @param matchAgainst An array of MIME types to check against. May include wildcards.
+   * @return true if the mimeType matches any of the matchAgainst strings
+   */
+  public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
+    for (String matchType : matchAgainst) {
+      if (mimeTypeMatches(mimeType, matchType)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Given an input stream and a transfer encoding, return a wrapped input stream for that encoding
+   * (or the original if none is required)
+   *
+   * @param in the input stream
+   * @param contentTransferEncoding the content transfer encoding
+   * @return a properly wrapped stream
+   */
+  public static InputStream getInputStreamForContentTransferEncoding(
+      InputStream in, String contentTransferEncoding) {
+    if (contentTransferEncoding != null) {
+      contentTransferEncoding = MimeUtility.getHeaderParameter(contentTransferEncoding, null);
+      if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
+        in = new QuotedPrintableInputStream(in);
+      } else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
+        in = new Base64InputStream(in, Base64.DEFAULT);
+      }
+    }
+    return in;
+  }
+
+  /** Removes any content transfer encoding from the stream and returns a Body. */
+  public static Body decodeBody(InputStream in, String contentTransferEncoding) throws IOException {
+    /*
+     * We'll remove any transfer encoding by wrapping the stream.
+     */
+    in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+    BinaryTempFileBody tempBody = new BinaryTempFileBody();
+    OutputStream out = tempBody.getOutputStream();
+    try {
+      IOUtils.copy(in, out);
+    } catch (Base64DataException bde) {
+      // TODO Need to fix this somehow
+      //String warning = "\n\n" + Email.getMessageDecodeErrorString();
+      //out.write(warning.getBytes());
+    } finally {
+      out.close();
+    }
+    return tempBody;
+  }
+
+  /**
+   * Recursively scan a Part (usually a Message) and sort out which of its children will be
+   * "viewable" and which will be attachments.
+   *
+   * @param part The part to be broken down
+   * @param viewables This arraylist will be populated with all parts that appear to be the
+   *     "message" (e.g. text/plain & text/html)
+   * @param attachments This arraylist will be populated with all parts that appear to be
+   *     attachments (including inlines)
+   * @throws MessagingException
+   */
+  public static void collectParts(Part part, ArrayList<Part> viewables, ArrayList<Part> attachments)
+      throws MessagingException {
+    String disposition = part.getDisposition();
+    String dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+    // If a disposition is not specified, default to "inline"
+    boolean inline =
+        TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType);
+    // The lower-case mime type
+    String mimeType = part.getMimeType().toLowerCase();
+
+    if (part.getBody() instanceof Multipart) {
+      // If the part is Multipart but not alternative it's either mixed or
+      // something we don't know about, which means we treat it as mixed
+      // per the spec. We just process its pieces recursively.
+      MimeMultipart mp = (MimeMultipart) part.getBody();
+      boolean foundHtml = false;
+      if (mp.getSubTypeForTest().equals("alternative")) {
+        for (int i = 0; i < mp.getCount(); i++) {
+          if (mp.getBodyPart(i).isMimeType("text/html")) {
+            foundHtml = true;
+            break;
+          }
+        }
+      }
+      for (int i = 0; i < mp.getCount(); i++) {
+        // See if we have text and html
+        BodyPart bp = mp.getBodyPart(i);
+        // If there's html, don't bother loading text
+        if (foundHtml && bp.isMimeType("text/plain")) {
+          continue;
+        }
+        collectParts(bp, viewables, attachments);
+      }
+    } else if (part.getBody() instanceof Message) {
+      // If the part is an embedded message we just continue to process
+      // it, pulling any viewables or attachments into the running list.
+      Message message = (Message) part.getBody();
+      collectParts(message, viewables, attachments);
+    } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) {
+      // We'll treat text and images as viewables
+      viewables.add(part);
+    } else {
+      // Everything else is an attachment.
+      attachments.add(part);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/internet/TextBody.java b/java/com/android/voicemail/impl/mail/internet/TextBody.java
new file mode 100644
index 0000000..dae5625
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/internet/TextBody.java
@@ -0,0 +1,59 @@
+/*
+ * 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.voicemail.impl.mail.internet;
+
+import android.util.Base64;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.MessagingException;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class TextBody implements Body {
+  String mBody;
+
+  public TextBody(String body) {
+    this.mBody = body;
+  }
+
+  @Override
+  public void writeTo(OutputStream out) throws IOException, MessagingException {
+    byte[] bytes = mBody.getBytes("UTF-8");
+    out.write(Base64.encode(bytes, Base64.CRLF));
+  }
+
+  /**
+   * Get the text of the body in it's unencoded format.
+   *
+   * @return
+   */
+  public String getText() {
+    return mBody;
+  }
+
+  /** Returns an InputStream that reads this body's text in UTF-8 format. */
+  @Override
+  public InputStream getInputStream() throws MessagingException {
+    try {
+      byte[] b = mBody.getBytes("UTF-8");
+      return new ByteArrayInputStream(b);
+    } catch (UnsupportedEncodingException usee) {
+      return null;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapConnection.java b/java/com/android/voicemail/impl/mail/store/ImapConnection.java
new file mode 100644
index 0000000..0a48dfc
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapConnection.java
@@ -0,0 +1,400 @@
+/*
+ * 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.voicemail.impl.mail.store;
+
+import android.util.ArraySet;
+import android.util.Base64;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.AuthenticationFailedException;
+import com.android.voicemail.impl.mail.CertificateValidationException;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
+import com.android.voicemail.impl.mail.store.imap.DigestMd5Utils;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.store.imap.ImapResponseParser;
+import com.android.voicemail.impl.mail.store.imap.ImapUtility;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+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;
+
+/** A cacheable class that stores the details for a single IMAP connection. */
+public class ImapConnection {
+  private final String TAG = "ImapConnection";
+
+  private String mLoginPhrase;
+  private ImapStore mImapStore;
+  private MailTransport mTransport;
+  private ImapResponseParser mParser;
+  private Set<String> mCapabilities = new ArraySet<>();
+
+  static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
+
+  /**
+   * Next tag to use. All connections associated to the same ImapStore instance share the same
+   * counter to make tests simpler. (Some of the tests involve multiple connections but only have a
+   * single counter to track the tag.)
+   */
+  private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
+
+  ImapConnection(ImapStore store) {
+    setStore(store);
+  }
+
+  void setStore(ImapStore store) {
+    // TODO: maybe we should throw an exception if the connection is not closed here,
+    // if it's not currently closed, then we won't reopen it, so if the credentials have
+    // changed, the connection will not be reestablished.
+    mImapStore = store;
+    mLoginPhrase = null;
+  }
+
+  /**
+   * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
+   * username and password.
+   *
+   * @return the login command string to sent to the IMAP server
+   */
+  String getLoginPhrase() {
+    if (mLoginPhrase == null) {
+      if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
+        // build the LOGIN string once (instead of over-and-over again.)
+        // apply the quoting here around the built-up password
+        mLoginPhrase =
+            ImapConstants.LOGIN
+                + " "
+                + mImapStore.getUsername()
+                + " "
+                + ImapUtility.imapQuoted(mImapStore.getPassword());
+      }
+    }
+    return mLoginPhrase;
+  }
+
+  public void open() throws IOException, MessagingException {
+    if (mTransport != null && mTransport.isOpen()) {
+      return;
+    }
+
+    try {
+      // copy configuration into a clean transport, if necessary
+      if (mTransport == null) {
+        mTransport = mImapStore.cloneTransport();
+      }
+
+      mTransport.open();
+
+      createParser();
+
+      // The server should greet us with something like
+      // * OK IMAP4rev1 Server
+      // consume the response before doing anything else.
+      ImapResponse response = mParser.readResponse(false);
+      if (!response.isOk()) {
+        mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE);
+        throw new MessagingException(
+            MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
+            "Invalid server initial response");
+      }
+
+      queryCapability();
+
+      maybeDoStartTls();
+
+      // LOGIN
+      doLogin();
+    } catch (SSLException e) {
+      LogUtils.d(TAG, "SSLException ", e);
+      mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION);
+      throw new CertificateValidationException(e.getMessage(), e);
+    } catch (IOException ioe) {
+      LogUtils.d(TAG, "IOException", ioe);
+      mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN);
+      throw ioe;
+    } finally {
+      destroyResponses();
+    }
+  }
+
+  void logout() {
+    try {
+      sendCommand(ImapConstants.LOGOUT, false);
+      if (!mParser.readResponse(true).is(0, ImapConstants.BYE)) {
+        VvmLog.e(TAG, "Server did not respond LOGOUT with BYE");
+      }
+      if (!mParser.readResponse(false).isOk()) {
+        VvmLog.e(TAG, "Server did not respond OK after LOGOUT");
+      }
+    } catch (IOException | MessagingException e) {
+      VvmLog.e(TAG, "Error while logging out:" + e);
+    }
+  }
+
+  /**
+   * Closes the connection and releases all resources. This connection can not be used again until
+   * {@link #setStore(ImapStore)} is called.
+   */
+  void close() {
+    if (mTransport != null) {
+      logout();
+      mTransport.close();
+      mTransport = null;
+    }
+    destroyResponses();
+    mParser = null;
+    mImapStore = null;
+  }
+
+  /** 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 {
+      if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
+        doDigestMd5Auth();
+      } else {
+        executeSimpleCommand(getLoginPhrase(), true);
+      }
+    } catch (ImapException ie) {
+      LogUtils.d(TAG, "ImapException", ie);
+      String status = ie.getStatus();
+      String statusMessage = ie.getStatusMessage();
+      String alertText = ie.getAlertText();
+
+      if (ImapConstants.NO.equals(status)) {
+        switch (statusMessage) {
+          case ImapConstants.NO_UNKNOWN_USER:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER);
+            break;
+          case ImapConstants.NO_UNKNOWN_CLIENT:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE);
+            break;
+          case ImapConstants.NO_INVALID_PASSWORD:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD);
+            break;
+          case ImapConstants.NO_MAILBOX_NOT_INITIALIZED:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED);
+            break;
+          case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED);
+            break;
+          case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED);
+            break;
+          case ImapConstants.NO_USER_IS_BLOCKED:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED);
+            break;
+          case ImapConstants.NO_APPLICATION_ERROR:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
+            break;
+          default:
+            mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL);
+        }
+        throw new AuthenticationFailedException(alertText, ie);
+      }
+
+      mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
+      throw new MessagingException(alertText, ie);
+    }
+  }
+
+  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) {
+          if (!disabledCapabilities.contains(capability)) {
+            mCapabilities.add(capability);
+          }
+        } else {
+          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}.
+   *
+   * <p>If we already have an {@link ImapResponseParser}, we {@link #destroyResponses()} and throw
+   * it away.
+   */
+  private void createParser() {
+    destroyResponses();
+    mParser = new ImapResponseParser(mTransport.getInputStream());
+  }
+
+  public void destroyResponses() {
+    if (mParser != null) {
+      mParser.destroyResponses();
+    }
+  }
+
+  public ImapResponse readResponse() throws IOException, MessagingException {
+    return mParser.readResponse(false);
+  }
+
+  public List<ImapResponse> executeSimpleCommand(String command)
+      throws IOException, MessagingException {
+    return executeSimpleCommand(command, false);
+  }
+
+  /**
+   * Send a single command to the server. The command will be preceded by an IMAP command tag and
+   * followed by \r\n (caller need not supply them). Execute a simple command at the server, a
+   * simple command being one that is sent in a single line of text
+   *
+   * @param command the command to send to the server
+   * @param sensitive whether the command should be redacted in logs (used for login)
+   * @return a list of ImapResponses
+   * @throws IOException
+   * @throws MessagingException
+   */
+  public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
+      throws IOException, MessagingException {
+    // TODO: It may be nice to catch IOExceptions and close the connection here.
+    // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
+    sendCommand(command, sensitive);
+    return getCommandResponses();
+  }
+
+  public String sendCommand(String command, boolean sensitive)
+      throws IOException, MessagingException {
+    open();
+
+    if (mTransport == null) {
+      throw new IOException("Null transport");
+    }
+    String tag = Integer.toString(mNextCommandTag.incrementAndGet());
+    String commandToSend = tag + " " + command;
+    mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
+    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
+   *
+   * @return a list of ImapResponses
+   * @throws IOException
+   * @throws MessagingException
+   */
+  List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
+    final List<ImapResponse> responses = new ArrayList<ImapResponse>();
+    ImapResponse response;
+    do {
+      response = mParser.readResponse(false);
+      responses.add(response);
+    } while (!(response.isTagged() || response.isContinuationRequest()));
+
+    if (!(response.isOk() || response.isContinuationRequest())) {
+      final String toString = response.toString();
+      final String status = response.getStatusOrEmpty().getString();
+      final String statusMessage = response.getStatusResponseTextOrEmpty().getString();
+      final String alert = response.getAlertTextOrEmpty().getString();
+      final String responseCode = response.getResponseCodeOrEmpty().getString();
+      destroyResponses();
+      throw new ImapException(toString, status, statusMessage, alert, responseCode);
+    }
+    return responses;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapFolder.java b/java/com/android/voicemail/impl/mail/store/ImapFolder.java
new file mode 100644
index 0000000..1d9b011
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapFolder.java
@@ -0,0 +1,797 @@
+/*
+ * 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.voicemail.impl.mail.store;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Base64DataException;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.mail.AuthenticationFailedException;
+import com.android.voicemail.impl.mail.Body;
+import com.android.voicemail.impl.mail.FetchProfile;
+import com.android.voicemail.impl.mail.Flag;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.Part;
+import com.android.voicemail.impl.mail.internet.BinaryTempFileBody;
+import com.android.voicemail.impl.mail.internet.MimeBodyPart;
+import com.android.voicemail.impl.mail.internet.MimeHeader;
+import com.android.voicemail.impl.mail.internet.MimeMultipart;
+import com.android.voicemail.impl.mail.internet.MimeUtility;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
+import com.android.voicemail.impl.mail.store.ImapStore.ImapMessage;
+import com.android.voicemail.impl.mail.store.imap.ImapConstants;
+import com.android.voicemail.impl.mail.store.imap.ImapElement;
+import com.android.voicemail.impl.mail.store.imap.ImapList;
+import com.android.voicemail.impl.mail.store.imap.ImapResponse;
+import com.android.voicemail.impl.mail.store.imap.ImapString;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.mail.utils.Utility;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+
+public class ImapFolder {
+  private static final String TAG = "ImapFolder";
+  private static final String[] PERMANENT_FLAGS = {
+    Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED
+  };
+  private static final int COPY_BUFFER_SIZE = 16 * 1024;
+
+  private final ImapStore mStore;
+  private final String mName;
+  private int mMessageCount = -1;
+  private ImapConnection mConnection;
+  private String mMode;
+  private boolean mExists;
+  /** A set of hashes that can be used to track dirtiness */
+  Object mHash[];
+
+  public static final String MODE_READ_ONLY = "mode_read_only";
+  public static final String MODE_READ_WRITE = "mode_read_write";
+
+  public ImapFolder(ImapStore store, String name) {
+    mStore = store;
+    mName = name;
+  }
+
+  /** Callback for each message retrieval. */
+  public interface MessageRetrievalListener {
+    public void messageRetrieved(Message message);
+  }
+
+  private void destroyResponses() {
+    if (mConnection != null) {
+      mConnection.destroyResponses();
+    }
+  }
+
+  public void open(String mode) throws MessagingException {
+    try {
+      if (isOpen()) {
+        throw new AssertionError("Duplicated open on ImapFolder");
+      }
+      synchronized (this) {
+        mConnection = mStore.getConnection();
+      }
+      // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
+      // $MDNSent)
+      // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
+      // NonJunk $MDNSent \*)] Flags permitted.
+      // * 23 EXISTS
+      // * 0 RECENT
+      // * OK [UIDVALIDITY 1125022061] UIDs valid
+      // * OK [UIDNEXT 57576] Predicted next UID
+      // 2 OK [READ-WRITE] Select completed.
+      try {
+        doSelect();
+      } catch (IOException ioe) {
+        throw ioExceptionHandler(mConnection, ioe);
+      } finally {
+        destroyResponses();
+      }
+    } catch (AuthenticationFailedException e) {
+      // Don't cache this connection, so we're forced to try connecting/login again
+      mConnection = null;
+      close(false);
+      throw e;
+    } catch (MessagingException e) {
+      mExists = false;
+      close(false);
+      throw e;
+    }
+  }
+
+  public boolean isOpen() {
+    return mExists && mConnection != null;
+  }
+
+  public String getMode() {
+    return mMode;
+  }
+
+  public void close(boolean expunge) {
+    if (expunge) {
+      try {
+        expunge();
+      } catch (MessagingException e) {
+        LogUtils.e(TAG, e, "Messaging Exception");
+      }
+    }
+    mMessageCount = -1;
+    synchronized (this) {
+      mConnection = null;
+    }
+  }
+
+  public int getMessageCount() {
+    return mMessageCount;
+  }
+
+  String[] getSearchUids(List<ImapResponse> responses) {
+    // S: * SEARCH 2 3 6
+    final ArrayList<String> uids = new ArrayList<String>();
+    for (ImapResponse response : responses) {
+      if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
+        continue;
+      }
+      // Found SEARCH response data
+      for (int i = 1; i < response.size(); i++) {
+        ImapString s = response.getStringOrEmpty(i);
+        if (s.isString()) {
+          uids.add(s.getString());
+        }
+      }
+    }
+    return uids.toArray(Utility.EMPTY_STRINGS);
+  }
+
+  @VisibleForTesting
+  String[] searchForUids(String searchCriteria) throws MessagingException {
+    checkOpen();
+    try {
+      try {
+        final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
+        final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
+        LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " + result.length);
+        return result;
+      } catch (ImapException me) {
+        LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me);
+        return Utility.EMPTY_STRINGS; // Not found
+      } catch (IOException ioe) {
+        LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe);
+        mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+        throw ioExceptionHandler(mConnection, ioe);
+      }
+    } finally {
+      destroyResponses();
+    }
+  }
+
+  @Nullable
+  public Message getMessage(String uid) throws MessagingException {
+    checkOpen();
+
+    final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
+    for (int i = 0; i < uids.length; i++) {
+      if (uids[i].equals(uid)) {
+        return new ImapMessage(uid, this);
+      }
+    }
+    LogUtils.e(TAG, "UID " + uid + " not found on server");
+    return null;
+  }
+
+  @VisibleForTesting
+  protected static boolean isAsciiString(String str) {
+    int len = str.length();
+    for (int i = 0; i < len; i++) {
+      char c = str.charAt(i);
+      if (c >= 128) return false;
+    }
+    return true;
+  }
+
+  public Message[] getMessages(String[] uids) throws MessagingException {
+    if (uids == null) {
+      uids = searchForUids("1:* NOT DELETED");
+    }
+    return getMessagesInternal(uids);
+  }
+
+  public Message[] getMessagesInternal(String[] uids) {
+    final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
+    for (int i = 0; i < uids.length; i++) {
+      final String uid = uids[i];
+      final ImapMessage message = new ImapMessage(uid, this);
+      messages.add(message);
+    }
+    return messages.toArray(Message.EMPTY_ARRAY);
+  }
+
+  public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
+      throws MessagingException {
+    try {
+      fetchInternal(messages, fp, listener);
+    } catch (RuntimeException e) { // Probably a parser error.
+      LogUtils.w(TAG, "Exception detected: " + e.getMessage());
+      throw e;
+    }
+  }
+
+  public void fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
+      throws MessagingException {
+    if (messages.length == 0) {
+      return;
+    }
+    checkOpen();
+    ArrayMap<String, Message> messageMap = new ArrayMap<String, Message>();
+    for (Message m : messages) {
+      messageMap.put(m.getUid(), m);
+    }
+
+    /*
+     * Figure out what command we are going to run:
+     * FLAGS     - UID FETCH (FLAGS)
+     * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
+     *                            HEADER.FIELDS (date subject from content-type to cc)])
+     * STRUCTURE - UID FETCH (BODYSTRUCTURE)
+     * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
+     * BODY      - UID FETCH (BODY.PEEK[])
+     * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
+     */
+
+    final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
+
+    fetchFields.add(ImapConstants.UID);
+    if (fp.contains(FetchProfile.Item.FLAGS)) {
+      fetchFields.add(ImapConstants.FLAGS);
+    }
+    if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+      fetchFields.add(ImapConstants.INTERNALDATE);
+      fetchFields.add(ImapConstants.RFC822_SIZE);
+      fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
+    }
+    if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+      fetchFields.add(ImapConstants.BODYSTRUCTURE);
+    }
+
+    if (fp.contains(FetchProfile.Item.BODY_SANE)) {
+      fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
+    }
+    if (fp.contains(FetchProfile.Item.BODY)) {
+      fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
+    }
+
+    // TODO Why are we only fetching the first part given?
+    final Part fetchPart = fp.getFirstPart();
+    if (fetchPart != null) {
+      final String[] partIds = fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
+      // TODO Why can a single part have more than one Id? And why should we only fetch
+      // the first id if there are more than one?
+      if (partIds != null) {
+        fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE + "[" + partIds[0] + "]");
+      }
+    }
+
+    try {
+      mConnection.sendCommand(
+          String.format(
+              Locale.US,
+              ImapConstants.UID_FETCH + " %s (%s)",
+              ImapStore.joinMessageUids(messages),
+              Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')),
+          false);
+      ImapResponse response;
+      do {
+        response = null;
+        try {
+          response = mConnection.readResponse();
+
+          if (!response.isDataResponse(1, ImapConstants.FETCH)) {
+            continue; // Ignore
+          }
+          final ImapList fetchList = response.getListOrEmpty(2);
+          final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID).getString();
+          if (TextUtils.isEmpty(uid)) continue;
+
+          ImapMessage message = (ImapMessage) messageMap.get(uid);
+          if (message == null) continue;
+
+          if (fp.contains(FetchProfile.Item.FLAGS)) {
+            final ImapList flags = fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
+            for (int i = 0, count = flags.size(); i < count; i++) {
+              final ImapString flag = flags.getStringOrEmpty(i);
+              if (flag.is(ImapConstants.FLAG_DELETED)) {
+                message.setFlagInternal(Flag.DELETED, true);
+              } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
+                message.setFlagInternal(Flag.ANSWERED, true);
+              } else if (flag.is(ImapConstants.FLAG_SEEN)) {
+                message.setFlagInternal(Flag.SEEN, true);
+              } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
+                message.setFlagInternal(Flag.FLAGGED, true);
+              }
+            }
+          }
+          if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+            final Date internalDate =
+                fetchList.getKeyedStringOrEmpty(ImapConstants.INTERNALDATE).getDateOrNull();
+            final int size =
+                fetchList.getKeyedStringOrEmpty(ImapConstants.RFC822_SIZE).getNumberOrZero();
+            final String header =
+                fetchList
+                    .getKeyedStringOrEmpty(ImapConstants.BODY_BRACKET_HEADER, true)
+                    .getString();
+
+            message.setInternalDate(internalDate);
+            message.setSize(size);
+            message.parse(Utility.streamFromAsciiString(header));
+          }
+          if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+            ImapList bs = fetchList.getKeyedListOrEmpty(ImapConstants.BODYSTRUCTURE);
+            if (!bs.isEmpty()) {
+              try {
+                parseBodyStructure(bs, message, ImapConstants.TEXT);
+              } catch (MessagingException e) {
+                LogUtils.v(TAG, e, "Error handling message");
+                message.setBody(null);
+              }
+            }
+          }
+          if (fp.contains(FetchProfile.Item.BODY) || fp.contains(FetchProfile.Item.BODY_SANE)) {
+            // Body is keyed by "BODY[]...".
+            // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
+            // TODO Should we accept "RFC822" as well??
+            ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
+            InputStream bodyStream = body.getAsStream();
+            message.parse(bodyStream);
+          }
+          if (fetchPart != null) {
+            InputStream bodyStream = fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
+            String encodings[] = fetchPart.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
+
+            String contentTransferEncoding = null;
+            if (encodings != null && encodings.length > 0) {
+              contentTransferEncoding = encodings[0];
+            } else {
+              // According to http://tools.ietf.org/html/rfc2045#section-6.1
+              // "7bit" is the default.
+              contentTransferEncoding = "7bit";
+            }
+
+            try {
+              // TODO Don't create 2 temp files.
+              // decodeBody creates BinaryTempFileBody, but we could avoid this
+              // if we implement ImapStringBody.
+              // (We'll need to share a temp file.  Protect it with a ref-count.)
+              message.setBody(
+                  decodeBody(
+                      mStore.getContext(),
+                      bodyStream,
+                      contentTransferEncoding,
+                      fetchPart.getSize(),
+                      listener));
+            } catch (Exception e) {
+              // TODO: Figure out what kinds of exceptions might actually be thrown
+              // from here. This blanket catch-all is because we're not sure what to
+              // do if we don't have a contentTransferEncoding, and we don't have
+              // time to figure out what exceptions might be thrown.
+              LogUtils.e(TAG, "Error fetching body %s", e);
+            }
+          }
+
+          if (listener != null) {
+            listener.messageRetrieved(message);
+          }
+        } finally {
+          destroyResponses();
+        }
+      } while (!response.isTagged());
+    } catch (IOException ioe) {
+      mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+      throw ioExceptionHandler(mConnection, ioe);
+    }
+  }
+
+  /**
+   * Removes any content transfer encoding from the stream and returns a Body. This code is
+   * taken/condensed from MimeUtility.decodeBody
+   */
+  private static Body decodeBody(
+      Context context,
+      InputStream in,
+      String contentTransferEncoding,
+      int size,
+      MessageRetrievalListener listener)
+      throws IOException {
+    // Get a properly wrapped input stream
+    in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+    BinaryTempFileBody tempBody = new BinaryTempFileBody();
+    OutputStream out = tempBody.getOutputStream();
+    try {
+      byte[] buffer = new byte[COPY_BUFFER_SIZE];
+      int n = 0;
+      int count = 0;
+      while (-1 != (n = in.read(buffer))) {
+        out.write(buffer, 0, n);
+        count += n;
+      }
+    } catch (Base64DataException bde) {
+      String warning = "\n\nThere was an error while decoding the message.";
+      out.write(warning.getBytes());
+    } finally {
+      out.close();
+    }
+    return tempBody;
+  }
+
+  public String[] getPermanentFlags() {
+    return PERMANENT_FLAGS;
+  }
+
+  /**
+   * Handle any untagged responses that the caller doesn't care to handle themselves.
+   *
+   * @param responses
+   */
+  private void handleUntaggedResponses(List<ImapResponse> responses) {
+    for (ImapResponse response : responses) {
+      handleUntaggedResponse(response);
+    }
+  }
+
+  /**
+   * Handle an untagged response that the caller doesn't care to handle themselves.
+   *
+   * @param response
+   */
+  private void handleUntaggedResponse(ImapResponse response) {
+    if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+      mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
+    }
+  }
+
+  private static void parseBodyStructure(ImapList bs, Part part, String id)
+      throws MessagingException {
+    if (bs.getElementOrNone(0).isList()) {
+      /*
+       * This is a multipart/*
+       */
+      MimeMultipart mp = new MimeMultipart();
+      for (int i = 0, count = bs.size(); i < count; i++) {
+        ImapElement e = bs.getElementOrNone(i);
+        if (e.isList()) {
+          /*
+           * For each part in the message we're going to add a new BodyPart and parse
+           * into it.
+           */
+          MimeBodyPart bp = new MimeBodyPart();
+          if (id.equals(ImapConstants.TEXT)) {
+            parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
+
+          } else {
+            parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
+          }
+          mp.addBodyPart(bp);
+
+        } else {
+          if (e.isString()) {
+            mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
+          }
+          break; // Ignore the rest of the list.
+        }
+      }
+      part.setBody(mp);
+    } else {
+      /*
+       * This is a body. We need to add as much information as we can find out about
+       * it to the Part.
+       */
+
+      /*
+      body type
+      body subtype
+      body parameter parenthesized list
+      body id
+      body description
+      body encoding
+      body size
+      */
+
+      final ImapString type = bs.getStringOrEmpty(0);
+      final ImapString subType = bs.getStringOrEmpty(1);
+      final String mimeType = (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
+
+      final ImapList bodyParams = bs.getListOrEmpty(2);
+      final ImapString cid = bs.getStringOrEmpty(3);
+      final ImapString encoding = bs.getStringOrEmpty(5);
+      final int size = bs.getStringOrEmpty(6).getNumberOrZero();
+
+      if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
+        // A body type of type MESSAGE and subtype RFC822
+        // contains, immediately after the basic fields, the
+        // envelope structure, body structure, and size in
+        // text lines of the encapsulated message.
+        // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
+        //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
+        /*
+         * This will be caught by fetch and handled appropriately.
+         */
+        throw new MessagingException(
+            "BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 + " not yet supported.");
+      }
+
+      /*
+       * Set the content type with as much information as we know right now.
+       */
+      final StringBuilder contentType = new StringBuilder(mimeType);
+
+      /*
+       * If there are body params we might be able to get some more information out
+       * of them.
+       */
+      for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
+
+        // TODO We need to convert " into %22, but
+        // because MimeUtility.getHeaderParameter doesn't recognize it,
+        // we can't fix it for now.
+        contentType.append(
+            String.format(
+                ";\n %s=\"%s\"",
+                bodyParams.getStringOrEmpty(i - 1).getString(),
+                bodyParams.getStringOrEmpty(i).getString()));
+      }
+
+      part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
+
+      // Extension items
+      final ImapList bodyDisposition;
+
+      if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
+        // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
+        // So, if it's not a list, use 10th element.
+        // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
+        bodyDisposition = bs.getListOrEmpty(9);
+      } else {
+        bodyDisposition = bs.getListOrEmpty(8);
+      }
+
+      final StringBuilder contentDisposition = new StringBuilder();
+
+      if (bodyDisposition.size() > 0) {
+        final String bodyDisposition0Str =
+            bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
+        if (!TextUtils.isEmpty(bodyDisposition0Str)) {
+          contentDisposition.append(bodyDisposition0Str);
+        }
+
+        final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
+        if (!bodyDispositionParams.isEmpty()) {
+          /*
+           * If there is body disposition information we can pull some more
+           * information about the attachment out.
+           */
+          for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
+
+            // TODO We need to convert " into %22.  See above.
+            contentDisposition.append(
+                String.format(
+                    Locale.US,
+                    ";\n %s=\"%s\"",
+                    bodyDispositionParams
+                        .getStringOrEmpty(i - 1)
+                        .getString()
+                        .toLowerCase(Locale.US),
+                    bodyDispositionParams.getStringOrEmpty(i).getString()));
+          }
+        }
+      }
+
+      if ((size > 0)
+          && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") == null)) {
+        contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
+      }
+
+      if (contentDisposition.length() > 0) {
+        /*
+         * Set the content disposition containing at least the size. Attachment
+         * handling code will use this down the road.
+         */
+        part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition.toString());
+      }
+
+      /*
+       * Set the Content-Transfer-Encoding header. Attachment code will use this
+       * to parse the body.
+       */
+      if (!encoding.isEmpty()) {
+        part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding.getString());
+      }
+
+      /*
+       * Set the Content-ID header.
+       */
+      if (!cid.isEmpty()) {
+        part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
+      }
+
+      if (size > 0) {
+        if (part instanceof ImapMessage) {
+          ((ImapMessage) part).setSize(size);
+        } else if (part instanceof MimeBodyPart) {
+          ((MimeBodyPart) part).setSize(size);
+        } else {
+          throw new MessagingException("Unknown part type " + part.toString());
+        }
+      }
+      part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
+    }
+  }
+
+  public Message[] expunge() throws MessagingException {
+    checkOpen();
+    try {
+      handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
+    } catch (IOException ioe) {
+      mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+      throw ioExceptionHandler(mConnection, ioe);
+    } finally {
+      destroyResponses();
+    }
+    return null;
+  }
+
+  public void setFlags(Message[] messages, String[] flags, boolean value)
+      throws MessagingException {
+    checkOpen();
+
+    String allFlags = "";
+    if (flags.length > 0) {
+      StringBuilder flagList = new StringBuilder();
+      for (int i = 0, count = flags.length; i < count; i++) {
+        String flag = flags[i];
+        if (flag == Flag.SEEN) {
+          flagList.append(" " + ImapConstants.FLAG_SEEN);
+        } else if (flag == Flag.DELETED) {
+          flagList.append(" " + ImapConstants.FLAG_DELETED);
+        } else if (flag == Flag.FLAGGED) {
+          flagList.append(" " + ImapConstants.FLAG_FLAGGED);
+        } else if (flag == Flag.ANSWERED) {
+          flagList.append(" " + ImapConstants.FLAG_ANSWERED);
+        }
+      }
+      allFlags = flagList.substring(1);
+    }
+    try {
+      mConnection.executeSimpleCommand(
+          String.format(
+              Locale.US,
+              ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
+              ImapStore.joinMessageUids(messages),
+              value ? "+" : "-",
+              allFlags));
+
+    } catch (IOException ioe) {
+      mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+      throw ioExceptionHandler(mConnection, ioe);
+    } finally {
+      destroyResponses();
+    }
+  }
+
+  /**
+   * Selects the folder for use. Before performing any operations on this folder, it must be
+   * selected.
+   */
+  private void doSelect() throws IOException, MessagingException {
+    final List<ImapResponse> responses =
+        mConnection.executeSimpleCommand(
+            String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName));
+
+    // Assume the folder is opened read-write; unless we are notified otherwise
+    mMode = MODE_READ_WRITE;
+    int messageCount = -1;
+    for (ImapResponse response : responses) {
+      if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+        messageCount = response.getStringOrEmpty(0).getNumberOrZero();
+      } else if (response.isOk()) {
+        final ImapString responseCode = response.getResponseCodeOrEmpty();
+        if (responseCode.is(ImapConstants.READ_ONLY)) {
+          mMode = MODE_READ_ONLY;
+        } else if (responseCode.is(ImapConstants.READ_WRITE)) {
+          mMode = MODE_READ_WRITE;
+        }
+      } else if (response.isTagged()) { // Not OK
+        mStore.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
+        throw new MessagingException(
+            "Can't open mailbox: " + response.getStatusResponseTextOrEmpty());
+      }
+    }
+    if (messageCount == -1) {
+      throw new MessagingException("Did not find message count during select");
+    }
+    mMessageCount = messageCount;
+    mExists = true;
+  }
+
+  public class Quota {
+
+    public final int occupied;
+    public final int total;
+
+    public Quota(int occupied, int total) {
+      this.occupied = occupied;
+      this.total = total;
+    }
+  }
+
+  public Quota getQuota() throws MessagingException {
+    try {
+      final List<ImapResponse> responses =
+          mConnection.executeSimpleCommand(
+              String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", mName));
+
+      for (ImapResponse response : responses) {
+        if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
+          continue;
+        }
+        ImapList list = response.getListOrEmpty(2);
+        for (int i = 0; i < list.size(); i += 3) {
+          if (!list.getStringOrEmpty(i).is("voice")) {
+            continue;
+          }
+          return new Quota(
+              list.getStringOrEmpty(i + 1).getNumber(-1),
+              list.getStringOrEmpty(i + 2).getNumber(-1));
+        }
+      }
+    } catch (IOException ioe) {
+      mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+      throw ioExceptionHandler(mConnection, ioe);
+    } finally {
+      destroyResponses();
+    }
+    return null;
+  }
+
+  private void checkOpen() throws MessagingException {
+    if (!isOpen()) {
+      throw new MessagingException("Folder " + mName + " is not open.");
+    }
+  }
+
+  private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
+    LogUtils.d(TAG, "IO Exception detected: ", ioe);
+    connection.close();
+    if (connection == mConnection) {
+      mConnection = null; // To prevent close() from returning the connection to the pool.
+      close(false);
+    }
+    return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
+  }
+
+  public Message createMessage(String uid) {
+    return new ImapMessage(uid, this);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/ImapStore.java b/java/com/android/voicemail/impl/mail/store/ImapStore.java
new file mode 100644
index 0000000..cadbe59
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/ImapStore.java
@@ -0,0 +1,181 @@
+/*
+ * 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.voicemail.impl.mail.store;
+
+import android.content.Context;
+import android.net.Network;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.Message;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.internet.MimeMessage;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImapStore {
+  /**
+   * A global suggestion to Store implementors on how much of the body should be returned on
+   * FetchProfile.Item.BODY_SANE requests. We'll use 125k now.
+   */
+  public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024);
+
+  private final Context mContext;
+  private final ImapHelper mHelper;
+  private final String mUsername;
+  private final String mPassword;
+  private final MailTransport mTransport;
+  private ImapConnection mConnection;
+
+  public static final int FLAG_NONE = 0x00; // No flags
+  public static final int FLAG_SSL = 0x01; // Use SSL
+  public static final int FLAG_TLS = 0x02; // Use TLS
+  public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication
+  public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates
+  public static final int FLAG_OAUTH = 0x10; // Use OAuth for authentication
+
+  /** Contains all the information necessary to log into an imap server */
+  public ImapStore(
+      Context context,
+      ImapHelper helper,
+      String username,
+      String password,
+      int port,
+      String serverName,
+      int flags,
+      Network network) {
+    mContext = context;
+    mHelper = helper;
+    mUsername = username;
+    mPassword = password;
+    mTransport = new MailTransport(context, this.getImapHelper(), network, serverName, port, flags);
+  }
+
+  public Context getContext() {
+    return mContext;
+  }
+
+  public ImapHelper getImapHelper() {
+    return mHelper;
+  }
+
+  public String getUsername() {
+    return mUsername;
+  }
+
+  public String getPassword() {
+    return mPassword;
+  }
+
+  /** Returns a clone of the transport associated with this store. */
+  MailTransport cloneTransport() {
+    return mTransport.clone();
+  }
+
+  /** Returns UIDs of Messages joined with "," as the separator. */
+  static String joinMessageUids(Message[] messages) {
+    StringBuilder sb = new StringBuilder();
+    boolean notFirst = false;
+    for (Message m : messages) {
+      if (notFirst) {
+        sb.append(',');
+      }
+      sb.append(m.getUid());
+      notFirst = true;
+    }
+    return sb.toString();
+  }
+
+  static class ImapMessage extends MimeMessage {
+    private ImapFolder mFolder;
+
+    ImapMessage(String uid, ImapFolder folder) {
+      mUid = uid;
+      mFolder = folder;
+    }
+
+    public void setSize(int size) {
+      mSize = size;
+    }
+
+    @Override
+    public void parse(InputStream in) throws IOException, MessagingException {
+      super.parse(in);
+    }
+
+    public void setFlagInternal(String flag, boolean set) throws MessagingException {
+      super.setFlag(flag, set);
+    }
+
+    @Override
+    public void setFlag(String flag, boolean set) throws MessagingException {
+      super.setFlag(flag, set);
+      mFolder.setFlags(new Message[] {this}, new String[] {flag}, set);
+    }
+  }
+
+  static class ImapException extends MessagingException {
+    private static final long serialVersionUID = 1L;
+
+    private final String mStatus;
+    private final String mStatusMessage;
+    private final String mAlertText;
+    private final String mResponseCode;
+
+    public ImapException(
+        String message,
+        String status,
+        String statusMessage,
+        String alertText,
+        String responseCode) {
+      super(message);
+      mStatus = status;
+      mStatusMessage = statusMessage;
+      mAlertText = alertText;
+      mResponseCode = responseCode;
+    }
+
+    public String getStatus() {
+      return mStatus;
+    }
+
+    public String getStatusMessage() {
+      return mStatusMessage;
+    }
+
+    public String getAlertText() {
+      return mAlertText;
+    }
+
+    public String getResponseCode() {
+      return mResponseCode;
+    }
+  }
+
+  public void closeConnection() {
+    if (mConnection != null) {
+      mConnection.close();
+      mConnection = null;
+    }
+  }
+
+  public ImapConnection getConnection() {
+    if (mConnection == null) {
+      mConnection = new ImapConnection(this);
+    }
+    return mConnection;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java b/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java
new file mode 100644
index 0000000..f156f67
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/DigestMd5Utils.java
@@ -0,0 +1,335 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import android.util.Base64;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.MailTransport;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.store.ImapStore;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+
+@SuppressWarnings("AndroidApiChecker") // Map.getOrDefault() is java8
+@TargetApi(VERSION_CODES.O)
+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) {
+        VvmLog.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/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java b/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java
new file mode 100644
index 0000000..88ec0ed
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapConstants.java
@@ -0,0 +1,138 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.store.ImapStore;
+import java.util.Locale;
+
+public final class ImapConstants {
+  private ImapConstants() {}
+
+  public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
+  public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
+  public static final String FETCH_FIELD_BODY_PEEK_SANE =
+      String.format(Locale.US, "BODY.PEEK[]<0.%d>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE);
+  public static final String FETCH_FIELD_HEADERS =
+      "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
+
+  public static final String ALERT = "ALERT";
+  public static final String APPEND = "APPEND";
+  public static final String AUTHENTICATE = "AUTHENTICATE";
+  public static final String BAD = "BAD";
+  public static final String BADCHARSET = "BADCHARSET";
+  public static final String BODY = "BODY";
+  public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
+  public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
+  public static final String BYE = "BYE";
+  public static final String CAPABILITY = "CAPABILITY";
+  public static final String CHECK = "CHECK";
+  public static final String CLOSE = "CLOSE";
+  public static final String COPY = "COPY";
+  public static final String COPYUID = "COPYUID";
+  public static final String CREATE = "CREATE";
+  public static final String DELETE = "DELETE";
+  public static final String EXAMINE = "EXAMINE";
+  public static final String EXISTS = "EXISTS";
+  public static final String EXPUNGE = "EXPUNGE";
+  public static final String FETCH = "FETCH";
+  public static final String FLAG_ANSWERED = "\\ANSWERED";
+  public static final String FLAG_DELETED = "\\DELETED";
+  public static final String FLAG_FLAGGED = "\\FLAGGED";
+  public static final String FLAG_NO_SELECT = "\\NOSELECT";
+  public static final String FLAG_SEEN = "\\SEEN";
+  public static final String FLAGS = "FLAGS";
+  public static final String FLAGS_SILENT = "FLAGS.SILENT";
+  public static final String ID = "ID";
+  public static final String INBOX = "INBOX";
+  public static final String INTERNALDATE = "INTERNALDATE";
+  public static final String LIST = "LIST";
+  public static final String LOGIN = "LOGIN";
+  public static final String LOGOUT = "LOGOUT";
+  public static final String LSUB = "LSUB";
+  public static final String NAMESPACE = "NAMESPACE";
+  public static final String NO = "NO";
+  public static final String NOOP = "NOOP";
+  public static final String OK = "OK";
+  public static final String PARSE = "PARSE";
+  public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
+  public static final String PREAUTH = "PREAUTH";
+  public static final String READ_ONLY = "READ-ONLY";
+  public static final String READ_WRITE = "READ-WRITE";
+  public static final String RENAME = "RENAME";
+  public static final String RFC822_SIZE = "RFC822.SIZE";
+  public static final String SEARCH = "SEARCH";
+  public static final String SELECT = "SELECT";
+  public static final String STARTTLS = "STARTTLS";
+  public static final String STATUS = "STATUS";
+  public static final String STORE = "STORE";
+  public static final String SUBSCRIBE = "SUBSCRIBE";
+  public static final String TEXT = "TEXT";
+  public static final String TRYCREATE = "TRYCREATE";
+  public static final String UID = "UID";
+  public static final String UID_COPY = "UID COPY";
+  public static final String UID_FETCH = "UID FETCH";
+  public static final String UID_SEARCH = "UID SEARCH";
+  public static final String UID_STORE = "UID STORE";
+  public static final String UIDNEXT = "UIDNEXT";
+  public static final String UIDPLUS = "UIDPLUS";
+  public static final String UIDVALIDITY = "UIDVALIDITY";
+  public static final String UNSEEN = "UNSEEN";
+  public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
+  public static final String XOAUTH2 = "XOAUTH2";
+  public static final String APPENDUID = "APPENDUID";
+  public static final String NIL = "NIL";
+
+  /** NO responses */
+  public static final String NO_COMMAND_NOT_ALLOWED = "command not allowed";
+
+  public static final String NO_RESERVATION_FAILED = "reservation failed";
+  public static final String NO_APPLICATION_ERROR = "application error";
+  public static final String NO_INVALID_PARAMETER = "invalid parameter";
+  public static final String NO_INVALID_COMMAND = "invalid command";
+  public static final String NO_UNKNOWN_COMMAND = "unknown command";
+  // AUTHENTICATE
+  // The subscriber can not be located in the system.
+  public static final String NO_UNKNOWN_USER = "unknown user";
+  // The Client Type or Protocol Version is unknown.
+  public static final String NO_UNKNOWN_CLIENT = "unknown client";
+  // The password received from the client does not match the password defined in the subscriber's
+  // profile.
+  public static final String NO_INVALID_PASSWORD = "invalid password";
+  // The subscriber's mailbox has not yet been initialised via the TUI
+  public static final String NO_MAILBOX_NOT_INITIALIZED = "mailbox not initialized";
+  // The subscriber has not been provisioned for the VVM service.
+  public static final String NO_SERVICE_IS_NOT_PROVISIONED = "service is not provisioned";
+  // The subscriber is provisioned for the VVM service but the VVM service is currently not active
+  public static final String NO_SERVICE_IS_NOT_ACTIVATED = "service is not activated";
+  // The Voice Mail Blocked flag in the subscriber's profile is set to YES.
+  public static final String NO_USER_IS_BLOCKED = "user is blocked";
+
+  /** extensions */
+  public static final String GETQUOTA = "GETQUOTA";
+
+  public static final String GETQUOTAROOT = "GETQUOTAROOT";
+  public static final String QUOTAROOT = "QUOTAROOT";
+  public static final String QUOTA = "QUOTA";
+
+  /** 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";
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java b/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java
new file mode 100644
index 0000000..ee255d1
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapElement.java
@@ -0,0 +1,124 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+/**
+ * Class representing "element"s in IMAP responses.
+ *
+ * <p>Class hierarchy:
+ *
+ * <pre>
+ * ImapElement
+ *   |
+ *   |-- ImapElement.NONE (for 'index out of range')
+ *   |
+ *   |-- ImapList (isList() == true)
+ *   |   |
+ *   |   |-- ImapList.EMPTY
+ *   |   |
+ *   |   --- ImapResponse
+ *   |
+ *   --- ImapString (isString() == true)
+ *       |
+ *       |-- ImapString.EMPTY
+ *       |
+ *       |-- ImapSimpleString
+ *       |
+ *       |-- ImapMemoryLiteral
+ *       |
+ *       --- ImapTempFileLiteral
+ * </pre>
+ */
+public abstract class ImapElement {
+  /**
+   * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index is out of
+   * range.
+   */
+  public static final ImapElement NONE =
+      new ImapElement() {
+        @Override
+        public void destroy() {
+          // Don't call super.destroy().
+          // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override
+        public boolean isList() {
+          return false;
+        }
+
+        @Override
+        public boolean isString() {
+          return false;
+        }
+
+        @Override
+        public String toString() {
+          return "[NO ELEMENT]";
+        }
+
+        @Override
+        public boolean equalsForTest(ImapElement that) {
+          return super.equalsForTest(that);
+        }
+      };
+
+  private boolean mDestroyed = false;
+
+  public abstract boolean isList();
+
+  public abstract boolean isString();
+
+  protected boolean isDestroyed() {
+    return mDestroyed;
+  }
+
+  /**
+   * Clean up the resources used by the instance. It's for removing a temp file used by {@link
+   * ImapTempFileLiteral}.
+   */
+  public void destroy() {
+    mDestroyed = true;
+  }
+
+  /** Throws {@link RuntimeException} if it's already destroyed. */
+  protected final void checkNotDestroyed() {
+    if (mDestroyed) {
+      throw new RuntimeException("Already destroyed");
+    }
+  }
+
+  /**
+   * Return a string that represents this object; it's purely for the debug purpose. Don't mistake
+   * it for {@link ImapString#getString}.
+   *
+   * <p>Abstract to force subclasses to implement it.
+   */
+  @Override
+  public abstract String toString();
+
+  /**
+   * The equals implementation that is intended to be used only for unit testing. (Because it may be
+   * heavy and has a special sense of "equal" for testing.)
+   */
+  public boolean equalsForTest(ImapElement that) {
+    if (that == null) {
+      return false;
+    }
+    return this.getClass() == that.getClass(); // Has to be the same class.
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapList.java b/java/com/android/voicemail/impl/mail/store/imap/ImapList.java
new file mode 100644
index 0000000..e4a6ec0
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapList.java
@@ -0,0 +1,226 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+import java.util.ArrayList;
+
+/** Class represents an IMAP list. */
+public class ImapList extends ImapElement {
+  /** {@link ImapList} representing an empty list. */
+  public static final ImapList EMPTY =
+      new ImapList() {
+        @Override
+        public void destroy() {
+          // Don't call super.destroy().
+          // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override
+        void add(ImapElement e) {
+          throw new RuntimeException();
+        }
+      };
+
+  private ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
+
+  /* package */ void add(ImapElement e) {
+    if (e == null) {
+      throw new RuntimeException("Can't add null");
+    }
+    mList.add(e);
+  }
+
+  @Override
+  public final boolean isString() {
+    return false;
+  }
+
+  @Override
+  public final boolean isList() {
+    return true;
+  }
+
+  public final int size() {
+    return mList.size();
+  }
+
+  public final boolean isEmpty() {
+    return size() == 0;
+  }
+
+  /**
+   * Return true if the element at {@code index} exists, is string, and equals to {@code s}. (case
+   * insensitive)
+   */
+  public final boolean is(int index, String s) {
+    return is(index, s, false);
+  }
+
+  /** Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}. */
+  public final boolean is(int index, String s, boolean prefixMatch) {
+    if (!prefixMatch) {
+      return getStringOrEmpty(index).is(s);
+    } else {
+      return getStringOrEmpty(index).startsWith(s);
+    }
+  }
+
+  /**
+   * Return the element at {@code index}. If {@code index} is out of range, returns {@link
+   * ImapElement#NONE}.
+   */
+  public final ImapElement getElementOrNone(int index) {
+    return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
+  }
+
+  /**
+   * Return the element at {@code index} if it's a list. If {@code index} is out of range or not a
+   * list, returns {@link ImapList#EMPTY}.
+   */
+  public final ImapList getListOrEmpty(int index) {
+    ImapElement el = getElementOrNone(index);
+    return el.isList() ? (ImapList) el : EMPTY;
+  }
+
+  /**
+   * Return the element at {@code index} if it's a string. If {@code index} is out of range or not a
+   * string, returns {@link ImapString#EMPTY}.
+   */
+  public final ImapString getStringOrEmpty(int index) {
+    ImapElement el = getElementOrNone(index);
+    return el.isString() ? (ImapString) el : ImapString.EMPTY;
+  }
+
+  /**
+   * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be at an
+   * even index.
+   */
+  /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
+    for (int i = 1; i < size(); i += 2) {
+      if (is(i - 1, key, prefixMatch)) {
+        return mList.get(i);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Return an {@link ImapList} keyed by {@code key}. Return {@link ImapList#EMPTY} if not found.
+   */
+  public final ImapList getKeyedListOrEmpty(String key) {
+    return getKeyedListOrEmpty(key, false);
+  }
+
+  /**
+   * Return an {@link ImapList} keyed by {@code key}. Return {@link ImapList#EMPTY} if not found.
+   */
+  public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
+    ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+    return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
+  }
+
+  /**
+   * Return an {@link ImapString} keyed by {@code key}. Return {@link ImapString#EMPTY} if not
+   * found.
+   */
+  public final ImapString getKeyedStringOrEmpty(String key) {
+    return getKeyedStringOrEmpty(key, false);
+  }
+
+  /**
+   * Return an {@link ImapString} keyed by {@code key}. Return {@link ImapString#EMPTY} if not
+   * found.
+   */
+  public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
+    ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+    return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
+  }
+
+  /** Return true if it contains {@code s}. */
+  public final boolean contains(String s) {
+    for (int i = 0; i < size(); i++) {
+      if (getStringOrEmpty(i).is(s)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public void destroy() {
+    if (mList != null) {
+      for (ImapElement e : mList) {
+        e.destroy();
+      }
+      mList = null;
+    }
+    super.destroy();
+  }
+
+  @Override
+  public String toString() {
+    return mList.toString();
+  }
+
+  /** Return the text representations of the contents concatenated with ",". */
+  public final String flatten() {
+    return flatten(new StringBuilder()).toString();
+  }
+
+  /**
+   * Returns text representations (i.e. getString()) of contents joined together with "," as the
+   * separator.
+   *
+   * <p>Only used for building the capability string passed to vendor policies.
+   *
+   * <p>We can't use toString(), because it's for debugging (meaning the format may change any
+   * time), and it won't expand literals.
+   */
+  private final StringBuilder flatten(StringBuilder sb) {
+    sb.append('[');
+    for (int i = 0; i < mList.size(); i++) {
+      if (i > 0) {
+        sb.append(',');
+      }
+      final ImapElement e = getElementOrNone(i);
+      if (e.isList()) {
+        getListOrEmpty(i).flatten(sb);
+      } else if (e.isString()) {
+        sb.append(getStringOrEmpty(i).getString());
+      }
+    }
+    sb.append(']');
+    return sb;
+  }
+
+  @Override
+  public boolean equalsForTest(ImapElement that) {
+    if (!super.equalsForTest(that)) {
+      return false;
+    }
+    ImapList thatList = (ImapList) that;
+    if (size() != thatList.size()) {
+      return false;
+    }
+    for (int i = 0; i < size(); i++) {
+      if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java b/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java
new file mode 100644
index 0000000..96a8c4a
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapMemoryLiteral.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 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.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/** Subclass of {@link ImapString} used for literals backed by an in-memory byte array. */
+public class ImapMemoryLiteral extends ImapString {
+  private final String TAG = "ImapMemoryLiteral";
+  private byte[] mData;
+
+  /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
+    // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
+    // copy....
+    mData = new byte[in.getLength()];
+    int pos = 0;
+    while (pos < mData.length) {
+      int read = in.read(mData, pos, mData.length - pos);
+      if (read < 0) {
+        break;
+      }
+      pos += read;
+    }
+    if (pos != mData.length) {
+      VvmLog.w(TAG, "length mismatch");
+    }
+  }
+
+  @Override
+  public void destroy() {
+    mData = null;
+    super.destroy();
+  }
+
+  @Override
+  public String getString() {
+    try {
+      return new String(mData, "US-ASCII");
+    } catch (UnsupportedEncodingException e) {
+      VvmLog.e(TAG, "Unsupported encoding: ", e);
+    }
+    return null;
+  }
+
+  @Override
+  public InputStream getAsStream() {
+    return new ByteArrayInputStream(mData);
+  }
+
+  @Override
+  public String toString() {
+    return String.format("{%d byte literal(memory)}", mData.length);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java b/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java
new file mode 100644
index 0000000..d53d458
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapResponse.java
@@ -0,0 +1,142 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+/** Class represents an IMAP response. */
+public class ImapResponse extends ImapList {
+  private final String mTag;
+  private final boolean mIsContinuationRequest;
+
+  /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
+    mTag = tag;
+    mIsContinuationRequest = isContinuationRequest;
+  }
+
+  /* package */ static boolean isStatusResponse(String symbol) {
+    return ImapConstants.OK.equalsIgnoreCase(symbol)
+        || ImapConstants.NO.equalsIgnoreCase(symbol)
+        || ImapConstants.BAD.equalsIgnoreCase(symbol)
+        || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
+        || ImapConstants.BYE.equalsIgnoreCase(symbol);
+  }
+
+  /** @return whether it's a tagged response. */
+  public boolean isTagged() {
+    return mTag != null;
+  }
+
+  /** @return whether it's a continuation request. */
+  public boolean isContinuationRequest() {
+    return mIsContinuationRequest;
+  }
+
+  public boolean isStatusResponse() {
+    return isStatusResponse(getStringOrEmpty(0).getString());
+  }
+
+  /** @return whether it's an OK response. */
+  public boolean isOk() {
+    return is(0, ImapConstants.OK);
+  }
+
+  /** @return whether it's an BAD response. */
+  public boolean isBad() {
+    return is(0, ImapConstants.BAD);
+  }
+
+  /** @return whether it's an NO response. */
+  public boolean isNo() {
+    return is(0, ImapConstants.NO);
+  }
+
+  /**
+   * @return whether it's an {@code responseType} data response. (i.e. not tagged).
+   * @param index where {@code responseType} should appear. e.g. 1 for "FETCH"
+   * @param responseType e.g. "FETCH"
+   */
+  public final boolean isDataResponse(int index, String responseType) {
+    return !isTagged() && getStringOrEmpty(index).is(responseType);
+  }
+
+  /**
+   * @return Response code (RFC 3501 7.1) if it's a status response.
+   *     <p>e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
+   */
+  public ImapString getResponseCodeOrEmpty() {
+    if (!isStatusResponse()) {
+      return ImapString.EMPTY; // Not a status response.
+    }
+    return getListOrEmpty(1).getStringOrEmpty(0);
+  }
+
+  /**
+   * @return Alert message it it has ALERT response code.
+   *     <p>e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
+   */
+  public ImapString getAlertTextOrEmpty() {
+    if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
+      return ImapString.EMPTY; // Not an ALERT
+    }
+    // The 3rd element contains all the rest of line.
+    return getStringOrEmpty(2);
+  }
+
+  /** @return Response text in a status response. */
+  public ImapString getStatusResponseTextOrEmpty() {
+    if (!isStatusResponse()) {
+      return ImapString.EMPTY;
+    }
+    return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
+  }
+
+  public ImapString getStatusOrEmpty() {
+    if (!isStatusResponse()) {
+      return ImapString.EMPTY;
+    }
+    return getStringOrEmpty(0);
+  }
+
+  @Override
+  public String toString() {
+    String tag = mTag;
+    if (isContinuationRequest()) {
+      tag = "+";
+    }
+    return "#" + tag + "# " + super.toString();
+  }
+
+  @Override
+  public boolean equalsForTest(ImapElement that) {
+    if (!super.equalsForTest(that)) {
+      return false;
+    }
+    final ImapResponse thatResponse = (ImapResponse) that;
+    if (mTag == null) {
+      if (thatResponse.mTag != null) {
+        return false;
+      }
+    } else {
+      if (!mTag.equals(thatResponse.mTag)) {
+        return false;
+      }
+    }
+    if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java b/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java
new file mode 100644
index 0000000..e37106a
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapResponseParser.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2010 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.voicemail.impl.mail.store.imap;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.mail.PeekableInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/** IMAP response parser. */
+public class ImapResponseParser {
+  private static final String TAG = "ImapResponseParser";
+
+  /** Literal larger than this will be stored in temp file. */
+  public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
+
+  /** Input stream */
+  private final PeekableInputStream mIn;
+
+  private final int mLiteralKeepInMemoryThreshold;
+
+  /** StringBuilder used by readUntil() */
+  private final StringBuilder mBufferReadUntil = new StringBuilder();
+
+  /** StringBuilder used by parseBareString() */
+  private final StringBuilder mParseBareString = new StringBuilder();
+
+  /**
+   * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from time
+   * to time to destroy them and clear it.
+   */
+  private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
+
+  /**
+   * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated in the
+   * same way EOF does.
+   */
+  public static class ByeException extends IOException {
+    public static final String MESSAGE = "Received BYE";
+
+    public ByeException() {
+      super(MESSAGE);
+    }
+  }
+
+  /** Public constructor for normal use. */
+  public ImapResponseParser(InputStream in) {
+    this(in, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
+  }
+
+  /** Constructor for testing to override the literal size threshold. */
+  /* package for test */ ImapResponseParser(InputStream in, int literalKeepInMemoryThreshold) {
+    mIn = new PeekableInputStream(in);
+    mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
+  }
+
+  private static IOException newEOSException() {
+    final String message = "End of stream reached";
+    VvmLog.d(TAG, message);
+    return new IOException(message);
+  }
+
+  /**
+   * Peek next one byte.
+   *
+   * <p>Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, we
+   * shouldn't see EOF during parsing.
+   */
+  private int peek() throws IOException {
+    final int next = mIn.peek();
+    if (next == -1) {
+      throw newEOSException();
+    }
+    return next;
+  }
+
+  /**
+   * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
+   *
+   * <p>Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, we
+   * shouldn't see EOF during parsing.
+   */
+  private int readByte() throws IOException {
+    int next = mIn.read();
+    if (next == -1) {
+      throw newEOSException();
+    }
+    return next;
+  }
+
+  /**
+   * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
+   *
+   * @see #readResponse()
+   */
+  public void destroyResponses() {
+    for (ImapResponse r : mResponsesToDestroy) {
+      r.destroy();
+    }
+    mResponsesToDestroy.clear();
+  }
+
+  /**
+   * Reads the next response available on the stream and returns an {@link ImapResponse} object that
+   * represents it.
+   *
+   * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse} is
+   * stored in the internal storage. When the {@link ImapResponse} is no longer used {@link
+   * #destroyResponses} should be called to destroy all the responses in the array.
+   *
+   * @param byeExpected is a untagged BYE response expected? If not proper cleanup will be done and
+   *     {@link ByeException} will be thrown.
+   * @return the parsed {@link ImapResponse} object.
+   * @exception ByeException when detects BYE and <code>byeExpected</code> is false.
+   */
+  public ImapResponse readResponse(boolean byeExpected) throws IOException, MessagingException {
+    ImapResponse response = null;
+    try {
+      response = parseResponse();
+    } catch (RuntimeException e) {
+      // Parser crash -- log network activities.
+      onParseError(e);
+      throw e;
+    } catch (IOException e) {
+      // Network error, or received an unexpected char.
+      onParseError(e);
+      throw e;
+    }
+
+    // Handle this outside of try-catch.  We don't have to dump protocol log when getting BYE.
+    if (!byeExpected && response.is(0, ImapConstants.BYE)) {
+      VvmLog.w(TAG, ByeException.MESSAGE);
+      response.destroy();
+      throw new ByeException();
+    }
+    mResponsesToDestroy.add(response);
+    return response;
+  }
+
+  private void onParseError(Exception e) {
+    // Read a few more bytes, so that the log will contain some more context, even if the parser
+    // crashes in the middle of a response.
+    // This also makes sure the byte in question will be logged, no matter where it crashes.
+    // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
+    // before actually reading it.
+    // However, we don't want to read too much, because then it may get into an email message.
+    try {
+      for (int i = 0; i < 4; i++) {
+        int b = readByte();
+        if (b == -1 || b == '\n') {
+          break;
+        }
+      }
+    } catch (IOException ignore) {
+    }
+    VvmLog.w(TAG, "Exception detected: " + e.getMessage());
+  }
+
+  /**
+   * Read next byte from stream and throw it away. If the byte is different from {@code expected}
+   * throw {@link MessagingException}.
+   */
+  /* package for test */ void expect(char expected) throws IOException {
+    final int next = readByte();
+    if (expected != next) {
+      throw new IOException(
+          String.format(
+              "Expected %04x (%c) but got %04x (%c)", (int) expected, expected, next, (char) next));
+    }
+  }
+
+  /**
+   * Read bytes until we find {@code end}, and return all as string. The {@code end} will be read
+   * (rather than peeked) and won't be included in the result.
+   */
+  /* package for test */ String readUntil(char end) throws IOException {
+    mBufferReadUntil.setLength(0);
+    for (; ; ) {
+      final int ch = readByte();
+      if (ch != end) {
+        mBufferReadUntil.append((char) ch);
+      } else {
+        return mBufferReadUntil.toString();
+      }
+    }
+  }
+
+  /** Read all bytes until \r\n. */
+  /* package */ String readUntilEol() throws IOException {
+    String ret = readUntil('\r');
+    expect('\n'); // TODO Should this really be error?
+    return ret;
+  }
+
+  /** Parse and return the response line. */
+  private ImapResponse parseResponse() throws IOException, MessagingException {
+    // We need to destroy the response if we get an exception.
+    // So, we first store the response that's being built in responseToDestroy, until it's
+    // completely built, at which point we copy it into responseToReturn and null out
+    // responseToDestroyt.
+    // If responseToDestroy is not null in finally, we destroy it because that means
+    // we got an exception somewhere.
+    ImapResponse responseToDestroy = null;
+    final ImapResponse responseToReturn;
+
+    try {
+      final int ch = peek();
+      if (ch == '+') { // Continuation request
+        readByte(); // skip +
+        expect(' ');
+        responseToDestroy = new ImapResponse(null, true);
+
+        // If it's continuation request, we don't really care what's in it.
+        responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+
+        // Response has successfully been built.  Let's return it.
+        responseToReturn = responseToDestroy;
+        responseToDestroy = null;
+      } else {
+        // Status response or response data
+        final String tag;
+        if (ch == '*') {
+          tag = null;
+          readByte(); // skip *
+          expect(' ');
+        } else {
+          tag = readUntil(' ');
+        }
+        responseToDestroy = new ImapResponse(tag, false);
+
+        final ImapString firstString = parseBareString();
+        responseToDestroy.add(firstString);
+
+        // parseBareString won't eat a space after the string, so we need to skip it,
+        // if exists.
+        // If the next char is not ' ', it should be EOL.
+        if (peek() == ' ') {
+          readByte(); // skip ' '
+
+          if (responseToDestroy.isStatusResponse()) { // It's a status response
+
+            // Is there a response code?
+            final int next = peek();
+            if (next == '[') {
+              responseToDestroy.add(parseList('[', ']'));
+              if (peek() == ' ') { // Skip following space
+                readByte();
+              }
+            }
+
+            String rest = readUntilEol();
+            if (!TextUtils.isEmpty(rest)) {
+              // The rest is free-form text.
+              responseToDestroy.add(new ImapSimpleString(rest));
+            }
+          } else { // It's a response data.
+            parseElements(responseToDestroy, '\0');
+          }
+        } else {
+          expect('\r');
+          expect('\n');
+        }
+
+        // Response has successfully been built.  Let's return it.
+        responseToReturn = responseToDestroy;
+        responseToDestroy = null;
+      }
+    } finally {
+      if (responseToDestroy != null) {
+        // We get an exception.
+        responseToDestroy.destroy();
+      }
+    }
+
+    return responseToReturn;
+  }
+
+  private ImapElement parseElement() throws IOException, MessagingException {
+    final int next = peek();
+    switch (next) {
+      case '(':
+        return parseList('(', ')');
+      case '[':
+        return parseList('[', ']');
+      case '"':
+        readByte(); // Skip "
+        return new ImapSimpleString(readUntil('"'));
+      case '{':
+        return parseLiteral();
+      case '\r': // CR
+        readByte(); // Consume \r
+        expect('\n'); // Should be followed by LF.
+        return null;
+      case '\n': // LF // There shouldn't be a bare LF, but just in case.
+        readByte(); // Consume \n
+        return null;
+      default:
+        return parseBareString();
+    }
+  }
+
+  /**
+   * Parses an atom.
+   *
+   * <p>Special case: If an atom contains '[', everything until the next ']' will be considered a
+   * part of the atom. (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
+   *
+   * <p>If the value is "NIL", returns an empty string.
+   */
+  private ImapString parseBareString() throws IOException, MessagingException {
+    mParseBareString.setLength(0);
+    for (; ; ) {
+      final int ch = peek();
+
+      // TODO Can we clean this up?  (This condition is from the old parser.)
+      if (ch == '('
+          || ch == ')'
+          || ch == '{'
+          || ch == ' '
+          ||
+          // ']' is not part of atom (it's in resp-specials)
+          ch == ']'
+          ||
+          // docs claim that flags are \ atom but atom isn't supposed to
+          // contain
+          // * and some flags contain *
+          // ch == '%' || ch == '*' ||
+          ch == '%'
+          ||
+          // TODO probably should not allow \ and should recognize
+          // it as a flag instead
+          // ch == '"' || ch == '\' ||
+          ch == '"'
+          || (0x00 <= ch && ch <= 0x1f)
+          || ch == 0x7f) {
+        if (mParseBareString.length() == 0) {
+          throw new MessagingException("Expected string, none found.");
+        }
+        String s = mParseBareString.toString();
+
+        // NIL will be always converted into the empty string.
+        if (ImapConstants.NIL.equalsIgnoreCase(s)) {
+          return ImapString.EMPTY;
+        }
+        return new ImapSimpleString(s);
+      } else if (ch == '[') {
+        // Eat all until next ']'
+        mParseBareString.append((char) readByte());
+        mParseBareString.append(readUntil(']'));
+        mParseBareString.append(']'); // readUntil won't include the end char.
+      } else {
+        mParseBareString.append((char) readByte());
+      }
+    }
+  }
+
+  private void parseElements(ImapList list, char end) throws IOException, MessagingException {
+    for (; ; ) {
+      for (; ; ) {
+        final int next = peek();
+        if (next == end) {
+          return;
+        }
+        if (next != ' ') {
+          break;
+        }
+        // Skip space
+        readByte();
+      }
+      final ImapElement el = parseElement();
+      if (el == null) { // EOL
+        return;
+      }
+      list.add(el);
+    }
+  }
+
+  private ImapList parseList(char opening, char closing) throws IOException, MessagingException {
+    expect(opening);
+    final ImapList list = new ImapList();
+    parseElements(list, closing);
+    expect(closing);
+    return list;
+  }
+
+  private ImapString parseLiteral() throws IOException, MessagingException {
+    expect('{');
+    final int size;
+    try {
+      size = Integer.parseInt(readUntil('}'));
+    } catch (NumberFormatException nfe) {
+      throw new MessagingException("Invalid length in literal");
+    }
+    if (size < 0) {
+      throw new MessagingException("Invalid negative length in literal");
+    }
+    expect('\r');
+    expect('\n');
+    FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
+    if (size > mLiteralKeepInMemoryThreshold) {
+      return new ImapTempFileLiteral(in);
+    } else {
+      return new ImapMemoryLiteral(in);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java b/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java
new file mode 100644
index 0000000..7cc866b
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapSimpleString.java
@@ -0,0 +1,59 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/** Subclass of {@link ImapString} used for non literals. */
+public class ImapSimpleString extends ImapString {
+  private final String TAG = "ImapSimpleString";
+  private String mString;
+
+  /* package */ ImapSimpleString(String string) {
+    mString = (string != null) ? string : "";
+  }
+
+  @Override
+  public void destroy() {
+    mString = null;
+    super.destroy();
+  }
+
+  @Override
+  public String getString() {
+    return mString;
+  }
+
+  @Override
+  public InputStream getAsStream() {
+    try {
+      return new ByteArrayInputStream(mString.getBytes("US-ASCII"));
+    } catch (UnsupportedEncodingException e) {
+      VvmLog.e(TAG, "Unsupported encoding: ", e);
+    }
+    return null;
+  }
+
+  @Override
+  public String toString() {
+    // Purposefully not return just mString, in order to prevent using it instead of getString.
+    return "\"" + mString + "\"";
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapString.java b/java/com/android/voicemail/impl/mail/store/imap/ImapString.java
new file mode 100644
index 0000000..d5c5551
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapString.java
@@ -0,0 +1,179 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.VvmLog;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Class represents an IMAP "element" that is not a list.
+ *
+ * <p>An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too.
+ * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]". See
+ * {@link ImapResponseParser}.
+ */
+public abstract class ImapString extends ImapElement {
+  private static final byte[] EMPTY_BYTES = new byte[0];
+
+  public static final ImapString EMPTY =
+      new ImapString() {
+        @Override
+        public void destroy() {
+          // Don't call super.destroy().
+          // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override
+        public String getString() {
+          return "";
+        }
+
+        @Override
+        public InputStream getAsStream() {
+          return new ByteArrayInputStream(EMPTY_BYTES);
+        }
+
+        @Override
+        public String toString() {
+          return "";
+        }
+      };
+
+  // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
+  // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
+  // handled by Locale.US
+  private static final SimpleDateFormat DATE_TIME_FORMAT =
+      new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
+
+  private boolean mIsInteger;
+  private int mParsedInteger;
+  private Date mParsedDate;
+
+  @Override
+  public final boolean isList() {
+    return false;
+  }
+
+  @Override
+  public final boolean isString() {
+    return true;
+  }
+
+  /**
+   * @return true if and only if the length of the string is larger than 0.
+   *     <p>Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
+   *     #parseBareString}. On the other hand, a quoted/literal string with value NIL (i.e. "NIL"
+   *     and {3}\r\nNIL) is treated literally.
+   */
+  public final boolean isEmpty() {
+    return getString().length() == 0;
+  }
+
+  public abstract String getString();
+
+  public abstract InputStream getAsStream();
+
+  /** @return whether it can be parsed as a number. */
+  public final boolean isNumber() {
+    if (mIsInteger) {
+      return true;
+    }
+    try {
+      mParsedInteger = Integer.parseInt(getString());
+      mIsInteger = true;
+      return true;
+    } catch (NumberFormatException e) {
+      return false;
+    }
+  }
+
+  /** @return value parsed as a number, or 0 if the string is not a number. */
+  public final int getNumberOrZero() {
+    return getNumber(0);
+  }
+
+  /** @return value parsed as a number, or {@code defaultValue} if the string is not a number. */
+  public final int getNumber(int defaultValue) {
+    if (!isNumber()) {
+      return defaultValue;
+    }
+    return mParsedInteger;
+  }
+
+  /** @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}. */
+  public final boolean isDate() {
+    if (mParsedDate != null) {
+      return true;
+    }
+    if (isEmpty()) {
+      return false;
+    }
+    try {
+      mParsedDate = DATE_TIME_FORMAT.parse(getString());
+      return true;
+    } catch (ParseException e) {
+      VvmLog.w("ImapString", getString() + " can't be parsed as a date.");
+      return false;
+    }
+  }
+
+  /** @return value it can be parsed as a {@link Date}, or null otherwise. */
+  public final Date getDateOrNull() {
+    if (!isDate()) {
+      return null;
+    }
+    return mParsedDate;
+  }
+
+  /** @return whether the value case-insensitively equals to {@code s}. */
+  public final boolean is(String s) {
+    if (s == null) {
+      return false;
+    }
+    return getString().equalsIgnoreCase(s);
+  }
+
+  /** @return whether the value case-insensitively starts with {@code s}. */
+  public final boolean startsWith(String prefix) {
+    if (prefix == null) {
+      return false;
+    }
+    final String me = this.getString();
+    if (me.length() < prefix.length()) {
+      return false;
+    }
+    return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
+  }
+
+  // To force subclasses to implement it.
+  @Override
+  public abstract String toString();
+
+  @Override
+  public final boolean equalsForTest(ImapElement that) {
+    if (!super.equalsForTest(that)) {
+      return false;
+    }
+    ImapString thatString = (ImapString) that;
+    return getString().equals(thatString.getString());
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java b/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java
new file mode 100644
index 0000000..ab64d85
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapTempFileLiteral.java
@@ -0,0 +1,119 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.FixedLengthInputStream;
+import com.android.voicemail.impl.mail.TempDirectory;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import com.android.voicemail.impl.mail.utils.Utility;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/** Subclass of {@link ImapString} used for literals backed by a temp file. */
+public class ImapTempFileLiteral extends ImapString {
+  private final String TAG = "ImapTempFileLiteral";
+
+  /* package for test */ final File mFile;
+
+  /** Size is purely for toString() */
+  private final int mSize;
+
+  /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
+    mSize = stream.getLength();
+    mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
+
+    // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
+    // so it'd simply cause a memory leak.
+    // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
+    // mFile.deleteOnExit();
+    OutputStream out = new FileOutputStream(mFile);
+    IOUtils.copy(stream, out);
+    out.close();
+  }
+
+  /**
+   * Make sure we delete the temp file.
+   *
+   * <p>We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
+   */
+  @Override
+  protected void finalize() throws Throwable {
+    try {
+      destroy();
+    } finally {
+      super.finalize();
+    }
+  }
+
+  @Override
+  public InputStream getAsStream() {
+    checkNotDestroyed();
+    try {
+      return new FileInputStream(mFile);
+    } catch (FileNotFoundException e) {
+      // It's probably possible if we're low on storage and the system clears the cache dir.
+      LogUtils.w(TAG, "ImapTempFileLiteral: Temp file not found");
+
+      // Return 0 byte stream as a dummy...
+      return new ByteArrayInputStream(new byte[0]);
+    }
+  }
+
+  @Override
+  public String getString() {
+    checkNotDestroyed();
+    try {
+      byte[] bytes = IOUtils.toByteArray(getAsStream());
+      // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
+      if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
+        throw new IOException();
+      }
+      return Utility.fromAscii(bytes);
+    } catch (IOException e) {
+      LogUtils.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+      return "";
+    }
+  }
+
+  @Override
+  public void destroy() {
+    try {
+      if (!isDestroyed() && mFile.exists()) {
+        mFile.delete();
+      }
+    } catch (RuntimeException re) {
+      // Just log and ignore.
+      LogUtils.w(TAG, "Failed to remove temp file: " + re.getMessage());
+    }
+    super.destroy();
+  }
+
+  @Override
+  public String toString() {
+    return String.format("{%d byte literal(file)}", mSize);
+  }
+
+  public boolean tempFileExistsForTest() {
+    return mFile.exists();
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java b/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java
new file mode 100644
index 0000000..a325cc2
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/store/imap/ImapUtility.java
@@ -0,0 +1,122 @@
+/*
+ * 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.voicemail.impl.mail.store.imap;
+
+import com.android.voicemail.impl.mail.utils.LogUtils;
+import java.util.ArrayList;
+
+/** Utility methods for use with IMAP. */
+public class ImapUtility {
+  public static final String TAG = "ImapUtility";
+  /**
+   * Apply quoting rules per IMAP RFC, quoted = DQUOTE *QUOTED-CHAR DQUOTE QUOTED-CHAR = <any
+   * TEXT-CHAR except quoted-specials> / "\" quoted-specials quoted-specials = DQUOTE / "\"
+   *
+   * <p>This is used primarily for IMAP login, but might be useful elsewhere.
+   *
+   * <p>NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check for
+   * trouble chars before calling the replace functions.
+   *
+   * @param s The string to be quoted.
+   * @return A copy of the string, having undergone quoting as described above
+   */
+  public static String imapQuoted(String s) {
+
+    // First, quote any backslashes by replacing \ with \\
+    // regex Pattern:  \\    (Java string const = \\\\)
+    // Substitute:     \\\\  (Java string const = \\\\\\\\)
+    String result = s.replaceAll("\\\\", "\\\\\\\\");
+
+    // Then, quote any double-quotes by replacing " with \"
+    // regex Pattern:  "    (Java string const = \")
+    // Substitute:     \\"  (Java string const = \\\\\")
+    result = result.replaceAll("\"", "\\\\\"");
+
+    // return string with quotes around it
+    return "\"" + result + "\"";
+  }
+
+  /**
+   * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a list of
+   * individual numbers. If the set is invalid, an empty array is returned.
+   *
+   * <pre>
+   * sequence-number = nz-number / "*"
+   * sequence-range  = sequence-number ":" sequence-number
+   * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+   * </pre>
+   */
+  public static String[] getImapSequenceValues(String set) {
+    ArrayList<String> list = new ArrayList<String>();
+    if (set != null) {
+      String[] setItems = set.split(",");
+      for (String item : setItems) {
+        if (item.indexOf(':') == -1) {
+          // simple item
+          try {
+            Integer.parseInt(item); // Don't need the value; just ensure it's valid
+            list.add(item);
+          } catch (NumberFormatException e) {
+            LogUtils.d(TAG, "Invalid UID value", e);
+          }
+        } else {
+          // range
+          for (String rangeItem : getImapRangeValues(item)) {
+            list.add(rangeItem);
+          }
+        }
+      }
+    }
+    String[] stringList = new String[list.size()];
+    return list.toArray(stringList);
+  }
+
+  /**
+   * Expand the given number range into a list of individual numbers. If the range is not valid, an
+   * empty array is returned.
+   *
+   * <pre>
+   * sequence-number = nz-number / "*"
+   * sequence-range  = sequence-number ":" sequence-number
+   * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+   * </pre>
+   */
+  public static String[] getImapRangeValues(String range) {
+    ArrayList<String> list = new ArrayList<String>();
+    try {
+      if (range != null) {
+        int colonPos = range.indexOf(':');
+        if (colonPos > 0) {
+          int first = Integer.parseInt(range.substring(0, colonPos));
+          int second = Integer.parseInt(range.substring(colonPos + 1));
+          if (first < second) {
+            for (int i = first; i <= second; i++) {
+              list.add(Integer.toString(i));
+            }
+          } else {
+            for (int i = first; i >= second; i--) {
+              list.add(Integer.toString(i));
+            }
+          }
+        }
+      }
+    } catch (NumberFormatException e) {
+      LogUtils.d(TAG, "Invalid range value", e);
+    }
+    String[] stringList = new String[list.size()];
+    return list.toArray(stringList);
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java b/java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java
new file mode 100644
index 0000000..c358610
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utility/CountingOutputStream.java
@@ -0,0 +1,48 @@
+/*
+ * 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.voicemail.impl.mail.utility;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A simple pass-thru OutputStream that also counts how many bytes are written to it and makes that
+ * count available to callers.
+ */
+public class CountingOutputStream extends OutputStream {
+  private long mCount;
+  private final OutputStream mOutputStream;
+
+  public CountingOutputStream(OutputStream outputStream) {
+    mOutputStream = outputStream;
+  }
+
+  public long getCount() {
+    return mCount;
+  }
+
+  @Override
+  public void write(byte[] buffer, int offset, int count) throws IOException {
+    mOutputStream.write(buffer, offset, count);
+    mCount += count;
+  }
+
+  @Override
+  public void write(int oneByte) throws IOException {
+    mOutputStream.write(oneByte);
+    mCount++;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java b/java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java
new file mode 100644
index 0000000..72649ac
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utility/EOLConvertingOutputStream.java
@@ -0,0 +1,48 @@
+/*
+ * 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.voicemail.impl.mail.utility;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class EOLConvertingOutputStream extends FilterOutputStream {
+  int lastChar;
+
+  public EOLConvertingOutputStream(OutputStream out) {
+    super(out);
+  }
+
+  @Override
+  public void write(int oneByte) throws IOException {
+    if (oneByte == '\n') {
+      if (lastChar != '\r') {
+        super.write('\r');
+      }
+    }
+    super.write(oneByte);
+    lastChar = oneByte;
+  }
+
+  @Override
+  public void flush() throws IOException {
+    if (lastChar == '\r') {
+      super.write('\n');
+      lastChar = '\n';
+    }
+    super.flush();
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/utils/LogUtils.java b/java/com/android/voicemail/impl/mail/utils/LogUtils.java
new file mode 100644
index 0000000..f6c3c6b
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utils/LogUtils.java
@@ -0,0 +1,345 @@
+/**
+ * Copyright (c) 2015 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.voicemail.impl.mail.utils;
+
+import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.voicemail.impl.VvmLog;
+import java.util.List;
+
+public class LogUtils {
+  public static final String TAG = "Email Log";
+
+  private static final String ACCOUNT_PREFIX = "account:";
+
+  /** Priority constant for the println method; use LogUtils.v. */
+  public static final int VERBOSE = Log.VERBOSE;
+
+  /** Priority constant for the println method; use LogUtils.d. */
+  public static final int DEBUG = Log.DEBUG;
+
+  /** Priority constant for the println method; use LogUtils.i. */
+  public static final int INFO = Log.INFO;
+
+  /** Priority constant for the println method; use LogUtils.w. */
+  public static final int WARN = Log.WARN;
+
+  /** Priority constant for the println method; use LogUtils.e. */
+  public static final int ERROR = Log.ERROR;
+
+  /**
+   * Used to enable/disable logging that we don't want included in production releases. This should
+   * be set to DEBUG for production releases, and VERBOSE for internal builds.
+   */
+  private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+
+  private static Boolean sDebugLoggingEnabledForTests = null;
+
+  /** Enable debug logging for unit tests. */
+  @VisibleForTesting
+  public static void setDebugLoggingEnabledForTests(boolean enabled) {
+    setDebugLoggingEnabledForTestsInternal(enabled);
+  }
+
+  protected static void setDebugLoggingEnabledForTestsInternal(boolean enabled) {
+    sDebugLoggingEnabledForTests = Boolean.valueOf(enabled);
+  }
+
+  /** Returns true if the build configuration prevents debug logging. */
+  @VisibleForTesting
+  public static boolean buildPreventsDebugLogging() {
+    return MAX_ENABLED_LOG_LEVEL > VERBOSE;
+  }
+
+  /** Returns a boolean indicating whether debug logging is enabled. */
+  protected static boolean isDebugLoggingEnabled(String tag) {
+    if (buildPreventsDebugLogging()) {
+      return false;
+    }
+    if (sDebugLoggingEnabledForTests != null) {
+      return sDebugLoggingEnabledForTests.booleanValue();
+    }
+    return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG);
+  }
+
+  /**
+   * Returns a String for the specified content provider uri. This will do sanitation of the uri to
+   * remove PII if debug logging is not enabled.
+   */
+  public static String contentUriToString(final Uri uri) {
+    return contentUriToString(TAG, uri);
+  }
+
+  /**
+   * Returns a String for the specified content provider uri. This will do sanitation of the uri to
+   * remove PII if debug logging is not enabled.
+   */
+  public static String contentUriToString(String tag, Uri uri) {
+    if (isDebugLoggingEnabled(tag)) {
+      // Debug logging has been enabled, so log the uri as is
+      return uri.toString();
+    } else {
+      // Debug logging is not enabled, we want to remove the email address from the uri.
+      List<String> pathSegments = uri.getPathSegments();
+
+      Uri.Builder builder =
+          new Uri.Builder()
+              .scheme(uri.getScheme())
+              .authority(uri.getAuthority())
+              .query(uri.getQuery())
+              .fragment(uri.getFragment());
+
+      // This assumes that the first path segment is the account
+      final String account = pathSegments.get(0);
+
+      builder = builder.appendPath(sanitizeAccountName(account));
+      for (int i = 1; i < pathSegments.size(); i++) {
+        builder.appendPath(pathSegments.get(i));
+      }
+      return builder.toString();
+    }
+  }
+
+  /** Sanitizes an account name. If debug logging is not enabled, a sanitized name is returned. */
+  public static String sanitizeAccountName(String accountName) {
+    if (TextUtils.isEmpty(accountName)) {
+      return "";
+    }
+
+    return ACCOUNT_PREFIX + sanitizeName(TAG, accountName);
+  }
+
+  public static String sanitizeName(final String tag, final String name) {
+    if (TextUtils.isEmpty(name)) {
+      return "";
+    }
+
+    if (isDebugLoggingEnabled(tag)) {
+      return name;
+    }
+
+    return String.valueOf(name.hashCode());
+  }
+
+  /**
+   * Checks to see whether or not a log for the specified tag is loggable at the specified level.
+   */
+  public static boolean isLoggable(String tag, int level) {
+    if (MAX_ENABLED_LOG_LEVEL > level) {
+      return false;
+    }
+    return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level);
+  }
+
+  /**
+   * Send a {@link #VERBOSE} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void v(String tag, String format, Object... args) {
+    if (isLoggable(tag, VERBOSE)) {
+      VvmLog.v(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #VERBOSE} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void v(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, VERBOSE)) {
+      VvmLog.v(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * Send a {@link #DEBUG} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void d(String tag, String format, Object... args) {
+    if (isLoggable(tag, DEBUG)) {
+      VvmLog.d(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #DEBUG} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void d(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, DEBUG)) {
+      VvmLog.d(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * Send a {@link #INFO} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void i(String tag, String format, Object... args) {
+    if (isLoggable(tag, INFO)) {
+      VvmLog.i(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #INFO} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void i(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, INFO)) {
+      VvmLog.i(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * Send a {@link #WARN} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void w(String tag, String format, Object... args) {
+    if (isLoggable(tag, WARN)) {
+      VvmLog.w(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #WARN} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void w(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, WARN)) {
+      VvmLog.w(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * Send a {@link #ERROR} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void e(String tag, String format, Object... args) {
+    if (isLoggable(tag, ERROR)) {
+      VvmLog.e(tag, String.format(format, args));
+    }
+  }
+
+  /**
+   * Send a {@link #ERROR} log message.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void e(String tag, Throwable tr, String format, Object... args) {
+    if (isLoggable(tag, ERROR)) {
+      VvmLog.e(tag, String.format(format, args), tr);
+    }
+  }
+
+  /**
+   * What a Terrible Failure: Report a condition that should never happen. The error will always be
+   * logged at level ASSERT with the call stack. Depending on system configuration, a report may be
+   * added to the {@link android.os.DropBoxManager} and/or the process may be terminated immediately
+   * with an error dialog.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void wtf(String tag, String format, Object... args) {
+    VvmLog.wtf(tag, String.format(format, args), new Error());
+  }
+
+  /**
+   * What a Terrible Failure: Report a condition that should never happen. The error will always be
+   * logged at level ASSERT with the call stack. Depending on system configuration, a report may be
+   * added to the {@link android.os.DropBoxManager} and/or the process may be terminated immediately
+   * with an error dialog.
+   *
+   * @param tag Used to identify the source of a log message. It usually identifies the class or
+   *     activity where the log call occurs.
+   * @param tr An exception to log
+   * @param format the format string (see {@link java.util.Formatter#format})
+   * @param args the list of arguments passed to the formatter. If there are more arguments than
+   *     required by {@code format}, additional arguments are ignored.
+   */
+  public static void wtf(String tag, Throwable tr, String format, Object... args) {
+    VvmLog.wtf(tag, String.format(format, args), tr);
+  }
+
+  public static String byteToHex(int b) {
+    return byteToHex(new StringBuilder(), b).toString();
+  }
+
+  public static StringBuilder byteToHex(StringBuilder sb, int b) {
+    b &= 0xFF;
+    sb.append("0123456789ABCDEF".charAt(b >> 4));
+    sb.append("0123456789ABCDEF".charAt(b & 0xF));
+    return sb;
+  }
+}
diff --git a/java/com/android/voicemail/impl/mail/utils/Utility.java b/java/com/android/voicemail/impl/mail/utils/Utility.java
new file mode 100644
index 0000000..4db1681
--- /dev/null
+++ b/java/com/android/voicemail/impl/mail/utils/Utility.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2015 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.voicemail.impl.mail.utils;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+
+/** Simple utility methods used in email functions. */
+public class Utility {
+  public static final Charset ASCII = Charset.forName("US-ASCII");
+
+  public static final String[] EMPTY_STRINGS = new String[0];
+
+  /**
+   * Returns a concatenated string containing the output of every Object's toString() method, each
+   * separated by the given separator character.
+   */
+  public static String combine(Object[] parts, char separator) {
+    if (parts == null) {
+      return null;
+    }
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < parts.length; i++) {
+      sb.append(parts[i].toString());
+      if (i < parts.length - 1) {
+        sb.append(separator);
+      }
+    }
+    return sb.toString();
+  }
+
+  /** Converts a String to ASCII bytes */
+  public static byte[] toAscii(String s) {
+    return encode(ASCII, s);
+  }
+
+  /** Builds a String from ASCII bytes */
+  public static String fromAscii(byte[] b) {
+    return decode(ASCII, b);
+  }
+
+  private static byte[] encode(Charset charset, String s) {
+    if (s == null) {
+      return null;
+    }
+    final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
+    final byte[] bytes = new byte[buffer.limit()];
+    buffer.get(bytes);
+    return bytes;
+  }
+
+  private static String decode(Charset charset, byte[] b) {
+    if (b == null) {
+      return null;
+    }
+    final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
+    return new String(cb.array(), 0, cb.length());
+  }
+
+  public static ByteArrayInputStream streamFromAsciiString(String ascii) {
+    return new ByteArrayInputStream(toAscii(ascii));
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/CvvmProtocol.java b/java/com/android/voicemail/impl/protocol/CvvmProtocol.java
new file mode 100644
index 0000000..a4b54f6
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/CvvmProtocol.java
@@ -0,0 +1,59 @@
+/*
+ * 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.voicemail.impl.protocol;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.sms.OmtpCvvmMessageSender;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+
+/**
+ * A flavor of OMTP protocol with a different mobile originated (MO) format
+ *
+ * <p>Used by carriers such as T-Mobile
+ */
+public class CvvmProtocol extends VisualVoicemailProtocol {
+
+  private static String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+  private static String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
+  private static String IMAP_CLOSE_NUT = "CLOSE_NUT";
+
+  @Override
+  public OmtpMessageSender createMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    return new OmtpCvvmMessageSender(
+        context, phoneAccountHandle, applicationPort, destinationNumber);
+  }
+
+  @Override
+  public String getCommand(String command) {
+    if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
+      return IMAP_CHANGE_TUI_PWD_FORMAT;
+    }
+    if (command == OmtpConstants.IMAP_CLOSE_NUT) {
+      return IMAP_CLOSE_NUT;
+    }
+    if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
+      return IMAP_CHANGE_VM_LANG_FORMAT;
+    }
+    return super.getCommand(command);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/OmtpProtocol.java b/java/com/android/voicemail/impl/protocol/OmtpProtocol.java
new file mode 100644
index 0000000..27aab8a
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/OmtpProtocol.java
@@ -0,0 +1,42 @@
+/*
+ * 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.voicemail.impl.protocol;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.OmtpStandardMessageSender;
+
+public class OmtpProtocol extends VisualVoicemailProtocol {
+
+  @Override
+  public OmtpMessageSender createMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    return new OmtpStandardMessageSender(
+        context,
+        phoneAccountHandle,
+        applicationPort,
+        destinationNumber,
+        OmtpConstants.CLIENT_TYPE_GOOGLE_10,
+        OmtpConstants.PROTOCOL_VERSION1_1,
+        null /*clientPrefix*/);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/ProtocolHelper.java b/java/com/android/voicemail/impl/protocol/ProtocolHelper.java
new file mode 100644
index 0000000..4d2e7cc
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/ProtocolHelper.java
@@ -0,0 +1,44 @@
+/*
+ * 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.voicemail.impl.protocol;
+
+import android.text.TextUtils;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+
+public class ProtocolHelper {
+
+  private static final String TAG = "ProtocolHelper";
+
+  public static OmtpMessageSender getMessageSender(
+      VisualVoicemailProtocol protocol, OmtpVvmCarrierConfigHelper config) {
+
+    int applicationPort = config.getApplicationPort();
+    String destinationNumber = config.getDestinationNumber();
+    if (TextUtils.isEmpty(destinationNumber)) {
+      VvmLog.w(TAG, "No destination number for this carrier.");
+      return null;
+    }
+
+    return protocol.createMessageSender(
+        config.getContext(),
+        config.getPhoneAccountHandle(),
+        (short) applicationPort,
+        destinationNumber);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java
new file mode 100644
index 0000000..6cf82f1
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocol.java
@@ -0,0 +1,106 @@
+/*
+ * 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.voicemail.impl.protocol;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.DefaultOmtpEventHandler;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.StatusMessage;
+
+public abstract class VisualVoicemailProtocol {
+
+  /** Activation should cause the carrier to respond with a STATUS SMS. */
+  public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) {
+    OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+    if (messageSender != null) {
+      messageSender.requestVvmActivation(sentIntent);
+    }
+  }
+
+  public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
+    OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+    if (messageSender != null) {
+      messageSender.requestVvmDeactivation(null);
+    }
+  }
+
+  public boolean supportsProvisioning() {
+    return false;
+  }
+
+  public void startProvisioning(
+      ActivationTask task,
+      PhoneAccountHandle handle,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor editor,
+      StatusMessage message,
+      Bundle data) {
+    // Do nothing
+  }
+
+  public void requestStatus(OmtpVvmCarrierConfigHelper config, @Nullable PendingIntent sentIntent) {
+    OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+    if (messageSender != null) {
+      messageSender.requestVvmStatus(sentIntent);
+    }
+  }
+
+  public abstract OmtpMessageSender createMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber);
+
+  /**
+   * Translate an OMTP IMAP command to the protocol specific one. For example, changing the TUI
+   * password on OMTP is XCHANGE_TUI_PWD, but on CVVM and VVM3 it is CHANGE_TUI_PWD.
+   *
+   * @param command A String command in {@link OmtpConstants}, the exact
+   *     instance should be used instead of its' value.
+   * @returns Translated command, or {@code null} if not available in this protocol
+   */
+  public String getCommand(String command) {
+    return command;
+  }
+
+  public void handleEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    DefaultOmtpEventHandler.handleEvent(context, config, status, event);
+  }
+
+  /**
+   * Given an VVM SMS with an unknown {@code event}, let the protocol attempt to translate it into
+   * an equivalent STATUS SMS. Returns {@code null} if it cannot be translated.
+   */
+  @Nullable
+  public Bundle translateStatusSmsBundle(
+      OmtpVvmCarrierConfigHelper config, String event, Bundle data) {
+    return null;
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java
new file mode 100644
index 0000000..056fb2e
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/VisualVoicemailProtocolFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.voicemail.impl.protocol;
+
+import android.content.res.Resources;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import com.android.voicemail.impl.VvmLog;
+
+public class VisualVoicemailProtocolFactory {
+
+  private static final String TAG = "VvmProtocolFactory";
+
+  private static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
+
+  @Nullable
+  public static VisualVoicemailProtocol create(Resources resources, String type) {
+    if (type == null) {
+      return null;
+    }
+    switch (type) {
+      case TelephonyManager.VVM_TYPE_OMTP:
+        return new OmtpProtocol();
+      case TelephonyManager.VVM_TYPE_CVVM:
+        return new CvvmProtocol();
+      case VVM_TYPE_VVM3:
+        return new Vvm3Protocol();
+      default:
+        VvmLog.e(TAG, "Unexpected visual voicemail type: " + type);
+    }
+    return null;
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
new file mode 100644
index 0000000..8bc3cc2
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3EventHandler.java
@@ -0,0 +1,307 @@
+/*
+ * 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.voicemail.impl.protocol;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.IntDef;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.DefaultOmtpEventHandler;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpEvents.Type;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.settings.VoicemailChangePinActivity;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom
+ * error codes into the voicemail status table so support on the dialer side is required.
+ *
+ * <p>TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured.
+ */
+public class Vvm3EventHandler {
+
+  private static final String TAG = "Vvm3EventHandler";
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+    VMS_DNS_FAILURE,
+    VMG_DNS_FAILURE,
+    SPG_DNS_FAILURE,
+    VMS_NO_CELLULAR,
+    VMG_NO_CELLULAR,
+    SPG_NO_CELLULAR,
+    VMS_TIMEOUT,
+    VMG_TIMEOUT,
+    STATUS_SMS_TIMEOUT,
+    SUBSCRIBER_BLOCKED,
+    UNKNOWN_USER,
+    UNKNOWN_DEVICE,
+    INVALID_PASSWORD,
+    MAILBOX_NOT_INITIALIZED,
+    SERVICE_NOT_PROVISIONED,
+    SERVICE_NOT_ACTIVATED,
+    USER_BLOCKED,
+    IMAP_GETQUOTA_ERROR,
+    IMAP_SELECT_ERROR,
+    IMAP_ERROR,
+    VMG_INTERNAL_ERROR,
+    VMG_DB_ERROR,
+    VMG_COMMUNICATION_ERROR,
+    SPG_URL_NOT_FOUND,
+    VMG_UNKNOWN_ERROR,
+    PIN_NOT_SET
+  })
+  public @interface ErrorCode {}
+
+  public static final int VMS_DNS_FAILURE = -9001;
+  public static final int VMG_DNS_FAILURE = -9002;
+  public static final int SPG_DNS_FAILURE = -9003;
+  public static final int VMS_NO_CELLULAR = -9004;
+  public static final int VMG_NO_CELLULAR = -9005;
+  public static final int SPG_NO_CELLULAR = -9006;
+  public static final int VMS_TIMEOUT = -9007;
+  public static final int VMG_TIMEOUT = -9008;
+  public static final int STATUS_SMS_TIMEOUT = -9009;
+
+  public static final int SUBSCRIBER_BLOCKED = -9990;
+  public static final int UNKNOWN_USER = -9991;
+  public static final int UNKNOWN_DEVICE = -9992;
+  public static final int INVALID_PASSWORD = -9993;
+  public static final int MAILBOX_NOT_INITIALIZED = -9994;
+  public static final int SERVICE_NOT_PROVISIONED = -9995;
+  public static final int SERVICE_NOT_ACTIVATED = -9996;
+  public static final int USER_BLOCKED = -9998;
+  public static final int IMAP_GETQUOTA_ERROR = -9997;
+  public static final int IMAP_SELECT_ERROR = -9989;
+  public static final int IMAP_ERROR = -9999;
+
+  public static final int VMG_INTERNAL_ERROR = -101;
+  public static final int VMG_DB_ERROR = -102;
+  public static final int VMG_COMMUNICATION_ERROR = -103;
+  public static final int SPG_URL_NOT_FOUND = -301;
+
+  // Non VVM3 codes:
+  public static final int VMG_UNKNOWN_ERROR = -1;
+  public static final int PIN_NOT_SET = -100;
+  // STATUS SMS returned st=U and rc!=2. The user cannot be provisioned and must contact customer
+  // support.
+  public static final int SUBSCRIBER_UNKNOWN = -99;
+
+  public static void handleEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    boolean handled = false;
+    switch (event.getType()) {
+      case Type.CONFIGURATION:
+        handled = handleConfigurationEvent(context, status, event);
+        break;
+      case Type.DATA_CHANNEL:
+        handled = handleDataChannelEvent(status, event);
+        break;
+      case Type.NOTIFICATION_CHANNEL:
+        handled = handleNotificationChannelEvent(status, event);
+        break;
+      case Type.OTHER:
+        handled = handleOtherEvent(status, event);
+        break;
+      default:
+        VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
+    }
+    if (!handled) {
+      DefaultOmtpEventHandler.handleEvent(context, config, status, event);
+    }
+  }
+
+  private static boolean handleConfigurationEvent(
+      Context context, VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case CONFIG_REQUEST_STATUS_SUCCESS:
+        if (!isPinRandomized(context, status.getPhoneAccountHandle())) {
+          return false;
+        } else {
+          postError(status, PIN_NOT_SET);
+        }
+        break;
+      case CONFIG_ACTIVATING_SUBSEQUENT:
+        if (isPinRandomized(context, status.getPhoneAccountHandle())) {
+          status.setConfigurationState(PIN_NOT_SET);
+        } else {
+          status.setConfigurationState(Status.CONFIGURATION_STATE_OK);
+        }
+        status
+            .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+            .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+            .apply();
+        break;
+      case CONFIG_DEFAULT_PIN_REPLACED:
+        postError(status, PIN_NOT_SET);
+        break;
+      case CONFIG_STATUS_SMS_TIME_OUT:
+        postError(status, STATUS_SMS_TIMEOUT);
+        break;
+      default:
+        return false;
+    }
+    return true;
+  }
+
+  private static boolean handleDataChannelEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case DATA_NO_CONNECTION:
+      case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
+      case DATA_ALL_SOCKET_CONNECTION_FAILED:
+        postError(status, VMS_NO_CELLULAR);
+        break;
+      case DATA_SSL_INVALID_HOST_NAME:
+      case DATA_CANNOT_ESTABLISH_SSL_SESSION:
+      case DATA_IOE_ON_OPEN:
+        postError(status, VMS_TIMEOUT);
+        break;
+      case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
+        postError(status, VMS_DNS_FAILURE);
+        break;
+      case DATA_BAD_IMAP_CREDENTIAL:
+        postError(status, IMAP_ERROR);
+        break;
+      case DATA_AUTH_UNKNOWN_USER:
+        postError(status, UNKNOWN_USER);
+        break;
+      case DATA_AUTH_UNKNOWN_DEVICE:
+        postError(status, UNKNOWN_DEVICE);
+        break;
+      case DATA_AUTH_INVALID_PASSWORD:
+        postError(status, INVALID_PASSWORD);
+        break;
+      case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
+        postError(status, MAILBOX_NOT_INITIALIZED);
+        break;
+      case DATA_AUTH_SERVICE_NOT_PROVISIONED:
+        postError(status, SERVICE_NOT_PROVISIONED);
+        break;
+      case DATA_AUTH_SERVICE_NOT_ACTIVATED:
+        postError(status, SERVICE_NOT_ACTIVATED);
+        break;
+      case DATA_AUTH_USER_IS_BLOCKED:
+        postError(status, USER_BLOCKED);
+        break;
+      case DATA_REJECTED_SERVER_RESPONSE:
+      case DATA_INVALID_INITIAL_SERVER_RESPONSE:
+      case DATA_SSL_EXCEPTION:
+        postError(status, IMAP_ERROR);
+        break;
+      default:
+        return false;
+    }
+    return true;
+  }
+
+  private static boolean handleNotificationChannelEvent(
+      VoicemailStatus.Editor unusedStatus, OmtpEvents unusedEvent) {
+    return false;
+  }
+
+  private static boolean handleOtherEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+    switch (event) {
+      case VVM3_NEW_USER_SETUP_FAILED:
+        postError(status, MAILBOX_NOT_INITIALIZED);
+        break;
+      case VVM3_VMG_DNS_FAILURE:
+        postError(status, VMG_DNS_FAILURE);
+        break;
+      case VVM3_SPG_DNS_FAILURE:
+        postError(status, SPG_DNS_FAILURE);
+        break;
+      case VVM3_VMG_CONNECTION_FAILED:
+        postError(status, VMG_NO_CELLULAR);
+        break;
+      case VVM3_SPG_CONNECTION_FAILED:
+        postError(status, SPG_NO_CELLULAR);
+        break;
+      case VVM3_VMG_TIMEOUT:
+        postError(status, VMG_TIMEOUT);
+        break;
+      case VVM3_SUBSCRIBER_PROVISIONED:
+        postError(status, SERVICE_NOT_ACTIVATED);
+        break;
+      case VVM3_SUBSCRIBER_BLOCKED:
+        postError(status, SUBSCRIBER_BLOCKED);
+        break;
+      case VVM3_SUBSCRIBER_UNKNOWN:
+        postError(status, SUBSCRIBER_UNKNOWN);
+        break;
+      default:
+        return false;
+    }
+    return true;
+  }
+
+  private static void postError(VoicemailStatus.Editor editor, @ErrorCode int errorCode) {
+    switch (errorCode) {
+      case VMG_DNS_FAILURE:
+      case SPG_DNS_FAILURE:
+      case VMG_NO_CELLULAR:
+      case SPG_NO_CELLULAR:
+      case VMG_TIMEOUT:
+      case SUBSCRIBER_BLOCKED:
+      case UNKNOWN_USER:
+      case UNKNOWN_DEVICE:
+      case INVALID_PASSWORD:
+      case MAILBOX_NOT_INITIALIZED:
+      case SERVICE_NOT_PROVISIONED:
+      case SERVICE_NOT_ACTIVATED:
+      case USER_BLOCKED:
+      case VMG_UNKNOWN_ERROR:
+      case SPG_URL_NOT_FOUND:
+      case VMG_INTERNAL_ERROR:
+      case VMG_DB_ERROR:
+      case VMG_COMMUNICATION_ERROR:
+      case PIN_NOT_SET:
+      case SUBSCRIBER_UNKNOWN:
+        editor.setConfigurationState(errorCode);
+        break;
+      case VMS_NO_CELLULAR:
+      case VMS_DNS_FAILURE:
+      case VMS_TIMEOUT:
+      case IMAP_GETQUOTA_ERROR:
+      case IMAP_SELECT_ERROR:
+      case IMAP_ERROR:
+        editor.setDataChannelState(errorCode);
+        break;
+      case STATUS_SMS_TIMEOUT:
+        editor.setNotificationChannelState(errorCode);
+        break;
+      default:
+        VvmLog.wtf(TAG, "unknown error code: " + errorCode);
+    }
+    editor.apply();
+  }
+
+  private static boolean isPinRandomized(Context context, PhoneAccountHandle phoneAccountHandle) {
+    if (phoneAccountHandle == null) {
+      // This should never happen.
+      VvmLog.e(TAG, "status editor has null phone account handle");
+      return false;
+    }
+    return VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
new file mode 100644
index 0000000..f293a4c
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3Protocol.java
@@ -0,0 +1,305 @@
+/*
+ * 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.voicemail.impl.protocol;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.settings.VoicemailChangePinActivity;
+import com.android.voicemail.impl.sms.OmtpMessageSender;
+import com.android.voicemail.impl.sms.StatusMessage;
+import com.android.voicemail.impl.sms.Vvm3MessageSender;
+import com.android.voicemail.impl.sync.VvmNetworkRequest;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.Locale;
+
+/**
+ * A flavor of OMTP protocol with a different provisioning process
+ *
+ * <p>Used by carriers such as Verizon Wireless
+ */
+@TargetApi(VERSION_CODES.O)
+public class Vvm3Protocol extends VisualVoicemailProtocol {
+
+  private static final String TAG = "Vvm3Protocol";
+
+  private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED";
+  private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd";
+  private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS";
+  private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url";
+
+  private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+  private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
+  private static final String IMAP_CLOSE_NUT = "CLOSE_NUT";
+
+  private static final String ISO639_Spanish = "es";
+
+  /**
+   * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link
+   * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value, the
+   * user can self-provision visual voicemail service. For other response codes, the user must
+   * contact customer support to resolve the issue.
+   */
+  private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2";
+
+  // Default prompt level when using the telephone user interface.
+  // Standard prompt when the user call into the voicemail, and no prompts when someone else is
+  // leaving a voicemail.
+  private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5";
+  private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6";
+
+  private static final int DEFAULT_PIN_LENGTH = 6;
+
+  @Override
+  public void startActivation(
+      OmtpVvmCarrierConfigHelper config, @Nullable PendingIntent sentIntent) {
+    // VVM3 does not support activation SMS.
+    // Send a status request which will start the provisioning process if the user is not
+    // provisioned.
+    VvmLog.i(TAG, "Activating");
+    config.requestStatus(sentIntent);
+  }
+
+  @Override
+  public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
+    // VVM3 does not support deactivation.
+    // do nothing.
+  }
+
+  @Override
+  public boolean supportsProvisioning() {
+    return true;
+  }
+
+  @Override
+  public void startProvisioning(
+      ActivationTask task,
+      PhoneAccountHandle phoneAccountHandle,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      StatusMessage message,
+      Bundle data) {
+    VvmLog.i(TAG, "start vvm3 provisioning");
+    if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) {
+      VvmLog.i(TAG, "Provisioning status: Unknown");
+      if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE.equals(message.getReturnCode())) {
+        VvmLog.i(TAG, "Self provisioning available, subscribing");
+        new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe();
+      } else {
+        config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN);
+      }
+    } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) {
+      VvmLog.i(TAG, "setting up new user");
+      // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
+      VisualVoicemailPreferences prefs =
+          new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle);
+      message.putStatus(prefs.edit()).apply();
+
+      startProvisionNewUser(task, phoneAccountHandle, config, status, message);
+    } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) {
+      VvmLog.i(TAG, "User provisioned but not activated, disabling VVM");
+      VisualVoicemailSettingsUtil.setEnabled(config.getContext(), phoneAccountHandle, false);
+    } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) {
+      VvmLog.i(TAG, "User blocked");
+      config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED);
+    }
+  }
+
+  @Override
+  public OmtpMessageSender createMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort, destinationNumber);
+  }
+
+  @Override
+  public void handleEvent(
+      Context context,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      OmtpEvents event) {
+    Vvm3EventHandler.handleEvent(context, config, status, event);
+  }
+
+  @Override
+  public String getCommand(String command) {
+    switch (command) {
+      case OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT:
+        return IMAP_CHANGE_TUI_PWD_FORMAT;
+      case OmtpConstants.IMAP_CLOSE_NUT:
+        return IMAP_CLOSE_NUT;
+      case OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT:
+        return IMAP_CHANGE_VM_LANG_FORMAT;
+      default:
+        return super.getCommand(command);
+    }
+  }
+
+  @Override
+  public Bundle translateStatusSmsBundle(
+      OmtpVvmCarrierConfigHelper config, String event, Bundle data) {
+    // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned
+    // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status
+    // so provisioning can be done.
+    if (!SMS_EVENT_UNRECOGNIZED.equals(event)) {
+      return null;
+    }
+    if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) {
+      return null;
+    }
+    Bundle bundle = new Bundle();
+    bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN);
+    bundle.putString(
+        OmtpConstants.RETURN_CODE, VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE);
+    String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY);
+    if (TextUtils.isEmpty(vmgUrl)) {
+      VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config");
+      return null;
+    }
+    bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl);
+    VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS");
+    return bundle;
+  }
+
+  private void startProvisionNewUser(
+      ActivationTask task,
+      PhoneAccountHandle phoneAccountHandle,
+      OmtpVvmCarrierConfigHelper config,
+      VoicemailStatus.Editor status,
+      StatusMessage message) {
+    try (NetworkWrapper wrapper =
+        VvmNetworkRequest.getNetwork(config, phoneAccountHandle, status)) {
+      Network network = wrapper.get();
+
+      VvmLog.i(TAG, "new user: network available");
+      try (ImapHelper helper =
+          new ImapHelper(config.getContext(), phoneAccountHandle, network, status)) {
+        // VVM3 has inconsistent error language code to OMTP. Just issue a raw command
+        // here.
+        // TODO(b/29082671): use LocaleList
+        if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_Spanish).getLanguage())) {
+          // Spanish
+          helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS);
+        } else {
+          // English
+          helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS);
+        }
+        VvmLog.i(TAG, "new user: language set");
+
+        if (setPin(config.getContext(), phoneAccountHandle, helper, message)) {
+          // Only close new user tutorial if the PIN has been changed.
+          helper.closeNewUserTutorial();
+          VvmLog.i(TAG, "new user: NUT closed");
+
+          config.requestStatus(null);
+        }
+      } catch (InitializingException | MessagingException | IOException e) {
+        config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED);
+        task.fail();
+        VvmLog.e(TAG, e.toString());
+      }
+    } catch (RequestFailedException e) {
+      config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+      task.fail();
+    }
+  }
+
+  private static boolean setPin(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      ImapHelper helper,
+      StatusMessage message)
+      throws IOException, MessagingException {
+    String defaultPin = getDefaultPin(message);
+    if (defaultPin == null) {
+      VvmLog.i(TAG, "cannot generate default PIN");
+      return false;
+    }
+
+    if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) {
+      // The pin was already set
+      VvmLog.i(TAG, "PIN already set");
+      return true;
+    }
+    String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle));
+    if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) {
+      VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin);
+      helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED);
+    }
+    VvmLog.i(TAG, "new user: PIN set");
+    return true;
+  }
+
+  @Nullable
+  private static String getDefaultPin(StatusMessage message) {
+    // The IMAP username is [phone number]@example.com
+    String username = message.getImapUserName();
+    try {
+      String number = username.substring(0, username.indexOf('@'));
+      if (number.length() < 4) {
+        VvmLog.e(TAG, "unable to extract number from IMAP username");
+        return null;
+      }
+      return "1" + number.substring(number.length() - 4);
+    } catch (StringIndexOutOfBoundsException e) {
+      VvmLog.e(TAG, "unable to extract number from IMAP username");
+      return null;
+    }
+  }
+
+  private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) {
+    VisualVoicemailPreferences preferences =
+        new VisualVoicemailPreferences(context, phoneAccountHandle);
+    // The OMTP pin length format is {min}-{max}
+    String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
+    if (lengths.length == 2) {
+      try {
+        return Integer.parseInt(lengths[0]);
+      } catch (NumberFormatException e) {
+        return DEFAULT_PIN_LENGTH;
+      }
+    }
+    return DEFAULT_PIN_LENGTH;
+  }
+
+  private static String generatePin(int length) {
+    SecureRandom random = new SecureRandom();
+    return String.format(Locale.US, "%010d", Math.abs(random.nextLong())).substring(0, length);
+  }
+}
diff --git a/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java b/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java
new file mode 100644
index 0000000..c8a74c8
--- /dev/null
+++ b/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java
@@ -0,0 +1,334 @@
+/*
+ * 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.voicemail.impl.protocol;
+
+import android.annotation.TargetApi;
+import android.net.Network;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.style.URLSpan;
+import android.util.ArrayMap;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sync.VvmNetworkRequest;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.HurlStack;
+import com.android.volley.toolbox.RequestFuture;
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.toolbox.Volley;
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required
+ * when the user is unprovisioned. This could happen when the user is on a legacy service, or
+ * switched over from devices that used other type of visual voicemail.
+ *
+ * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find
+ * the self provisioning gateway URL that we can modify voicemail services.
+ *
+ * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us
+ * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the
+ * subscription. This link should be clicked through cellular network, and have cookies enabled.
+ *
+ * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or
+ * ready user.
+ */
+@TargetApi(VERSION_CODES.O)
+public class Vvm3Subscriber {
+
+  private static final String TAG = "Vvm3Subscriber";
+
+  private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL";
+  private static final String SPG_URL_TAG = "spgurl";
+  private static final String TRANSACTION_ID_TAG = "transactionid";
+  //language=XML
+  private static final String VMG_XML_REQUEST_FORMAT =
+      ""
+          + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+          + "<VMGVVMRequest>"
+          + "  <MessageHeader>"
+          + "    <transactionid>%1$s</transactionid>"
+          + "  </MessageHeader>"
+          + "  <MessageBody>"
+          + "    <mdn>%2$s</mdn>"
+          + "    <operation>%3$s</operation>"
+          + "    <source>Device</source>"
+          + "    <devicemodel>%4$s</devicemodel>"
+          + "  </MessageBody>"
+          + "</VMGVVMRequest>";
+
+  static final String VMG_URL_KEY = "vmg_url";
+
+  // Self provisioning POST key/values. VVM3 API 2.1.0 12.3
+  private static final String SPG_VZW_MDN_PARAM = "VZW_MDN";
+  private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE";
+  private static final String SPG_VZW_SERVICE_BASIC = "BVVM";
+  private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL";
+  // Value for all android device
+  private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G";
+  private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN";
+  private static final String SPG_APP_TOKEN = "q8e3t5u2o1";
+  private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM";
+  private static final String SPG_LANGUAGE_EN = "ENGLISH";
+
+  private static final String BASIC_SUBSCRIBE_LINK_TEXT = "Subscribe to Basic Visual Voice Mail";
+
+  private static final int REQUEST_TIMEOUT_SECONDS = 30;
+
+  private final ActivationTask mTask;
+  private final PhoneAccountHandle mHandle;
+  private final OmtpVvmCarrierConfigHelper mHelper;
+  private final VoicemailStatus.Editor mStatus;
+  private final Bundle mData;
+
+  private final String mNumber;
+
+  private RequestQueue mRequestQueue;
+
+  private static class ProvisioningException extends Exception {
+
+    public ProvisioningException(String message) {
+      super(message);
+    }
+  }
+
+  static {
+    // Set the default cookie handler to retain session data for the self provisioning gateway.
+    // Note; this is not ideal as it is application-wide, and can easily get clobbered.
+    // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually
+    // managing cookies will greatly increase complexity.
+    CookieManager cookieManager = new CookieManager();
+    CookieHandler.setDefault(cookieManager);
+  }
+
+  @WorkerThread
+  public Vvm3Subscriber(
+      ActivationTask task,
+      PhoneAccountHandle handle,
+      OmtpVvmCarrierConfigHelper helper,
+      VoicemailStatus.Editor status,
+      Bundle data) {
+    Assert.isNotMainThread();
+    mTask = task;
+    mHandle = handle;
+    mHelper = helper;
+    mStatus = status;
+    mData = data;
+
+    // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
+    // is not included in the status SMS, thus no other way to get the current phone number.
+    mNumber =
+        mHelper
+            .getContext()
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(mHandle)
+            .getLine1Number();
+  }
+
+  @WorkerThread
+  public void subscribe() {
+    Assert.isNotMainThread();
+    // Cellular data is required to subscribe.
+    // processSubscription() is called after network is available.
+    VvmLog.i(TAG, "Subscribing");
+
+    try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) {
+      Network network = wrapper.get();
+      VvmLog.d(TAG, "provisioning: network available");
+      mRequestQueue =
+          Volley.newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network));
+      processSubscription();
+    } catch (RequestFailedException e) {
+      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
+      mTask.fail();
+    }
+  }
+
+  private void processSubscription() {
+    try {
+      String gatewayUrl = getSelfProvisioningGateway();
+      String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
+      String subscribeLink = findSubscribeLink(selfProvisionResponse);
+      clickSubscribeLink(subscribeLink);
+    } catch (ProvisioningException e) {
+      VvmLog.e(TAG, e.toString());
+      mTask.fail();
+    }
+  }
+
+  /** Get the URL to perform self-provisioning from the voicemail management gateway. */
+  private String getSelfProvisioningGateway() throws ProvisioningException {
+    VvmLog.i(TAG, "retrieving SPG URL");
+    String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
+    return extractText(response, SPG_URL_TAG);
+  }
+
+  /**
+   * Sent a request to the self-provisioning gateway, which will return us with a webpage. The page
+   * might contain a "Subscribe to Basic Visual Voice Mail" link to complete the subscription. The
+   * cookie from this response and cellular data is required to click the link.
+   */
+  private String getSelfProvisionResponse(String url) throws ProvisioningException {
+    VvmLog.i(TAG, "Retrieving self provisioning response");
+
+    RequestFuture<String> future = RequestFuture.newFuture();
+
+    StringRequest stringRequest =
+        new StringRequest(Request.Method.POST, url, future, future) {
+          @Override
+          protected Map<String, String> getParams() {
+            Map<String, String> params = new ArrayMap<>();
+            params.put(SPG_VZW_MDN_PARAM, mNumber);
+            params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
+            params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
+            params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
+            // Language to display the subscription page. The page is never shown to the user
+            // so just use English.
+            params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
+            return params;
+          }
+        };
+
+    mRequestQueue.add(stringRequest);
+    try {
+      return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    } catch (InterruptedException | ExecutionException | TimeoutException e) {
+      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
+      throw new ProvisioningException(e.toString());
+    }
+  }
+
+  private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
+    VvmLog.i(TAG, "Clicking subscribe link");
+    RequestFuture<String> future = RequestFuture.newFuture();
+
+    StringRequest stringRequest =
+        new StringRequest(Request.Method.POST, subscribeLink, future, future);
+    mRequestQueue.add(stringRequest);
+    try {
+      // A new STATUS SMS will be sent after this request.
+      future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    } catch (TimeoutException | ExecutionException | InterruptedException e) {
+      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
+      throw new ProvisioningException(e.toString());
+    }
+    // It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
+    // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
+    // manually retry if it took too long.
+  }
+
+  private String vvm3XmlRequest(String operation) throws ProvisioningException {
+    VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
+    String voicemailManagementGateway = mData.getString(VMG_URL_KEY);
+    if (voicemailManagementGateway == null) {
+      VvmLog.e(TAG, "voicemailManagementGateway url unknown");
+      return null;
+    }
+    String transactionId = createTransactionId();
+    String body =
+        String.format(
+            Locale.US, VMG_XML_REQUEST_FORMAT, transactionId, mNumber, operation, Build.MODEL);
+
+    RequestFuture<String> future = RequestFuture.newFuture();
+    StringRequest stringRequest =
+        new StringRequest(Request.Method.POST, voicemailManagementGateway, future, future) {
+          @Override
+          public byte[] getBody() throws AuthFailureError {
+            return body.getBytes();
+          }
+        };
+    mRequestQueue.add(stringRequest);
+
+    try {
+      String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+      if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
+        throw new ProvisioningException("transactionId mismatch");
+      }
+      return response;
+    } catch (InterruptedException | ExecutionException | TimeoutException e) {
+      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
+      throw new ProvisioningException(e.toString());
+    }
+  }
+
+  private String findSubscribeLink(String response) throws ProvisioningException {
+    Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
+    URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
+    StringBuilder fulltext = new StringBuilder();
+    for (URLSpan span : spans) {
+      String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
+      if (BASIC_SUBSCRIBE_LINK_TEXT.equals(text)) {
+        return span.getURL();
+      }
+      fulltext.append(text);
+    }
+    throw new ProvisioningException("Subscribe link not found: " + fulltext);
+  }
+
+  private String createTransactionId() {
+    return String.valueOf(Math.abs(new Random().nextLong()));
+  }
+
+  private String extractText(String xml, String tag) throws ProvisioningException {
+    Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
+    Matcher matcher = pattern.matcher(xml);
+    if (matcher.find()) {
+      return matcher.group(1);
+    }
+    throw new ProvisioningException("Tag " + tag + " not found in xml response");
+  }
+
+  private static class NetworkSpecifiedHurlStack extends HurlStack {
+
+    private final Network mNetwork;
+
+    public NetworkSpecifiedHurlStack(Network network) {
+      mNetwork = network;
+    }
+
+    @Override
+    protected HttpURLConnection createConnection(URL url) throws IOException {
+      return (HttpURLConnection) mNetwork.openConnection(url);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml b/java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml
new file mode 100644
index 0000000..50c9277
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/layout/voicemail_change_pin.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2014, 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.
+*/
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:gravity="center_horizontal"
+  android:orientation="vertical">
+  <!-- header text ('Enter Pin') -->
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="0dp"
+    android:layout_weight="1"
+    android:orientation="vertical"
+    android:paddingTop="48dp"
+    android:paddingStart="48dp"
+    android:paddingEnd="48dp">
+    <TextView
+      android:id="@+id/headerText"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:gravity="center"
+      android:lines="2"
+      android:textAppearance="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"
+      android:accessibilityLiveRegion="polite"/>
+
+    <!-- hint text ('PIN too short') -->
+    <TextView
+      android:id="@+id/hintText"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:gravity="center"
+      android:lines="2"/>
+
+    <!-- error text ('PIN too short') -->
+    <TextView
+      android:id="@+id/errorText"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:gravity="center"
+      android:lines="2"
+      android:textColor="@android:color/holo_red_dark"/>
+
+    <!-- Password entry field -->
+    <EditText
+      android:id="@+id/pin_entry"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_gravity="center"
+      android:gravity="center"
+      android:imeOptions="actionNext|flagNoExtractUi"
+      android:inputType="numberPassword"
+      android:textSize="24sp"/>
+  </LinearLayout>
+
+  <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:gravity="end"
+    android:orientation="horizontal">
+
+    <!-- left : cancel -->
+    <Button
+      android:id="@+id/cancel_button"
+      android:layout_width="0dp"
+      android:layout_weight="1"
+      android:layout_height="wrap_content"
+      android:text="@string/change_pin_cancel_label"/>
+
+    <!-- right : continue -->
+    <Button
+      android:id="@+id/next_button"
+      android:layout_width="0dp"
+      android:layout_weight="1"
+      android:layout_height="wrap_content"
+      android:text="@string/change_pin_continue_label"/>
+
+  </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/voicemail/impl/res/values/arrays.xml b/java/com/android/voicemail/impl/res/values/arrays.xml
new file mode 100644
index 0000000..95714cf
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/arrays.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 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.
+-->
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemail/impl/res/values/attrs.xml b/java/com/android/voicemail/impl/res/values/attrs.xml
new file mode 100644
index 0000000..a1195c7
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/attrs.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<resources>
+
+  <attr name="preferenceBackgroundColor" format="color"/>
+</resources>
diff --git a/java/com/android/voicemail/impl/res/values/colors.xml b/java/com/android/voicemail/impl/res/values/colors.xml
new file mode 100644
index 0000000..8a897ab
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemail/impl/res/values/config.xml b/java/com/android/voicemail/impl/res/values/config.xml
new file mode 100644
index 0000000..2f56030
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/config.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemail/impl/res/values/dimens.xml b/java/com/android/voicemail/impl/res/values/dimens.xml
new file mode 100644
index 0000000..e66ca09
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemail/impl/res/values/ids.xml b/java/com/android/voicemail/impl/res/values/ids.xml
new file mode 100644
index 0000000..84c685a
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<resources>
+
+</resources>
\ No newline at end of file
diff --git a/java/com/android/voicemail/impl/res/values/strings.xml b/java/com/android/voicemail/impl/res/values/strings.xml
new file mode 100644
index 0000000..6c3d552
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/strings.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+  <!-- Title of the "Voicemail" settings screen, with a text label identifying which SIM the settings are for. -->
+  <string translatable="false" name="voicemail_settings_with_label">Voicemail (<xliff:g id="subscriptionlabel" example="Mock Carrier">%s</xliff:g>)</string>
+
+  <!-- Call settings screen, setting option name -->
+  <string translatable="false" name="voicemail_settings_title">Voicemail</string>
+
+  <!-- DO NOT TRANSLATE. Internal key for a voicemail notification preference. -->
+  <string translatable="false" name="voicemail_notification_ringtone_key">voicemail_notification_ringtone_key</string>
+  <!-- DO NOT TRANSLATE. Internal key for a voicemail notification preference. -->
+  <string translatable="false" name="voicemail_notification_vibrate_key">voicemail_notification_vibrate_key</string>
+
+  <!-- Title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
+  <string name="voicemail_notification_vibrate_when_title">Vibrate</string>
+  <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
+  <string name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
+
+  <!-- Voicemail ringtone title. The user clicks on this preference to select
+         which sound to play when a voicemail notification is received.
+         [CHAR LIMIT=30] -->
+  <string name="voicemail_notification_ringtone_title">Sound</string>
+  <string translatable="false" name="voicemail_advanced_settings_key">voicemail_advanced_settings_key</string>
+
+  <!-- Title for advanced settings in the voicemail settings -->
+  <string name="voicemail_advanced_settings_title">Advanced Settings</string>
+
+  <!-- DO NOT TRANSLATE. Internal key for a visual voicemail preference. -->
+    <string translatable="false" name="voicemail_visual_voicemail_key">
+        voicemail_visual_voicemail_key
+    </string>
+  <!-- DO NOT TRANSLATE. Internal key for a visual voicemail archive preference. -->
+    <string translatable="false" name="voicemail_visual_voicemail_archive_key">
+        archive_is_enabled
+    </string>
+  <!-- DO NOT TRANSLATE. Internal key for a voicemail change pin preference. -->
+  <string translatable="false" name="voicemail_change_pin_key">voicemail_change_pin_key</string>
+
+  <!-- Visual voicemail on/off title [CHAR LIMIT=40] -->
+  <string translatable="false" name="voicemail_visual_voicemail_switch_title">Visual Voicemail</string>
+
+  <!-- Visual voicemail archive on/off title [CHAR LIMIT=40] -->
+  <string translatable="false" name="voicemail_visual_voicemail_auto_archive_switch_title">
+    Voicemail Auto Archive
+  </string>
+
+  <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+  <string translatable="false" name="voicemail_set_pin_dialog_title">Set PIN</string>
+  <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+  <string translatable="false" name="voicemail_change_pin_dialog_title">Change PIN</string>
+
+  <!-- Hint for the old PIN field in the change vociemail PIN dialog -->
+  <string translatable="false" name="vm_change_pin_old_pin">Old PIN</string>
+  <!-- Hint for the new PIN field in the change vociemail PIN dialog -->
+  <string translatable="false" name="vm_change_pin_new_pin">New PIN</string>
+
+  <!-- Message on the dialog when PIN changing is in progress -->
+  <string translatable="false" name="vm_change_pin_progress_message">Please wait.</string>
+  <!-- Error message for the voicemail PIN change if the PIN is too short -->
+  <string translatable="false" name="vm_change_pin_error_too_short">The new PIN is too short.</string>
+  <!-- Error message for the voicemail PIN change if the PIN is too long -->
+  <string translatable="false" name="vm_change_pin_error_too_long">The new PIN is too long.</string>
+  <!-- Error message for the voicemail PIN change if the PIN is too weak -->
+  <string translatable="false" name="vm_change_pin_error_too_weak">The new PIN is too weak. A strong password should not have continuous sequence or repeated digits.</string>
+  <!-- Error message for the voicemail PIN change if the old PIN entered doesn't match  -->
+  <string translatable="false" name="vm_change_pin_error_mismatch">The old PIN does not match.</string>
+  <!-- Error message for the voicemail PIN change if the new PIN contains invalid character -->
+  <string translatable="false" name="vm_change_pin_error_invalid">The new PIN contains invalid characters.</string>
+  <!-- Error message for the voicemail PIN change if operation has failed -->
+  <string translatable="false" name="vm_change_pin_error_system_error">Unable to change PIN</string>
+  <!-- Message to replace the transcription if a visual voicemail message is not supported-->
+  <string translatable="false" name="vvm_unsupported_message_format">Unsupported message type, call <xliff:g id="number" example="*86">%s</xliff:g> to listen.</string>
+
+  <!-- The title for the change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_title">Change Voicemail PIN</string>
+  <!-- The label for the continue button in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_continue_label">Continue</string>
+  <!-- The label for the cancel button in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_cancel_label">Cancel</string>
+  <!-- The label for the ok button in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_ok_label">Ok</string>
+  <!-- The title for the enter old pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_enter_old_pin_header">Confirm your old PIN</string>
+  <!-- The hint for the enter old pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_enter_old_pin_hint">Enter your voicemail PIN to continue.</string>
+  <!-- The title for the enter new pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_enter_new_pin_header">Set a new PIN</string>
+  <!-- The hint for the enter new pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_enter_new_pin_hint">PIN must be <xliff:g id="min" example="4">%1$d</xliff:g>-<xliff:g id="max" example="7">%2$d</xliff:g> digits.</string>
+  <!-- The title for the confirm new pin step in change voicemail PIN activity -->
+  <string translatable="false" name="change_pin_confirm_pin_header">Confirm your PIN</string>
+  <!-- The error message for th confirm new pin step in change voicemail PIN activity, if the pin doen't match the one previously entered -->
+  <string translatable="false" name="change_pin_confirm_pins_dont_match">PINs don\'t match</string>
+  <!-- The toast to show after the voicemail PIN has been successfully changed -->
+  <string translatable="false" name="change_pin_succeeded">Voicemail PIN updated</string>
+  <!-- The error message to show if the server reported an error while attempting to change the voicemail PIN -->
+  <string translatable="false" name="change_pin_system_error">Unable to set PIN</string>
+</resources>
diff --git a/java/com/android/voicemail/impl/res/values/styles.xml b/java/com/android/voicemail/impl/res/values/styles.xml
new file mode 100644
index 0000000..8a897ab
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/values/styles.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml b/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml
new file mode 100644
index 0000000..2243733
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/xml/voicemail_settings.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+  android:title="@string/voicemail_settings_title">
+
+  <com.android.voicemail.impl.settings.VoicemailRingtonePreference
+    android:key="@string/voicemail_notification_ringtone_key"
+    android:title="@string/voicemail_notification_ringtone_title"
+    android:persistent="false"
+    android:ringtoneType="notification" />
+
+  <CheckBoxPreference
+    android:key="@string/voicemail_notification_vibrate_key"
+    android:title="@string/voicemail_notification_vibrate_when_title"
+    android:persistent="true" />
+
+  <SwitchPreference
+    android:key="@string/voicemail_visual_voicemail_key"
+    android:title="@string/voicemail_visual_voicemail_switch_title"/>"
+
+  <SwitchPreference
+    android:key="@string/voicemail_visual_voicemail_archive_key"
+    android:dependency="@string/voicemail_visual_voicemail_key"
+    android:title="@string/voicemail_visual_voicemail_auto_archive_switch_title"/>"
+  <Preference
+    android:key="@string/voicemail_change_pin_key"
+    android:title="@string/voicemail_change_pin_dialog_title"/>
+
+  <PreferenceScreen
+    android:key="@string/voicemail_advanced_settings_key"
+    android:title="@string/voicemail_advanced_settings_title">
+    </PreferenceScreen>
+</PreferenceScreen>
diff --git a/java/com/android/voicemail/impl/res/xml/vvm_config.xml b/java/com/android/voicemail/impl/res/xml/vvm_config.xml
new file mode 100644
index 0000000..230d40f
--- /dev/null
+++ b/java/com/android/voicemail/impl/res/xml/vvm_config.xml
@@ -0,0 +1,148 @@
+<?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>
+  </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"/>
+    <string-array name="vvm_disabled_capabilities_string_array">
+      <!-- b/32365569 -->
+      <item value="STARTTLS"/>
+    </string-array>
+  </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>
+
+  <pbundle_as_map>
+    <!-- Verizon USA -->
+    <string-array name="mccmnc">
+      <item value="310004"/>
+      <item value="310010"/>
+      <item value="310012"/>
+      <item value="310013"/>
+      <item value="310590"/>
+      <item value="310890"/>
+      <item value="310910"/>
+      <item value="311110"/>
+      <item value="311270"/>
+      <item value="311271"/>
+      <item value="311272"/>
+      <item value="311273"/>
+      <item value="311274"/>
+      <item value="311275"/>
+      <item value="311276"/>
+      <item value="311277"/>
+      <item value="311278"/>
+      <item value="311279"/>
+      <item value="311280"/>
+      <item value="311281"/>
+      <item value="311282"/>
+      <item value="311283"/>
+      <item value="311284"/>
+      <item value="311285"/>
+      <item value="311286"/>
+      <item value="311287"/>
+      <item value="311288"/>
+      <item value="311289"/>
+      <item value="311390"/>
+      <item value="311480"/>
+      <item value="311481"/>
+      <item value="311482"/>
+      <item value="311483"/>
+      <item value="311484"/>
+      <item value="311485"/>
+      <item value="311486"/>
+      <item value="311487"/>
+      <item value="311488"/>
+      <item value="311489"/>
+    </string-array>
+
+    <int
+      name="vvm_port_number_int"
+      value="0"/>
+    <string name="vvm_destination_number_string">900080006200</string>
+    <string name="vvm_type_string">vvm_type_vvm3</string>
+    <string name="vvm_client_prefix_string">//VZWVVM</string>
+    <boolean
+      name="vvm_cellular_data_required_bool"
+      value="true"/>
+    <boolean
+      name="vvm_legacy_mode_enabled_bool"
+      value="true"/>
+    <!-- VVM3 specific value for the voicemail management gateway to use if the SMS didn't provide
+         one -->
+    <string name="default_vmg_url">https://mobile.vzw.com/VMGIMS/VMServices</string>
+  </pbundle_as_map>
+</list>
diff --git a/java/com/android/voicemail/impl/scheduling/BaseTask.java b/java/com/android/voicemail/impl/scheduling/BaseTask.java
new file mode 100644
index 0000000..4cc6dd5
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/BaseTask.java
@@ -0,0 +1,202 @@
+/*
+ * 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.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.annotation.CallSuper;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.NeededForTesting;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides common utilities for task implementations, such as execution time and managing {@link
+ * Policy}
+ */
+public abstract class BaseTask implements Task {
+
+  private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+  private Context mContext;
+
+  private int mId;
+  private PhoneAccountHandle mPhoneAccountHandle;
+
+  private boolean mHasStarted;
+  private volatile boolean mHasFailed;
+
+  @NonNull private final List<Policy> mPolicies = new ArrayList<>();
+
+  private long mExecutionTime;
+
+  private static Clock sClock = new Clock();
+
+  protected BaseTask(int id) {
+    mId = id;
+    mExecutionTime = getTimeMillis();
+  }
+
+  /**
+   * Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link
+   * #onCreate(Context, Intent, int, int)} returns.
+   */
+  @MainThread
+  public void setId(int id) {
+    Assert.isMainThread();
+    mId = id;
+  }
+
+  @MainThread
+  public boolean hasStarted() {
+    Assert.isMainThread();
+    return mHasStarted;
+  }
+
+  @MainThread
+  public boolean hasFailed() {
+    Assert.isMainThread();
+    return mHasFailed;
+  }
+
+  public Context getContext() {
+    return mContext;
+  }
+
+  public PhoneAccountHandle getPhoneAccountHandle() {
+    return mPhoneAccountHandle;
+  }
+  /**
+   * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will
+   * be missed.
+   */
+  @MainThread
+  public BaseTask addPolicy(Policy policy) {
+    Assert.isMainThread();
+    mPolicies.add(policy);
+    return this;
+  }
+
+  /**
+   * Indicate the task has failed. {@link Policy#onFail()} will be triggered once the execution
+   * ends. This mechanism is used by policies for actions such as determining whether to schedule a
+   * retry. Must be call inside {@link #onExecuteInBackgroundThread()}
+   */
+  @WorkerThread
+  public void fail() {
+    Assert.isNotMainThread();
+    mHasFailed = true;
+  }
+
+  @MainThread
+  public void setExecutionTime(long timeMillis) {
+    Assert.isMainThread();
+    mExecutionTime = timeMillis;
+  }
+
+  public long getTimeMillis() {
+    return sClock.getTimeMillis();
+  }
+
+  /**
+   * Creates an intent that can be used to restart the current task. Derived class should build
+   * their intent upon this.
+   */
+  public Intent createRestartIntent() {
+    return createIntent(getContext(), this.getClass(), mPhoneAccountHandle);
+  }
+
+  /**
+   * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class
+   * should build their intent upon this.
+   */
+  public static Intent createIntent(
+      Context context, Class<? extends BaseTask> task, PhoneAccountHandle phoneAccountHandle) {
+    Intent intent = TaskSchedulerService.createIntent(context, task);
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+    return intent;
+  }
+
+  @Override
+  public TaskId getId() {
+    return new TaskId(mId, mPhoneAccountHandle);
+  }
+
+  @Override
+  @CallSuper
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    mContext = context;
+    mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+    for (Policy policy : mPolicies) {
+      policy.onCreate(this, intent, flags, startId);
+    }
+  }
+
+  @Override
+  public long getReadyInMilliSeconds() {
+    return mExecutionTime - getTimeMillis();
+  }
+
+  @Override
+  @CallSuper
+  public void onBeforeExecute() {
+    for (Policy policy : mPolicies) {
+      policy.onBeforeExecute();
+    }
+    mHasStarted = true;
+  }
+
+  @Override
+  @CallSuper
+  public void onCompleted() {
+    if (mHasFailed) {
+      for (Policy policy : mPolicies) {
+        policy.onFail();
+      }
+    }
+
+    for (Policy policy : mPolicies) {
+      policy.onCompleted();
+    }
+  }
+
+  @Override
+  public void onDuplicatedTaskAdded(Task task) {
+    for (Policy policy : mPolicies) {
+      policy.onDuplicatedTaskAdded();
+    }
+  }
+
+  @NeededForTesting
+  static class Clock {
+
+    public long getTimeMillis() {
+      return SystemClock.elapsedRealtime();
+    }
+  }
+
+  /** Used to replace the clock with an deterministic clock */
+  @NeededForTesting
+  static void setClockForTesting(Clock clock) {
+    sClock = clock;
+  }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/BlockerTask.java b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
new file mode 100644
index 0000000..353508d
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/BlockerTask.java
@@ -0,0 +1,51 @@
+/*
+ * 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.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import com.android.voicemail.impl.VvmLog;
+
+/** Task to block another task of the same ID from being queued for a certain amount of time. */
+public class BlockerTask extends BaseTask {
+
+  private static final String TAG = "BlockerTask";
+
+  public static final String EXTRA_TASK_ID = "extra_task_id";
+  public static final String EXTRA_BLOCK_FOR_MILLIS = "extra_block_for_millis";
+
+  public BlockerTask() {
+    super(TASK_INVALID);
+  }
+
+  @Override
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+    setId(intent.getIntExtra(EXTRA_TASK_ID, TASK_INVALID));
+    setExecutionTime(getTimeMillis() + intent.getIntExtra(EXTRA_BLOCK_FOR_MILLIS, 0));
+  }
+
+  @Override
+  public void onExecuteInBackgroundThread() {
+    // Do nothing.
+  }
+
+  @Override
+  public void onDuplicatedTaskAdded(Task task) {
+    VvmLog.v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
+  }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
new file mode 100644
index 0000000..8b2fe70
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/MinimalIntervalPolicy.java
@@ -0,0 +1,62 @@
+/*
+ * 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.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+
+/**
+ * If a task with this policy succeeds, a {@link BlockerTask} with the same {@link TaskId} of the
+ * task will be queued immediately, preventing the same task from running for a certain amount of
+ * time.
+ */
+public class MinimalIntervalPolicy implements Policy {
+
+  BaseTask mTask;
+  TaskId mId;
+  int mBlockForMillis;
+
+  public MinimalIntervalPolicy(int blockForMillis) {
+    mBlockForMillis = blockForMillis;
+  }
+
+  @Override
+  public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+    mTask = task;
+    mId = mTask.getId();
+  }
+
+  @Override
+  public void onBeforeExecute() {}
+
+  @Override
+  public void onCompleted() {
+    if (!mTask.hasFailed()) {
+      Intent intent =
+          mTask.createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle);
+      intent.putExtra(BlockerTask.EXTRA_TASK_ID, mId.id);
+      intent.putExtra(BlockerTask.EXTRA_BLOCK_FOR_MILLIS, mBlockForMillis);
+      mTask.getContext().startService(intent);
+    }
+  }
+
+  @Override
+  public void onFail() {}
+
+  @Override
+  public void onDuplicatedTaskAdded() {}
+}
diff --git a/java/com/android/voicemail/impl/scheduling/Policy.java b/java/com/android/voicemail/impl/scheduling/Policy.java
new file mode 100644
index 0000000..6077821
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/Policy.java
@@ -0,0 +1,36 @@
+/*
+ * 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.voicemail.impl.scheduling;
+
+import android.content.Intent;
+
+/**
+ * A set of listeners managed by {@link BaseTask} for common behaviors such as retrying. Call {@link
+ * BaseTask#addPolicy(Policy)} to add a policy.
+ */
+public interface Policy {
+
+  void onCreate(BaseTask task, Intent intent, int flags, int startId);
+
+  void onBeforeExecute();
+
+  void onCompleted();
+
+  void onFail();
+
+  void onDuplicatedTaskAdded();
+}
diff --git a/java/com/android/voicemail/impl/scheduling/PostponePolicy.java b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
new file mode 100644
index 0000000..e24df0c
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/PostponePolicy.java
@@ -0,0 +1,68 @@
+/*
+ * 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.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * A task with Postpone policy will not be executed immediately. It will wait for a while and if a
+ * duplicated task is queued during the duration, the task will be postponed further. The task will
+ * only be executed if no new task was added in postponeMillis. Useful to batch small tasks in quick
+ * succession together.
+ */
+public class PostponePolicy implements Policy {
+
+  private static final String TAG = "PostponePolicy";
+
+  private final int mPostponeMillis;
+  private BaseTask mTask;
+
+  public PostponePolicy(int postponeMillis) {
+    mPostponeMillis = postponeMillis;
+  }
+
+  @Override
+  public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+    mTask = task;
+    mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+  }
+
+  @Override
+  public void onBeforeExecute() {
+    // Do nothing
+  }
+
+  @Override
+  public void onCompleted() {
+    // Do nothing
+  }
+
+  @Override
+  public void onFail() {
+    // Do nothing
+  }
+
+  @Override
+  public void onDuplicatedTaskAdded() {
+    if (mTask.hasStarted()) {
+      return;
+    }
+    VvmLog.d(TAG, "postponing " + mTask);
+    mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+  }
+}
diff --git a/java/com/android/voicemail/impl/scheduling/RetryPolicy.java b/java/com/android/voicemail/impl/scheduling/RetryPolicy.java
new file mode 100644
index 0000000..a8e4a3d
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/RetryPolicy.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.voicemail.impl.scheduling;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * A task with this policy will automatically re-queue itself if {@link BaseTask#fail()} has been
+ * called during {@link BaseTask#onExecuteInBackgroundThread()}. A task will be retried at most
+ * <code>retryLimit</code> times and with a <code>retryDelayMillis</code> interval in between.
+ */
+public class RetryPolicy implements Policy {
+
+  private static final String TAG = "RetryPolicy";
+  private static final String EXTRA_RETRY_COUNT = "extra_retry_count";
+
+  private final int mRetryLimit;
+  private final int mRetryDelayMillis;
+
+  private BaseTask mTask;
+
+  private int mRetryCount;
+  private boolean mFailed;
+
+  private VoicemailStatus.DeferredEditor mVoicemailStatusEditor;
+
+  public RetryPolicy(int retryLimit, int retryDelayMillis) {
+    mRetryLimit = retryLimit;
+    mRetryDelayMillis = retryDelayMillis;
+  }
+
+  private boolean hasMoreRetries() {
+    return mRetryCount < mRetryLimit;
+  }
+
+  /**
+   * Error status should only be set if retries has exhausted or the task is successful. Status
+   * writes to this editor will be deferred until the task has ended, and will only be committed if
+   * the task is successful or there are no retries left.
+   */
+  public VoicemailStatus.Editor getVoicemailStatusEditor() {
+    return mVoicemailStatusEditor;
+  }
+
+  @Override
+  public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+    mTask = task;
+    mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0);
+    if (mRetryCount > 0) {
+      VvmLog.d(
+          TAG,
+          "retry #" + mRetryCount + " for " + mTask + " queued, executing in " + mRetryDelayMillis);
+      mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis);
+    }
+    PhoneAccountHandle phoneAccountHandle = task.getPhoneAccountHandle();
+    if (phoneAccountHandle == null) {
+      VvmLog.e(TAG, "null phone account for phoneAccountHandle " + task.getPhoneAccountHandle());
+      // This should never happen, but continue on if it does. The status write will be
+      // discarded.
+    }
+    mVoicemailStatusEditor = VoicemailStatus.deferredEdit(task.getContext(), phoneAccountHandle);
+  }
+
+  @Override
+  public void onBeforeExecute() {}
+
+  @Override
+  public void onCompleted() {
+    if (!mFailed || !hasMoreRetries()) {
+      if (!mFailed) {
+        VvmLog.d(TAG, mTask.toString() + " completed successfully");
+      }
+      if (!hasMoreRetries()) {
+        VvmLog.d(TAG, "Retry limit for " + mTask + " reached");
+      }
+      VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues());
+      mVoicemailStatusEditor.deferredApply();
+      return;
+    }
+    VvmLog.i(TAG, "discarding deferred status: " + mVoicemailStatusEditor.getValues());
+    Intent intent = mTask.createRestartIntent();
+    intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1);
+
+    mTask.getContext().startService(intent);
+  }
+
+  @Override
+  public void onFail() {
+    mFailed = true;
+  }
+
+  @Override
+  public void onDuplicatedTaskAdded() {}
+}
diff --git a/java/com/android/voicemail/impl/scheduling/Task.java b/java/com/android/voicemail/impl/scheduling/Task.java
new file mode 100644
index 0000000..2d08f5b
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/Task.java
@@ -0,0 +1,128 @@
+/*
+ * 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.voicemail.impl.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import java.util.Objects;
+
+/**
+ * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a intent to
+ * the scheduler, The task must be constructable with the intent. Specifically, It must have a
+ * constructor with zero arguments, and have all relevant data packed inside the intent. Use {@link
+ * TaskSchedulerService#createIntent(Context, Class)} to create a intent that will construct the
+ * Task.
+ *
+ * <p>Only {@link #onExecuteInBackgroundThread()} is run on the worker thread.
+ */
+public interface Task {
+
+  /**
+   * TaskId to indicate it has not be set. If a task does not provide a default TaskId it should be
+   * set before {@link Task#onCreate(Context, Intent, int, int) returns}
+   */
+  int TASK_INVALID = -1;
+
+  /**
+   * TaskId to indicate it should always be queued regardless of duplicates. {@link
+   * Task#onDuplicatedTaskAdded(Task)} will never be called on tasks with this TaskId.
+   */
+  int TASK_ALLOW_DUPLICATES = -2;
+
+  int TASK_UPLOAD = 1;
+  int TASK_SYNC = 2;
+  int TASK_ACTIVATION = 3;
+
+  /**
+   * Used to differentiate between types of tasks. If a task with the same TaskId is already in the
+   * queue the new task will be rejected.
+   */
+  class TaskId {
+
+    /** Indicates the operation type of the task. */
+    public final int id;
+    /**
+     * Same operation for a different phoneAccountHandle is allowed. phoneAccountHandle is used to
+     * differentiate phone accounts in multi-SIM scenario. For example, each SIM can queue a sync
+     * task for their own.
+     */
+    public final PhoneAccountHandle phoneAccountHandle;
+
+    public TaskId(int id, PhoneAccountHandle phoneAccountHandle) {
+      this.id = id;
+      this.phoneAccountHandle = phoneAccountHandle;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+      if (!(object instanceof TaskId)) {
+        return false;
+      }
+      TaskId other = (TaskId) object;
+      return id == other.id && phoneAccountHandle.equals(other.phoneAccountHandle);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(id, phoneAccountHandle);
+    }
+  }
+
+  TaskId getId();
+
+  @MainThread
+  void onCreate(Context context, Intent intent, int flags, int startId);
+
+  /**
+   * @return number of milliSeconds the scheduler should wait before running this task. A value less
+   *     than {@link TaskSchedulerService#READY_TOLERANCE_MILLISECONDS} will be considered ready. If
+   *     no tasks are ready, the scheduler will sleep for this amount of time before doing another
+   *     check (it will still wake if a new task is added). The first task in the queue that is
+   *     ready will be executed.
+   */
+  @MainThread
+  long getReadyInMilliSeconds();
+
+  /**
+   * Called on the main thread when the scheduler is about to send the task into the worker thread,
+   * calling {@link #onExecuteInBackgroundThread()}
+   */
+  @MainThread
+  void onBeforeExecute();
+
+  /** The actual payload of the task, executed on the worker thread. */
+  @WorkerThread
+  void onExecuteInBackgroundThread();
+
+  /**
+   * Called on the main thread when {@link #onExecuteInBackgroundThread()} has finished or thrown an
+   * uncaught exception. The task is already removed from the queue at this point, and a same task
+   * can be queued again.
+   */
+  @MainThread
+  void onCompleted();
+
+  /**
+   * Another task with the same TaskId has been added. Necessary data can be retrieved from the
+   * other task, and after this returns the task will be discarded.
+   */
+  @MainThread
+  void onDuplicatedTaskAdded(Task task);
+}
diff --git a/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
new file mode 100644
index 0000000..81bd36f
--- /dev/null
+++ b/java/com/android/voicemail/impl/scheduling/TaskSchedulerService.java
@@ -0,0 +1,396 @@
+/*
+ * 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.voicemail.impl.scheduling;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.scheduling.Task.TaskId;
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time,
+ * and same task cannot exist in the queue at the same time. The service will be started when a
+ * intent is received, and stopped when there are no more tasks in the queue.
+ */
+public class TaskSchedulerService extends Service {
+
+  private static final String TAG = "VvmTaskScheduler";
+
+  private static final String ACTION_WAKEUP = "action_wakeup";
+
+  private static final int READY_TOLERANCE_MILLISECONDS = 100;
+
+  /**
+   * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
+   * future.
+   *
+   * <p>A short sleep will continue to held the wake lock and use {@link
+   * Handler#postDelayed(Runnable, long)} to wait for the next task.
+   *
+   * <p>A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
+   * exact and will wake up the device. Note: as this service is run in the telephony process it
+   * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
+   * unbundled version should take doze into account.
+   */
+  private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
+  /**
+   * When there are no more tasks to be run the service should be stopped. But when all tasks has
+   * finished there might still be more tasks in the message queue waiting to be processed,
+   * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
+   * the service to make sure there are no pending messages.
+   */
+  private static final int STOP_DELAY_MILLISECONDS = 5_000;
+
+  private static final String EXTRA_CLASS_NAME = "extra_class_name";
+
+  private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock";
+
+  // The thread to run tasks on
+  private volatile WorkerThreadHandler mWorkerThreadHandler;
+
+  private Context mContext = this;
+  /**
+   * Used by tests to turn task handling into a single threaded process by calling {@link
+   * Handler#handleMessage(Message)} directly
+   */
+  private MessageSender mMessageSender = new MessageSender();
+
+  private MainThreadHandler mMainThreadHandler;
+
+  private WakeLock mWakeLock;
+
+  /** Main thread only, access through {@link #getTasks()} */
+  private final Queue<Task> mTasks = new ArrayDeque<>();
+
+  private boolean mWorkerThreadIsBusy = false;
+
+  private final Runnable mStopServiceWithDelay =
+      new Runnable() {
+        @Override
+        public void run() {
+          VvmLog.d(TAG, "Stopping service");
+          stopSelf();
+        }
+      };
+  /** Should attempt to run the next task when a task has finished or been added. */
+  private boolean mTaskAutoRunDisabledForTesting = false;
+
+  @VisibleForTesting
+  final class WorkerThreadHandler extends Handler {
+
+    public WorkerThreadHandler(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    @WorkerThread
+    public void handleMessage(Message msg) {
+      Assert.isNotMainThread();
+      Task task = (Task) msg.obj;
+      try {
+        VvmLog.v(TAG, "executing task " + task);
+        task.onExecuteInBackgroundThread();
+      } catch (Throwable throwable) {
+        VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
+      }
+
+      Message schedulerMessage = mMainThreadHandler.obtainMessage();
+      schedulerMessage.obj = task;
+      mMessageSender.send(schedulerMessage);
+    }
+  }
+
+  @VisibleForTesting
+  final class MainThreadHandler extends Handler {
+
+    public MainThreadHandler(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    @MainThread
+    public void handleMessage(Message msg) {
+      Assert.isMainThread();
+      Task task = (Task) msg.obj;
+      getTasks().remove(task);
+      task.onCompleted();
+      mWorkerThreadIsBusy = false;
+      maybeRunNextTask();
+    }
+  }
+
+  @Override
+  @MainThread
+  public void onCreate() {
+    super.onCreate();
+    mWakeLock =
+        getSystemService(PowerManager.class)
+            .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
+    mWakeLock.setReferenceCounted(false);
+    HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
+    thread.start();
+
+    mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper());
+    mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
+  }
+
+  @Override
+  public void onDestroy() {
+    mWorkerThreadHandler.getLooper().quit();
+    mWakeLock.release();
+  }
+
+  @Override
+  @MainThread
+  public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+    Assert.isMainThread();
+    // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping
+    // the service.
+    mWakeLock.acquire();
+    if (ACTION_WAKEUP.equals(intent.getAction())) {
+      VvmLog.d(TAG, "woke up by AlarmManager");
+    } else {
+      Task task = createTask(intent, flags, startId);
+      if (task == null) {
+        VvmLog.e(TAG, "cannot create task form intent");
+      } else {
+        addTask(task);
+      }
+    }
+    maybeRunNextTask();
+    // STICKY means the service will be automatically restarted will the last intent if it is
+    // killed.
+    return START_NOT_STICKY;
+  }
+
+  @MainThread
+  @VisibleForTesting
+  void addTask(Task task) {
+    Assert.isMainThread();
+    if (task.getId().id == Task.TASK_INVALID) {
+      throw new AssertionError("Task id was not set to a valid value before adding.");
+    }
+    if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) {
+      Task oldTask = getTask(task.getId());
+      if (oldTask != null) {
+        oldTask.onDuplicatedTaskAdded(task);
+        return;
+      }
+    }
+    mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
+    getTasks().add(task);
+    maybeRunNextTask();
+  }
+
+  @MainThread
+  @Nullable
+  private Task getTask(TaskId taskId) {
+    Assert.isMainThread();
+    for (Task task : getTasks()) {
+      if (task.getId().equals(taskId)) {
+        return task;
+      }
+    }
+    return null;
+  }
+
+  @MainThread
+  private Queue<Task> getTasks() {
+    Assert.isMainThread();
+    return mTasks;
+  }
+
+  /** Create an intent that will queue the <code>task</code> */
+  public static Intent createIntent(Context context, Class<? extends Task> task) {
+    Intent intent = new Intent(context, TaskSchedulerService.class);
+    intent.putExtra(EXTRA_CLASS_NAME, task.getName());
+    return intent;
+  }
+
+  @VisibleForTesting
+  @MainThread
+  @Nullable
+  Task createTask(@Nullable Intent intent, int flags, int startId) {
+    Assert.isMainThread();
+    if (intent == null) {
+      return null;
+    }
+    String className = intent.getStringExtra(EXTRA_CLASS_NAME);
+    VvmLog.d(TAG, "create task:" + className);
+    if (className == null) {
+      throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
+    }
+    try {
+      Task task = (Task) Class.forName(className).newInstance();
+      task.onCreate(mContext, intent, flags, startId);
+      return task;
+    } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  @MainThread
+  private void maybeRunNextTask() {
+    Assert.isMainThread();
+    if (mWorkerThreadIsBusy) {
+      return;
+    }
+    if (mTaskAutoRunDisabledForTesting) {
+      // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
+      // to run the next task.
+      return;
+    }
+
+    runNextTask();
+  }
+
+  @VisibleForTesting
+  @MainThread
+  void runNextTask() {
+    Assert.isMainThread();
+    // The current alarm is no longer valid, a new one will be set up if required.
+    getSystemService(AlarmManager.class).cancel(getWakeupIntent());
+    if (getTasks().isEmpty()) {
+      prepareStop();
+      return;
+    }
+    Long minimalWaitTime = null;
+    for (Task task : getTasks()) {
+      long waitTime = task.getReadyInMilliSeconds();
+      if (waitTime < READY_TOLERANCE_MILLISECONDS) {
+        task.onBeforeExecute();
+        Message message = mWorkerThreadHandler.obtainMessage();
+        message.obj = task;
+        mWorkerThreadIsBusy = true;
+        mMessageSender.send(message);
+        return;
+      } else {
+        if (minimalWaitTime == null || waitTime < minimalWaitTime) {
+          minimalWaitTime = waitTime;
+        }
+      }
+    }
+    VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
+    if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
+      // No tasks are currently ready. Sleep until the next one should be.
+      // If a new task is added during the sleep the service will wake immediately.
+      sleep(minimalWaitTime);
+    }
+  }
+
+  private void sleep(long timeMillis) {
+    if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
+      mMainThreadHandler.postDelayed(
+          new Runnable() {
+            @Override
+            public void run() {
+              maybeRunNextTask();
+            }
+          },
+          timeMillis);
+      return;
+    }
+
+    // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
+    // optimize the battery usage. As this service currently run in the telephony process the
+    // OS give it privileges to behave the same as setExact(), but set() is the targeted
+    // behavior once this is unbundled.
+    getSystemService(AlarmManager.class)
+        .set(
+            AlarmManager.ELAPSED_REALTIME_WAKEUP,
+            SystemClock.elapsedRealtime() + timeMillis,
+            getWakeupIntent());
+    mWakeLock.release();
+    VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
+  }
+
+  private PendingIntent getWakeupIntent() {
+    Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
+    return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+  }
+
+  private void prepareStop() {
+    VvmLog.d(
+        TAG,
+        "No more tasks, stopping service if no task are added in "
+            + STOP_DELAY_MILLISECONDS
+            + " millis");
+    mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
+  }
+
+  static class MessageSender {
+
+    public void send(Message message) {
+      message.sendToTarget();
+    }
+  }
+
+  @NeededForTesting
+  void setContextForTest(Context context) {
+    mContext = context;
+  }
+
+  @NeededForTesting
+  void setTaskAutoRunDisabledForTest(boolean value) {
+    mTaskAutoRunDisabledForTesting = value;
+  }
+
+  @NeededForTesting
+  void setMessageSenderForTest(MessageSender sender) {
+    mMessageSender = sender;
+  }
+
+  @NeededForTesting
+  void clearTasksForTest() {
+    mTasks.clear();
+  }
+
+  @Override
+  @Nullable
+  public IBinder onBind(Intent intent) {
+    return new LocalBinder();
+  }
+
+  @NeededForTesting
+  class LocalBinder extends Binder {
+
+    @NeededForTesting
+    public TaskSchedulerService getService() {
+      return TaskSchedulerService.this;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java b/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
new file mode 100644
index 0000000..7e4a6a7
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VisualVoicemailSettingsUtil.java
@@ -0,0 +1,91 @@
+/*
+ * 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.voicemail.impl.settings;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Save whether or not a particular account is enabled in shared to be retrieved later. */
+public class VisualVoicemailSettingsUtil {
+
+  private static final String IS_ENABLED_KEY = "is_enabled";
+  // Flag name used for configuration
+  public static final String ALLOW_VOICEMAIL_ARCHIVE = "allow_voicemail_archive";
+
+  public static void setEnabled(
+      Context context, PhoneAccountHandle phoneAccount, boolean isEnabled) {
+    new VisualVoicemailPreferences(context, phoneAccount)
+        .edit()
+        .putBoolean(IS_ENABLED_KEY, isEnabled)
+        .apply();
+    OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccount);
+    if (isEnabled) {
+      config.startActivation();
+    } else {
+      VvmAccountManager.removeAccount(context, phoneAccount);
+      config.startDeactivation();
+    }
+  }
+
+  public static void setArchiveEnabled(
+      Context context, PhoneAccountHandle phoneAccount, boolean isEnabled) {
+    new VisualVoicemailPreferences(context, phoneAccount)
+        .edit()
+        .putBoolean(context.getString(R.string.voicemail_visual_voicemail_archive_key), isEnabled)
+        .apply();
+  }
+
+  public static boolean isEnabled(Context context, PhoneAccountHandle phoneAccount) {
+    if (phoneAccount == null) {
+      return false;
+    }
+
+    VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+    if (prefs.contains(IS_ENABLED_KEY)) {
+      // isEnableByDefault is a bit expensive, so don't use it as default value of
+      // getBoolean(). The "false" here should never be actually used.
+      return prefs.getBoolean(IS_ENABLED_KEY, false);
+    }
+    return new OmtpVvmCarrierConfigHelper(context, phoneAccount).isEnabledByDefault();
+  }
+
+  public static boolean isArchiveEnabled(Context context, PhoneAccountHandle phoneAccount) {
+    Assert.isNotNull(phoneAccount);
+
+    VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+    return prefs.getBoolean(
+        context.getString(R.string.voicemail_visual_voicemail_archive_key), false);
+  }
+
+  /**
+   * Whether the client enabled status is explicitly set by user or by default(Whether carrier VVM
+   * app is installed). This is used to determine whether to disable the client when the carrier VVM
+   * app is installed. If the carrier VVM app is installed the client should give priority to it if
+   * the settings are not touched.
+   */
+  public static boolean isEnabledUserSet(Context context, PhoneAccountHandle phoneAccount) {
+    if (phoneAccount == null) {
+      return false;
+    }
+    VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+    return prefs.contains(IS_ENABLED_KEY);
+  }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java b/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java
new file mode 100644
index 0000000..f288a5b
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailChangePinActivity.java
@@ -0,0 +1,624 @@
+/*
+ * 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.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputFilter.LengthFilter;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpConstants.ChangePinResult;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.mail.MessagingException;
+import com.android.voicemail.impl.sync.VvmNetworkRequestCallback;
+
+/**
+ * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing
+ * traditional voicemail through phone call. The intent to launch this activity must contain {@link
+ * #EXTRA_PHONE_ACCOUNT_HANDLE}
+ */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailChangePinActivity extends Activity
+    implements OnClickListener, OnEditorActionListener, TextWatcher {
+
+  private static final String TAG = "VmChangePinActivity";
+
+  public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+  private static final String KEY_DEFAULT_OLD_PIN = "default_old_pin";
+
+  private static final int MESSAGE_HANDLE_RESULT = 1;
+
+  private PhoneAccountHandle mPhoneAccountHandle;
+  private OmtpVvmCarrierConfigHelper mConfig;
+
+  private int mPinMinLength;
+  private int mPinMaxLength;
+
+  private State mUiState = State.Initial;
+  private String mOldPin;
+  private String mFirstPin;
+
+  private ProgressDialog mProgressDialog;
+
+  private TextView mHeaderText;
+  private TextView mHintText;
+  private TextView mErrorText;
+  private EditText mPinEntry;
+  private Button mCancelButton;
+  private Button mNextButton;
+
+  private Handler mHandler =
+      new Handler() {
+        @Override
+        public void handleMessage(Message message) {
+          if (message.what == MESSAGE_HANDLE_RESULT) {
+            mUiState.handleResult(VoicemailChangePinActivity.this, message.arg1);
+          }
+        }
+      };
+
+  private enum State {
+    /**
+     * Empty state to handle initial state transition. Will immediately switch into {@link
+     * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin} if
+     * not.
+     */
+    Initial,
+    /**
+     * Prompt the user to enter old PIN. The PIN will be verified with the server before proceeding
+     * to {@link #EnterNewPin}.
+     */
+    EnterOldPin {
+      @Override
+      public void onEnter(VoicemailChangePinActivity activity) {
+        activity.setHeader(R.string.change_pin_enter_old_pin_header);
+        activity.mHintText.setText(R.string.change_pin_enter_old_pin_hint);
+        activity.mNextButton.setText(R.string.change_pin_continue_label);
+        activity.mErrorText.setText(null);
+      }
+
+      @Override
+      public void onInputChanged(VoicemailChangePinActivity activity) {
+        activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0);
+      }
+
+      @Override
+      public void handleNext(VoicemailChangePinActivity activity) {
+        activity.mOldPin = activity.getCurrentPasswordInput();
+        activity.verifyOldPin();
+      }
+
+      @Override
+      public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+        if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+          activity.updateState(State.EnterNewPin);
+        } else {
+          CharSequence message = activity.getChangePinResultMessage(result);
+          activity.showError(message);
+          activity.mPinEntry.setText("");
+        }
+      }
+    },
+    /**
+     * The default old PIN is found. Show a blank screen while verifying with the server to make
+     * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}. If
+     * not, the user probably changed the PIN through other means, proceed to {@link #EnterOldPin}.
+     * If any other issue caused the verifying to fail, show an error and exit.
+     */
+    VerifyOldPin {
+      @Override
+      public void onEnter(VoicemailChangePinActivity activity) {
+        activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE);
+        activity.verifyOldPin();
+      }
+
+      @Override
+      public void handleResult(
+          final VoicemailChangePinActivity activity, @ChangePinResult int result) {
+        if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+          activity.updateState(State.EnterNewPin);
+        } else if (result == OmtpConstants.CHANGE_PIN_SYSTEM_ERROR) {
+          activity
+              .getWindow()
+              .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+          activity.showError(
+              activity.getString(R.string.change_pin_system_error),
+              new OnDismissListener() {
+                @Override
+                public void onDismiss(DialogInterface dialog) {
+                  activity.finish();
+                }
+              });
+        } else {
+          VvmLog.e(TAG, "invalid default old PIN: " + activity.getChangePinResultMessage(result));
+          // If the default old PIN is rejected by the server, the PIN is probably changed
+          // through other means, or the generated pin is invalid
+          // Wipe the default old PIN so the old PIN input box will be shown to the user
+          // on the next time.
+          setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
+          activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
+          activity.updateState(State.EnterOldPin);
+        }
+      }
+
+      @Override
+      public void onLeave(VoicemailChangePinActivity activity) {
+        activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE);
+      }
+    },
+    /**
+     * Let the user enter the new PIN and validate the format. Only length is enforced, PIN strength
+     * check relies on the server. After a valid PIN is entered, proceed to {@link #ConfirmNewPin}
+     */
+    EnterNewPin {
+      @Override
+      public void onEnter(VoicemailChangePinActivity activity) {
+        activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header);
+        activity.mNextButton.setText(R.string.change_pin_continue_label);
+        activity.mHintText.setText(
+            activity.getString(
+                R.string.change_pin_enter_new_pin_hint,
+                activity.mPinMinLength,
+                activity.mPinMaxLength));
+      }
+
+      @Override
+      public void onInputChanged(VoicemailChangePinActivity activity) {
+        String password = activity.getCurrentPasswordInput();
+        if (password.length() == 0) {
+          activity.setNextEnabled(false);
+          return;
+        }
+        CharSequence error = activity.validatePassword(password);
+        if (error != null) {
+          activity.mErrorText.setText(error);
+          activity.setNextEnabled(false);
+        } else {
+          activity.mErrorText.setText(null);
+          activity.setNextEnabled(true);
+        }
+      }
+
+      @Override
+      public void handleNext(VoicemailChangePinActivity activity) {
+        CharSequence errorMsg;
+        errorMsg = activity.validatePassword(activity.getCurrentPasswordInput());
+        if (errorMsg != null) {
+          activity.showError(errorMsg);
+          return;
+        }
+        activity.mFirstPin = activity.getCurrentPasswordInput();
+        activity.updateState(State.ConfirmNewPin);
+      }
+    },
+    /**
+     * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a PIN
+     * change to the server. Finish the activity if succeeded. Return to {@link #EnterOldPin} if the
+     * old PIN is rejected, {@link #EnterNewPin} for other failure.
+     */
+    ConfirmNewPin {
+      @Override
+      public void onEnter(VoicemailChangePinActivity activity) {
+        activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header);
+        activity.mHintText.setText(null);
+        activity.mNextButton.setText(R.string.change_pin_ok_label);
+      }
+
+      @Override
+      public void onInputChanged(VoicemailChangePinActivity activity) {
+        if (activity.getCurrentPasswordInput().length() == 0) {
+          activity.setNextEnabled(false);
+          return;
+        }
+        if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) {
+          activity.setNextEnabled(true);
+          activity.mErrorText.setText(null);
+        } else {
+          activity.setNextEnabled(false);
+          activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match);
+        }
+      }
+
+      @Override
+      public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+        if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+          // If the PIN change succeeded we no longer know what the old (current) PIN is.
+          // Wipe the default old PIN so the old PIN input box will be shown to the user
+          // on the next time.
+          setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
+          activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
+
+          activity.finish();
+
+          Toast.makeText(
+                  activity, activity.getString(R.string.change_pin_succeeded), Toast.LENGTH_SHORT)
+              .show();
+        } else {
+          CharSequence message = activity.getChangePinResultMessage(result);
+          VvmLog.i(TAG, "Change PIN failed: " + message);
+          activity.showError(message);
+          if (result == OmtpConstants.CHANGE_PIN_MISMATCH) {
+            // Somehow the PIN has changed, prompt to enter the old PIN again.
+            activity.updateState(State.EnterOldPin);
+          } else {
+            // The new PIN failed to fulfil other restrictions imposed by the server.
+            activity.updateState(State.EnterNewPin);
+          }
+        }
+      }
+
+      @Override
+      public void handleNext(VoicemailChangePinActivity activity) {
+        activity.processPinChange(activity.mOldPin, activity.mFirstPin);
+      }
+    };
+
+    /** The activity has switched from another state to this one. */
+    public void onEnter(VoicemailChangePinActivity activity) {
+      // Do nothing
+    }
+
+    /**
+     * The user has typed something into the PIN input field. Also called after {@link
+     * #onEnter(VoicemailChangePinActivity)}
+     */
+    public void onInputChanged(VoicemailChangePinActivity activity) {
+      // Do nothing
+    }
+
+    /** The asynchronous call to change the PIN on the server has returned. */
+    public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+      // Do nothing
+    }
+
+    /** The user has pressed the "next" button. */
+    public void handleNext(VoicemailChangePinActivity activity) {
+      // Do nothing
+    }
+
+    /** The activity has switched from this state to another one. */
+    public void onLeave(VoicemailChangePinActivity activity) {
+      // Do nothing
+    }
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+
+    mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+    mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle);
+    setContentView(R.layout.voicemail_change_pin);
+    setTitle(R.string.change_pin_title);
+
+    readPinLength();
+
+    View view = findViewById(android.R.id.content);
+
+    mCancelButton = (Button) view.findViewById(R.id.cancel_button);
+    mCancelButton.setOnClickListener(this);
+    mNextButton = (Button) view.findViewById(R.id.next_button);
+    mNextButton.setOnClickListener(this);
+
+    mPinEntry = (EditText) view.findViewById(R.id.pin_entry);
+    mPinEntry.setOnEditorActionListener(this);
+    mPinEntry.addTextChangedListener(this);
+    if (mPinMaxLength != 0) {
+      mPinEntry.setFilters(new InputFilter[] {new LengthFilter(mPinMaxLength)});
+    }
+
+    mHeaderText = (TextView) view.findViewById(R.id.headerText);
+    mHintText = (TextView) view.findViewById(R.id.hintText);
+    mErrorText = (TextView) view.findViewById(R.id.errorText);
+
+    if (isDefaultOldPinSet(this, mPhoneAccountHandle)) {
+      mOldPin = getDefaultOldPin(this, mPhoneAccountHandle);
+      updateState(State.VerifyOldPin);
+    } else {
+      updateState(State.EnterOldPin);
+    }
+  }
+
+  private void handleOmtpEvent(OmtpEvents event) {
+    mConfig.handleEvent(getVoicemailStatusEditor(), event);
+  }
+
+  private VoicemailStatus.Editor getVoicemailStatusEditor() {
+    // This activity does not have any automatic retry mechanism, errors should be written right
+    // away.
+    return VoicemailStatus.edit(this, mPhoneAccountHandle);
+  }
+
+  /** Extracts the pin length requirement sent by the server with a STATUS SMS. */
+  private void readPinLength() {
+    VisualVoicemailPreferences preferences =
+        new VisualVoicemailPreferences(this, mPhoneAccountHandle);
+    // The OMTP pin length format is {min}-{max}
+    String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
+    if (lengths.length == 2) {
+      try {
+        mPinMinLength = Integer.parseInt(lengths[0]);
+        mPinMaxLength = Integer.parseInt(lengths[1]);
+      } catch (NumberFormatException e) {
+        mPinMinLength = 0;
+        mPinMaxLength = 0;
+      }
+    } else {
+      mPinMinLength = 0;
+      mPinMaxLength = 0;
+    }
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    updateState(mUiState);
+  }
+
+  public void handleNext() {
+    if (mPinEntry.length() == 0) {
+      return;
+    }
+    mUiState.handleNext(this);
+  }
+
+  @Override
+  public void onClick(View v) {
+    if (v.getId() == R.id.next_button) {
+      handleNext();
+    } else if (v.getId() == R.id.cancel_button) {
+      finish();
+    }
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    if (item.getItemId() == android.R.id.home) {
+      onBackPressed();
+      return true;
+    }
+    return super.onOptionsItemSelected(item);
+  }
+
+  @Override
+  public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+    if (!mNextButton.isEnabled()) {
+      return true;
+    }
+    // Check if this was the result of hitting the enter or "done" key
+    if (actionId == EditorInfo.IME_NULL
+        || actionId == EditorInfo.IME_ACTION_DONE
+        || actionId == EditorInfo.IME_ACTION_NEXT) {
+      handleNext();
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public void afterTextChanged(Editable s) {
+    mUiState.onInputChanged(this);
+  }
+
+  @Override
+  public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+    // Do nothing
+  }
+
+  @Override
+  public void onTextChanged(CharSequence s, int start, int before, int count) {
+    // Do nothing
+  }
+
+  /**
+   * After replacing the default PIN with a random PIN, call this to store the random PIN. The
+   * stored PIN will be automatically entered when the user attempts to change the PIN.
+   */
+  public static void setDefaultOldPIN(
+      Context context, PhoneAccountHandle phoneAccountHandle, String pin) {
+    new VisualVoicemailPreferences(context, phoneAccountHandle)
+        .edit()
+        .putString(KEY_DEFAULT_OLD_PIN, pin)
+        .apply();
+  }
+
+  public static boolean isDefaultOldPinSet(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return getDefaultOldPin(context, phoneAccountHandle) != null;
+  }
+
+  private static String getDefaultOldPin(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return new VisualVoicemailPreferences(context, phoneAccountHandle)
+        .getString(KEY_DEFAULT_OLD_PIN);
+  }
+
+  private String getCurrentPasswordInput() {
+    return mPinEntry.getText().toString();
+  }
+
+  private void updateState(State state) {
+    State previousState = mUiState;
+    mUiState = state;
+    if (previousState != state) {
+      previousState.onLeave(this);
+      mPinEntry.setText("");
+      mUiState.onEnter(this);
+    }
+    mUiState.onInputChanged(this);
+  }
+
+  /**
+   * Validates PIN and returns a message to display if PIN fails test.
+   *
+   * @param password the raw password the user typed in
+   * @return error message to show to user or null if password is OK
+   */
+  private CharSequence validatePassword(String password) {
+    if (mPinMinLength == 0 && mPinMaxLength == 0) {
+      // Invalid length requirement is sent by the server, just accept anything and let the
+      // server decide.
+      return null;
+    }
+
+    if (password.length() < mPinMinLength) {
+      return getString(R.string.vm_change_pin_error_too_short);
+    }
+    return null;
+  }
+
+  private void setHeader(int text) {
+    mHeaderText.setText(text);
+    mPinEntry.setContentDescription(mHeaderText.getText());
+  }
+
+  /**
+   * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not
+   * {@link OmtpConstants#CHANGE_PIN_SUCCESS}
+   */
+  private CharSequence getChangePinResultMessage(@ChangePinResult int result) {
+    switch (result) {
+      case OmtpConstants.CHANGE_PIN_TOO_SHORT:
+        return getString(R.string.vm_change_pin_error_too_short);
+      case OmtpConstants.CHANGE_PIN_TOO_LONG:
+        return getString(R.string.vm_change_pin_error_too_long);
+      case OmtpConstants.CHANGE_PIN_TOO_WEAK:
+        return getString(R.string.vm_change_pin_error_too_weak);
+      case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER:
+        return getString(R.string.vm_change_pin_error_invalid);
+      case OmtpConstants.CHANGE_PIN_MISMATCH:
+        return getString(R.string.vm_change_pin_error_mismatch);
+      case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR:
+        return getString(R.string.vm_change_pin_error_system_error);
+      default:
+        VvmLog.wtf(TAG, "Unexpected ChangePinResult " + result);
+        return null;
+    }
+  }
+
+  private void verifyOldPin() {
+    processPinChange(mOldPin, mOldPin);
+  }
+
+  private void setNextEnabled(boolean enabled) {
+    mNextButton.setEnabled(enabled);
+  }
+
+  private void showError(CharSequence message) {
+    showError(message, null);
+  }
+
+  private void showError(CharSequence message, @Nullable OnDismissListener callback) {
+    new AlertDialog.Builder(this)
+        .setMessage(message)
+        .setPositiveButton(android.R.string.ok, null)
+        .setOnDismissListener(callback)
+        .show();
+  }
+
+  /** Asynchronous call to change the PIN on the server. */
+  private void processPinChange(String oldPin, String newPin) {
+    mProgressDialog = new ProgressDialog(this);
+    mProgressDialog.setCancelable(false);
+    mProgressDialog.setMessage(getString(R.string.vm_change_pin_progress_message));
+    mProgressDialog.show();
+
+    ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback(oldPin, newPin);
+    callback.requestNetwork();
+  }
+
+  private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+    private final String mOldPin;
+    private final String mNewPin;
+
+    public ChangePinNetworkRequestCallback(String oldPin, String newPin) {
+      super(
+          mConfig, mPhoneAccountHandle, VoicemailChangePinActivity.this.getVoicemailStatusEditor());
+      mOldPin = oldPin;
+      mNewPin = newPin;
+    }
+
+    @Override
+    public void onAvailable(Network network) {
+      super.onAvailable(network);
+      try (ImapHelper helper =
+          new ImapHelper(
+              VoicemailChangePinActivity.this,
+              mPhoneAccountHandle,
+              network,
+              getVoicemailStatusEditor())) {
+
+        @ChangePinResult int result = helper.changePin(mOldPin, mNewPin);
+        sendResult(result);
+      } catch (InitializingException | MessagingException e) {
+        VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e);
+        sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
+      }
+    }
+
+    @Override
+    public void onFailed(String reason) {
+      super.onFailed(reason);
+      sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
+    }
+
+    private void sendResult(@ChangePinResult int result) {
+      VvmLog.i(TAG, "Change PIN result: " + result);
+      if (mProgressDialog.isShowing()
+          && !VoicemailChangePinActivity.this.isDestroyed()
+          && !VoicemailChangePinActivity.this.isFinishing()) {
+        mProgressDialog.dismiss();
+      } else {
+        VvmLog.i(TAG, "Dialog not visible, not dismissing");
+      }
+      mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget();
+      releaseNetwork();
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java b/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java
new file mode 100644
index 0000000..22c729c
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailRingtonePreference.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 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.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.RingtonePreference;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.util.AttributeSet;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.SettingsUtil;
+
+/**
+ * Looks up the voicemail ringtone's name asynchronously and updates the preference's summary when
+ * it is created or updated.
+ */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailRingtonePreference extends RingtonePreference {
+
+  /** Callback when the ringtone name has been fetched. */
+  public interface VoicemailRingtoneNameChangeListener {
+    void onVoicemailRingtoneNameChanged(CharSequence name);
+  }
+
+  private static final int MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY = 1;
+
+  private PhoneAccountHandle phoneAccountHandle;
+  private final TelephonyManager telephonyManager;
+
+  private VoicemailRingtoneNameChangeListener mVoicemailRingtoneNameChangeListener;
+  private Runnable mVoicemailRingtoneLookupRunnable;
+  private final Handler mVoicemailRingtoneLookupComplete =
+      new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+          switch (msg.what) {
+            case MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY:
+              if (mVoicemailRingtoneNameChangeListener != null) {
+                mVoicemailRingtoneNameChangeListener.onVoicemailRingtoneNameChanged(
+                    (CharSequence) msg.obj);
+              }
+              setSummary((CharSequence) msg.obj);
+              break;
+            default:
+              Assert.fail();
+          }
+        }
+      };
+
+  public VoicemailRingtonePreference(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    telephonyManager = context.getSystemService(TelephonyManager.class);
+  }
+
+  public void init(PhoneAccountHandle phoneAccountHandle, CharSequence oldRingtoneName) {
+    this.phoneAccountHandle = phoneAccountHandle;
+    setSummary(oldRingtoneName);
+    mVoicemailRingtoneLookupRunnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            SettingsUtil.getRingtoneName(
+                getContext(),
+                mVoicemailRingtoneLookupComplete,
+                telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle),
+                MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY);
+          }
+        };
+
+    updateRingtoneName();
+  }
+
+  public void setVoicemailRingtoneNameChangeListener(VoicemailRingtoneNameChangeListener l) {
+    mVoicemailRingtoneNameChangeListener = l;
+  }
+
+  @Override
+  protected Uri onRestoreRingtone() {
+    return telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle);
+  }
+
+  @Override
+  protected void onSaveRingtone(Uri ringtoneUri) {
+    telephonyManager.setVoicemailRingtoneUri(phoneAccountHandle, ringtoneUri);
+    updateRingtoneName();
+  }
+
+  private void updateRingtoneName() {
+    new Thread(mVoicemailRingtoneLookupRunnable).start();
+  }
+}
diff --git a/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java b/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
new file mode 100644
index 0000000..a5b94a7
--- /dev/null
+++ b/java/com/android/voicemail/impl/settings/VoicemailSettingsFragment.java
@@ -0,0 +1,202 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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.voicemail.impl.settings;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.R;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.sync.VvmAccountManager;
+
+/** Fragment for voicemail settings. */
+@TargetApi(VERSION_CODES.O)
+public class VoicemailSettingsFragment extends PreferenceFragment
+    implements Preference.OnPreferenceChangeListener,
+        VoicemailRingtonePreference.VoicemailRingtoneNameChangeListener {
+
+  private static final String TAG = "VmSettingsActivity";
+
+  private PhoneAccountHandle phoneAccountHandle;
+  private OmtpVvmCarrierConfigHelper omtpVvmCarrierConfigHelper;
+
+  private VoicemailRingtonePreference voicemailRingtonePreference;
+  private CheckBoxPreference voicemailVibration;
+  private SwitchPreference voicemailVisualVoicemail;
+  private SwitchPreference autoArchiveSwitchPreference;
+  private Preference voicemailChangePinPreference;
+  private PreferenceScreen advancedSettings;
+
+  // The ringtone name is retrieved with an async call. Cache the old name so there will be no jank
+  // during transition.
+  private CharSequence oldRingtoneName = "";
+
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+
+    phoneAccountHandle =
+        getContext()
+            .getSystemService(TelecomManager.class)
+            .getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
+
+    omtpVvmCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+
+    PreferenceScreen preferenceScreen = getPreferenceScreen();
+    if (preferenceScreen != null) {
+      preferenceScreen.removeAll();
+    }
+
+    addPreferencesFromResource(R.xml.voicemail_settings);
+
+    PreferenceScreen prefSet = getPreferenceScreen();
+
+    voicemailRingtonePreference =
+        (VoicemailRingtonePreference)
+            findPreference(getString(R.string.voicemail_notification_ringtone_key));
+    voicemailRingtonePreference.setVoicemailRingtoneNameChangeListener(this);
+    voicemailRingtonePreference.init(phoneAccountHandle, oldRingtoneName);
+
+    voicemailVibration =
+        (CheckBoxPreference) findPreference(getString(R.string.voicemail_notification_vibrate_key));
+    voicemailVibration.setOnPreferenceChangeListener(this);
+    voicemailVibration.setChecked(
+        getContext()
+            .getSystemService(TelephonyManager.class)
+            .isVoicemailVibrationEnabled(phoneAccountHandle));
+
+    voicemailVisualVoicemail =
+        (SwitchPreference) findPreference(getString(R.string.voicemail_visual_voicemail_key));
+
+    autoArchiveSwitchPreference =
+        (SwitchPreference)
+            findPreference(getString(R.string.voicemail_visual_voicemail_archive_key));
+    autoArchiveSwitchPreference.setOnPreferenceChangeListener(this);
+    autoArchiveSwitchPreference.setChecked(
+        VisualVoicemailSettingsUtil.isArchiveEnabled(getContext(), phoneAccountHandle));
+
+    if (!ConfigProviderBindings.get(getContext())
+        .getBoolean(VisualVoicemailSettingsUtil.ALLOW_VOICEMAIL_ARCHIVE, true)) {
+      getPreferenceScreen().removePreference(autoArchiveSwitchPreference);
+    }
+
+    voicemailChangePinPreference = findPreference(getString(R.string.voicemail_change_pin_key));
+    Intent changePinIntent = new Intent(new Intent(getContext(), VoicemailChangePinActivity.class));
+    changePinIntent.putExtra(
+        VoicemailChangePinActivity.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+
+    voicemailChangePinPreference.setIntent(changePinIntent);
+    if (VoicemailChangePinActivity.isDefaultOldPinSet(getContext(), phoneAccountHandle)) {
+      voicemailChangePinPreference.setTitle(R.string.voicemail_set_pin_dialog_title);
+    } else {
+      voicemailChangePinPreference.setTitle(R.string.voicemail_change_pin_dialog_title);
+    }
+
+    if (omtpVvmCarrierConfigHelper.isValid()) {
+      voicemailVisualVoicemail.setOnPreferenceChangeListener(this);
+      voicemailVisualVoicemail.setChecked(
+          VisualVoicemailSettingsUtil.isEnabled(getContext(), phoneAccountHandle));
+      if (!isVisualVoicemailActivated()) {
+        prefSet.removePreference(voicemailChangePinPreference);
+      }
+    } else {
+      prefSet.removePreference(voicemailVisualVoicemail);
+      prefSet.removePreference(voicemailChangePinPreference);
+    }
+
+    advancedSettings =
+        (PreferenceScreen) findPreference(getString(R.string.voicemail_advanced_settings_key));
+    Intent advancedSettingsIntent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+    advancedSettingsIntent.putExtra(TelephonyManager.EXTRA_HIDE_PUBLIC_SETTINGS, true);
+    advancedSettings.setIntent(advancedSettingsIntent);
+  }
+
+  @Override
+  public void onPause() {
+    super.onPause();
+  }
+
+  /**
+   * Implemented to support onPreferenceChangeListener to look for preference changes.
+   *
+   * @param preference is the preference to be changed
+   * @param objValue should be the value of the selection, NOT its localized display value.
+   */
+  @Override
+  public boolean onPreferenceChange(Preference preference, Object objValue) {
+    VvmLog.d(TAG, "onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\"");
+    if (preference.getKey().equals(voicemailVisualVoicemail.getKey())) {
+      boolean isEnabled = (boolean) objValue;
+      VisualVoicemailSettingsUtil.setEnabled(getContext(), phoneAccountHandle, isEnabled);
+      PreferenceScreen prefSet = getPreferenceScreen();
+      if (isVisualVoicemailActivated()) {
+        prefSet.addPreference(voicemailChangePinPreference);
+      } else {
+        prefSet.removePreference(voicemailChangePinPreference);
+      }
+    } else if (preference.getKey().equals(autoArchiveSwitchPreference.getKey())) {
+      logArchiveToggle((boolean) objValue);
+      VisualVoicemailSettingsUtil.setArchiveEnabled(
+          getContext(), phoneAccountHandle, (boolean) objValue);
+    } else if (preference.getKey().equals(voicemailVibration.getKey())) {
+      getContext()
+          .getSystemService(TelephonyManager.class)
+          .setVoicemailVibrationEnabled(phoneAccountHandle, (boolean) objValue);
+    }
+
+    // Always let the preference setting proceed.
+    return true;
+  }
+
+  private void logArchiveToggle(boolean userTurnedOn) {
+    if (userTurnedOn) {
+      Logger.get(getContext())
+          .logImpression(DialerImpression.Type.VVM_USER_TURNED_ARCHIVE_ON_FROM_SETTINGS);
+    } else {
+      Logger.get(getContext())
+          .logImpression(DialerImpression.Type.VVM_USER_TURNED_ARCHIVE_OFF_FROM_SETTINGS);
+    }
+  }
+
+  @Override
+  public void onVoicemailRingtoneNameChanged(CharSequence name) {
+    oldRingtoneName = name;
+  }
+
+  private boolean isVisualVoicemailActivated() {
+    if (!VisualVoicemailSettingsUtil.isEnabled(getContext(), phoneAccountHandle)) {
+      return false;
+    }
+    return VvmAccountManager.isAccountActivated(getContext(), phoneAccountHandle);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java b/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
new file mode 100644
index 0000000..1d1a639
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/LegacyModeSmsHandler.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 Google Inc. All Rights Reserved.
+ *
+ * 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.voicemail.impl.sms;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.TelephonyManagerStub;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Class ot handle voicemail SMS under legacy mode
+ *
+ * @see OmtpVvmCarrierConfigHelper#isLegacyModeEnabled()
+ */
+public class LegacyModeSmsHandler {
+
+  private static final String TAG = "LegacyModeSmsHandler";
+
+  public static void handle(Context context, VisualVoicemailSms sms) {
+    VvmLog.v(TAG, "processing VVM SMS on legacy mode");
+    String eventType = sms.getPrefix();
+    Bundle data = sms.getFields();
+    PhoneAccountHandle handle = sms.getPhoneAccountHandle();
+
+    if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+      SyncMessage message = new SyncMessage(data);
+      VvmLog.v(
+          TAG, "Received SYNC sms for " + handle + " with event " + message.getSyncTriggerEvent());
+
+      switch (message.getSyncTriggerEvent()) {
+        case OmtpConstants.NEW_MESSAGE:
+        case OmtpConstants.MAILBOX_UPDATE:
+          // The user has called into the voicemail and the new message count could
+          // change.
+          // For some carriers new message count could be set to 0 even if there are still
+          // unread messages, to clear the message waiting indicator.
+          VvmLog.v(TAG, "updating MWI");
+
+          // Setting voicemail message count to non-zero will show the telephony voicemail
+          // notification, and zero will clear it.
+          TelephonyManagerStub.showVoicemailNotification(message.getNewMessageCount());
+          break;
+        default:
+          break;
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java
new file mode 100644
index 0000000..5fc5e70
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpCvvmMessageSender.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * 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.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpConstants;
+
+/** An implementation of the OmtpMessageSender for T-Mobile. */
+public class OmtpCvvmMessageSender extends OmtpMessageSender {
+  public OmtpCvvmMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    super(context, phoneAccountHandle, applicationPort, destinationNumber);
+  }
+
+  @Override
+  public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+    sendCvvmMessage(OmtpConstants.ACTIVATE_REQUEST, sentIntent);
+  }
+
+  @Override
+  public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+    sendCvvmMessage(OmtpConstants.DEACTIVATE_REQUEST, sentIntent);
+  }
+
+  @Override
+  public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+    sendCvvmMessage(OmtpConstants.STATUS_REQUEST, sentIntent);
+  }
+
+  private void sendCvvmMessage(String request, PendingIntent sentIntent) {
+    StringBuilder sb = new StringBuilder().append(request);
+    sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
+    appendField(sb, "dt", "15");
+    sendSms(sb.toString(), sentIntent);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java b/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java
new file mode 100644
index 0000000..ef0bf10
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpMessageReceiver.java
@@ -0,0 +1,161 @@
+/*
+ * 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.voicemail.impl.sms;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpService;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.Voicemail.Builder;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.sync.OmtpVvmSyncService;
+import com.android.voicemail.impl.sync.SyncOneTask;
+import com.android.voicemail.impl.sync.SyncTask;
+import com.android.voicemail.impl.sync.VoicemailsQueryHelper;
+import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
+
+/** Receive SMS messages and send for processing by the OMTP visual voicemail source. */
+@TargetApi(VERSION_CODES.O)
+public class OmtpMessageReceiver extends BroadcastReceiver {
+
+  private static final String TAG = "OmtpMessageReceiver";
+
+  private Context mContext;
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    mContext = context;
+    VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
+    PhoneAccountHandle phone = sms.getPhoneAccountHandle();
+
+    if (phone == null) {
+      // This should never happen
+      VvmLog.i(TAG, "Received message for null phone account");
+      return;
+    }
+
+    if (!context.getSystemService(UserManager.class).isUserUnlocked()) {
+      VvmLog.i(TAG, "Received message on locked device");
+      // LegacyModeSmsHandler can handle new message notifications without storage access
+      LegacyModeSmsHandler.handle(context, sms);
+      // A full sync will happen after the device is unlocked, so nothing else need to be
+      // done.
+      return;
+    }
+
+    OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, phone);
+    if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phone)) {
+      if (helper.isLegacyModeEnabled()) {
+        LegacyModeSmsHandler.handle(context, sms);
+      } else {
+        VvmLog.i(TAG, "Received vvm message for disabled vvm source.");
+      }
+      return;
+    }
+
+    String eventType = sms.getPrefix();
+    Bundle data = sms.getFields();
+
+    if (eventType == null || data == null) {
+      VvmLog.e(TAG, "Unparsable VVM SMS received, ignoring");
+      return;
+    }
+
+    if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+      SyncMessage message = new SyncMessage(data);
+
+      VvmLog.v(
+          TAG, "Received SYNC sms for " + phone + " with event " + message.getSyncTriggerEvent());
+      processSync(phone, message);
+    } else if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+      VvmLog.v(TAG, "Received Status sms for " + phone);
+      // If the STATUS SMS is initiated by ActivationTask the TaskSchedulerService will reject
+      // the follow request. Providing the data will also prevent ActivationTask from
+      // requesting another STATUS SMS. The following task will only run if the carrier
+      // spontaneous send a STATUS SMS, in that case, the VVM service should be reactivated.
+      ActivationTask.start(context, phone, data);
+    } else {
+      VvmLog.w(TAG, "Unknown prefix: " + eventType);
+      VisualVoicemailProtocol protocol = helper.getProtocol();
+      if (protocol == null) {
+        return;
+      }
+      Bundle statusData = helper.getProtocol().translateStatusSmsBundle(helper, eventType, data);
+      if (statusData != null) {
+        VvmLog.i(TAG, "Protocol recognized the SMS as STATUS, activating");
+        ActivationTask.start(context, phone, data);
+      }
+    }
+  }
+
+  /**
+   * A sync message has two purposes: to signal a new voicemail message, and to indicate the
+   * voicemails on the server have changed remotely (usually through the TUI). Save the new message
+   * to the voicemail provider if it is the former case and perform a full sync in the latter case.
+   *
+   * @param message The sync message to extract data from.
+   */
+  private void processSync(PhoneAccountHandle phone, SyncMessage message) {
+    switch (message.getSyncTriggerEvent()) {
+      case OmtpConstants.NEW_MESSAGE:
+        if (!OmtpConstants.VOICE.equals(message.getContentType())) {
+          VvmLog.i(
+              TAG,
+              "Non-voice message of type '" + message.getContentType() + "' received, ignoring");
+          return;
+        }
+
+        Builder builder =
+            Voicemail.createForInsertion(message.getTimestampMillis(), message.getSender())
+                .setPhoneAccount(phone)
+                .setSourceData(message.getId())
+                .setDuration(message.getLength())
+                .setSourcePackage(mContext.getPackageName());
+        Voicemail voicemail = builder.build();
+
+        VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
+        if (queryHelper.isVoicemailUnique(voicemail)) {
+          Uri uri = VoicemailDatabaseUtil.insert(mContext, voicemail);
+          voicemail = builder.setId(ContentUris.parseId(uri)).setUri(uri).build();
+          SyncOneTask.start(mContext, phone, voicemail);
+        }
+        break;
+      case OmtpConstants.MAILBOX_UPDATE:
+        SyncTask.start(mContext, phone, OmtpVvmSyncService.SYNC_DOWNLOAD_ONLY);
+        break;
+      case OmtpConstants.GREETINGS_UPDATE:
+        // Not implemented in V1
+        break;
+      default:
+        VvmLog.e(TAG, "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
+        break;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpMessageSender.java
new file mode 100644
index 0000000..6c9333f
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpMessageSender.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * 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.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.telephony.VisualVoicemailService;
+import com.android.voicemail.impl.OmtpConstants;
+
+/**
+ * Send client originated OMTP messages to the OMTP server.
+ *
+ * <p>Uses {@link PendingIntent} instead of a call back to notify when the message is sent. This is
+ * primarily to keep the implementation simple and reuse what the underlying {@link SmsManager}
+ * interface provides.
+ *
+ * <p>Provides simple APIs to send different types of mobile originated OMTP SMS to the VVM server.
+ */
+public abstract class OmtpMessageSender {
+  protected static final String TAG = "OmtpMessageSender";
+  protected final Context mContext;
+  protected final PhoneAccountHandle mPhoneAccountHandle;
+  protected final short mApplicationPort;
+  protected final String mDestinationNumber;
+
+  public OmtpMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    mContext = context;
+    mPhoneAccountHandle = phoneAccountHandle;
+    mApplicationPort = applicationPort;
+    mDestinationNumber = destinationNumber;
+  }
+
+  /**
+   * Sends a request to the VVM server to activate VVM for the current subscriber.
+   *
+   * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+   *     sent, or failed.
+   */
+  public void requestVvmActivation(@Nullable PendingIntent sentIntent) {}
+
+  /**
+   * Sends a request to the VVM server to deactivate VVM for the current subscriber.
+   *
+   * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+   *     sent, or failed.
+   */
+  public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {}
+
+  /**
+   * Send a request to the VVM server to get account status of the current subscriber.
+   *
+   * @param sentIntent If not NULL this PendingIntent is broadcast when the message is successfully
+   *     sent, or failed.
+   */
+  public void requestVvmStatus(@Nullable PendingIntent sentIntent) {}
+
+  protected void sendSms(String text, PendingIntent sentIntent) {
+    VisualVoicemailService.sendVisualVoicemailSms(
+        mContext, mPhoneAccountHandle, mDestinationNumber, mApplicationPort, text, sentIntent);
+  }
+
+  protected void appendField(StringBuilder sb, String field, Object value) {
+    sb.append(field).append(OmtpConstants.SMS_KEY_VALUE_SEPARATOR).append(value);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java b/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java
new file mode 100644
index 0000000..7974699
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/OmtpStandardMessageSender.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * 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.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.voicemail.impl.OmtpConstants;
+
+/** A implementation of the OmtpMessageSender using the standard OMTP sms protocol. */
+public class OmtpStandardMessageSender extends OmtpMessageSender {
+  private final String mClientType;
+  private final String mProtocolVersion;
+  private final String mClientPrefix;
+
+  /**
+   * Creates a new instance of OmtpStandardMessageSender.
+   *
+   * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
+   *     Otherwise, a standard text SMS is sent.
+   * @param destinationNumber Destination number to be used.
+   * @param clientType The "ct" field to be set in the MO message. This is the value used by the VVM
+   *     server to identify the client. Certain VVM servers require a specific agreed value for this
+   *     field.
+   * @param protocolVersion OMTP protocol version.
+   * @param clientPrefix The client prefix requested to be used by the server in its MT messages.
+   */
+  public OmtpStandardMessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber,
+      String clientType,
+      String protocolVersion,
+      String clientPrefix) {
+    super(context, phoneAccountHandle, applicationPort, destinationNumber);
+    mClientType = clientType;
+    mProtocolVersion = protocolVersion;
+    mClientPrefix = clientPrefix;
+  }
+
+  // Activate message:
+  // V1.1: Activate:pv=<value>;ct=<value>
+  // V1.2: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+  // V1.3: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+  @Override
+  public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+    StringBuilder sb = new StringBuilder().append(OmtpConstants.ACTIVATE_REQUEST);
+
+    appendProtocolVersionAndClientType(sb);
+    if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_2)
+        || TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
+      appendApplicationPort(sb);
+      appendClientPrefix(sb);
+    }
+
+    sendSms(sb.toString(), sentIntent);
+  }
+
+  // Deactivate message:
+  // V1.1: Deactivate:pv=<value>;ct=<string>
+  // V1.2: Deactivate:pv=<value>;ct=<string>
+  // V1.3: Deactivate:pv=<value>;ct=<string>
+  @Override
+  public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+    StringBuilder sb = new StringBuilder().append(OmtpConstants.DEACTIVATE_REQUEST);
+    appendProtocolVersionAndClientType(sb);
+
+    sendSms(sb.toString(), sentIntent);
+  }
+
+  // Status message:
+  // V1.1: STATUS
+  // V1.2: STATUS
+  // V1.3: STATUS:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+  @Override
+  public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+    StringBuilder sb = new StringBuilder().append(OmtpConstants.STATUS_REQUEST);
+
+    if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
+      appendProtocolVersionAndClientType(sb);
+      appendApplicationPort(sb);
+      appendClientPrefix(sb);
+    }
+
+    sendSms(sb.toString(), sentIntent);
+  }
+
+  private void appendProtocolVersionAndClientType(StringBuilder sb) {
+    sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
+    appendField(sb, OmtpConstants.PROTOCOL_VERSION, mProtocolVersion);
+    sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+    appendField(sb, OmtpConstants.CLIENT_TYPE, mClientType);
+  }
+
+  private void appendApplicationPort(StringBuilder sb) {
+    sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+    appendField(sb, OmtpConstants.APPLICATION_PORT, mApplicationPort);
+  }
+
+  private void appendClientPrefix(StringBuilder sb) {
+    sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+    sb.append(mClientPrefix);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/StatusMessage.java b/java/com/android/voicemail/impl/sms/StatusMessage.java
new file mode 100644
index 0000000..a5766a6
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/StatusMessage.java
@@ -0,0 +1,201 @@
+/*
+ * 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.voicemail.impl.sms;
+
+import android.os.Bundle;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Structured data representation of OMTP STATUS message.
+ *
+ * <p>The getters will return null if the field was not set in the message body or it could not be
+ * parsed.
+ */
+public class StatusMessage {
+  // NOTE: Following Status SMS fields are not yet parsed, as they do not seem
+  // to be useful for initial omtp source implementation.
+  // lang, g_len, vs_len, pw_len, pm, gm, vtc, vt
+
+  private final String mProvisioningStatus;
+  private final String mStatusReturnCode;
+  private final String mSubscriptionUrl;
+  private final String mServerAddress;
+  private final String mTuiAccessNumber;
+  private final String mClientSmsDestinationNumber;
+  private final String mImapPort;
+  private final String mImapUserName;
+  private final String mImapPassword;
+  private final String mSmtpPort;
+  private final String mSmtpUserName;
+  private final String mSmtpPassword;
+  private final String mTuiPasswordLength;
+
+  @Override
+  public String toString() {
+    return "StatusMessage [mProvisioningStatus="
+        + mProvisioningStatus
+        + ", mStatusReturnCode="
+        + mStatusReturnCode
+        + ", mSubscriptionUrl="
+        + mSubscriptionUrl
+        + ", mServerAddress="
+        + mServerAddress
+        + ", mTuiAccessNumber="
+        + mTuiAccessNumber
+        + ", mClientSmsDestinationNumber="
+        + mClientSmsDestinationNumber
+        + ", mImapPort="
+        + mImapPort
+        + ", mImapUserName="
+        + mImapUserName
+        + ", mImapPassword="
+        + VvmLog.pii(mImapPassword)
+        + ", mSmtpPort="
+        + mSmtpPort
+        + ", mSmtpUserName="
+        + mSmtpUserName
+        + ", mSmtpPassword="
+        + VvmLog.pii(mSmtpPassword)
+        + ", mTuiPasswordLength="
+        + mTuiPasswordLength
+        + "]";
+  }
+
+  public StatusMessage(Bundle wrappedData) {
+    mProvisioningStatus = unquote(getString(wrappedData, OmtpConstants.PROVISIONING_STATUS));
+    mStatusReturnCode = getString(wrappedData, OmtpConstants.RETURN_CODE);
+    mSubscriptionUrl = getString(wrappedData, OmtpConstants.SUBSCRIPTION_URL);
+    mServerAddress = getString(wrappedData, OmtpConstants.SERVER_ADDRESS);
+    mTuiAccessNumber = getString(wrappedData, OmtpConstants.TUI_ACCESS_NUMBER);
+    mClientSmsDestinationNumber =
+        getString(wrappedData, OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER);
+    mImapPort = getString(wrappedData, OmtpConstants.IMAP_PORT);
+    mImapUserName = getString(wrappedData, OmtpConstants.IMAP_USER_NAME);
+    mImapPassword = getString(wrappedData, OmtpConstants.IMAP_PASSWORD);
+    mSmtpPort = getString(wrappedData, OmtpConstants.SMTP_PORT);
+    mSmtpUserName = getString(wrappedData, OmtpConstants.SMTP_USER_NAME);
+    mSmtpPassword = getString(wrappedData, OmtpConstants.SMTP_PASSWORD);
+    mTuiPasswordLength = getString(wrappedData, OmtpConstants.TUI_PASSWORD_LENGTH);
+  }
+
+  private static String unquote(String string) {
+    if (string.length() < 2) {
+      return string;
+    }
+    if (string.startsWith("\"") && string.endsWith("\"")) {
+      return string.substring(1, string.length() - 1);
+    }
+    return string;
+  }
+
+  /** @return the subscriber's VVM provisioning status. */
+  public String getProvisioningStatus() {
+    return mProvisioningStatus;
+  }
+
+  /** @return the return-code of the status SMS. */
+  public String getReturnCode() {
+    return mStatusReturnCode;
+  }
+
+  /**
+   * @return the URL of the voicemail server. This is the URL to send the users to for subscribing
+   *     to the visual voicemail service.
+   */
+  @NeededForTesting
+  public String getSubscriptionUrl() {
+    return mSubscriptionUrl;
+  }
+
+  /**
+   * @return the voicemail server address. Either server IP address or fully qualified domain name.
+   */
+  public String getServerAddress() {
+    return mServerAddress;
+  }
+
+  /**
+   * @return the Telephony User Interface number to call to access voicemails directly from the IVR.
+   */
+  @NeededForTesting
+  public String getTuiAccessNumber() {
+    return mTuiAccessNumber;
+  }
+
+  /** @return the number to which client originated SMSes should be sent to. */
+  @NeededForTesting
+  public String getClientSmsDestinationNumber() {
+    return mClientSmsDestinationNumber;
+  }
+
+  /** @return the IMAP server port to talk to. */
+  public String getImapPort() {
+    return mImapPort;
+  }
+
+  /** @return the IMAP user name to be used for authentication. */
+  public String getImapUserName() {
+    return mImapUserName;
+  }
+
+  /** @return the IMAP password to be used for authentication. */
+  public String getImapPassword() {
+    return mImapPassword;
+  }
+
+  /** @return the SMTP server port to talk to. */
+  @NeededForTesting
+  public String getSmtpPort() {
+    return mSmtpPort;
+  }
+
+  /** @return the SMTP user name to be used for SMTP authentication. */
+  @NeededForTesting
+  public String getSmtpUserName() {
+    return mSmtpUserName;
+  }
+
+  /** @return the SMTP password to be used for SMTP authentication. */
+  @NeededForTesting
+  public String getSmtpPassword() {
+    return mSmtpPassword;
+  }
+
+  public String getTuiPasswordLength() {
+    return mTuiPasswordLength;
+  }
+
+  private static String getString(Bundle bundle, String key) {
+    String value = bundle.getString(key);
+    if (value == null) {
+      return "";
+    }
+    return value;
+  }
+
+  /** Saves a StatusMessage to the {@link VisualVoicemailPreferences}. Not all fields are saved. */
+  public VisualVoicemailPreferences.Editor putStatus(VisualVoicemailPreferences.Editor editor) {
+    return editor
+        .putString(OmtpConstants.IMAP_PORT, getImapPort())
+        .putString(OmtpConstants.SERVER_ADDRESS, getServerAddress())
+        .putString(OmtpConstants.IMAP_USER_NAME, getImapUserName())
+        .putString(OmtpConstants.IMAP_PASSWORD, getImapPassword())
+        .putString(OmtpConstants.TUI_PASSWORD_LENGTH, getTuiPasswordLength());
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java b/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java
new file mode 100644
index 0000000..d178628
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/StatusSmsFetcher.java
@@ -0,0 +1,162 @@
+/*
+ * 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.voicemail.impl.sms;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.OmtpService;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.protocol.VisualVoicemailProtocol;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/** Intercepts a incoming STATUS SMS with a blocking call. */
+@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
+@TargetApi(VERSION_CODES.O)
+public class StatusSmsFetcher extends BroadcastReceiver implements Closeable {
+
+  private static final String TAG = "VvmStatusSmsFetcher";
+
+  private static final long STATUS_SMS_TIMEOUT_MILLIS = 60_000;
+
+  private static final String ACTION_REQUEST_SENT_INTENT =
+      "com.android.voicemailomtp.sms.REQUEST_SENT";
+  private static final int ACTION_REQUEST_SENT_REQUEST_CODE = 0;
+
+  private CompletableFuture<Bundle> mFuture = new CompletableFuture<>();
+
+  private final Context mContext;
+  private final PhoneAccountHandle mPhoneAccountHandle;
+
+  public StatusSmsFetcher(Context context, PhoneAccountHandle phoneAccountHandle) {
+    mContext = context;
+    mPhoneAccountHandle = phoneAccountHandle;
+    IntentFilter filter = new IntentFilter(ACTION_REQUEST_SENT_INTENT);
+    filter.addAction(OmtpService.ACTION_SMS_RECEIVED);
+    context.registerReceiver(this, filter);
+  }
+
+  @Override
+  public void close() throws IOException {
+    mContext.unregisterReceiver(this);
+  }
+
+  @WorkerThread
+  @Nullable
+  public Bundle get()
+      throws InterruptedException, ExecutionException, TimeoutException, CancellationException {
+    Assert.isNotMainThread();
+    return mFuture.get(STATUS_SMS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+  }
+
+  public PendingIntent getSentIntent() {
+    Intent intent = new Intent(ACTION_REQUEST_SENT_INTENT);
+    intent.setPackage(mContext.getPackageName());
+    // Because the receiver is registered dynamically, implicit intent must be used.
+    // There should only be a single status SMS request at a time.
+    return PendingIntent.getBroadcast(
+        mContext, ACTION_REQUEST_SENT_REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+  }
+
+  @Override
+  @MainThread
+  public void onReceive(Context context, Intent intent) {
+    Assert.isMainThread();
+    if (ACTION_REQUEST_SENT_INTENT.equals(intent.getAction())) {
+      int resultCode = getResultCode();
+
+      if (resultCode == Activity.RESULT_OK) {
+        VvmLog.d(TAG, "Request SMS successfully sent");
+        return;
+      }
+
+      VvmLog.e(TAG, "Request SMS send failed: " + sentSmsResultToString(resultCode));
+      mFuture.cancel(true);
+      return;
+    }
+
+    VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
+
+    if (!mPhoneAccountHandle.equals(sms.getPhoneAccountHandle())) {
+      return;
+    }
+    String eventType = sms.getPrefix();
+
+    if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+      mFuture.complete(sms.getFields());
+      return;
+    }
+
+    if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+      return;
+    }
+
+    VvmLog.i(
+        TAG,
+        "VVM SMS with event " + eventType + " received, attempting to translate to STATUS SMS");
+    OmtpVvmCarrierConfigHelper helper =
+        new OmtpVvmCarrierConfigHelper(context, mPhoneAccountHandle);
+    VisualVoicemailProtocol protocol = helper.getProtocol();
+    if (protocol == null) {
+      return;
+    }
+    Bundle translatedBundle = protocol.translateStatusSmsBundle(helper, eventType, sms.getFields());
+
+    if (translatedBundle != null) {
+      VvmLog.i(TAG, "Translated to STATUS SMS");
+      mFuture.complete(translatedBundle);
+    }
+  }
+
+  private static String sentSmsResultToString(int resultCode) {
+    switch (resultCode) {
+      case Activity.RESULT_OK:
+        return "OK";
+      case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+        return "RESULT_ERROR_GENERIC_FAILURE";
+      case SmsManager.RESULT_ERROR_NO_SERVICE:
+        return "RESULT_ERROR_GENERIC_FAILURE";
+      case SmsManager.RESULT_ERROR_NULL_PDU:
+        return "RESULT_ERROR_GENERIC_FAILURE";
+      case SmsManager.RESULT_ERROR_RADIO_OFF:
+        return "RESULT_ERROR_GENERIC_FAILURE";
+      default:
+        return "UNKNOWN CODE: " + resultCode;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/SyncMessage.java b/java/com/android/voicemail/impl/sms/SyncMessage.java
new file mode 100644
index 0000000..3cfa1a7
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/SyncMessage.java
@@ -0,0 +1,161 @@
+/*
+ * 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.voicemail.impl.sms;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import com.android.voicemail.impl.NeededForTesting;
+import com.android.voicemail.impl.OmtpConstants;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * Structured data representation of an OMTP SYNC message.
+ *
+ * <p>Getters will return null if the field was not set in the message body or it could not be
+ * parsed.
+ */
+public class SyncMessage {
+  // Sync event that triggered this message.
+  private final String mSyncTriggerEvent;
+  // Total number of new messages on the server.
+  private final int mNewMessageCount;
+  // UID of the new message.
+  private final String mMessageId;
+  // Length of the message.
+  private final int mMessageLength;
+  // Content type (voice, video, fax...) of the new message.
+  private final String mContentType;
+  // Sender of the new message.
+  private final String mSender;
+  // Timestamp (in millis) of the new message.
+  private final long mMsgTimeMillis;
+
+  @Override
+  public String toString() {
+    return "SyncMessage [mSyncTriggerEvent="
+        + mSyncTriggerEvent
+        + ", mNewMessageCount="
+        + mNewMessageCount
+        + ", mMessageId="
+        + mMessageId
+        + ", mMessageLength="
+        + mMessageLength
+        + ", mContentType="
+        + mContentType
+        + ", mSender="
+        + mSender
+        + ", mMsgTimeMillis="
+        + mMsgTimeMillis
+        + "]";
+  }
+
+  public SyncMessage(Bundle wrappedData) {
+    mSyncTriggerEvent = getString(wrappedData, OmtpConstants.SYNC_TRIGGER_EVENT);
+    mMessageId = getString(wrappedData, OmtpConstants.MESSAGE_UID);
+    mMessageLength = getInt(wrappedData, OmtpConstants.MESSAGE_LENGTH);
+    mContentType = getString(wrappedData, OmtpConstants.CONTENT_TYPE);
+    mSender = getString(wrappedData, OmtpConstants.SENDER);
+    mNewMessageCount = getInt(wrappedData, OmtpConstants.NUM_MESSAGE_COUNT);
+    mMsgTimeMillis = parseTime(wrappedData.getString(OmtpConstants.TIME));
+  }
+
+  private static long parseTime(@Nullable String value) {
+    if (value == null) {
+      return 0L;
+    }
+    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.
+   */
+  public String getSyncTriggerEvent() {
+    return mSyncTriggerEvent;
+  }
+
+  /** @return the number of new messages stored on the voicemail server. */
+  @NeededForTesting
+  public int getNewMessageCount() {
+    return mNewMessageCount;
+  }
+
+  /**
+   * @return the message ID of the new message.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  public String getId() {
+    return mMessageId;
+  }
+
+  /**
+   * @return the content type of the new message.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  @NeededForTesting
+  public String getContentType() {
+    return mContentType;
+  }
+
+  /**
+   * @return the message length of the new message.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  public int getLength() {
+    return mMessageLength;
+  }
+
+  /**
+   * @return the sender's phone number of the new message specified as MSISDN.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  public String getSender() {
+    return mSender;
+  }
+
+  /**
+   * @return the timestamp as milliseconds for the new message.
+   *     <p>Expected to be set only for {@link OmtpConstants#NEW_MESSAGE}
+   */
+  public long getTimestampMillis() {
+    return mMsgTimeMillis;
+  }
+
+  private static int getInt(Bundle wrappedData, String key) {
+    String value = wrappedData.getString(key);
+    if (value == null) {
+      return 0;
+    }
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      return 0;
+    }
+  }
+
+  private static String getString(Bundle wrappedData, String key) {
+    String value = wrappedData.getString(key);
+    if (value == null) {
+      return "";
+    }
+    return value;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java b/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java
new file mode 100644
index 0000000..1f17692
--- /dev/null
+++ b/java/com/android/voicemail/impl/sms/Vvm3MessageSender.java
@@ -0,0 +1,57 @@
+/*
+ * 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.voicemail.impl.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+
+public class Vvm3MessageSender extends OmtpMessageSender {
+
+  /**
+   * Creates a new instance of Vvm3MessageSender.
+   *
+   * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
+   *     Otherwise, a standard text SMS is sent.
+   */
+  public Vvm3MessageSender(
+      Context context,
+      PhoneAccountHandle phoneAccountHandle,
+      short applicationPort,
+      String destinationNumber) {
+    super(context, phoneAccountHandle, applicationPort, destinationNumber);
+  }
+
+  @Override
+  public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+    // Activation not supported for VVM3, send a status request instead.
+    requestVvmStatus(sentIntent);
+  }
+
+  @Override
+  public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+    // Deactivation not supported for VVM3, do nothing
+  }
+
+  @Override
+  public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+    // Status message:
+    // STATUS
+    StringBuilder sb = new StringBuilder().append("STATUS");
+    sendSms(sb.toString(), sentIntent);
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java
new file mode 100644
index 0000000..5a2fe14
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncReceiver.java
@@ -0,0 +1,54 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import java.util.List;
+
+public class OmtpVvmSyncReceiver extends BroadcastReceiver {
+
+  private static final String TAG = "OmtpVvmSyncReceiver";
+
+  @Override
+  public void onReceive(final Context context, Intent intent) {
+    if (VoicemailContract.ACTION_SYNC_VOICEMAIL.equals(intent.getAction())) {
+      VvmLog.v(TAG, "Sync intent received");
+
+      List<PhoneAccountHandle> accounts =
+          context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
+      for (PhoneAccountHandle phoneAccount : accounts) {
+        if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
+          continue;
+        }
+        if (!VvmAccountManager.isAccountActivated(context, phoneAccount)) {
+          VvmLog.i(TAG, "Unactivated account " + phoneAccount + " found, activating");
+          ActivationTask.start(context, phoneAccount, null);
+        } else {
+          SyncTask.start(context, phoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
+        }
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
new file mode 100644
index 0000000..c255019
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/OmtpVvmSyncService.java
@@ -0,0 +1,340 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.voicemail.impl.ActivationTask;
+import com.android.voicemail.impl.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.fetch.VoicemailFetchedCallback;
+import com.android.voicemail.impl.imap.ImapHelper;
+import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
+import com.android.voicemail.impl.utils.VoicemailDatabaseUtil;
+import java.util.List;
+import java.util.Map;
+
+/** Sync OMTP visual voicemail. */
+@TargetApi(VERSION_CODES.O)
+public class OmtpVvmSyncService {
+
+  private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
+
+  /** Signifies a sync with both uploading to the server and downloading from the server. */
+  public static final String SYNC_FULL_SYNC = "full_sync";
+  /** Only upload to the server. */
+  public static final String SYNC_UPLOAD_ONLY = "upload_only";
+  /** Only download from the server. */
+  public static final String SYNC_DOWNLOAD_ONLY = "download_only";
+  /** Only download single voicemail transcription. */
+  public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = "download_one_transcription";
+  /** Threshold for whether we should archive and delete voicemails from the remote VM server. */
+  private static final float AUTO_DELETE_ARCHIVE_VM_THRESHOLD = 0.75f;
+
+  private final Context mContext;
+
+  private VoicemailsQueryHelper mQueryHelper;
+
+  public OmtpVvmSyncService(Context context) {
+    mContext = context;
+    mQueryHelper = new VoicemailsQueryHelper(mContext);
+  }
+
+  public void sync(
+      BaseTask task,
+      String action,
+      PhoneAccountHandle phoneAccount,
+      Voicemail voicemail,
+      VoicemailStatus.Editor status) {
+    Assert.isTrue(phoneAccount != null);
+    VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
+    setupAndSendRequest(task, phoneAccount, voicemail, action, status);
+  }
+
+  private void setupAndSendRequest(
+      BaseTask task,
+      PhoneAccountHandle phoneAccount,
+      Voicemail voicemail,
+      String action,
+      VoicemailStatus.Editor status) {
+    if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) {
+      VvmLog.v(TAG, "Sync requested for disabled account");
+      return;
+    }
+    if (!VvmAccountManager.isAccountActivated(mContext, phoneAccount)) {
+      ActivationTask.start(mContext, phoneAccount, null);
+      return;
+    }
+
+    OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount);
+    // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data
+    // channel errors, which should happen when the task starts, not when it ends. It is the
+    // "Sync in progress..." status.
+    config.handleEvent(
+        VoicemailStatus.edit(mContext, phoneAccount), OmtpEvents.DATA_IMAP_OPERATION_STARTED);
+    try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) {
+      if (network == null) {
+        VvmLog.e(TAG, "unable to acquire network");
+        task.fail();
+        return;
+      }
+      doSync(task, network.get(), phoneAccount, voicemail, action, status);
+    } catch (RequestFailedException e) {
+      config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+      task.fail();
+    }
+  }
+
+  private void doSync(
+      BaseTask task,
+      Network network,
+      PhoneAccountHandle phoneAccount,
+      Voicemail voicemail,
+      String action,
+      VoicemailStatus.Editor status) {
+    try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) {
+      boolean success;
+      if (voicemail == null) {
+        success = syncAll(action, imapHelper, phoneAccount);
+      } else {
+        success = syncOne(imapHelper, voicemail, phoneAccount);
+      }
+      if (success) {
+        // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
+        imapHelper.updateQuota();
+        autoDeleteAndArchiveVM(imapHelper, phoneAccount);
+        imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
+      } else {
+        task.fail();
+      }
+    } catch (InitializingException e) {
+      VvmLog.w(TAG, "Can't retrieve Imap credentials.", e);
+      return;
+    }
+  }
+
+  /**
+   * If the VM quota exceeds {@value AUTO_DELETE_ARCHIVE_VM_THRESHOLD}, we should archive the VMs
+   * and delete them from the server to ensure new VMs can be received.
+   */
+  private void autoDeleteAndArchiveVM(
+      ImapHelper imapHelper, PhoneAccountHandle phoneAccountHandle) {
+
+    if (ConfigProviderBindings.get(mContext)
+            .getBoolean(VisualVoicemailSettingsUtil.ALLOW_VOICEMAIL_ARCHIVE, true)
+        && isArchiveEnabled(mContext, phoneAccountHandle)) {
+      if ((float) imapHelper.getOccuupiedQuota() / (float) imapHelper.getTotalQuota()
+          > AUTO_DELETE_ARCHIVE_VM_THRESHOLD) {
+        deleteAndArchiveVM(imapHelper);
+        imapHelper.updateQuota();
+        Logger.get(mContext)
+            .logImpression(DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETED_VM_FROM_SERVER);
+      } else {
+        VvmLog.i(TAG, "no need to archive and auto delete VM, quota below threshold");
+      }
+    } else {
+      VvmLog.i(TAG, "autoDeleteAndArchiveVM is turned off");
+      Logger.get(mContext).logImpression(DialerImpression.Type.VVM_ARCHIVE_AUTO_DELETE_TURNED_OFF);
+    }
+  }
+
+  private static boolean isArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return VisualVoicemailSettingsUtil.isArchiveEnabled(context, phoneAccountHandle)
+        && VisualVoicemailSettingsUtil.isEnabled(context, phoneAccountHandle);
+  }
+
+  private void deleteAndArchiveVM(ImapHelper imapHelper) {
+    // Archive column should only be used for 0 and above
+    Assert.isTrue(BuildCompat.isAtLeastO());
+    // The number of voicemails that exceed our threshold and should be deleted from the server
+    int numVoicemails =
+        imapHelper.getOccuupiedQuota()
+            - ((int) AUTO_DELETE_ARCHIVE_VM_THRESHOLD * imapHelper.getTotalQuota());
+    List<Voicemail> oldestVoicemails = mQueryHelper.oldestVoicemailsOnServer(numVoicemails);
+    if (!oldestVoicemails.isEmpty()) {
+      mQueryHelper.markArchivedInDatabase(oldestVoicemails);
+      imapHelper.markMessagesAsDeleted(oldestVoicemails);
+      VvmLog.i(
+          TAG,
+          String.format(
+              "successfully archived and deleted %d voicemails", oldestVoicemails.size()));
+    } else {
+      VvmLog.w(TAG, "remote voicemail server is empty");
+    }
+  }
+
+  private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
+    boolean uploadSuccess = true;
+    boolean downloadSuccess = true;
+
+    if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
+      uploadSuccess = upload(imapHelper);
+    }
+    if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
+      downloadSuccess = download(imapHelper, account);
+    }
+
+    VvmLog.v(
+        TAG,
+        "upload succeeded: ["
+            + String.valueOf(uploadSuccess)
+            + "] download succeeded: ["
+            + String.valueOf(downloadSuccess)
+            + "]");
+
+    return uploadSuccess && downloadSuccess;
+  }
+
+  private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, PhoneAccountHandle account) {
+    if (shouldPerformPrefetch(account, imapHelper)) {
+      VoicemailFetchedCallback callback =
+          new VoicemailFetchedCallback(mContext, voicemail.getUri(), account);
+      imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
+    }
+
+    return imapHelper.fetchTranscription(
+        new TranscriptionFetchedCallback(mContext, voicemail), voicemail.getSourceData());
+  }
+
+  private boolean upload(ImapHelper imapHelper) {
+    List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
+    List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
+
+    boolean success = true;
+
+    if (deletedVoicemails.size() > 0) {
+      if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
+        // We want to delete selectively instead of all the voicemails for this provider
+        // in case the state changed since the IMAP query was completed.
+        mQueryHelper.deleteFromDatabase(deletedVoicemails);
+      } else {
+        success = false;
+      }
+    }
+
+    if (readVoicemails.size() > 0) {
+      if (imapHelper.markMessagesAsRead(readVoicemails)) {
+        mQueryHelper.markCleanInDatabase(readVoicemails);
+      } else {
+        success = false;
+      }
+    }
+
+    return success;
+  }
+
+  private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
+    List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
+    List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
+
+    if (localVoicemails == null || serverVoicemails == null) {
+      // Null value means the query failed.
+      return false;
+    }
+
+    Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
+
+    // Go through all the local voicemails and check if they are on the server.
+    // They may be read or deleted on the server but not locally. Perform the
+    // appropriate local operation if the status differs from the server. Remove
+    // the messages that exist both locally and on the server to know which server
+    // messages to insert locally.
+    // Voicemails that were removed automatically from the server, are marked as
+    // archived and are stored locally. We do not delete them, as they were removed from the server
+    // by design (to make space).
+    for (int i = 0; i < localVoicemails.size(); i++) {
+      Voicemail localVoicemail = localVoicemails.get(i);
+      Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
+
+      // Do not delete voicemails that are archived marked as archived.
+      if (remoteVoicemail == null) {
+        mQueryHelper.deleteNonArchivedFromDatabase(localVoicemail);
+      } else {
+        if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
+          mQueryHelper.markReadInDatabase(localVoicemail);
+        }
+
+        if (!TextUtils.isEmpty(remoteVoicemail.getTranscription())
+            && TextUtils.isEmpty(localVoicemail.getTranscription())) {
+          mQueryHelper.updateWithTranscription(localVoicemail, remoteVoicemail.getTranscription());
+        }
+      }
+    }
+
+    // The leftover messages are messages that exist on the server but not locally.
+    boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
+    for (Voicemail remoteVoicemail : remoteMap.values()) {
+      Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail);
+      if (prefetchEnabled) {
+        VoicemailFetchedCallback fetchedCallback =
+            new VoicemailFetchedCallback(mContext, uri, account);
+        imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
+      }
+    }
+
+    return true;
+  }
+
+  private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
+    OmtpVvmCarrierConfigHelper carrierConfigHelper =
+        new OmtpVvmCarrierConfigHelper(mContext, account);
+    return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
+  }
+
+  /** Builds a map from provider data to message for the given collection of voicemails. */
+  private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
+    Map<String, Voicemail> map = new ArrayMap<String, Voicemail>();
+    for (Voicemail message : messages) {
+      map.put(message.getSourceData(), message);
+    }
+    return map;
+  }
+
+  /** Callback for {@link ImapHelper#fetchTranscription(TranscriptionFetchedCallback, String)} */
+  public static class TranscriptionFetchedCallback {
+
+    private Context mContext;
+    private Voicemail mVoicemail;
+
+    public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
+      mContext = context;
+      mVoicemail = voicemail;
+    }
+
+    public void setVoicemailTranscription(String transcription) {
+      VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
+      queryHelper.updateWithTranscription(mVoicemail, transcription);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/SyncOneTask.java b/java/com/android/voicemail/impl/sync/SyncOneTask.java
new file mode 100644
index 0000000..f970150
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/SyncOneTask.java
@@ -0,0 +1,78 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Voicemail;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+
+/**
+ * Task to download a single voicemail from the server. This task is initiated by a SMS notifying
+ * the new voicemail arrival, and ignores the duplicated tasks constraint.
+ */
+public class SyncOneTask extends BaseTask {
+
+  private static final int RETRY_TIMES = 2;
+  private static final int RETRY_INTERVAL_MILLIS = 5_000;
+
+  private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+  private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
+  private static final String EXTRA_VOICEMAIL = "extra_voicemail";
+
+  private PhoneAccountHandle mPhone;
+  private String mSyncType;
+  private Voicemail mVoicemail;
+
+  public static void start(Context context, PhoneAccountHandle phone, Voicemail voicemail) {
+    Intent intent = BaseTask.createIntent(context, SyncOneTask.class, phone);
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
+    intent.putExtra(EXTRA_SYNC_TYPE, OmtpVvmSyncService.SYNC_DOWNLOAD_ONE_TRANSCRIPTION);
+    intent.putExtra(EXTRA_VOICEMAIL, voicemail);
+    context.startService(intent);
+  }
+
+  public SyncOneTask() {
+    super(TASK_ALLOW_DUPLICATES);
+    addPolicy(new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS));
+  }
+
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+    mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+    mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
+    mVoicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL);
+  }
+
+  @Override
+  public void onExecuteInBackgroundThread() {
+    OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+    service.sync(this, mSyncType, mPhone, mVoicemail, VoicemailStatus.edit(getContext(), mPhone));
+  }
+
+  @Override
+  public Intent createRestartIntent() {
+    Intent intent = super.createRestartIntent();
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
+    intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
+    intent.putExtra(EXTRA_VOICEMAIL, mVoicemail);
+    return intent;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/SyncTask.java b/java/com/android/voicemail/impl/sync/SyncTask.java
new file mode 100644
index 0000000..71c9841
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/SyncTask.java
@@ -0,0 +1,75 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.MinimalIntervalPolicy;
+import com.android.voicemail.impl.scheduling.RetryPolicy;
+
+/** System initiated sync request. */
+public class SyncTask extends BaseTask {
+
+  // Try sync for a total of 5 times, should take around 5 minutes before finally giving up.
+  private static final int RETRY_TIMES = 4;
+  private static final int RETRY_INTERVAL_MILLIS = 5_000;
+  private static final int MINIMAL_INTERVAL_MILLIS = 60_000;
+
+  private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+  private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
+
+  private final RetryPolicy mRetryPolicy;
+
+  private PhoneAccountHandle mPhone;
+  private String mSyncType;
+
+  public static void start(Context context, PhoneAccountHandle phone, String syncType) {
+    Intent intent = BaseTask.createIntent(context, SyncTask.class, phone);
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
+    intent.putExtra(EXTRA_SYNC_TYPE, syncType);
+    context.startService(intent);
+  }
+
+  public SyncTask() {
+    super(TASK_SYNC);
+    mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
+    addPolicy(mRetryPolicy);
+    addPolicy(new MinimalIntervalPolicy(MINIMAL_INTERVAL_MILLIS));
+  }
+
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+    mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+    mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
+  }
+
+  @Override
+  public void onExecuteInBackgroundThread() {
+    OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+    service.sync(this, mSyncType, mPhone, null, mRetryPolicy.getVoicemailStatusEditor());
+  }
+
+  @Override
+  public Intent createRestartIntent() {
+    Intent intent = super.createRestartIntent();
+    intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
+    intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
+    return intent;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/UploadTask.java b/java/com/android/voicemail/impl/sync/UploadTask.java
new file mode 100644
index 0000000..7d1a797
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/UploadTask.java
@@ -0,0 +1,69 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import com.android.voicemail.impl.scheduling.BaseTask;
+import com.android.voicemail.impl.scheduling.PostponePolicy;
+
+/**
+ * Upload task triggered by database changes. Will wait until the database has been stable for
+ * {@link #POSTPONE_MILLIS} to execute.
+ */
+public class UploadTask extends BaseTask {
+
+  private static final String TAG = "VvmUploadTask";
+
+  private static final int POSTPONE_MILLIS = 5_000;
+
+  public UploadTask() {
+    super(TASK_UPLOAD);
+    addPolicy(new PostponePolicy(POSTPONE_MILLIS));
+  }
+
+  public static void start(Context context, PhoneAccountHandle phoneAccountHandle) {
+    Intent intent = BaseTask.createIntent(context, UploadTask.class, phoneAccountHandle);
+    context.startService(intent);
+  }
+
+  @Override
+  public void onCreate(Context context, Intent intent, int flags, int startId) {
+    super.onCreate(context, intent, flags, startId);
+  }
+
+  @Override
+  public void onExecuteInBackgroundThread() {
+    OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+
+    PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+    if (phoneAccountHandle == null) {
+      // This should never happen
+      VvmLog.e(TAG, "null phone account for phoneAccountHandle " + getPhoneAccountHandle());
+      return;
+    }
+    service.sync(
+        this,
+        OmtpVvmSyncService.SYNC_UPLOAD_ONLY,
+        phoneAccountHandle,
+        null,
+        VoicemailStatus.edit(getContext(), phoneAccountHandle));
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java b/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java
new file mode 100644
index 0000000..eaca3c4
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VoicemailProviderChangeReceiver.java
@@ -0,0 +1,40 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
+
+/** Receives changes to the voicemail provider so they can be sent to the voicemail server. */
+public class VoicemailProviderChangeReceiver extends BroadcastReceiver {
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    boolean isSelfChanged = intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false);
+    if (!isSelfChanged) {
+      for (PhoneAccountHandle phoneAccount : VvmAccountManager.getActiveAccounts(context)) {
+        if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
+          continue;
+        }
+        UploadTask.start(context, phoneAccount);
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java
new file mode 100644
index 0000000..4ef19da
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VoicemailStatusQueryHelper.java
@@ -0,0 +1,113 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.telecom.PhoneAccountHandle;
+
+/** Construct queries to interact with the voicemail status table. */
+public class VoicemailStatusQueryHelper {
+
+  static final String[] PROJECTION =
+      new String[] {
+        Status._ID, // 0
+        Status.CONFIGURATION_STATE, // 1
+        Status.NOTIFICATION_CHANNEL_STATE, // 2
+        Status.SOURCE_PACKAGE // 3
+      };
+
+  public static final int _ID = 0;
+  public static final int CONFIGURATION_STATE = 1;
+  public static final int NOTIFICATION_CHANNEL_STATE = 2;
+  public static final int SOURCE_PACKAGE = 3;
+
+  private Context mContext;
+  private ContentResolver mContentResolver;
+  private Uri mSourceUri;
+
+  public VoicemailStatusQueryHelper(Context context) {
+    mContext = context;
+    mContentResolver = context.getContentResolver();
+    mSourceUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
+  }
+
+  /**
+   * Check if the configuration state for the voicemail source is "ok", meaning that the source is
+   * set up.
+   *
+   * @param phoneAccount The phone account for the voicemail source to check.
+   * @return {@code true} if the voicemail source is configured, {@code} false otherwise, including
+   *     if the voicemail source is not registered in the table.
+   */
+  public boolean isVoicemailSourceConfigured(PhoneAccountHandle phoneAccount) {
+    return isFieldEqualTo(phoneAccount, CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
+  }
+
+  /**
+   * Check if the notifications channel of a voicemail source is active. That is, when a new
+   * voicemail is available, if the server able to notify the device.
+   *
+   * @return {@code true} if notifications channel is active, {@code false} otherwise.
+   */
+  public boolean isNotificationsChannelActive(PhoneAccountHandle phoneAccount) {
+    return isFieldEqualTo(
+        phoneAccount, NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
+  }
+
+  /**
+   * Check if a field for an entry in the status table is equal to a specific value.
+   *
+   * @param phoneAccount The phone account of the voicemail source to query for.
+   * @param columnIndex The column index of the field in the returned query.
+   * @param value The value to compare against.
+   * @return {@code true} if the stored value is equal to the provided value. {@code false}
+   *     otherwise.
+   */
+  private boolean isFieldEqualTo(PhoneAccountHandle phoneAccount, int columnIndex, int value) {
+    Cursor cursor = null;
+    if (phoneAccount != null) {
+      String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
+      String phoneAccountId = phoneAccount.getId();
+      if (phoneAccountComponentName == null || phoneAccountId == null) {
+        return false;
+      }
+      try {
+        String whereClause =
+            Status.PHONE_ACCOUNT_COMPONENT_NAME
+                + "=? AND "
+                + Status.PHONE_ACCOUNT_ID
+                + "=? AND "
+                + Status.SOURCE_PACKAGE
+                + "=?";
+        String[] whereArgs = {phoneAccountComponentName, phoneAccountId, mContext.getPackageName()};
+        cursor = mContentResolver.query(mSourceUri, PROJECTION, whereClause, whereArgs, null);
+        if (cursor != null && cursor.moveToFirst()) {
+          return cursor.getInt(columnIndex) == value;
+        }
+      } finally {
+        if (cursor != null) {
+          cursor.close();
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
new file mode 100644
index 0000000..d129406
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VoicemailsQueryHelper.java
@@ -0,0 +1,295 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.Voicemail;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Construct queries to interact with the voicemails table. */
+public class VoicemailsQueryHelper {
+  static final String[] PROJECTION =
+      new String[] {
+        Voicemails._ID, // 0
+        Voicemails.SOURCE_DATA, // 1
+        Voicemails.IS_READ, // 2
+        Voicemails.DELETED, // 3
+        Voicemails.TRANSCRIPTION // 4
+      };
+
+  public static final int _ID = 0;
+  public static final int SOURCE_DATA = 1;
+  public static final int IS_READ = 2;
+  public static final int DELETED = 3;
+  public static final int TRANSCRIPTION = 4;
+
+  static final String READ_SELECTION =
+      Voicemails.DIRTY + "=1 AND " + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1";
+  static final String DELETED_SELECTION = Voicemails.DELETED + "=1";
+  static final String ARCHIVED_SELECTION = Voicemails.ARCHIVED + "=0";
+
+  private Context mContext;
+  private ContentResolver mContentResolver;
+  private Uri mSourceUri;
+
+  public VoicemailsQueryHelper(Context context) {
+    mContext = context;
+    mContentResolver = context.getContentResolver();
+    mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName());
+  }
+
+  /**
+   * Get all the local read voicemails that have not been synced to the server.
+   *
+   * @return A list of read voicemails.
+   */
+  public List<Voicemail> getReadVoicemails() {
+    return getLocalVoicemails(READ_SELECTION);
+  }
+
+  /**
+   * Get all the locally deleted voicemails that have not been synced to the server.
+   *
+   * @return A list of deleted voicemails.
+   */
+  public List<Voicemail> getDeletedVoicemails() {
+    return getLocalVoicemails(DELETED_SELECTION);
+  }
+
+  /**
+   * Get all voicemails locally stored.
+   *
+   * @return A list of all locally stored voicemails.
+   */
+  public List<Voicemail> getAllVoicemails() {
+    return getLocalVoicemails(null);
+  }
+
+  /**
+   * Utility method to make queries to the voicemail database.
+   *
+   * @param selection A filter declaring which rows to return. {@code null} returns all rows.
+   * @return A list of voicemails according to the selection statement.
+   */
+  private List<Voicemail> getLocalVoicemails(String selection) {
+    Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null);
+    if (cursor == null) {
+      return null;
+    }
+    try {
+      List<Voicemail> voicemails = new ArrayList<Voicemail>();
+      while (cursor.moveToNext()) {
+        final long id = cursor.getLong(_ID);
+        final String sourceData = cursor.getString(SOURCE_DATA);
+        final boolean isRead = cursor.getInt(IS_READ) == 1;
+        final String transcription = cursor.getString(TRANSCRIPTION);
+        Voicemail voicemail =
+            Voicemail.createForUpdate(id, sourceData)
+                .setIsRead(isRead)
+                .setTranscription(transcription)
+                .build();
+        voicemails.add(voicemail);
+      }
+      return voicemails;
+    } finally {
+      cursor.close();
+    }
+  }
+
+  /**
+   * Deletes a list of voicemails from the voicemail content provider.
+   *
+   * @param voicemails The list of voicemails to delete
+   * @return The number of voicemails deleted
+   */
+  public int deleteFromDatabase(List<Voicemail> voicemails) {
+    int count = voicemails.size();
+    if (count == 0) {
+      return 0;
+    }
+
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < count; i++) {
+      if (i > 0) {
+        sb.append(",");
+      }
+      sb.append(voicemails.get(i).getId());
+    }
+
+    String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
+    return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
+  }
+
+  /** Utility method to delete a single voicemail that is not archived. */
+  public void deleteNonArchivedFromDatabase(Voicemail voicemail) {
+    mContentResolver.delete(
+        Voicemails.CONTENT_URI,
+        Voicemails._ID + "=? AND " + Voicemails.ARCHIVED + "= 0",
+        new String[] {Long.toString(voicemail.getId())});
+  }
+
+  public int markReadInDatabase(List<Voicemail> voicemails) {
+    int count = voicemails.size();
+    for (int i = 0; i < count; i++) {
+      markReadInDatabase(voicemails.get(i));
+    }
+    return count;
+  }
+
+  /** Utility method to mark single message as read. */
+  public void markReadInDatabase(Voicemail voicemail) {
+    Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Voicemails.IS_READ, "1");
+    mContentResolver.update(uri, contentValues, null, null);
+  }
+
+  /**
+   * Sends an update command to the voicemail content provider for a list of voicemails. From the
+   * view of the provider, since the updater is the owner of the entry, a blank "update" means that
+   * the voicemail source is indicating that the server has up-to-date information on the voicemail.
+   * This flips the "dirty" bit to "0".
+   *
+   * @param voicemails The list of voicemails to update
+   * @return The number of voicemails updated
+   */
+  public int markCleanInDatabase(List<Voicemail> voicemails) {
+    int count = voicemails.size();
+    for (int i = 0; i < count; i++) {
+      markCleanInDatabase(voicemails.get(i));
+    }
+    return count;
+  }
+
+  /** Utility method to mark single message as clean. */
+  public void markCleanInDatabase(Voicemail voicemail) {
+    Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+    ContentValues contentValues = new ContentValues();
+    mContentResolver.update(uri, contentValues, null, null);
+  }
+
+  /** Utility method to add a transcription to the voicemail. */
+  public void updateWithTranscription(Voicemail voicemail, String transcription) {
+    Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Voicemails.TRANSCRIPTION, transcription);
+    mContentResolver.update(uri, contentValues, null, null);
+  }
+
+  /**
+   * Voicemail is unique if the tuple of (phone account component name, phone account id, source
+   * data) is unique. If the phone account is missing, we also consider this unique since it's
+   * simply an "unknown" account.
+   *
+   * @param voicemail The voicemail to check if it is unique.
+   * @return {@code true} if the voicemail is unique, {@code false} otherwise.
+   */
+  public boolean isVoicemailUnique(Voicemail voicemail) {
+    Cursor cursor = null;
+    PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
+    if (phoneAccount != null) {
+      String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
+      String phoneAccountId = phoneAccount.getId();
+      String sourceData = voicemail.getSourceData();
+      if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
+        return true;
+      }
+      try {
+        String whereClause =
+            Voicemails.PHONE_ACCOUNT_COMPONENT_NAME
+                + "=? AND "
+                + Voicemails.PHONE_ACCOUNT_ID
+                + "=? AND "
+                + Voicemails.SOURCE_DATA
+                + "=?";
+        String[] whereArgs = {phoneAccountComponentName, phoneAccountId, sourceData};
+        cursor = mContentResolver.query(mSourceUri, PROJECTION, whereClause, whereArgs, null);
+        if (cursor.getCount() == 0) {
+          return true;
+        } else {
+          return false;
+        }
+      } finally {
+        if (cursor != null) {
+          cursor.close();
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Marks voicemails in the local database as archived. This indicates that the voicemails from the
+   * server were removed automatically to make space for new voicemails, and are stored locally on
+   * the users devices, without a corresponding server copy.
+   */
+  public void markArchivedInDatabase(List<Voicemail> voicemails) {
+    for (Voicemail voicemail : voicemails) {
+      markArchiveInDatabase(voicemail);
+    }
+  }
+
+  /** Utility method to mark single voicemail as archived. */
+  public void markArchiveInDatabase(Voicemail voicemail) {
+    Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Voicemails.ARCHIVED, "1");
+    mContentResolver.update(uri, contentValues, null, null);
+  }
+
+  /** Find the oldest voicemails that are on the device, and also on the server. */
+  @TargetApi(VERSION_CODES.M) // used for try with resources
+  public List<Voicemail> oldestVoicemailsOnServer(int numVoicemails) {
+    if (numVoicemails <= 0) {
+      Assert.fail("Query for remote voicemails cannot be <= 0");
+    }
+
+    String sortAndLimit = "date ASC limit " + numVoicemails;
+
+    try (Cursor cursor =
+        mContentResolver.query(mSourceUri, null, ARCHIVED_SELECTION, null, sortAndLimit)) {
+
+      Assert.isNotNull(cursor);
+
+      List<Voicemail> voicemails = new ArrayList<>();
+      while (cursor.moveToNext()) {
+        final String sourceData = cursor.getString(SOURCE_DATA);
+        Voicemail voicemail = Voicemail.createForUpdate(cursor.getLong(_ID), sourceData).build();
+        voicemails.add(voicemail);
+      }
+
+      if (voicemails.size() != numVoicemails) {
+        Assert.fail(
+            String.format(
+                "voicemail count (%d) doesn't matched expected (%d)",
+                voicemails.size(), numVoicemails));
+      }
+      return voicemails;
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmAccountManager.java b/java/com/android/voicemail/impl/sync/VvmAccountManager.java
new file mode 100644
index 0000000..05f6494
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmAccountManager.java
@@ -0,0 +1,79 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpConstants;
+import com.android.voicemail.impl.VisualVoicemailPreferences;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.sms.StatusMessage;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tracks the activation state of a visual voicemail phone account. An account is considered
+ * activated if it has valid connection information from the {@link StatusMessage} stored on the
+ * device. Once activation/provisioning is completed, {@link #addAccount(Context,
+ * PhoneAccountHandle, StatusMessage)} should be called to store the connection information. When an
+ * account is removed or if the connection information is deemed invalid, {@link
+ * #removeAccount(Context, PhoneAccountHandle)} should be called to clear the connection information
+ * and allow reactivation.
+ */
+public class VvmAccountManager {
+  public static final String TAG = "VvmAccountManager";
+
+  private static final String IS_ACCOUNT_ACTIVATED = "is_account_activated";
+
+  public static void addAccount(
+      Context context, PhoneAccountHandle phoneAccountHandle, StatusMessage statusMessage) {
+    VisualVoicemailPreferences preferences =
+        new VisualVoicemailPreferences(context, phoneAccountHandle);
+    statusMessage.putStatus(preferences.edit()).putBoolean(IS_ACCOUNT_ACTIVATED, true).apply();
+  }
+
+  public static void removeAccount(Context context, PhoneAccountHandle phoneAccount) {
+    VoicemailStatus.disable(context, phoneAccount);
+    VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, phoneAccount);
+    preferences
+        .edit()
+        .putBoolean(IS_ACCOUNT_ACTIVATED, false)
+        .putString(OmtpConstants.IMAP_USER_NAME, null)
+        .putString(OmtpConstants.IMAP_PASSWORD, null)
+        .apply();
+  }
+
+  public static boolean isAccountActivated(Context context, PhoneAccountHandle phoneAccount) {
+    Assert.isNotNull(phoneAccount);
+    VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, phoneAccount);
+    return preferences.getBoolean(IS_ACCOUNT_ACTIVATED, false);
+  }
+
+  @NonNull
+  public static List<PhoneAccountHandle> getActiveAccounts(Context context) {
+    List<PhoneAccountHandle> results = new ArrayList<>();
+    for (PhoneAccountHandle phoneAccountHandle :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+      if (isAccountActivated(context, phoneAccountHandle)) {
+        results.add(phoneAccountHandle);
+      }
+    }
+    return results;
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java
new file mode 100644
index 0000000..189dc8f
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+import java.io.Closeable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Class to retrieve a {@link Network} synchronously. {@link #getNetwork(OmtpVvmCarrierConfigHelper,
+ * PhoneAccountHandle)} will block until a suitable network is retrieved or it has failed.
+ */
+@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
+@TargetApi(VERSION_CODES.O)
+public class VvmNetworkRequest {
+
+  private static final String TAG = "VvmNetworkRequest";
+
+  /**
+   * A wrapper around a Network returned by a {@link VvmNetworkRequestCallback}, which should be
+   * closed once not needed anymore.
+   */
+  public static class NetworkWrapper implements Closeable {
+
+    private final Network mNetwork;
+    private final VvmNetworkRequestCallback mCallback;
+
+    private NetworkWrapper(Network network, VvmNetworkRequestCallback callback) {
+      mNetwork = network;
+      mCallback = callback;
+    }
+
+    public Network get() {
+      return mNetwork;
+    }
+
+    @Override
+    public void close() {
+      mCallback.releaseNetwork();
+    }
+  }
+
+  public static class RequestFailedException extends Exception {
+
+    private RequestFailedException(Throwable cause) {
+      super(cause);
+    }
+  }
+
+  @NonNull
+  public static NetworkWrapper getNetwork(
+      OmtpVvmCarrierConfigHelper config, PhoneAccountHandle handle, VoicemailStatus.Editor status)
+      throws RequestFailedException {
+    FutureNetworkRequestCallback callback =
+        new FutureNetworkRequestCallback(config, handle, status);
+    callback.requestNetwork();
+    try {
+      return callback.getFuture().get();
+    } catch (InterruptedException | ExecutionException e) {
+      callback.releaseNetwork();
+      VvmLog.e(TAG, "can't get future network", e);
+      throw new RequestFailedException(e);
+    }
+  }
+
+  private static class FutureNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+    /**
+     * {@link CompletableFuture#get()} will block until {@link CompletableFuture# complete(Object) }
+     * has been called on the other thread.
+     */
+    private final CompletableFuture<NetworkWrapper> mFuture = new CompletableFuture<>();
+
+    public FutureNetworkRequestCallback(
+        OmtpVvmCarrierConfigHelper config,
+        PhoneAccountHandle phoneAccount,
+        VoicemailStatus.Editor status) {
+      super(config, phoneAccount, status);
+    }
+
+    public Future<NetworkWrapper> getFuture() {
+      return mFuture;
+    }
+
+    @Override
+    public void onAvailable(Network network) {
+      super.onAvailable(network);
+      mFuture.complete(new NetworkWrapper(network, this));
+    }
+
+    @Override
+    public void onFailed(String reason) {
+      super.onFailed(reason);
+      mFuture.complete(null);
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
new file mode 100644
index 0000000..067eff8
--- /dev/null
+++ b/java/com/android/voicemail/impl/sync/VvmNetworkRequestCallback.java
@@ -0,0 +1,183 @@
+/*
+ * 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.voicemail.impl.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.CallSuper;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.voicemail.impl.OmtpEvents;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VoicemailStatus;
+import com.android.voicemail.impl.VvmLog;
+
+/**
+ * Base class for network request call backs for visual voicemail syncing with the Imap server. This
+ * handles retries and network requests.
+ */
+@TargetApi(VERSION_CODES.O)
+public abstract class VvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
+
+  private static final String TAG = "VvmNetworkRequest";
+
+  // Timeout used to call ConnectivityManager.requestNetwork
+  private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
+
+  public static final String NETWORK_REQUEST_FAILED_TIMEOUT = "timeout";
+  public static final String NETWORK_REQUEST_FAILED_LOST = "lost";
+
+  protected Context mContext;
+  protected PhoneAccountHandle mPhoneAccount;
+  protected NetworkRequest mNetworkRequest;
+  private ConnectivityManager mConnectivityManager;
+  private final OmtpVvmCarrierConfigHelper mCarrierConfigHelper;
+  private final VoicemailStatus.Editor mStatus;
+  private boolean mRequestSent = false;
+  private boolean mResultReceived = false;
+
+  public VvmNetworkRequestCallback(
+      Context context, PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
+    mContext = context;
+    mPhoneAccount = phoneAccount;
+    mStatus = status;
+    mCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, mPhoneAccount);
+    mNetworkRequest = createNetworkRequest();
+  }
+
+  public VvmNetworkRequestCallback(
+      OmtpVvmCarrierConfigHelper config,
+      PhoneAccountHandle phoneAccount,
+      VoicemailStatus.Editor status) {
+    mContext = config.getContext();
+    mPhoneAccount = phoneAccount;
+    mStatus = status;
+    mCarrierConfigHelper = config;
+    mNetworkRequest = createNetworkRequest();
+  }
+
+  public VoicemailStatus.Editor getVoicemailStatusEditor() {
+    return mStatus;
+  }
+
+  /**
+   * @return NetworkRequest for a proper transport type. Use only cellular network if the carrier
+   *     requires it. Otherwise use whatever available.
+   */
+  private NetworkRequest createNetworkRequest() {
+
+    NetworkRequest.Builder builder =
+        new NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+    TelephonyManager telephonyManager =
+        mContext
+            .getSystemService(TelephonyManager.class)
+            .createForPhoneAccountHandle(mPhoneAccount);
+    // At this point mPhoneAccount should always be valid and telephonyManager will never be null
+    Assert.isNotNull(telephonyManager);
+    if (mCarrierConfigHelper.isCellularDataRequired()) {
+      VvmLog.d(TAG, "Transport type: CELLULAR");
+      builder
+          .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+          .setNetworkSpecifier(telephonyManager.getNetworkSpecifier());
+    } else {
+      VvmLog.d(TAG, "Transport type: ANY");
+    }
+    return builder.build();
+  }
+
+  public NetworkRequest getNetworkRequest() {
+    return mNetworkRequest;
+  }
+
+  @Override
+  @CallSuper
+  public void onLost(Network network) {
+    VvmLog.d(TAG, "onLost");
+    mResultReceived = true;
+    onFailed(NETWORK_REQUEST_FAILED_LOST);
+  }
+
+  @Override
+  @CallSuper
+  public void onAvailable(Network network) {
+    super.onAvailable(network);
+    mResultReceived = true;
+  }
+
+  @CallSuper
+  public void onUnavailable() {
+    // TODO: b/32637799 this is hidden, do we really need this?
+    mResultReceived = true;
+    onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
+  }
+
+  public void requestNetwork() {
+    if (mRequestSent == true) {
+      VvmLog.e(TAG, "requestNetwork() called twice");
+      return;
+    }
+    mRequestSent = true;
+    getConnectivityManager().requestNetwork(getNetworkRequest(), this);
+    /**
+     * Somehow requestNetwork() with timeout doesn't work, and it's a hidden method. Implement our
+     * own timeout mechanism instead.
+     */
+    Handler handler = new Handler(Looper.getMainLooper());
+    handler.postDelayed(
+        new Runnable() {
+          @Override
+          public void run() {
+            if (mResultReceived == false) {
+              onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
+            }
+          }
+        },
+        NETWORK_REQUEST_TIMEOUT_MILLIS);
+  }
+
+  public void releaseNetwork() {
+    VvmLog.d(TAG, "releaseNetwork");
+    getConnectivityManager().unregisterNetworkCallback(this);
+  }
+
+  public ConnectivityManager getConnectivityManager() {
+    if (mConnectivityManager == null) {
+      mConnectivityManager =
+          (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+    return mConnectivityManager;
+  }
+
+  @CallSuper
+  public void onFailed(String reason) {
+    VvmLog.d(TAG, "onFailed: " + reason);
+    if (mCarrierConfigHelper.isCellularDataRequired()) {
+      mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+    } else {
+      mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION);
+    }
+    releaseNetwork();
+  }
+}
diff --git a/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java b/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java
new file mode 100644
index 0000000..bbc1d66
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/IndentingPrintWriter.java
@@ -0,0 +1,155 @@
+/*
+ * 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.voicemail.impl.utils;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Arrays;
+
+/**
+ * Lightweight wrapper around {@link PrintWriter} that automatically indents newlines based on
+ * internal state. It also automatically wraps long lines based on given line length.
+ *
+ * <p>Delays writing indent until first actual write on a newline, enabling indent modification
+ * after newline.
+ */
+public class IndentingPrintWriter extends PrintWriter {
+
+  private final String mSingleIndent;
+  private final int mWrapLength;
+
+  /** Mutable version of current indent */
+  private StringBuilder mIndentBuilder = new StringBuilder();
+  /** Cache of current {@link #mIndentBuilder} value */
+  private char[] mCurrentIndent;
+  /** Length of current line being built, excluding any indent */
+  private int mCurrentLength;
+
+  /**
+   * Flag indicating if we're currently sitting on an empty line, and that next write should be
+   * prefixed with the current indent.
+   */
+  private boolean mEmptyLine = true;
+
+  private char[] mSingleChar = new char[1];
+
+  public IndentingPrintWriter(Writer writer, String singleIndent) {
+    this(writer, singleIndent, -1);
+  }
+
+  public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) {
+    super(writer);
+    mSingleIndent = singleIndent;
+    mWrapLength = wrapLength;
+  }
+
+  public void increaseIndent() {
+    mIndentBuilder.append(mSingleIndent);
+    mCurrentIndent = null;
+  }
+
+  public void decreaseIndent() {
+    mIndentBuilder.delete(0, mSingleIndent.length());
+    mCurrentIndent = null;
+  }
+
+  public void printPair(String key, Object value) {
+    print(key + "=" + String.valueOf(value) + " ");
+  }
+
+  public void printPair(String key, Object[] value) {
+    print(key + "=" + Arrays.toString(value) + " ");
+  }
+
+  public void printHexPair(String key, int value) {
+    print(key + "=0x" + Integer.toHexString(value) + " ");
+  }
+
+  @Override
+  public void println() {
+    write('\n');
+  }
+
+  @Override
+  public void write(int c) {
+    mSingleChar[0] = (char) c;
+    write(mSingleChar, 0, 1);
+  }
+
+  @Override
+  public void write(String s, int off, int len) {
+    final char[] buf = new char[len];
+    s.getChars(off, len - off, buf, 0);
+    write(buf, 0, len);
+  }
+
+  @Override
+  public void write(char[] buf, int offset, int count) {
+    final int indentLength = mIndentBuilder.length();
+    final int bufferEnd = offset + count;
+    int lineStart = offset;
+    int lineEnd = offset;
+
+    // March through incoming buffer looking for newlines
+    while (lineEnd < bufferEnd) {
+      char ch = buf[lineEnd++];
+      mCurrentLength++;
+      if (ch == '\n') {
+        maybeWriteIndent();
+        super.write(buf, lineStart, lineEnd - lineStart);
+        lineStart = lineEnd;
+        mEmptyLine = true;
+        mCurrentLength = 0;
+      }
+
+      // Wrap if we've pushed beyond line length
+      if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
+        if (!mEmptyLine) {
+          // Give ourselves a fresh line to work with
+          super.write('\n');
+          mEmptyLine = true;
+          mCurrentLength = lineEnd - lineStart;
+        } else {
+          // We need more than a dedicated line, slice it hard
+          maybeWriteIndent();
+          super.write(buf, lineStart, lineEnd - lineStart);
+          super.write('\n');
+          mEmptyLine = true;
+          lineStart = lineEnd;
+          mCurrentLength = 0;
+        }
+      }
+    }
+
+    if (lineStart != lineEnd) {
+      maybeWriteIndent();
+      super.write(buf, lineStart, lineEnd - lineStart);
+    }
+  }
+
+  private void maybeWriteIndent() {
+    if (mEmptyLine) {
+      mEmptyLine = false;
+      if (mIndentBuilder.length() != 0) {
+        if (mCurrentIndent == null) {
+          mCurrentIndent = mIndentBuilder.toString().toCharArray();
+        }
+        super.write(mCurrentIndent, 0, mCurrentIndent.length);
+      }
+    }
+  }
+}
diff --git a/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java b/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java
new file mode 100644
index 0000000..711d6a8
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/VoicemailDatabaseUtil.java
@@ -0,0 +1,85 @@
+/*
+ * 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.voicemail.impl.utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.impl.Voicemail;
+import java.util.List;
+
+public class VoicemailDatabaseUtil {
+
+  /**
+   * Inserts a new voicemail into the voicemail content provider.
+   *
+   * @param context The context of the app doing the inserting
+   * @param voicemail Data to be inserted
+   * @return {@link Uri} of the newly inserted {@link Voicemail}
+   * @hide
+   */
+  public static Uri insert(Context context, Voicemail voicemail) {
+    ContentResolver contentResolver = context.getContentResolver();
+    ContentValues contentValues = getContentValues(voicemail);
+    return contentResolver.insert(
+        Voicemails.buildSourceUri(context.getPackageName()), contentValues);
+  }
+
+  /**
+   * Inserts a list of voicemails into the voicemail content provider.
+   *
+   * @param context The context of the app doing the inserting
+   * @param voicemails Data to be inserted
+   * @return the number of voicemails inserted
+   * @hide
+   */
+  public static int insert(Context context, List<Voicemail> voicemails) {
+    for (Voicemail voicemail : voicemails) {
+      insert(context, voicemail);
+    }
+    return voicemails.size();
+  }
+
+  /** Maps structured {@link Voicemail} to {@link ContentValues} in content provider. */
+  private static ContentValues getContentValues(Voicemail voicemail) {
+    ContentValues contentValues = new ContentValues();
+    contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis()));
+    contentValues.put(Voicemails.NUMBER, voicemail.getNumber());
+    contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration()));
+    contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage());
+    contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData());
+    contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0);
+    contentValues.put(Voicemails.IS_OMTP_VOICEMAIL, 1);
+
+    PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
+    if (phoneAccount != null) {
+      contentValues.put(
+          Voicemails.PHONE_ACCOUNT_COMPONENT_NAME,
+          phoneAccount.getComponentName().flattenToString());
+      contentValues.put(Voicemails.PHONE_ACCOUNT_ID, phoneAccount.getId());
+    }
+
+    if (voicemail.getTranscription() != null) {
+      contentValues.put(Voicemails.TRANSCRIPTION, voicemail.getTranscription());
+    }
+
+    return contentValues;
+  }
+}
diff --git a/java/com/android/voicemail/impl/utils/VvmDumpHandler.java b/java/com/android/voicemail/impl/utils/VvmDumpHandler.java
new file mode 100644
index 0000000..5290f2c
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/VvmDumpHandler.java
@@ -0,0 +1,43 @@
+/*
+ * 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.voicemail.impl.utils;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
+import com.android.voicemail.impl.VvmLog;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public class VvmDumpHandler {
+
+  public static void dump(Context context, FileDescriptor fd, PrintWriter writer, String[] args) {
+    IndentingPrintWriter indentedWriter = new IndentingPrintWriter(writer, "  ");
+    indentedWriter.println("******* OmtpVvm *******");
+    indentedWriter.println("======= Configs =======");
+    indentedWriter.increaseIndent();
+    for (PhoneAccountHandle handle :
+        context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
+      OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, handle);
+      indentedWriter.println(config.toString());
+    }
+    indentedWriter.decreaseIndent();
+    indentedWriter.println("======== Logs =========");
+    VvmLog.dump(fd, indentedWriter, args);
+  }
+}
diff --git a/java/com/android/voicemail/impl/utils/XmlUtils.java b/java/com/android/voicemail/impl/utils/XmlUtils.java
new file mode 100644
index 0000000..f5703f3
--- /dev/null
+++ b/java/com/android/voicemail/impl/utils/XmlUtils.java
@@ -0,0 +1,238 @@
+/*
+ * 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.voicemail.impl.utils;
+
+import android.util.ArrayMap;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+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;
+  }
+}
diff --git a/java/com/android/voicemail/permissions.xml b/java/com/android/voicemail/permissions.xml
new file mode 100644
index 0000000..adb4b6f
--- /dev/null
+++ b/java/com/android/voicemail/permissions.xml
@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.android.voicemailomtp">
+
+  <uses-sdk
+    android:minSdkVersion="23"
+    android:targetSdkVersion="25"/>
+
+  <!-- Applications using this module should merge these permissions using android_manifest_merge -->
+
+  <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
+  <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+  <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/>
+  <uses-permission android:name="android.permission.WAKE_LOCK"/>
+  <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+  <uses-permission android:name="android.permission.SEND_SMS"/>
+  <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+  <uses-permission android:name="android.permission.INTERNET"/>
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+
+  <application/>
+</manifest>
diff --git a/java/com/android/voicemail/stub/StubVoicemailClient.java b/java/com/android/voicemail/stub/StubVoicemailClient.java
new file mode 100644
index 0000000..9481a0e
--- /dev/null
+++ b/java/com/android/voicemail/stub/StubVoicemailClient.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 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.voicemail.stub;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemail.VoicemailClient;
+import java.util.List;
+import javax.inject.Inject;
+
+/**
+ * A no-op version of the voicemail module for build targets that don't support the new OTMP client.
+ */
+public final class StubVoicemailClient implements VoicemailClient {
+  @Inject
+  public StubVoicemailClient() {}
+
+  @Override
+  public void appendOmtpVoicemailSelectionClause(
+      Context context, StringBuilder where, List<String> selectionArgs) {}
+
+  @Override
+  public String getSettingsFragment() {
+    return null;
+  }
+
+  @Override
+  public boolean isVoicemailArchiveEnabled(Context context, PhoneAccountHandle phoneAccountHandle) {
+    return false;
+  }
+
+  @Override
+  public void setVoicemailArchiveEnabled(
+      Context context, PhoneAccountHandle phoneAccountHandle, boolean value) {}
+}
diff --git a/java/com/android/voicemail/stub/StubVoicemailModule.java b/java/com/android/voicemail/stub/StubVoicemailModule.java
new file mode 100644
index 0000000..6c1552c
--- /dev/null
+++ b/java/com/android/voicemail/stub/StubVoicemailModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 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.voicemail.stub;
+
+import com.android.voicemail.VoicemailClient;
+import dagger.Binds;
+import dagger.Module;
+import javax.inject.Singleton;
+
+/**
+ * A no-op version of the voicemail module for build targets that don't support the new OTMP client.
+ */
+@Module
+public abstract class StubVoicemailModule {
+
+  @Binds
+  @Singleton
+  public abstract VoicemailClient bindVoicemailClient(StubVoicemailClient voicemailClient);
+}
diff --git a/java/com/android/voicemail/testing/TestVoicemailModule.java b/java/com/android/voicemail/testing/TestVoicemailModule.java
new file mode 100644
index 0000000..8b7b34c
--- /dev/null
+++ b/java/com/android/voicemail/testing/TestVoicemailModule.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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.voicemail.testing;
+
+import com.android.voicemail.VoicemailClient;
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** Used to set a mock voicemail client for unit tests. */
+@Module
+public final class TestVoicemailModule {
+  private static VoicemailClient voicemailClient;
+
+  public static void setVoicemailClient(VoicemailClient voicemailClient) {
+    TestVoicemailModule.voicemailClient = voicemailClient;
+  }
+
+  @Provides
+  @Singleton
+  public static VoicemailClient provideVoicemailClient() {
+    return voicemailClient;
+  }
+}