Merge "Implement UI for changing voicemail PIN" into nyc-mr1-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ddc2dbe..9778d2c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -732,5 +732,11 @@
                 <data android:scheme="package"/>
             </intent-filter>
         </receiver>
+
+        <activity android:name=".settings.VoicemailChangePinActivity"
+          android:exported="false"
+          android:theme="@style/DialerSettingsLight"
+          android:windowSoftInputMode="stateVisible|adjustResize">
+          </activity>
     </application>
 </manifest>
diff --git a/res/layout/voicemail_change_pin.xml b/res/layout/voicemail_change_pin.xml
new file mode 100644
index 0000000..ba0d823
--- /dev/null
+++ b/res/layout/voicemail_change_pin.xml
@@ -0,0 +1,95 @@
+<?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:padding="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/res/values/strings.xml b/res/values/strings.xml
index a18ee87..d549653 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -336,7 +336,7 @@
     <string name="vm_change_pin_new_pin">New PIN</string>
 
     <!-- Message on the dialog when PIN changing is in progress -->
-    <string name="vm_change_pin_progress_message">Changing PIN</string>
+    <string name="vm_change_pin_progress_message">Please wait.</string>
     <!-- Error message for the voicemail PIN change if the PIN is too short -->
     <string 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 -->
@@ -1258,6 +1258,8 @@
     <string name="voicemail_visual_voicemail_switch_title">Visual Voicemail</string>
 
     <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+    <string name="voicemail_set_pin_dialog_title">Set PIN</string>
+    <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
     <string name="voicemail_change_pin_dialog_title">Change PIN</string>
 
     <!-- Voicemail ringtone title. The user clicks on this preference to select
@@ -1353,4 +1355,28 @@
         There are too many active calls. Please end or merge existing calls before placing a new one.
     </string>
 
+    <!-- The title for the change voicemail PIN activity -->
+    <string name="change_pin_title">Change Voicemail PIN</string>
+    <!-- The label for the continue button in change voicemail PIN activity -->
+    <string name="change_pin_continue_label">Continue</string>
+    <!-- The label for the cancel button in change voicemail PIN activity -->
+    <string name="change_pin_cancel_label">Cancel</string>
+    <!-- The label for the ok button in change voicemail PIN activity -->
+    <string name="change_pin_ok_label">Ok</string>
+    <!-- The title for the enter old pin step in change voicemail PIN activity -->
+    <string 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 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 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 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 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 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 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 name="change_pin_system_error">Unable to set PIN</string>
 </resources>
diff --git a/res/xml/voicemail_settings.xml b/res/xml/voicemail_settings.xml
index 734d9d7..e1dafb0 100644
--- a/res/xml/voicemail_settings.xml
+++ b/res/xml/voicemail_settings.xml
@@ -65,8 +65,7 @@
         android:key="@string/voicemail_visual_voicemail_key"
         android:title="@string/voicemail_visual_voicemail_switch_title" />"
 
-    <com.android.phone.settings.VoicemailChangePinDialogPreference
-        android:key="@string/voicemail_change_pin_key"
-        android:title="@string/voicemail_change_pin_dialog_title" />
-
+    <Preference
+      android:key="@string/voicemail_change_pin_key"
+      android:title="@string/voicemail_change_pin_dialog_title" />
 </PreferenceScreen>
diff --git a/src/com/android/phone/settings/VisualVoicemailSettingsUtil.java b/src/com/android/phone/settings/VisualVoicemailSettingsUtil.java
index d7e573e..c38b595 100644
--- a/src/com/android/phone/settings/VisualVoicemailSettingsUtil.java
+++ b/src/com/android/phone/settings/VisualVoicemailSettingsUtil.java
@@ -16,49 +16,31 @@
 package com.android.phone.settings;
 
 import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
 import android.telecom.PhoneAccountHandle;
 
 import com.android.internal.telephony.Phone;
 import com.android.phone.PhoneUtils;
 import com.android.phone.R;
-import com.android.phone.vvm.omtp.OmtpConstants;
 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
-import com.android.phone.vvm.omtp.sms.StatusMessage;
+import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
 import com.android.phone.vvm.omtp.utils.PhoneAccountHandleConverter;
 
 /**
- * Save visual voicemail login values and whether or not a particular account is enabled 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.
+ * Save whether or not a particular account is enabled in shared to be retrieved later.
  */
 public class VisualVoicemailSettingsUtil {
-    private static final String VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX =
-            "visual_voicemail_";
 
     private static final String IS_ENABLED_KEY = "is_enabled";
-    // Record the timestamp of the last full sync so that duplicate syncs can be reduced.
-    private static final String LAST_FULL_SYNC_TIMESTAMP = "last_full_sync_timestamp";
-    // Constant indicating that there has never been a full sync.
-    public static final long NO_PRIOR_FULL_SYNC = -1;
 
-    // Setting for how often retries should be done.
-    private static final String SYNC_RETRY_INTERVAL = "sync_retry_interval";
-    private static final long MAX_SYNC_RETRY_INTERVAL_MS = 86400000;   // 24 hours
-    private static final long DEFAULT_SYNC_RETRY_INTERVAL_MS = 900000; // 15 minutes
 
-    public static void setVisualVoicemailEnabled(Context context, PhoneAccountHandle phoneAccount,
+    public static void setEnabled(Context context, PhoneAccountHandle phoneAccount,
             boolean isEnabled) {
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        prefs.edit()
-                .putBoolean(getVisualVoicemailSharedPrefsKey(IS_ENABLED_KEY, phoneAccount),
-                        isEnabled)
+        new VisualVoicemailPreferences(context, phoneAccount).edit()
+                .putBoolean(IS_ENABLED_KEY, isEnabled)
                 .apply();
     }
 
-    public static boolean isVisualVoicemailEnabled(Context context,
+    public static boolean isEnabled(Context context,
             PhoneAccountHandle phoneAccount) {
         if (phoneAccount == null) {
             return false;
@@ -67,19 +49,18 @@
             return false;
         }
 
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        String key = getVisualVoicemailSharedPrefsKey(IS_ENABLED_KEY, phoneAccount);
-        if (prefs.contains(key)) {
+        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(key, false);
+            return prefs.getBoolean(IS_ENABLED_KEY, false);
         }
         return new OmtpVvmCarrierConfigHelper(context,
                 PhoneAccountHandleConverter.toSubId(phoneAccount)).isEnabledByDefault();
     }
 
-    public static boolean isVisualVoicemailEnabled(Phone phone) {
-        return isVisualVoicemailEnabled(phone.getContext(),
+    public static boolean isEnabled(Phone phone) {
+        return isEnabled(phone.getContext(),
                 PhoneUtils.makePstnPhoneAccountHandle(phone));
     }
 
@@ -89,82 +70,12 @@
      * 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 isVisualVoicemailUserSet(Context context,
+    public static boolean isEnabledUserSet(Context context,
             PhoneAccountHandle phoneAccount) {
         if (phoneAccount == null) {
             return false;
         }
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        return prefs.contains(getVisualVoicemailSharedPrefsKey(IS_ENABLED_KEY, phoneAccount));
-    }
-
-    public static void setVisualVoicemailCredentialsFromStatusMessage(Context context,
-            PhoneAccountHandle phoneAccount, StatusMessage message) {
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        SharedPreferences.Editor editor = prefs.edit();
-
-        editor.putString(
-                getVisualVoicemailSharedPrefsKey(OmtpConstants.IMAP_PORT, phoneAccount),
-                message.getImapPort());
-        editor.putString(
-                getVisualVoicemailSharedPrefsKey(OmtpConstants.SERVER_ADDRESS, phoneAccount),
-                message.getServerAddress());
-        editor.putString(
-                getVisualVoicemailSharedPrefsKey(OmtpConstants.IMAP_USER_NAME, phoneAccount),
-                message.getImapUserName());
-        editor.putString(
-                getVisualVoicemailSharedPrefsKey(OmtpConstants.IMAP_PASSWORD, phoneAccount),
-                message.getImapPassword());
-        editor.commit();
-    }
-
-    public static String getVisualVoicemailCredentials(Context context, String key,
-            PhoneAccountHandle phoneAccount) {
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        return prefs.getString(getVisualVoicemailSharedPrefsKey(key, phoneAccount), null);
-    }
-
-    public static long getVisualVoicemailRetryInterval(Context context,
-            PhoneAccountHandle phoneAccount) {
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        return prefs.getLong(getVisualVoicemailSharedPrefsKey(SYNC_RETRY_INTERVAL, phoneAccount),
-                DEFAULT_SYNC_RETRY_INTERVAL_MS);
-    }
-
-    public static void resetVisualVoicemailRetryInterval(Context context,
-            PhoneAccountHandle phoneAccount) {
-        setVisualVoicemailRetryInterval(context, phoneAccount, DEFAULT_SYNC_RETRY_INTERVAL_MS);
-    }
-
-    public static void setVisualVoicemailRetryInterval(Context context,
-            PhoneAccountHandle phoneAccount, long interval) {
-        SharedPreferences.Editor editor =
-                PreferenceManager.getDefaultSharedPreferences(context).edit();
-        editor.putLong(getVisualVoicemailSharedPrefsKey(SYNC_RETRY_INTERVAL, phoneAccount),
-                Math.min(interval, MAX_SYNC_RETRY_INTERVAL_MS));
-        editor.commit();
-    }
-
-    public static void setVisualVoicemailLastFullSyncTime(Context context,
-            PhoneAccountHandle phoneAccount, long timestamp) {
-        SharedPreferences.Editor editor =
-                PreferenceManager.getDefaultSharedPreferences(context).edit();
-        editor.putLong(getVisualVoicemailSharedPrefsKey(LAST_FULL_SYNC_TIMESTAMP, phoneAccount),
-                timestamp);
-        editor.commit();
-
-    }
-
-    public static long getVisualVoicemailLastFullSyncTime(Context context,
-            PhoneAccountHandle phoneAccount) {
-        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        return prefs.getLong(
-                getVisualVoicemailSharedPrefsKey(LAST_FULL_SYNC_TIMESTAMP, phoneAccount),
-                NO_PRIOR_FULL_SYNC);
-    }
-
-    public static String getVisualVoicemailSharedPrefsKey(String key,
-            PhoneAccountHandle phoneAccount) {
-        return VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX + key + "_" + phoneAccount.getId();
+        VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+        return prefs.contains(IS_ENABLED_KEY);
     }
 }
diff --git a/src/com/android/phone/settings/VoicemailChangePinActivity.java b/src/com/android/phone/settings/VoicemailChangePinActivity.java
new file mode 100644
index 0000000..68cc621
--- /dev/null
+++ b/src/com/android/phone/settings/VoicemailChangePinActivity.java
@@ -0,0 +1,615 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.phone.settings;
+
+import android.annotation.Nullable;
+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.content.SharedPreferences;
+import android.net.Network;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.PreferenceManager;
+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.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.phone.PhoneUtils;
+import com.android.phone.R;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.vvm.omtp.OmtpConstants;
+import com.android.phone.vvm.omtp.OmtpConstants.ChangePinResult;
+import com.android.phone.vvm.omtp.OmtpEvents;
+import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
+import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
+import com.android.phone.vvm.omtp.VvmLog;
+import com.android.phone.vvm.omtp.imap.ImapHelper;
+import com.android.phone.vvm.omtp.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}
+ */
+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(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.mConfig.handleEvent(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();
+                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().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.mConfig.handleEvent(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);
+                    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);
+
+        migrateDefaultOldPin();
+
+        if (isDefaultOldPinSet(this, mPhoneAccountHandle)) {
+            mOldPin = getDefaultOldPin(this, mPhoneAccountHandle);
+            updateState(State.VerifyOldPin);
+        } else {
+            updateState(State.EnterOldPin);
+        }
+    }
+
+    /**
+     * 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);
+    }
+
+    public void onClick(View v) {
+        switch (v.getId()) {
+            case R.id.next_button:
+                handleNext();
+                break;
+
+            case R.id.cancel_button:
+                finish();
+                break;
+        }
+    }
+
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        // 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;
+    }
+
+    public void afterTextChanged(Editable s) {
+        mUiState.onInputChanged(this);
+    }
+
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        // Do nothing
+    }
+
+    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);
+    }
+
+    /**
+     * Storage location has changed mid development. Migrate from the old location to avoid losing
+     * tester's default old pin.
+     */
+    private void migrateDefaultOldPin() {
+        String key = "voicemail_pin_dialog_preference_"
+                + PhoneUtils.getSubIdForPhoneAccountHandle(mPhoneAccountHandle)
+                + "_default_old_pin";
+
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+        if (preferences.contains(key)) {
+            setDefaultOldPIN(this, mPhoneAccountHandle, preferences.getString(key, null));
+            preferences.edit().putString(key, null).apply();
+        }
+    }
+
+    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);
+            mOldPin = oldPin;
+            mNewPin = newPin;
+        }
+
+        @Override
+        public void onAvailable(Network network) {
+            super.onAvailable(network);
+            try (ImapHelper helper =
+                new ImapHelper(VoicemailChangePinActivity.this, mPhoneAccountHandle, network)){
+
+                @ChangePinResult int result =
+                        helper.changePin(mOldPin, mNewPin);
+                sendResult(result);
+            } catch (MessagingException 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) {
+            mProgressDialog.dismiss();
+            mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget();
+            releaseNetwork();
+        }
+    }
+
+}
diff --git a/src/com/android/phone/settings/VoicemailChangePinDialogPreference.java b/src/com/android/phone/settings/VoicemailChangePinDialogPreference.java
deleted file mode 100644
index 3411228..0000000
--- a/src/com/android/phone/settings/VoicemailChangePinDialogPreference.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.phone.settings;
-
-import android.annotation.Nullable;
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.net.Network;
-import android.preference.DialogPreference;
-import android.preference.PreferenceManager;
-import android.telecom.PhoneAccountHandle;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import android.widget.EditText;
-
-import com.android.phone.PhoneUtils;
-import com.android.phone.R;
-import com.android.phone.common.mail.MessagingException;
-import com.android.phone.vvm.omtp.OmtpConstants;
-import com.android.phone.vvm.omtp.OmtpConstants.ChangePinResult;
-import com.android.phone.vvm.omtp.OmtpEvents;
-import com.android.phone.vvm.omtp.imap.ImapHelper;
-import com.android.phone.vvm.omtp.sync.VvmNetworkRequestCallback;
-
-/**
- * Dialog to change the voicemail PIN. The TUI PIN is used when accessing traditional voicemail through
- * phone call.
- */
-public class VoicemailChangePinDialogPreference extends DialogPreference {
-
-    private static final String TAG = "VmChangePinDialog";
-
-    private EditText mOldPin;
-    private EditText mNewPin;
-    private PhoneAccountHandle mPhoneAccountHandle;
-
-    private ProgressDialog mProgressDialog;
-
-    private static final String DEFAULT_OLD_PIN_KEY = "default_old_pin";
-
-    public VoicemailChangePinDialogPreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public VoicemailChangePinDialogPreference(Context context, AttributeSet attrs,
-            int defStyle) {
-        super(context, attrs, defStyle);
-    }
-
-    @Override
-    protected View onCreateDialogView() {
-        setDialogLayoutResource(R.layout.voicemail_dialog_change_pin);
-
-        View dialog = super.onCreateDialogView();
-
-        mOldPin = (EditText) dialog.findViewById(R.id.vm_old_pin);
-        mNewPin = (EditText) dialog.findViewById(R.id.vm_new_pin);
-        String defaultOldPin = getDefaultOldPin(getContext(), mPhoneAccountHandle);
-        if (defaultOldPin != null) {
-            // If the old PIN was set by the system, read its' value and hide the input box.
-            mOldPin.setText(defaultOldPin);
-            mOldPin.setVisibility(View.GONE);
-            dialog.findViewById(R.id.vm_old_pin_label).setVisibility(View.GONE);
-        }
-        return dialog;
-    }
-
-    @Override
-    protected void onDialogClosed(boolean positiveResult) {
-        if (positiveResult) {
-            processPinChange();
-        }
-        super.onDialogClosed(positiveResult);
-    }
-
-    public VoicemailChangePinDialogPreference setPhoneAccountHandle(PhoneAccountHandle handle) {
-        mPhoneAccountHandle = handle;
-        return this;
-    }
-
-    @Nullable
-    public static String getDefaultOldPin(Context context, PhoneAccountHandle handle) {
-        return getSharedPreference(context)
-                .getString(getPerPhoneAccountKey(handle, DEFAULT_OLD_PIN_KEY), null);
-    }
-
-    public static void setDefaultOldPIN(Context context, PhoneAccountHandle handle,
-            @Nullable String pin) {
-        SharedPreferences preferences = getSharedPreference(context);
-        preferences.edit()
-                .putString(getPerPhoneAccountKey(handle, DEFAULT_OLD_PIN_KEY), pin)
-                .apply();
-    }
-
-    private static String getPerPhoneAccountKey(PhoneAccountHandle handle, String key) {
-        return "voicemail_pin_dialog_preference_"
-                + PhoneUtils.getSubIdForPhoneAccountHandle(handle) + "_" + key;
-    }
-
-    private static SharedPreferences getSharedPreference(Context context) {
-        return PreferenceManager.getDefaultSharedPreferences(context);
-    }
-
-    private void processPinChange() {
-        mProgressDialog = new ProgressDialog(getContext());
-        mProgressDialog.setCancelable(false);
-        mProgressDialog.setMessage(getContext().getString(R.string.vm_change_pin_progress_message));
-        mProgressDialog.show();
-
-        ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback();
-        callback.requestNetwork();
-    }
-
-    private void finishPinChange() {
-        mProgressDialog.dismiss();
-    }
-
-    private void showError(@ChangePinResult int result) {
-        if (result != OmtpConstants.CHANGE_PIN_SUCCESS) {
-            CharSequence message;
-            switch (result) {
-                case OmtpConstants.CHANGE_PIN_TOO_SHORT:
-                    message = getContext().getString(R.string.vm_change_pin_error_too_short);
-                    break;
-                case OmtpConstants.CHANGE_PIN_TOO_LONG:
-                    message = getContext().getString(R.string.vm_change_pin_error_too_long);
-                    break;
-
-                case OmtpConstants.CHANGE_PIN_TOO_WEAK:
-                    message = getContext().getString(R.string.vm_change_pin_error_too_weak);
-                    break;
-                case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER:
-                    message = getContext().getString(R.string.vm_change_pin_error_invalid);
-                    break;
-                case OmtpConstants.CHANGE_PIN_MISMATCH:
-                    message = getContext().getString(R.string.vm_change_pin_error_mismatch);
-                    break;
-                case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR:
-                    message = getContext().getString(R.string.vm_change_pin_error_system_error);
-                    break;
-                default:
-                    Log.wtf(TAG, "Unexpected ChangePinResult " + result);
-                    return;
-            }
-            new AlertDialog.Builder(getContext())
-                    .setMessage(message)
-                    .setPositiveButton(android.R.string.ok, null)
-                    .show();
-        }
-    }
-
-    private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback {
-
-        public ChangePinNetworkRequestCallback() {
-            super(getContext(), mPhoneAccountHandle);
-        }
-
-        @Override
-        public void onAvailable(Network network) {
-            super.onAvailable(network);
-            try (ImapHelper helper = new ImapHelper(getContext(), mPhoneAccountHandle, network)) {
-                @ChangePinResult int result =
-                        helper.changePin(mOldPin.getText().toString(),
-                                mNewPin.getText().toString());
-                finishPinChange();
-                if (result != OmtpConstants.CHANGE_PIN_SUCCESS) {
-                    showError(result);
-                }
-
-                if (result == OmtpConstants.CHANGE_PIN_SUCCESS
-                        || result == OmtpConstants.CHANGE_PIN_MISMATCH) {
-                    // If the PIN change succeeded we no longer know what the old (current) PIN is.
-                    // If the default old PIN is rejected by the server, the PIN is probably changed
-                    // through other means.
-                    // Wipe the default old PIN so the old PIN input box will be shown to the user
-                    // on the next time.
-                    setDefaultOldPIN(mContext, mPhoneAccountHandle, null);
-                    helper.handleEvent(OmtpEvents.CONFIG_PIN_SET);
-                }
-            } catch (MessagingException e) {
-                finishPinChange();
-                showError(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
-            }
-
-        }
-
-        @Override
-        public void onFailed(String reason) {
-            super.onFailed(reason);
-            finishPinChange();
-            showError(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
-        }
-    }
-}
diff --git a/src/com/android/phone/settings/VoicemailSettingsActivity.java b/src/com/android/phone/settings/VoicemailSettingsActivity.java
index b10af6e..af4f2ad 100644
--- a/src/com/android/phone/settings/VoicemailSettingsActivity.java
+++ b/src/com/android/phone/settings/VoicemailSettingsActivity.java
@@ -205,7 +205,7 @@
     private VoicemailRingtonePreference mVoicemailNotificationRingtone;
     private CheckBoxPreference mVoicemailNotificationVibrate;
     private SwitchPreference mVoicemailVisualVoicemail;
-    private VoicemailChangePinDialogPreference mVoicemailChangePinPreference;
+    private Preference mVoicemailChangePinPreference;
 
     //*********************************************************************************************
     // Preference Activity Methods
@@ -266,18 +266,24 @@
         mVoicemailVisualVoicemail = (SwitchPreference) findPreference(
                 getResources().getString(R.string.voicemail_visual_voicemail_key));
 
-        mVoicemailChangePinPreference = (VoicemailChangePinDialogPreference) findPreference(
+        mVoicemailChangePinPreference = findPreference(
                 getResources().getString(R.string.voicemail_change_pin_key));
-        mVoicemailChangePinPreference
-                .setPhoneAccountHandle(PhoneUtils.makePstnPhoneAccountHandle(mPhone));
+        PhoneAccountHandle phoneAccountHandle = PhoneUtils.makePstnPhoneAccountHandle(mPhone);
+        Intent changePinIntent = new Intent(new Intent(this, VoicemailChangePinActivity.class));
+        changePinIntent.putExtra(VoicemailChangePinActivity.EXTRA_PHONE_ACCOUNT_HANDLE,
+                phoneAccountHandle);
+
+        mVoicemailChangePinPreference.setIntent(changePinIntent);
+        if (VoicemailChangePinActivity.isDefaultOldPinSet(this, phoneAccountHandle)) {
+            mVoicemailChangePinPreference.setTitle(R.string.voicemail_set_pin_dialog_title);
+        } else {
+            mVoicemailChangePinPreference.setTitle(R.string.voicemail_change_pin_dialog_title);
+        }
 
         if (mOmtpVvmCarrierConfigHelper.isValid()) {
             mVoicemailVisualVoicemail.setOnPreferenceChangeListener(this);
             mVoicemailVisualVoicemail.setChecked(
-                    VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(mPhone));
-
-            mVoicemailChangePinPreference
-                    .setPhoneAccountHandle(PhoneUtils.makePstnPhoneAccountHandle(mPhone));
+                    VisualVoicemailSettingsUtil.isEnabled(mPhone));
         } else {
             prefSet.removePreference(mVoicemailVisualVoicemail);
             prefSet.removePreference(mVoicemailChangePinPreference);
@@ -405,7 +411,7 @@
             boolean isEnabled = (boolean) objValue;
             PhoneAccountHandle handle = PhoneUtils.makePstnPhoneAccountHandle(mPhone);
             VisualVoicemailSettingsUtil
-                    .setVisualVoicemailEnabled(mPhone.getContext(), handle, isEnabled);
+                    .setEnabled(mPhone.getContext(), handle, isEnabled);
             PreferenceScreen prefSet = getPreferenceScreen();
             if (isEnabled) {
                 OmtpVvmSourceManager.getInstance(mPhone.getContext()).addPhoneStateListener(mPhone);
diff --git a/src/com/android/phone/vvm/omtp/OmtpConstants.java b/src/com/android/phone/vvm/omtp/OmtpConstants.java
index 8975b59..3f5722f 100644
--- a/src/com/android/phone/vvm/omtp/OmtpConstants.java
+++ b/src/com/android/phone/vvm/omtp/OmtpConstants.java
@@ -125,6 +125,7 @@
     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";
diff --git a/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java b/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
index e8ae403..04fb8e5 100644
--- a/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
+++ b/src/com/android/phone/vvm/omtp/OmtpVvmCarrierConfigHelper.java
@@ -33,6 +33,7 @@
 import com.android.phone.vvm.omtp.protocol.VisualVoicemailProtocol;
 import com.android.phone.vvm.omtp.protocol.VisualVoicemailProtocolFactory;
 import com.android.phone.vvm.omtp.sms.StatusMessage;
+import com.android.phone.vvm.omtp.utils.PhoneAccountHandleConverter;
 
 import java.util.Arrays;
 import java.util.Set;
@@ -96,6 +97,8 @@
     private final VisualVoicemailProtocol mProtocol;
     private final PersistableBundle mTelephonyConfig;
 
+    private PhoneAccountHandle mPhoneAccountHandle;
+
     public OmtpVvmCarrierConfigHelper(Context context, int subId) {
         mContext = context;
         mSubId = subId;
@@ -110,6 +113,11 @@
         mProtocol = VisualVoicemailProtocolFactory.create(mVvmType);
     }
 
+    public OmtpVvmCarrierConfigHelper(Context context, PhoneAccountHandle handle) {
+        this(context, PhoneAccountHandleConverter.toSubId(handle));
+        mPhoneAccountHandle = handle;
+    }
+
     @VisibleForTesting
     OmtpVvmCarrierConfigHelper(PersistableBundle carrierConfig,
             PersistableBundle telephonyConfig) {
@@ -129,6 +137,13 @@
         return mSubId;
     }
 
+    public PhoneAccountHandle getPhoneAccountHandle() {
+        if (mPhoneAccountHandle == null) {
+            mPhoneAccountHandle = PhoneAccountHandleConverter.fromSubId(mSubId);
+        }
+        return mPhoneAccountHandle;
+    }
+
     /**
      * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a
      * known protocol.
diff --git a/src/com/android/phone/vvm/omtp/SimChangeReceiver.java b/src/com/android/phone/vvm/omtp/SimChangeReceiver.java
index f22711a..375109d 100644
--- a/src/com/android/phone/vvm/omtp/SimChangeReceiver.java
+++ b/src/com/android/phone/vvm/omtp/SimChangeReceiver.java
@@ -88,7 +88,7 @@
         if (carrierConfigHelper.isValid()) {
             PhoneAccountHandle phoneAccount = PhoneAccountHandleConverter.fromSubId(subId);
 
-            if (VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(context, phoneAccount)) {
+            if (VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
                 VvmLog.i(TAG, "Sim state or carrier config changed: requesting"
                         + " activation for " + subId);
 
diff --git a/src/com/android/phone/vvm/omtp/VisualVoicemailPreferences.java b/src/com/android/phone/vvm/omtp/VisualVoicemailPreferences.java
new file mode 100644
index 0000000..be51ea9
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/VisualVoicemailPreferences.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.phone.vvm.omtp;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.phone.NeededForTesting;
+
+import java.util.Set;
+
+/**
+ * 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 {
+
+    private static final String VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX =
+            "visual_voicemail_";
+
+    private final SharedPreferences mPreferences;
+    private final PhoneAccountHandle mPhoneAccountHandle;
+
+    public VisualVoicemailPreferences(Context context, PhoneAccountHandle phoneAccountHandle) {
+        mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+        mPhoneAccountHandle = phoneAccountHandle;
+    }
+
+    public class Editor {
+
+        private final SharedPreferences.Editor mEditor;
+
+        private Editor() {
+            mEditor = mPreferences.edit();
+        }
+
+        public void apply() {
+            mEditor.apply();
+        }
+
+        public Editor putBoolean(String key, boolean value) {
+            mEditor.putBoolean(getKey(key), value);
+            return this;
+        }
+
+        @NeededForTesting
+        public Editor putFloat(String key, float value) {
+            mEditor.putFloat(getKey(key), value);
+            return this;
+        }
+
+        public Editor putInt(String key, int value) {
+            mEditor.putInt(getKey(key), value);
+            return this;
+        }
+
+        @NeededForTesting
+        public Editor putLong(String key, long value) {
+            mEditor.putLong(getKey(key), value);
+            return this;
+        }
+
+        public Editor putString(String key, String value) {
+            mEditor.putString(getKey(key), value);
+            return this;
+        }
+
+        @NeededForTesting
+        public Editor putStringSet(String key, Set<String> value) {
+            mEditor.putStringSet(getKey(key), value);
+            return this;
+        }
+    }
+
+    public Editor edit() {
+        return new Editor();
+    }
+
+    public boolean getBoolean(String key, boolean defValue) {
+        return getValue(key, defValue);
+    }
+
+    @NeededForTesting
+    public float getFloat(String key, float defValue) {
+        return getValue(key, defValue);
+    }
+
+    public int getInt(String key, int defValue) {
+        return getValue(key, defValue);
+    }
+
+    @NeededForTesting
+    public long getLong(String key, long defValue) {
+        return getValue(key, defValue);
+    }
+
+    public String getString(String key, String defValue) {
+        return getValue(key, defValue);
+    }
+
+    @Nullable
+    public String getString(String key) {
+        return getValue(key, null);
+    }
+
+    @NeededForTesting
+    public Set<String> getStringSet(String key, Set<String> defValue) {
+        return getValue(key, defValue);
+    }
+
+    public boolean contains(String key) {
+        return mPreferences.contains(getKey(key));
+    }
+
+    private <T> T getValue(String key, T defValue) {
+        if (!contains(key)) {
+            return defValue;
+        }
+        Object object = mPreferences.getAll().get(getKey(key));
+        if (object == null) {
+            return defValue;
+        }
+        return (T) object;
+    }
+
+    private String getKey(String key) {
+        return VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX + key + "_" + mPhoneAccountHandle.getId();
+    }
+}
diff --git a/src/com/android/phone/vvm/omtp/VvmPackageInstallReceiver.java b/src/com/android/phone/vvm/omtp/VvmPackageInstallReceiver.java
index 8a0495b..7c20065 100644
--- a/src/com/android/phone/vvm/omtp/VvmPackageInstallReceiver.java
+++ b/src/com/android/phone/vvm/omtp/VvmPackageInstallReceiver.java
@@ -48,7 +48,7 @@
         OmtpVvmSourceManager vvmSourceManager = OmtpVvmSourceManager.getInstance(context);
         Set<PhoneAccountHandle> phoneAccounts = vvmSourceManager.getOmtpVvmSources();
         for (PhoneAccountHandle phoneAccount : phoneAccounts) {
-            if (VisualVoicemailSettingsUtil.isVisualVoicemailUserSet(context, phoneAccount)) {
+            if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) {
                 // Skip the check if this voicemail source's setting is overridden by the user.
                 continue;
             }
diff --git a/src/com/android/phone/vvm/omtp/imap/ImapHelper.java b/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
index 908d0f7..4db02d0 100644
--- a/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
+++ b/src/com/android/phone/vvm/omtp/imap/ImapHelper.java
@@ -16,11 +16,9 @@
 package com.android.phone.vvm.omtp.imap;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.net.ConnectivityManager;
 import android.net.Network;
 import android.net.NetworkInfo;
-import android.preference.PreferenceManager;
 import android.provider.VoicemailContract;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.Voicemail;
@@ -44,11 +42,11 @@
 import com.android.phone.common.mail.store.imap.ImapConstants;
 import com.android.phone.common.mail.store.imap.ImapResponse;
 import com.android.phone.common.mail.utils.LogUtils;
-import com.android.phone.settings.VisualVoicemailSettingsUtil;
 import com.android.phone.vvm.omtp.OmtpConstants;
 import com.android.phone.vvm.omtp.OmtpConstants.ChangePinResult;
 import com.android.phone.vvm.omtp.OmtpEvents;
 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
+import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
 import com.android.phone.vvm.omtp.VvmLog;
 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback;
 import com.android.phone.vvm.omtp.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
@@ -78,7 +76,7 @@
     private final PhoneAccountHandle mPhoneAccount;
     private final Network mNetwork;
 
-    SharedPreferences mPrefs;
+    VisualVoicemailPreferences mPrefs;
     private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_";
     private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_";
 
@@ -93,18 +91,16 @@
         mNetwork = network;
         mConfig = new OmtpVvmCarrierConfigHelper(context,
                 PhoneUtils.getSubIdForPhoneAccountHandle(phoneAccount));
+        mPrefs = new VisualVoicemailPreferences(context,
+                phoneAccount);
         try {
             TempDirectory.setTempDirectory(context);
 
-            String username = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
-                    OmtpConstants.IMAP_USER_NAME, phoneAccount);
-            String password = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
-                    OmtpConstants.IMAP_PASSWORD, phoneAccount);
-            String serverName = VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
-                    OmtpConstants.SERVER_ADDRESS, phoneAccount);
+            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(
-                    VisualVoicemailSettingsUtil.getVisualVoicemailCredentials(context,
-                            OmtpConstants.IMAP_PORT, phoneAccount));
+                    mPrefs.getString(OmtpConstants.IMAP_PORT, null));
             int auth = ImapStore.FLAG_NONE;
 
             int sslPort = mConfig.getSslPort();
@@ -120,11 +116,10 @@
             LogUtils.w(TAG, "Could not parse port number");
         }
 
-        mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
-        mQuotaOccupied = mPrefs.getInt(getSharedPrefsKey(PREF_KEY_QUOTA_OCCUPIED),
-                VoicemailContract.Status.QUOTA_UNAVAILABLE);
-        mQuotaTotal = mPrefs.getInt(getSharedPrefsKey(PREF_KEY_QUOTA_TOTAL),
-                VoicemailContract.Status.QUOTA_UNAVAILABLE);
+        mQuotaOccupied = mPrefs
+                .getInt(PREF_KEY_QUOTA_OCCUPIED, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+        mQuotaTotal = mPrefs
+                .getInt(PREF_KEY_QUOTA_TOTAL, VoicemailContract.Status.QUOTA_UNAVAILABLE);
     }
 
     @Override
@@ -500,8 +495,8 @@
                 .setQuota(mQuotaOccupied, mQuotaTotal)
                 .apply();
         mPrefs.edit()
-                .putInt(getSharedPrefsKey(PREF_KEY_QUOTA_OCCUPIED), mQuotaOccupied)
-                .putInt(getSharedPrefsKey(PREF_KEY_QUOTA_TOTAL), mQuotaTotal)
+                .putInt(PREF_KEY_QUOTA_OCCUPIED, mQuotaOccupied)
+                .putInt(PREF_KEY_QUOTA_TOTAL, mQuotaTotal)
                 .apply();
         VvmLog.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal);
     }
@@ -702,8 +697,4 @@
             IoUtils.closeQuietly(out);
         }
     }
-
-    private String getSharedPrefsKey(String key) {
-        return VisualVoicemailSettingsUtil.getVisualVoicemailSharedPrefsKey(key, mPhoneAccount);
-    }
 }
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/protocol/Vvm3EventHandler.java b/src/com/android/phone/vvm/omtp/protocol/Vvm3EventHandler.java
index 8eacb99..3645407 100644
--- a/src/com/android/phone/vvm/omtp/protocol/Vvm3EventHandler.java
+++ b/src/com/android/phone/vvm/omtp/protocol/Vvm3EventHandler.java
@@ -22,7 +22,7 @@
 import android.util.Log;
 
 import com.android.phone.VoicemailStatus;
-import com.android.phone.settings.VoicemailChangePinDialogPreference;
+import com.android.phone.settings.VoicemailChangePinActivity;
 import com.android.phone.vvm.omtp.DefaultOmtpEventHandler;
 import com.android.phone.vvm.omtp.OmtpEvents;
 import com.android.phone.vvm.omtp.OmtpEvents.Type;
@@ -116,7 +116,7 @@
             case CONFIG_REQUEST_STATUS_SUCCESS:
                 PhoneAccountHandle handle = PhoneAccountHandleConverter
                         .fromSubId(config.getSubId());
-                if (VoicemailChangePinDialogPreference.getDefaultOldPin(context, handle) == null) {
+                if (VoicemailChangePinActivity.isDefaultOldPinSet(context, handle)) {
                     return false;
                 } else {
                     postError(context, config, PIN_NOT_SET);
diff --git a/src/com/android/phone/vvm/omtp/protocol/Vvm3Protocol.java b/src/com/android/phone/vvm/omtp/protocol/Vvm3Protocol.java
index fadf104..d15786f 100644
--- a/src/com/android/phone/vvm/omtp/protocol/Vvm3Protocol.java
+++ b/src/com/android/phone/vvm/omtp/protocol/Vvm3Protocol.java
@@ -25,10 +25,11 @@
 
 import com.android.phone.common.mail.MessagingException;
 import com.android.phone.settings.VisualVoicemailSettingsUtil;
-import com.android.phone.settings.VoicemailChangePinDialogPreference;
+import com.android.phone.settings.VoicemailChangePinActivity;
 import com.android.phone.vvm.omtp.OmtpConstants;
 import com.android.phone.vvm.omtp.OmtpEvents;
 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
+import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
 import com.android.phone.vvm.omtp.VvmLog;
 import com.android.phone.vvm.omtp.imap.ImapHelper;
 import com.android.phone.vvm.omtp.sms.OmtpMessageSender;
@@ -61,7 +62,7 @@
     private static String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5";
     private static String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6";
 
-    private static final int PIN_LENGTH = 6;
+    private static final int DEFAULT_PIN_LENGTH = 6;
 
     @Override
     public void startActivation(OmtpVvmCarrierConfigHelper config) {
@@ -92,13 +93,16 @@
             new Vvm3Subscriber(phoneAccountHandle, config, data).subscribe();
         } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) {
             VvmLog.i(TAG, "setting up new user");
-            VisualVoicemailSettingsUtil.setVisualVoicemailCredentialsFromStatusMessage(
-                    config.getContext(), phoneAccountHandle, message);
+            // 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(phoneAccountHandle, config, message);
         } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) {
             VvmLog.i(TAG, "User provisioned but not activated, disabling VVM");
             VisualVoicemailSettingsUtil
-                    .setVisualVoicemailEnabled(config.getContext(), phoneAccountHandle, false);
+                    .setEnabled(config.getContext(), phoneAccountHandle, false);
         } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) {
             VvmLog.i(TAG, "User blocked");
             config.handleEvent(OmtpEvents.VVM3_SUBSCRIBER_BLOCKED);
@@ -192,16 +196,14 @@
                 return false;
             }
 
-            if (VoicemailChangePinDialogPreference.getDefaultOldPin(mContext, mPhoneAccount)
-                    != null) {
+            if (VoicemailChangePinActivity.isDefaultOldPinSet(mContext, mPhoneAccount)) {
                 // The pin was already set
                 VvmLog.i(TAG, "PIN already set");
                 return true;
             }
-            String newPin = generatePin();
+            String newPin = generatePin(getMinimumPinLength(mContext, mPhoneAccount));
             if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) {
-                VoicemailChangePinDialogPreference
-                        .setDefaultOldPIN(mContext, mPhoneAccount, newPin);
+                VoicemailChangePinActivity.setDefaultOldPIN(mContext, mPhoneAccount, newPin);
                 helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED);
             }
             VvmLog.i(TAG, "new user: PIN set");
@@ -227,10 +229,25 @@
         }
     }
 
-    private static String generatePin() {
+    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();
-        // TODO(b/29102412): generate base on the length requirement from the server
-        return String.format("%010d", Math.abs(random.nextLong())).substring(0, PIN_LENGTH);
+        return String.format(Locale.US, "%010d", Math.abs(random.nextLong()))
+                .substring(0, length);
 
     }
 }
diff --git a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
index 5bf7900..30ea43d 100644
--- a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
+++ b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
@@ -31,6 +31,7 @@
 import com.android.phone.vvm.omtp.OmtpConstants;
 import com.android.phone.vvm.omtp.OmtpEvents;
 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
+import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
 import com.android.phone.vvm.omtp.VvmLog;
 import com.android.phone.vvm.omtp.sync.OmtpVvmSourceManager;
 import com.android.phone.vvm.omtp.sync.OmtpVvmSyncService;
@@ -41,6 +42,7 @@
  * Receive SMS messages and send for processing by the OMTP visual voicemail source.
  */
 public class OmtpMessageReceiver extends BroadcastReceiver {
+
     private static final String TAG = "OmtpMessageReceiver";
 
     private Context mContext;
@@ -63,7 +65,7 @@
         }
 
         OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, subId);
-        if (!VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(mContext, phone)) {
+        if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phone)) {
             if (helper.isLegacyModeEnabled()) {
                 LegacyModeSmsHandler.handle(context, intent, phone);
             } else {
@@ -146,7 +148,7 @@
             default:
                 VvmLog.e(TAG,
                         "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
-               break;
+                break;
         }
 
         if (serviceIntent != null) {
@@ -163,10 +165,8 @@
             helper.handleEvent(OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS);
 
             // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
-            VisualVoicemailSettingsUtil.setVisualVoicemailCredentialsFromStatusMessage(
-                    mContext,
-                    phone,
-                    message);
+            VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(mContext, phone);
+            message.putStatus(prefs.edit()).apply();
 
             // Add the source to indicate that it is active.
             vvmSourceManager.addSource(phone);
diff --git a/src/com/android/phone/vvm/omtp/sms/StatusMessage.java b/src/com/android/phone/vvm/omtp/sms/StatusMessage.java
index f9d972f..65455d0 100644
--- a/src/com/android/phone/vvm/omtp/sms/StatusMessage.java
+++ b/src/com/android/phone/vvm/omtp/sms/StatusMessage.java
@@ -20,6 +20,7 @@
 
 import com.android.phone.NeededForTesting;
 import com.android.phone.vvm.omtp.OmtpConstants;
+import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
 
 /**
  * Structured data representation of OMTP STATUS message.
@@ -44,6 +45,7 @@
     private final String mSmtpPort;
     private final String mSmtpUserName;
     private final String mSmtpPassword;
+    private final String mTuiPasswordLength;
 
     @Override
     public String toString() {
@@ -58,7 +60,8 @@
                 + ", mImapPassword=" + Log.pii(mImapPassword)
                 + ", mSmtpPort=" + mSmtpPort
                 + ", mSmtpUserName=" + mSmtpUserName
-                + ", mSmtpPassword=" + Log.pii(mSmtpPassword) + "]";
+                + ", mSmtpPassword=" + Log.pii(mSmtpPassword)
+                + ", mTuiPasswordLength=" + mTuiPasswordLength + "]";
     }
 
     public StatusMessage(Bundle wrappedData) {
@@ -75,6 +78,7 @@
         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) {
@@ -180,6 +184,10 @@
         return mSmtpPassword;
     }
 
+    public String getTuiPasswordLength() {
+        return mTuiPasswordLength;
+    }
+
     private static String getString(Bundle bundle, String key) {
         String value = bundle.getString(key);
         if (value == null) {
@@ -187,4 +195,16 @@
         }
         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());
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncService.java b/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncService.java
index 9884e9d..7e62829 100644
--- a/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncService.java
+++ b/src/com/android/phone/vvm/omtp/sync/OmtpVvmSyncService.java
@@ -33,6 +33,7 @@
 import com.android.phone.settings.VisualVoicemailSettingsUtil;
 import com.android.phone.vvm.omtp.OmtpEvents;
 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
+import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
 import com.android.phone.vvm.omtp.VvmLog;
 import com.android.phone.vvm.omtp.fetch.VoicemailFetchedCallback;
 import com.android.phone.vvm.omtp.imap.ImapHelper;
@@ -87,6 +88,11 @@
     // Minimum time allowed between manual syncs
     private static final int MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS = 3 * 1000;
 
+    // Record the timestamp of the last full sync so that duplicate syncs can be reduced.
+    private static final String LAST_FULL_SYNC_TIMESTAMP = "last_full_sync_timestamp";
+    // Constant indicating that there has never been a full sync.
+    public static final long NO_PRIOR_FULL_SYNC = -1;
+
     private VoicemailsQueryHelper mQueryHelper;
 
     public OmtpVvmSyncService() {
@@ -100,19 +106,6 @@
 
     public static Intent getSyncIntent(Context context, String action,
             PhoneAccountHandle phoneAccount, Voicemail voicemail, boolean firstAttempt) {
-        if (firstAttempt) {
-            if (phoneAccount != null) {
-                VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context,
-                        phoneAccount);
-            } else {
-                OmtpVvmSourceManager vvmSourceManager =
-                        OmtpVvmSourceManager.getInstance(context);
-                Set<PhoneAccountHandle> sources = vvmSourceManager.getOmtpVvmSources();
-                for (PhoneAccountHandle source : sources) {
-                    VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(context, source);
-                }
-            }
-        }
 
         Intent serviceIntent = new Intent(context, OmtpVvmSyncService.class);
         serviceIntent.setAction(action);
@@ -194,14 +187,14 @@
 
     private void setupAndSendRequest(PhoneAccountHandle phoneAccount, Voicemail voicemail,
             String action, boolean isManualSync) {
-        if (!VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount)) {
+        if (!VisualVoicemailSettingsUtil.isEnabled(this, phoneAccount)) {
             VvmLog.v(TAG, "Sync requested for disabled account");
             return;
         }
 
         if (SYNC_FULL_SYNC.equals(action)) {
-            long lastSyncTime = VisualVoicemailSettingsUtil.getVisualVoicemailLastFullSyncTime(
-                    this, phoneAccount);
+            long lastSyncTime = new VisualVoicemailPreferences(this, phoneAccount)
+                    .getLong(LAST_FULL_SYNC_TIMESTAMP, NO_PRIOR_FULL_SYNC);
             long currentTime = System.currentTimeMillis();
             int minimumInterval = isManualSync ? MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS
                     : MINIMUM_MANUAL_SYNC_INTERVAL_MILLIS;
@@ -221,8 +214,9 @@
                 VoicemailStatus.edit(this, phoneAccount).apply();
                 return;
             }
-            VisualVoicemailSettingsUtil.setVisualVoicemailLastFullSyncTime(
-                    this, phoneAccount, currentTime);
+            new VisualVoicemailPreferences(this, phoneAccount).edit()
+                    .putLong(LAST_FULL_SYNC_TIMESTAMP, currentTime)
+                    .apply();
         }
 
         VvmNetworkRequestCallback networkCallback = new SyncNetworkRequestCallback(this,
@@ -238,8 +232,6 @@
                 try (ImapHelper imapHelper = new ImapHelper(this, phoneAccount, network)) {
                     if (!imapHelper.isSuccessfullyInitialized()) {
                         VvmLog.w(TAG, "Can't retrieve Imap credentials.");
-                        VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this,
-                                phoneAccount);
                         return;
                     }
 
@@ -251,17 +243,14 @@
                     }
                     imapHelper.updateQuota();
 
-                    // Need to check again for whether visual voicemail is enabled because it could
-                    // have been disabled while waiting for the response from the network.
-                    if (VisualVoicemailSettingsUtil.isVisualVoicemailEnabled(this, phoneAccount) &&
-                            !success) {
+                    // Need to check again for whether visual voicemail is enabled because it could have
+                    // been disabled while waiting for the response from the network.
+                    if (VisualVoicemailSettingsUtil.isEnabled(this, phoneAccount) &&
+                        !success) {
                         retryCount--;
                         VvmLog.v(TAG, "Retrying " + action);
                     } else {
                         // Nothing more to do here, just exit.
-                        VisualVoicemailSettingsUtil.resetVisualVoicemailRetryInterval(this,
-                                phoneAccount);
-
                         imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
                         return;
                     }
@@ -420,24 +409,6 @@
         return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
     }
 
-    protected void setRetryAlarm(PhoneAccountHandle phoneAccount, String action) {
-        Intent serviceIntent = new Intent(this, OmtpVvmSyncService.class);
-        serviceIntent.setAction(action);
-        serviceIntent.putExtra(OmtpVvmSyncService.EXTRA_PHONE_ACCOUNT, phoneAccount);
-        PendingIntent pendingIntent = PendingIntent.getService(this, 0, serviceIntent, 0);
-        long retryInterval = VisualVoicemailSettingsUtil.getVisualVoicemailRetryInterval(this,
-                phoneAccount);
-
-        VvmLog.v(TAG, "Retrying " + action + " in " + retryInterval + "ms");
-
-        AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
-        alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + retryInterval,
-                pendingIntent);
-
-        VisualVoicemailSettingsUtil.setVisualVoicemailRetryInterval(this, phoneAccount,
-                retryInterval * 2);
-    }
-
     /**
      * Builds a map from provider data to message for the given collection of voicemails.
      */
diff --git a/tests/src/com/android/phone/vvm/omtp/StatusMessageTest.java b/tests/src/com/android/phone/vvm/omtp/StatusMessageTest.java
index fd3aa2c..707463a 100644
--- a/tests/src/com/android/phone/vvm/omtp/StatusMessageTest.java
+++ b/tests/src/com/android/phone/vvm/omtp/StatusMessageTest.java
@@ -40,6 +40,7 @@
         bundle.putString(OmtpConstants.SMTP_PORT, "s1234");
         bundle.putString(OmtpConstants.SMTP_USER_NAME, "susername");
         bundle.putString(OmtpConstants.SMTP_PASSWORD, "spassword");
+        bundle.putString(OmtpConstants.TUI_PASSWORD_LENGTH, "4-7");
 
         StatusMessage message = new StatusMessage(bundle);
         assertEquals("status", message.getProvisioningStatus());
@@ -54,6 +55,7 @@
         assertEquals("s1234", message.getSmtpPort());
         assertEquals("susername", message.getSmtpUserName());
         assertEquals("spassword", message.getSmtpPassword());
+        assertEquals("4-7", message.getTuiPasswordLength());
     }
 
     public void testSyncMessage_EmptyBundle() {
@@ -70,5 +72,6 @@
         assertEquals("", message.getSmtpPort());
         assertEquals("", message.getSmtpUserName());
         assertEquals("", message.getSmtpPassword());
+        assertEquals("", message.getTuiPasswordLength());
     }
 }
diff --git a/tests/src/com/android/phone/vvm/omtp/VisualVoicemailPreferencesTest.java b/tests/src/com/android/phone/vvm/omtp/VisualVoicemailPreferencesTest.java
new file mode 100644
index 0000000..1ae7899
--- /dev/null
+++ b/tests/src/com/android/phone/vvm/omtp/VisualVoicemailPreferencesTest.java
@@ -0,0 +1,81 @@
+package com.android.phone.vvm.omtp;
+
+import android.content.ComponentName;
+import android.telecom.PhoneAccountHandle;
+import android.test.AndroidTestCase;
+import android.util.ArraySet;
+
+import java.util.Arrays;
+
+public class VisualVoicemailPreferencesTest extends AndroidTestCase {
+
+    public void testWriteRead() {
+        VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(),
+                createFakeHandle("testWriteRead"));
+        preferences.edit()
+                .putBoolean("boolean", true)
+                .putFloat("float", 0.5f)
+                .putInt("int", 123)
+                .putLong("long", 456)
+                .putString("string", "foo")
+                .putStringSet("stringset", new ArraySet<>(Arrays.asList("bar", "baz")))
+                .apply();
+
+        assertTrue(preferences.contains("boolean"));
+        assertTrue(preferences.contains("float"));
+        assertTrue(preferences.contains("int"));
+        assertTrue(preferences.contains("long"));
+        assertTrue(preferences.contains("string"));
+        assertTrue(preferences.contains("stringset"));
+
+        assertEquals(true, preferences.getBoolean("boolean", false));
+        assertEquals(0.5f, preferences.getFloat("float", 0));
+        assertEquals(123, preferences.getInt("int", 0));
+        assertEquals(456, preferences.getLong("long", 0));
+        assertEquals("foo", preferences.getString("string", null));
+        assertEquals(new ArraySet<>(Arrays.asList("bar", "baz")),
+                preferences.getStringSet("stringset", null));
+    }
+
+    public void testReadDefault() {
+        VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(),
+                createFakeHandle("testReadDefault"));
+
+        assertFalse(preferences.contains("boolean"));
+        assertFalse(preferences.contains("float"));
+        assertFalse(preferences.contains("int"));
+        assertFalse(preferences.contains("long"));
+        assertFalse(preferences.contains("string"));
+        assertFalse(preferences.contains("stringset"));
+
+        assertEquals(true, preferences.getBoolean("boolean", true));
+        assertEquals(2.5f, preferences.getFloat("float", 2.5f));
+        assertEquals(321, preferences.getInt("int", 321));
+        assertEquals(654, preferences.getLong("long", 654));
+        assertEquals("foo2", preferences.getString("string", "foo2"));
+        assertEquals(new ArraySet<>(Arrays.asList("bar2", "baz2")),
+                preferences.getStringSet(
+                        "stringset", new ArraySet<>(Arrays.asList("bar2", "baz2"))));
+    }
+
+    public void testReadDefaultNull() {
+        VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(),
+                createFakeHandle("testReadDefaultNull"));
+        assertNull(preferences.getString("string", null));
+        assertNull(preferences.getStringSet("stringset", null));
+    }
+
+    public void testDifferentHandle() {
+        VisualVoicemailPreferences preferences1 = new VisualVoicemailPreferences(getContext(),
+                createFakeHandle("testDifferentHandle1"));
+        VisualVoicemailPreferences preferences2 = new VisualVoicemailPreferences(getContext(),
+                createFakeHandle("testDifferentHandle1"));
+
+        preferences1.edit().putString("string", "foo");
+        assertFalse(preferences2.contains("string"));
+    }
+
+    private PhoneAccountHandle createFakeHandle(String id) {
+        return new PhoneAccountHandle(new ComponentName(getContext(), this.getClass()), id);
+    }
+}