1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.phone.settings; 18 19import android.annotation.Nullable; 20import android.app.Activity; 21import android.app.AlertDialog; 22import android.app.ProgressDialog; 23import android.content.Context; 24import android.content.DialogInterface; 25import android.content.DialogInterface.OnDismissListener; 26import android.content.SharedPreferences; 27import android.net.Network; 28import android.os.Bundle; 29import android.os.Handler; 30import android.os.Message; 31import android.preference.PreferenceManager; 32import android.telecom.PhoneAccountHandle; 33import android.text.Editable; 34import android.text.InputFilter; 35import android.text.InputFilter.LengthFilter; 36import android.text.TextWatcher; 37import android.view.KeyEvent; 38import android.view.MenuItem; 39import android.view.View; 40import android.view.View.OnClickListener; 41import android.view.WindowManager; 42import android.view.inputmethod.EditorInfo; 43import android.widget.Button; 44import android.widget.EditText; 45import android.widget.TextView; 46import android.widget.TextView.OnEditorActionListener; 47import android.widget.Toast; 48import com.android.phone.PhoneUtils; 49import com.android.phone.R; 50import com.android.phone.VoicemailStatus; 51import com.android.phone.common.mail.MessagingException; 52import com.android.phone.vvm.omtp.OmtpConstants; 53import com.android.phone.vvm.omtp.OmtpConstants.ChangePinResult; 54import com.android.phone.vvm.omtp.OmtpEvents; 55import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper; 56import com.android.phone.vvm.omtp.VisualVoicemailPreferences; 57import com.android.phone.vvm.omtp.VvmLog; 58import com.android.phone.vvm.omtp.imap.ImapHelper; 59import com.android.phone.vvm.omtp.imap.ImapHelper.InitializingException; 60import com.android.phone.vvm.omtp.sync.VvmNetworkRequestCallback; 61 62/** 63 * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing 64 * traditional voicemail through phone call. The intent to launch this activity must contain {@link 65 * #EXTRA_PHONE_ACCOUNT_HANDLE} 66 */ 67public class VoicemailChangePinActivity extends Activity implements OnClickListener, 68 OnEditorActionListener, TextWatcher { 69 70 private static final String TAG = "VmChangePinActivity"; 71 72 public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle"; 73 74 private static final String KEY_DEFAULT_OLD_PIN = "default_old_pin"; 75 76 private static final int MESSAGE_HANDLE_RESULT = 1; 77 78 private PhoneAccountHandle mPhoneAccountHandle; 79 private OmtpVvmCarrierConfigHelper mConfig; 80 81 private int mPinMinLength; 82 private int mPinMaxLength; 83 84 private State mUiState = State.Initial; 85 private String mOldPin; 86 private String mFirstPin; 87 88 private ProgressDialog mProgressDialog; 89 90 private TextView mHeaderText; 91 private TextView mHintText; 92 private TextView mErrorText; 93 private EditText mPinEntry; 94 private Button mCancelButton; 95 private Button mNextButton; 96 97 private Handler mHandler = new Handler() { 98 @Override 99 public void handleMessage(Message message) { 100 if (message.what == MESSAGE_HANDLE_RESULT) { 101 mUiState.handleResult(VoicemailChangePinActivity.this, message.arg1); 102 } 103 } 104 }; 105 106 private enum State { 107 /** 108 * Empty state to handle initial state transition. Will immediately switch into {@link 109 * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin} 110 * if not. 111 */ 112 Initial, 113 /** 114 * Prompt the user to enter old PIN. The PIN will be verified with the server before 115 * proceeding to {@link #EnterNewPin}. 116 */ 117 EnterOldPin { 118 @Override 119 public void onEnter(VoicemailChangePinActivity activity) { 120 activity.setHeader(R.string.change_pin_enter_old_pin_header); 121 activity.mHintText.setText(R.string.change_pin_enter_old_pin_hint); 122 activity.mNextButton.setText(R.string.change_pin_continue_label); 123 activity.mErrorText.setText(null); 124 } 125 126 @Override 127 public void onInputChanged(VoicemailChangePinActivity activity) { 128 activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0); 129 } 130 131 132 @Override 133 public void handleNext(VoicemailChangePinActivity activity) { 134 activity.mOldPin = activity.getCurrentPasswordInput(); 135 activity.verifyOldPin(); 136 } 137 138 @Override 139 public void handleResult(VoicemailChangePinActivity activity, 140 @ChangePinResult int result) { 141 if (result == OmtpConstants.CHANGE_PIN_SUCCESS) { 142 activity.updateState(State.EnterNewPin); 143 } else { 144 CharSequence message = activity.getChangePinResultMessage(result); 145 activity.showError(message); 146 activity.mPinEntry.setText(""); 147 } 148 } 149 }, 150 /** 151 * The default old PIN is found. Show a blank screen while verifying with the server to make 152 * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}. 153 * If not, the user probably changed the PIN through other means, proceed to {@link 154 * #EnterOldPin}. If any other issue caused the verifying to fail, show an error and exit. 155 */ 156 VerifyOldPin { 157 @Override 158 public void onEnter(VoicemailChangePinActivity activity) { 159 activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE); 160 activity.verifyOldPin(); 161 } 162 163 @Override 164 public void handleResult(VoicemailChangePinActivity activity, 165 @ChangePinResult int result) { 166 if (result == OmtpConstants.CHANGE_PIN_SUCCESS) { 167 activity.updateState(State.EnterNewPin); 168 } else if (result == OmtpConstants.CHANGE_PIN_SYSTEM_ERROR) { 169 activity.getWindow().setSoftInputMode( 170 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); 171 activity.showError(activity.getString(R.string.change_pin_system_error), 172 new OnDismissListener() { 173 @Override 174 public void onDismiss(DialogInterface dialog) { 175 activity.finish(); 176 } 177 }); 178 } else { 179 VvmLog.e(TAG, "invalid default old PIN: " + activity 180 .getChangePinResultMessage(result)); 181 // If the default old PIN is rejected by the server, the PIN is probably changed 182 // through other means, or the generated pin is invalid 183 // Wipe the default old PIN so the old PIN input box will be shown to the user 184 // on the next time. 185 setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null); 186 activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET); 187 activity.updateState(State.EnterOldPin); 188 } 189 } 190 191 @Override 192 public void onLeave(VoicemailChangePinActivity activity) { 193 activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE); 194 } 195 }, 196 /** 197 * Let the user enter the new PIN and validate the format. Only length is enforced, PIN 198 * strength check relies on the server. After a valid PIN is entered, proceed to {@link 199 * #ConfirmNewPin} 200 */ 201 EnterNewPin { 202 @Override 203 public void onEnter(VoicemailChangePinActivity activity) { 204 activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header); 205 activity.mNextButton.setText(R.string.change_pin_continue_label); 206 activity.mHintText.setText( 207 activity.getString(R.string.change_pin_enter_new_pin_hint, 208 activity.mPinMinLength, activity.mPinMaxLength)); 209 } 210 211 @Override 212 public void onInputChanged(VoicemailChangePinActivity activity) { 213 String password = activity.getCurrentPasswordInput(); 214 if (password.length() == 0) { 215 activity.setNextEnabled(false); 216 return; 217 } 218 CharSequence error = activity.validatePassword(password); 219 if (error != null) { 220 activity.mErrorText.setText(error); 221 activity.setNextEnabled(false); 222 } else { 223 activity.mErrorText.setText(null); 224 activity.setNextEnabled(true); 225 } 226 } 227 228 @Override 229 public void handleNext(VoicemailChangePinActivity activity) { 230 CharSequence errorMsg; 231 errorMsg = activity.validatePassword(activity.getCurrentPasswordInput()); 232 if (errorMsg != null) { 233 activity.showError(errorMsg); 234 return; 235 } 236 activity.mFirstPin = activity.getCurrentPasswordInput(); 237 activity.updateState(State.ConfirmNewPin); 238 } 239 }, 240 /** 241 * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a 242 * PIN change to the server. Finish the activity if succeeded. Return to {@link 243 * #EnterOldPin} if the old PIN is rejected, {@link #EnterNewPin} for other failure. 244 */ 245 ConfirmNewPin { 246 @Override 247 public void onEnter(VoicemailChangePinActivity activity) { 248 activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header); 249 activity.mHintText.setText(null); 250 activity.mNextButton.setText(R.string.change_pin_ok_label); 251 } 252 253 @Override 254 public void onInputChanged(VoicemailChangePinActivity activity) { 255 if (activity.getCurrentPasswordInput().length() == 0) { 256 activity.setNextEnabled(false); 257 return; 258 } 259 if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) { 260 activity.setNextEnabled(true); 261 activity.mErrorText.setText(null); 262 } else { 263 activity.setNextEnabled(false); 264 activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match); 265 } 266 } 267 268 @Override 269 public void handleResult(VoicemailChangePinActivity activity, 270 @ChangePinResult int result) { 271 if (result == OmtpConstants.CHANGE_PIN_SUCCESS) { 272 // If the PIN change succeeded we no longer know what the old (current) PIN is. 273 // Wipe the default old PIN so the old PIN input box will be shown to the user 274 // on the next time. 275 setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null); 276 activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET); 277 278 activity.finish(); 279 280 Toast.makeText(activity, activity.getString(R.string.change_pin_succeeded), 281 Toast.LENGTH_SHORT).show(); 282 } else { 283 CharSequence message = activity.getChangePinResultMessage(result); 284 VvmLog.i(TAG, "Change PIN failed: " + message); 285 activity.showError(message); 286 if (result == OmtpConstants.CHANGE_PIN_MISMATCH) { 287 // Somehow the PIN has changed, prompt to enter the old PIN again. 288 activity.updateState(State.EnterOldPin); 289 } else { 290 // The new PIN failed to fulfil other restrictions imposed by the server. 291 activity.updateState(State.EnterNewPin); 292 } 293 294 } 295 296 } 297 298 @Override 299 public void handleNext(VoicemailChangePinActivity activity) { 300 activity.processPinChange(activity.mOldPin, activity.mFirstPin); 301 } 302 }; 303 304 /** 305 * The activity has switched from another state to this one. 306 */ 307 public void onEnter(VoicemailChangePinActivity activity) { 308 // Do nothing 309 } 310 311 /** 312 * The user has typed something into the PIN input field. Also called after {@link 313 * #onEnter(VoicemailChangePinActivity)} 314 */ 315 public void onInputChanged(VoicemailChangePinActivity activity) { 316 // Do nothing 317 } 318 319 /** 320 * The asynchronous call to change the PIN on the server has returned. 321 */ 322 public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) { 323 // Do nothing 324 } 325 326 /** 327 * The user has pressed the "next" button. 328 */ 329 public void handleNext(VoicemailChangePinActivity activity) { 330 // Do nothing 331 } 332 333 /** 334 * The activity has switched from this state to another one. 335 */ 336 public void onLeave(VoicemailChangePinActivity activity) { 337 // Do nothing 338 } 339 340 } 341 342 @Override 343 public void onCreate(Bundle savedInstanceState) { 344 super.onCreate(savedInstanceState); 345 346 mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE); 347 mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle); 348 setContentView(R.layout.voicemail_change_pin); 349 setTitle(R.string.change_pin_title); 350 351 readPinLength(); 352 353 View view = findViewById(android.R.id.content); 354 355 mCancelButton = (Button) view.findViewById(R.id.cancel_button); 356 mCancelButton.setOnClickListener(this); 357 mNextButton = (Button) view.findViewById(R.id.next_button); 358 mNextButton.setOnClickListener(this); 359 360 mPinEntry = (EditText) view.findViewById(R.id.pin_entry); 361 mPinEntry.setOnEditorActionListener(this); 362 mPinEntry.addTextChangedListener(this); 363 if (mPinMaxLength != 0) { 364 mPinEntry.setFilters(new InputFilter[]{new LengthFilter(mPinMaxLength)}); 365 } 366 367 368 mHeaderText = (TextView) view.findViewById(R.id.headerText); 369 mHintText = (TextView) view.findViewById(R.id.hintText); 370 mErrorText = (TextView) view.findViewById(R.id.errorText); 371 372 migrateDefaultOldPin(); 373 374 if (isDefaultOldPinSet(this, mPhoneAccountHandle)) { 375 mOldPin = getDefaultOldPin(this, mPhoneAccountHandle); 376 updateState(State.VerifyOldPin); 377 } else { 378 updateState(State.EnterOldPin); 379 } 380 } 381 382 private void handleOmtpEvent(OmtpEvents event) { 383 mConfig.handleEvent(getVoicemailStatusEditor(), event); 384 } 385 386 private VoicemailStatus.Editor getVoicemailStatusEditor() { 387 // This activity does not have any automatic retry mechanism, errors should be written right 388 // away. 389 return VoicemailStatus.edit(this, mPhoneAccountHandle); 390 } 391 392 /** 393 * Extracts the pin length requirement sent by the server with a STATUS SMS. 394 */ 395 private void readPinLength() { 396 VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this, 397 mPhoneAccountHandle); 398 // The OMTP pin length format is {min}-{max} 399 String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-"); 400 if (lengths.length == 2) { 401 try { 402 mPinMinLength = Integer.parseInt(lengths[0]); 403 mPinMaxLength = Integer.parseInt(lengths[1]); 404 } catch (NumberFormatException e) { 405 mPinMinLength = 0; 406 mPinMaxLength = 0; 407 } 408 } else { 409 mPinMinLength = 0; 410 mPinMaxLength = 0; 411 } 412 } 413 414 @Override 415 public void onResume() { 416 super.onResume(); 417 updateState(mUiState); 418 419 } 420 421 public void handleNext() { 422 if (mPinEntry.length() == 0) { 423 return; 424 } 425 mUiState.handleNext(this); 426 } 427 428 public void onClick(View v) { 429 switch (v.getId()) { 430 case R.id.next_button: 431 handleNext(); 432 break; 433 434 case R.id.cancel_button: 435 finish(); 436 break; 437 } 438 } 439 440 @Override 441 public boolean onOptionsItemSelected(MenuItem item) { 442 if (item.getItemId() == android.R.id.home) { 443 onBackPressed(); 444 return true; 445 } 446 return super.onOptionsItemSelected(item); 447 } 448 449 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 450 if (!mNextButton.isEnabled()) { 451 return true; 452 } 453 // Check if this was the result of hitting the enter or "done" key 454 if (actionId == EditorInfo.IME_NULL 455 || actionId == EditorInfo.IME_ACTION_DONE 456 || actionId == EditorInfo.IME_ACTION_NEXT) { 457 handleNext(); 458 return true; 459 } 460 return false; 461 } 462 463 public void afterTextChanged(Editable s) { 464 mUiState.onInputChanged(this); 465 } 466 467 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 468 // Do nothing 469 } 470 471 public void onTextChanged(CharSequence s, int start, int before, int count) { 472 // Do nothing 473 } 474 475 /** 476 * After replacing the default PIN with a random PIN, call this to store the random PIN. The 477 * stored PIN will be automatically entered when the user attempts to change the PIN. 478 */ 479 public static void setDefaultOldPIN(Context context, PhoneAccountHandle phoneAccountHandle, 480 String pin) { 481 new VisualVoicemailPreferences(context, phoneAccountHandle) 482 .edit().putString(KEY_DEFAULT_OLD_PIN, pin).apply(); 483 } 484 485 public static boolean isDefaultOldPinSet(Context context, 486 PhoneAccountHandle phoneAccountHandle) { 487 return getDefaultOldPin(context, phoneAccountHandle) != null; 488 } 489 490 private static String getDefaultOldPin(Context context, PhoneAccountHandle phoneAccountHandle) { 491 return new VisualVoicemailPreferences(context, phoneAccountHandle) 492 .getString(KEY_DEFAULT_OLD_PIN); 493 } 494 495 /** 496 * Storage location has changed mid development. Migrate from the old location to avoid losing 497 * tester's default old pin. 498 */ 499 private void migrateDefaultOldPin() { 500 String key = "voicemail_pin_dialog_preference_" 501 + PhoneUtils.getSubIdForPhoneAccountHandle(mPhoneAccountHandle) 502 + "_default_old_pin"; 503 504 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); 505 if (preferences.contains(key)) { 506 setDefaultOldPIN(this, mPhoneAccountHandle, preferences.getString(key, null)); 507 preferences.edit().putString(key, null).apply(); 508 } 509 } 510 511 private String getCurrentPasswordInput() { 512 return mPinEntry.getText().toString(); 513 } 514 515 private void updateState(State state) { 516 State previousState = mUiState; 517 mUiState = state; 518 if (previousState != state) { 519 previousState.onLeave(this); 520 mPinEntry.setText(""); 521 mUiState.onEnter(this); 522 } 523 mUiState.onInputChanged(this); 524 } 525 526 /** 527 * Validates PIN and returns a message to display if PIN fails test. 528 * 529 * @param password the raw password the user typed in 530 * @return error message to show to user or null if password is OK 531 */ 532 private CharSequence validatePassword(String password) { 533 if (mPinMinLength == 0 && mPinMaxLength == 0) { 534 // Invalid length requirement is sent by the server, just accept anything and let the 535 // server decide. 536 return null; 537 } 538 539 if (password.length() < mPinMinLength) { 540 return getString(R.string.vm_change_pin_error_too_short); 541 } 542 return null; 543 } 544 545 private void setHeader(int text) { 546 mHeaderText.setText(text); 547 mPinEntry.setContentDescription(mHeaderText.getText()); 548 } 549 550 /** 551 * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not 552 * {@link OmtpConstants#CHANGE_PIN_SUCCESS} 553 */ 554 private CharSequence getChangePinResultMessage(@ChangePinResult int result) { 555 switch (result) { 556 case OmtpConstants.CHANGE_PIN_TOO_SHORT: 557 return getString(R.string.vm_change_pin_error_too_short); 558 case OmtpConstants.CHANGE_PIN_TOO_LONG: 559 return getString(R.string.vm_change_pin_error_too_long); 560 case OmtpConstants.CHANGE_PIN_TOO_WEAK: 561 return getString(R.string.vm_change_pin_error_too_weak); 562 case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER: 563 return getString(R.string.vm_change_pin_error_invalid); 564 case OmtpConstants.CHANGE_PIN_MISMATCH: 565 return getString(R.string.vm_change_pin_error_mismatch); 566 case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR: 567 return getString(R.string.vm_change_pin_error_system_error); 568 default: 569 VvmLog.wtf(TAG, "Unexpected ChangePinResult " + result); 570 return null; 571 } 572 } 573 574 private void verifyOldPin() { 575 processPinChange(mOldPin, mOldPin); 576 } 577 578 private void setNextEnabled(boolean enabled) { 579 mNextButton.setEnabled(enabled); 580 } 581 582 583 private void showError(CharSequence message) { 584 showError(message, null); 585 } 586 587 private void showError(CharSequence message, @Nullable OnDismissListener callback) { 588 new AlertDialog.Builder(this) 589 .setMessage(message) 590 .setPositiveButton(android.R.string.ok, null) 591 .setOnDismissListener(callback) 592 .show(); 593 } 594 595 /** 596 * Asynchronous call to change the PIN on the server. 597 */ 598 private void processPinChange(String oldPin, String newPin) { 599 mProgressDialog = new ProgressDialog(this); 600 mProgressDialog.setCancelable(false); 601 mProgressDialog.setMessage(getString(R.string.vm_change_pin_progress_message)); 602 mProgressDialog.show(); 603 604 ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback(oldPin, 605 newPin); 606 callback.requestNetwork(); 607 } 608 609 private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback { 610 611 private final String mOldPin; 612 private final String mNewPin; 613 614 public ChangePinNetworkRequestCallback(String oldPin, String newPin) { 615 super(mConfig, mPhoneAccountHandle, 616 VoicemailChangePinActivity.this.getVoicemailStatusEditor()); 617 mOldPin = oldPin; 618 mNewPin = newPin; 619 } 620 621 @Override 622 public void onAvailable(Network network) { 623 super.onAvailable(network); 624 try (ImapHelper helper = 625 new ImapHelper(VoicemailChangePinActivity.this, mPhoneAccountHandle, network, 626 getVoicemailStatusEditor())) { 627 628 @ChangePinResult int result = 629 helper.changePin(mOldPin, mNewPin); 630 sendResult(result); 631 } catch (InitializingException | MessagingException e) { 632 VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e); 633 sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR); 634 } 635 } 636 637 @Override 638 public void onFailed(String reason) { 639 super.onFailed(reason); 640 sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR); 641 } 642 643 private void sendResult(@ChangePinResult int result) { 644 VvmLog.i(TAG, "Change PIN result: " + result); 645 if (mProgressDialog.isShowing() && !VoicemailChangePinActivity.this.isDestroyed() && 646 !VoicemailChangePinActivity.this.isFinishing()) { 647 mProgressDialog.dismiss(); 648 } else { 649 VvmLog.i(TAG, "Dialog not visible, not dismissing"); 650 } 651 mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget(); 652 releaseNetwork(); 653 } 654 } 655 656} 657