Implement UI for changing voicemail PIN

The temporary dialog for chagin PIN is replaced.

The setting still reside in Dialer->settings->call->voicemail
https://screenshot.googleplex.com/gKzDdP9DnHH

To change the PIN, the user must enter their old PIN first.
https://screenshot.googleplex.com/jMBcrkiJquJ
Which will be checked with the server.
https://screenshot.googleplex.com/38iz4wOySF9
https://screenshot.googleplex.com/mxjnXgSWNAH

After the old PIN is confirmed, the user then proceed to enter the new
PIN.
The length requirement from the server will be enforced.
https://screenshot.googleplex.com/d7cigtR08di
https://screenshot.googleplex.com/0MVVzViuArP
https://screenshot.googleplex.com/mwnRda213HO

The user then must confirm their new PIN
https://screenshot.googleplex.com/4R9T5m3sPp4
https://screenshot.googleplex.com/GHmqSDxPr1z

The change will be commited to the server
https://screenshot.googleplex.com/38iz4wOySF9

If it succeeded, the user will return to the setting screen, and a
toast will be shown.
https://screenshot.googleplex.com/a7qPxQOvPJm
else an error message will be shown, and the user will return to the
enter new PIN step.

If the default PIN was set by the OMTP client before, the user will be
asked to "Set PIN" instead
https://screenshot.googleplex.com/RPYRxqOFSkw

The default PIN will be validated first
https://screenshot.googleplex.com/bYZcROA560B
If the server rejects the default PIN, The flow will continue as the
regular change PIN process. Else the enter old pin step is by passed
and the user will go to the enter new PIN step directly.
If other error happens in this step, a message will be shown
https://screenshot.googleplex.com/YRKLo5VmGzL
and the user will then return to the settings screen

+ All phone account dependent storaged is moved to
  VisualVoicemailPreferences.
- Retry in OmtpSyncService is removed. It was never ran, and a new retry
  mechanism will be added later.

Fixes: 29082418
Fixes: 29102412
Fixes: 29903609

Change-Id: I28dcc08113120abedd907fa8faffd3eb00bd87b4
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 b570744..096c17d 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 b238c8d..95a8a32 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) {
@@ -87,13 +88,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);
@@ -187,16 +191,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");
@@ -222,10 +224,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 c930a98..6eda4af 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 {
@@ -136,7 +138,7 @@
             default:
                 VvmLog.e(TAG,
                         "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
-               break;
+                break;
         }
 
         if (serviceIntent != null) {
@@ -153,10 +155,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);
+    }
+}