| /* |
| * 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.MenuItem; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.WindowManager; |
| import android.view.inputmethod.EditorInfo; |
| import android.widget.Button; |
| import android.widget.EditText; |
| import android.widget.TextView; |
| import android.widget.TextView.OnEditorActionListener; |
| import android.widget.Toast; |
| import com.android.phone.PhoneUtils; |
| import com.android.phone.R; |
| import com.android.phone.VoicemailStatus; |
| 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.imap.ImapHelper.InitializingException; |
| 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.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET); |
| activity.updateState(State.EnterOldPin); |
| } |
| } |
| |
| @Override |
| public void onLeave(VoicemailChangePinActivity activity) { |
| activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE); |
| } |
| }, |
| /** |
| * Let the user enter the new PIN and validate the format. Only length is enforced, PIN |
| * strength check relies on the server. After a valid PIN is entered, proceed to {@link |
| * #ConfirmNewPin} |
| */ |
| EnterNewPin { |
| @Override |
| public void onEnter(VoicemailChangePinActivity activity) { |
| activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header); |
| activity.mNextButton.setText(R.string.change_pin_continue_label); |
| activity.mHintText.setText( |
| activity.getString(R.string.change_pin_enter_new_pin_hint, |
| activity.mPinMinLength, activity.mPinMaxLength)); |
| } |
| |
| @Override |
| public void onInputChanged(VoicemailChangePinActivity activity) { |
| String password = activity.getCurrentPasswordInput(); |
| if (password.length() == 0) { |
| activity.setNextEnabled(false); |
| return; |
| } |
| CharSequence error = activity.validatePassword(password); |
| if (error != null) { |
| activity.mErrorText.setText(error); |
| activity.setNextEnabled(false); |
| } else { |
| activity.mErrorText.setText(null); |
| activity.setNextEnabled(true); |
| } |
| } |
| |
| @Override |
| public void handleNext(VoicemailChangePinActivity activity) { |
| CharSequence errorMsg; |
| errorMsg = activity.validatePassword(activity.getCurrentPasswordInput()); |
| if (errorMsg != null) { |
| activity.showError(errorMsg); |
| return; |
| } |
| activity.mFirstPin = activity.getCurrentPasswordInput(); |
| activity.updateState(State.ConfirmNewPin); |
| } |
| }, |
| /** |
| * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a |
| * PIN change to the server. Finish the activity if succeeded. Return to {@link |
| * #EnterOldPin} if the old PIN is rejected, {@link #EnterNewPin} for other failure. |
| */ |
| ConfirmNewPin { |
| @Override |
| public void onEnter(VoicemailChangePinActivity activity) { |
| activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header); |
| activity.mHintText.setText(null); |
| activity.mNextButton.setText(R.string.change_pin_ok_label); |
| } |
| |
| @Override |
| public void onInputChanged(VoicemailChangePinActivity activity) { |
| if (activity.getCurrentPasswordInput().length() == 0) { |
| activity.setNextEnabled(false); |
| return; |
| } |
| if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) { |
| activity.setNextEnabled(true); |
| activity.mErrorText.setText(null); |
| } else { |
| activity.setNextEnabled(false); |
| activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match); |
| } |
| } |
| |
| @Override |
| public void handleResult(VoicemailChangePinActivity activity, |
| @ChangePinResult int result) { |
| if (result == OmtpConstants.CHANGE_PIN_SUCCESS) { |
| // If the PIN change succeeded we no longer know what the old (current) PIN is. |
| // Wipe the default old PIN so the old PIN input box will be shown to the user |
| // on the next time. |
| setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null); |
| activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET); |
| |
| activity.finish(); |
| |
| Toast.makeText(activity, activity.getString(R.string.change_pin_succeeded), |
| Toast.LENGTH_SHORT).show(); |
| } else { |
| CharSequence message = activity.getChangePinResultMessage(result); |
| VvmLog.i(TAG, "Change PIN failed: " + message); |
| activity.showError(message); |
| if (result == OmtpConstants.CHANGE_PIN_MISMATCH) { |
| // Somehow the PIN has changed, prompt to enter the old PIN again. |
| activity.updateState(State.EnterOldPin); |
| } else { |
| // The new PIN failed to fulfil other restrictions imposed by the server. |
| activity.updateState(State.EnterNewPin); |
| } |
| |
| } |
| |
| } |
| |
| @Override |
| public void handleNext(VoicemailChangePinActivity activity) { |
| activity.processPinChange(activity.mOldPin, activity.mFirstPin); |
| } |
| }; |
| |
| /** |
| * The activity has switched from another state to this one. |
| */ |
| public void onEnter(VoicemailChangePinActivity activity) { |
| // Do nothing |
| } |
| |
| /** |
| * The user has typed something into the PIN input field. Also called after {@link |
| * #onEnter(VoicemailChangePinActivity)} |
| */ |
| public void onInputChanged(VoicemailChangePinActivity activity) { |
| // Do nothing |
| } |
| |
| /** |
| * The asynchronous call to change the PIN on the server has returned. |
| */ |
| public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) { |
| // Do nothing |
| } |
| |
| /** |
| * The user has pressed the "next" button. |
| */ |
| public void handleNext(VoicemailChangePinActivity activity) { |
| // Do nothing |
| } |
| |
| /** |
| * The activity has switched from this state to another one. |
| */ |
| public void onLeave(VoicemailChangePinActivity activity) { |
| // Do nothing |
| } |
| |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE); |
| mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle); |
| setContentView(R.layout.voicemail_change_pin); |
| setTitle(R.string.change_pin_title); |
| |
| readPinLength(); |
| |
| View view = findViewById(android.R.id.content); |
| |
| mCancelButton = (Button) view.findViewById(R.id.cancel_button); |
| mCancelButton.setOnClickListener(this); |
| mNextButton = (Button) view.findViewById(R.id.next_button); |
| mNextButton.setOnClickListener(this); |
| |
| mPinEntry = (EditText) view.findViewById(R.id.pin_entry); |
| mPinEntry.setOnEditorActionListener(this); |
| mPinEntry.addTextChangedListener(this); |
| if (mPinMaxLength != 0) { |
| mPinEntry.setFilters(new InputFilter[]{new LengthFilter(mPinMaxLength)}); |
| } |
| |
| |
| mHeaderText = (TextView) view.findViewById(R.id.headerText); |
| mHintText = (TextView) view.findViewById(R.id.hintText); |
| mErrorText = (TextView) view.findViewById(R.id.errorText); |
| |
| migrateDefaultOldPin(); |
| |
| if (isDefaultOldPinSet(this, mPhoneAccountHandle)) { |
| mOldPin = getDefaultOldPin(this, mPhoneAccountHandle); |
| updateState(State.VerifyOldPin); |
| } else { |
| updateState(State.EnterOldPin); |
| } |
| } |
| |
| private void handleOmtpEvent(OmtpEvents event) { |
| mConfig.handleEvent(getVoicemailStatusEditor(), event); |
| } |
| |
| private VoicemailStatus.Editor getVoicemailStatusEditor() { |
| // This activity does not have any automatic retry mechanism, errors should be written right |
| // away. |
| return VoicemailStatus.edit(this, mPhoneAccountHandle); |
| } |
| |
| /** |
| * Extracts the pin length requirement sent by the server with a STATUS SMS. |
| */ |
| private void readPinLength() { |
| VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this, |
| mPhoneAccountHandle); |
| // The OMTP pin length format is {min}-{max} |
| String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-"); |
| if (lengths.length == 2) { |
| try { |
| mPinMinLength = Integer.parseInt(lengths[0]); |
| mPinMaxLength = Integer.parseInt(lengths[1]); |
| } catch (NumberFormatException e) { |
| mPinMinLength = 0; |
| mPinMaxLength = 0; |
| } |
| } else { |
| mPinMinLength = 0; |
| mPinMaxLength = 0; |
| } |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| updateState(mUiState); |
| |
| } |
| |
| public void handleNext() { |
| if (mPinEntry.length() == 0) { |
| return; |
| } |
| mUiState.handleNext(this); |
| } |
| |
| public void onClick(View v) { |
| switch (v.getId()) { |
| case R.id.next_button: |
| handleNext(); |
| break; |
| |
| case R.id.cancel_button: |
| finish(); |
| break; |
| } |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| if (item.getItemId() == android.R.id.home) { |
| onBackPressed(); |
| return true; |
| } |
| return super.onOptionsItemSelected(item); |
| } |
| |
| public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { |
| if (!mNextButton.isEnabled()) { |
| return true; |
| } |
| // Check if this was the result of hitting the enter or "done" key |
| if (actionId == EditorInfo.IME_NULL |
| || actionId == EditorInfo.IME_ACTION_DONE |
| || actionId == EditorInfo.IME_ACTION_NEXT) { |
| handleNext(); |
| return true; |
| } |
| return false; |
| } |
| |
| 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, |
| VoicemailChangePinActivity.this.getVoicemailStatusEditor()); |
| mOldPin = oldPin; |
| mNewPin = newPin; |
| } |
| |
| @Override |
| public void onAvailable(Network network) { |
| super.onAvailable(network); |
| try (ImapHelper helper = |
| new ImapHelper(VoicemailChangePinActivity.this, mPhoneAccountHandle, network, |
| getVoicemailStatusEditor())) { |
| |
| @ChangePinResult int result = |
| helper.changePin(mOldPin, mNewPin); |
| sendResult(result); |
| } catch (InitializingException | MessagingException e) { |
| VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e); |
| sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR); |
| } |
| } |
| |
| @Override |
| public void onFailed(String reason) { |
| super.onFailed(reason); |
| sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR); |
| } |
| |
| private void sendResult(@ChangePinResult int result) { |
| VvmLog.i(TAG, "Change PIN result: " + result); |
| if (mProgressDialog.isShowing() && !VoicemailChangePinActivity.this.isDestroyed() && |
| !VoicemailChangePinActivity.this.isFinishing()) { |
| mProgressDialog.dismiss(); |
| } else { |
| VvmLog.i(TAG, "Dialog not visible, not dismissing"); |
| } |
| mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget(); |
| releaseNetwork(); |
| } |
| } |
| |
| } |