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 17 package com.android.dialer.voicemail.settings; 18 19 import android.annotation.TargetApi; 20 import android.app.Activity; 21 import android.app.AlertDialog; 22 import android.app.ProgressDialog; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.DialogInterface.OnDismissListener; 26 import android.os.Build.VERSION_CODES; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.support.annotation.Nullable; 31 import android.telecom.PhoneAccountHandle; 32 import android.text.Editable; 33 import android.text.InputFilter; 34 import android.text.InputFilter.LengthFilter; 35 import android.text.TextWatcher; 36 import android.view.KeyEvent; 37 import android.view.MenuItem; 38 import android.view.View; 39 import android.view.View.OnClickListener; 40 import android.view.WindowManager; 41 import android.view.inputmethod.EditorInfo; 42 import android.widget.Button; 43 import android.widget.EditText; 44 import android.widget.TextView; 45 import android.widget.TextView.OnEditorActionListener; 46 import android.widget.Toast; 47 import com.android.dialer.common.LogUtil; 48 import com.android.dialer.common.concurrent.DialerExecutor; 49 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 50 import com.android.dialer.common.concurrent.DialerExecutorComponent; 51 import com.android.dialer.logging.DialerImpression; 52 import com.android.dialer.logging.Logger; 53 import com.android.voicemail.PinChanger; 54 import com.android.voicemail.PinChanger.ChangePinResult; 55 import com.android.voicemail.PinChanger.PinSpecification; 56 import com.android.voicemail.VoicemailClient; 57 import com.android.voicemail.VoicemailComponent; 58 import java.lang.ref.WeakReference; 59 60 /** 61 * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing 62 * traditional voicemail through phone call. The intent to launch this activity must contain {@link 63 * VoicemailClient#PARAM_PHONE_ACCOUNT_HANDLE} 64 */ 65 @TargetApi(VERSION_CODES.O) 66 public class VoicemailChangePinActivity extends Activity 67 implements OnClickListener, OnEditorActionListener, TextWatcher { 68 69 private static final String TAG = "VmChangePinActivity"; 70 71 private static final int MESSAGE_HANDLE_RESULT = 1; 72 73 private PhoneAccountHandle phoneAccountHandle; 74 private PinChanger pinChanger; 75 76 private static class ChangePinParams { 77 PinChanger pinChanger; 78 PhoneAccountHandle phoneAccountHandle; 79 String oldPin; 80 String newPin; 81 } 82 83 private DialerExecutor<ChangePinParams> changePinExecutor; 84 85 private int pinMinLength; 86 private int pinMaxLength; 87 88 private State uiState = State.Initial; 89 private String oldPin; 90 private String firstPin; 91 92 private ProgressDialog progressDialog; 93 94 private TextView headerText; 95 private TextView hintText; 96 private TextView errorText; 97 private EditText pinEntry; 98 private Button cancelButton; 99 private Button nextButton; 100 101 private Handler handler = new ChangePinHandler(new WeakReference<>(this)); 102 103 private enum State { 104 /** 105 * Empty state to handle initial state transition. Will immediately switch into {@link 106 * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin} if 107 * not. 108 */ 109 Initial, 110 /** 111 * Prompt the user to enter old PIN. The PIN will be verified with the server before proceeding 112 * to {@link #EnterNewPin}. 113 */ 114 EnterOldPin { 115 @Override onEnter(VoicemailChangePinActivity activity)116 public void onEnter(VoicemailChangePinActivity activity) { 117 activity.setHeader(R.string.change_pin_enter_old_pin_header); 118 activity.hintText.setText(R.string.change_pin_enter_old_pin_hint); 119 activity.nextButton.setText(R.string.change_pin_continue_label); 120 activity.errorText.setText(null); 121 } 122 123 @Override onInputChanged(VoicemailChangePinActivity activity)124 public void onInputChanged(VoicemailChangePinActivity activity) { 125 activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0); 126 } 127 128 @Override handleNext(VoicemailChangePinActivity activity)129 public void handleNext(VoicemailChangePinActivity activity) { 130 activity.oldPin = activity.getCurrentPasswordInput(); 131 activity.verifyOldPin(); 132 } 133 134 @Override handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result)135 public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) { 136 if (result == PinChanger.CHANGE_PIN_SUCCESS) { 137 activity.updateState(State.EnterNewPin); 138 } else { 139 CharSequence message = activity.getChangePinResultMessage(result); 140 activity.showError(message); 141 activity.pinEntry.setText(""); 142 } 143 } 144 }, 145 /** 146 * The default old PIN is found. Show a blank screen while verifying with the server to make 147 * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}. If 148 * not, the user probably changed the PIN through other means, proceed to {@link #EnterOldPin}. 149 * If any other issue caused the verifying to fail, show an error and exit. 150 */ 151 VerifyOldPin { 152 @Override onEnter(VoicemailChangePinActivity activity)153 public void onEnter(VoicemailChangePinActivity activity) { 154 activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE); 155 activity.verifyOldPin(); 156 } 157 158 @Override handleResult( final VoicemailChangePinActivity activity, @ChangePinResult int result)159 public void handleResult( 160 final VoicemailChangePinActivity activity, @ChangePinResult int result) { 161 if (result == PinChanger.CHANGE_PIN_SUCCESS) { 162 activity.updateState(State.EnterNewPin); 163 } else if (result == PinChanger.CHANGE_PIN_SYSTEM_ERROR) { 164 activity 165 .getWindow() 166 .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); 167 activity.showError( 168 activity.getString(R.string.change_pin_system_error), 169 new OnDismissListener() { 170 @Override 171 public void onDismiss(DialogInterface dialog) { 172 activity.finish(); 173 } 174 }); 175 } else { 176 LogUtil.e(TAG, "invalid default old PIN: " + activity.getChangePinResultMessage(result)); 177 // If the default old PIN is rejected by the server, the PIN is probably changed 178 // through other means, or the generated pin is invalid 179 // Wipe the default old PIN so the old PIN input box will be shown to the user 180 // on the next time. 181 activity.pinChanger.setScrambledPin(null); 182 activity.updateState(State.EnterOldPin); 183 } 184 } 185 186 @Override onLeave(VoicemailChangePinActivity activity)187 public void onLeave(VoicemailChangePinActivity activity) { 188 activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE); 189 } 190 }, 191 /** 192 * Let the user enter the new PIN and validate the format. Only length is enforced, PIN strength 193 * check relies on the server. After a valid PIN is entered, proceed to {@link #ConfirmNewPin} 194 */ 195 EnterNewPin { 196 @Override onEnter(VoicemailChangePinActivity activity)197 public void onEnter(VoicemailChangePinActivity activity) { 198 activity.headerText.setText(R.string.change_pin_enter_new_pin_header); 199 activity.nextButton.setText(R.string.change_pin_continue_label); 200 activity.hintText.setText( 201 activity.getString( 202 R.string.change_pin_enter_new_pin_hint, 203 activity.pinMinLength, 204 activity.pinMaxLength)); 205 } 206 207 @Override onInputChanged(VoicemailChangePinActivity activity)208 public void onInputChanged(VoicemailChangePinActivity activity) { 209 String password = activity.getCurrentPasswordInput(); 210 if (password.length() == 0) { 211 activity.setNextEnabled(false); 212 return; 213 } 214 CharSequence error = activity.validatePassword(password); 215 if (error != null) { 216 activity.errorText.setText(error); 217 activity.setNextEnabled(false); 218 } else { 219 activity.errorText.setText(null); 220 activity.setNextEnabled(true); 221 } 222 } 223 224 @Override handleNext(VoicemailChangePinActivity activity)225 public void handleNext(VoicemailChangePinActivity activity) { 226 CharSequence errorMsg; 227 errorMsg = activity.validatePassword(activity.getCurrentPasswordInput()); 228 if (errorMsg != null) { 229 activity.showError(errorMsg); 230 return; 231 } 232 activity.firstPin = activity.getCurrentPasswordInput(); 233 activity.updateState(State.ConfirmNewPin); 234 } 235 }, 236 /** 237 * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a PIN 238 * change to the server. Finish the activity if succeeded. Return to {@link #EnterOldPin} if the 239 * old PIN is rejected, {@link #EnterNewPin} for other failure. 240 */ 241 ConfirmNewPin { 242 @Override onEnter(VoicemailChangePinActivity activity)243 public void onEnter(VoicemailChangePinActivity activity) { 244 activity.headerText.setText(R.string.change_pin_confirm_pin_header); 245 activity.hintText.setText(null); 246 activity.nextButton.setText(R.string.change_pin_ok_label); 247 } 248 249 @Override onInputChanged(VoicemailChangePinActivity activity)250 public void onInputChanged(VoicemailChangePinActivity activity) { 251 if (activity.getCurrentPasswordInput().length() == 0) { 252 activity.setNextEnabled(false); 253 return; 254 } 255 if (activity.getCurrentPasswordInput().equals(activity.firstPin)) { 256 activity.setNextEnabled(true); 257 activity.errorText.setText(null); 258 } else { 259 activity.setNextEnabled(false); 260 activity.errorText.setText(R.string.change_pin_confirm_pins_dont_match); 261 } 262 } 263 264 @Override handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result)265 public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) { 266 if (result == PinChanger.CHANGE_PIN_SUCCESS) { 267 // If the PIN change succeeded we no longer know what the old (current) PIN is. 268 // Wipe the default old PIN so the old PIN input box will be shown to the user 269 // on the next time. 270 activity.pinChanger.setScrambledPin(null); 271 272 activity.finish(); 273 Logger.get(activity).logImpression(DialerImpression.Type.VVM_CHANGE_PIN_COMPLETED); 274 Toast.makeText( 275 activity, activity.getString(R.string.change_pin_succeeded), Toast.LENGTH_SHORT) 276 .show(); 277 } else { 278 CharSequence message = activity.getChangePinResultMessage(result); 279 LogUtil.i(TAG, "Change PIN failed: " + message); 280 activity.showError(message); 281 if (result == PinChanger.CHANGE_PIN_MISMATCH) { 282 // Somehow the PIN has changed, prompt to enter the old PIN again. 283 activity.updateState(State.EnterOldPin); 284 } else { 285 // The new PIN failed to fulfil other restrictions imposed by the server. 286 activity.updateState(State.EnterNewPin); 287 } 288 } 289 } 290 291 @Override handleNext(VoicemailChangePinActivity activity)292 public void handleNext(VoicemailChangePinActivity activity) { 293 activity.processPinChange(activity.oldPin, activity.firstPin); 294 } 295 }; 296 297 /** The activity has switched from another state to this one. */ onEnter(VoicemailChangePinActivity activity)298 public void onEnter(VoicemailChangePinActivity activity) { 299 // Do nothing 300 } 301 302 /** 303 * The user has typed something into the PIN input field. Also called after {@link 304 * #onEnter(VoicemailChangePinActivity)} 305 */ onInputChanged(VoicemailChangePinActivity activity)306 public void onInputChanged(VoicemailChangePinActivity activity) { 307 // Do nothing 308 } 309 310 /** The asynchronous call to change the PIN on the server has returned. */ handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result)311 public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) { 312 // Do nothing 313 } 314 315 /** The user has pressed the "next" button. */ handleNext(VoicemailChangePinActivity activity)316 public void handleNext(VoicemailChangePinActivity activity) { 317 // Do nothing 318 } 319 320 /** The activity has switched from this state to another one. */ onLeave(VoicemailChangePinActivity activity)321 public void onLeave(VoicemailChangePinActivity activity) { 322 // Do nothing 323 } 324 } 325 326 @Override onCreate(Bundle savedInstanceState)327 public void onCreate(Bundle savedInstanceState) { 328 super.onCreate(savedInstanceState); 329 330 phoneAccountHandle = getIntent().getParcelableExtra(VoicemailClient.PARAM_PHONE_ACCOUNT_HANDLE); 331 pinChanger = 332 VoicemailComponent.get(this) 333 .getVoicemailClient() 334 .createPinChanger(getApplicationContext(), phoneAccountHandle); 335 setContentView(R.layout.voicemail_change_pin); 336 setTitle(R.string.change_pin_title); 337 338 readPinLength(); 339 340 View view = findViewById(android.R.id.content); 341 342 cancelButton = (Button) view.findViewById(R.id.cancel_button); 343 cancelButton.setOnClickListener(this); 344 nextButton = (Button) view.findViewById(R.id.next_button); 345 nextButton.setOnClickListener(this); 346 347 pinEntry = (EditText) view.findViewById(R.id.pin_entry); 348 pinEntry.setOnEditorActionListener(this); 349 pinEntry.addTextChangedListener(this); 350 if (pinMaxLength != 0) { 351 pinEntry.setFilters(new InputFilter[] {new LengthFilter(pinMaxLength)}); 352 } 353 354 headerText = (TextView) view.findViewById(R.id.headerText); 355 hintText = (TextView) view.findViewById(R.id.hintText); 356 errorText = (TextView) view.findViewById(R.id.errorText); 357 358 changePinExecutor = 359 DialerExecutorComponent.get(this) 360 .dialerExecutorFactory() 361 .createUiTaskBuilder(getFragmentManager(), "changePin", new ChangePinWorker()) 362 .onSuccess(this::sendResult) 363 .onFailure((tr) -> sendResult(PinChanger.CHANGE_PIN_SYSTEM_ERROR)) 364 .build(); 365 366 if (isPinScrambled(this, phoneAccountHandle)) { 367 oldPin = pinChanger.getScrambledPin(); 368 updateState(State.VerifyOldPin); 369 } else { 370 updateState(State.EnterOldPin); 371 } 372 } 373 374 /** Extracts the pin length requirement sent by the server with a STATUS SMS. */ readPinLength()375 private void readPinLength() { 376 PinSpecification pinSpecification = pinChanger.getPinSpecification(); 377 pinMinLength = pinSpecification.minLength; 378 pinMaxLength = pinSpecification.maxLength; 379 } 380 381 @Override onResume()382 public void onResume() { 383 super.onResume(); 384 updateState(uiState); 385 } 386 handleNext()387 public void handleNext() { 388 if (pinEntry.length() == 0) { 389 return; 390 } 391 uiState.handleNext(this); 392 } 393 394 @Override onClick(View v)395 public void onClick(View v) { 396 if (v.getId() == R.id.next_button) { 397 handleNext(); 398 } else if (v.getId() == R.id.cancel_button) { 399 finish(); 400 } 401 } 402 403 @Override onOptionsItemSelected(MenuItem item)404 public boolean onOptionsItemSelected(MenuItem item) { 405 if (item.getItemId() == android.R.id.home) { 406 onBackPressed(); 407 return true; 408 } 409 return super.onOptionsItemSelected(item); 410 } 411 412 @Override onEditorAction(TextView v, int actionId, KeyEvent event)413 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 414 if (!nextButton.isEnabled()) { 415 return true; 416 } 417 // Check if this was the result of hitting the enter or "done" key 418 if (actionId == EditorInfo.IME_NULL 419 || actionId == EditorInfo.IME_ACTION_DONE 420 || actionId == EditorInfo.IME_ACTION_NEXT) { 421 handleNext(); 422 return true; 423 } 424 return false; 425 } 426 427 @Override afterTextChanged(Editable s)428 public void afterTextChanged(Editable s) { 429 uiState.onInputChanged(this); 430 } 431 432 @Override beforeTextChanged(CharSequence s, int start, int count, int after)433 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 434 // Do nothing 435 } 436 437 @Override onTextChanged(CharSequence s, int start, int before, int count)438 public void onTextChanged(CharSequence s, int start, int before, int count) { 439 // Do nothing 440 } 441 442 /** 443 * After replacing the default PIN with a random PIN, call this to store the random PIN. The 444 * stored PIN will be automatically entered when the user attempts to change the PIN. 445 */ isPinScrambled(Context context, PhoneAccountHandle phoneAccountHandle)446 public static boolean isPinScrambled(Context context, PhoneAccountHandle phoneAccountHandle) { 447 return VoicemailComponent.get(context) 448 .getVoicemailClient() 449 .createPinChanger(context, phoneAccountHandle) 450 .getScrambledPin() 451 != null; 452 } 453 getCurrentPasswordInput()454 private String getCurrentPasswordInput() { 455 return pinEntry.getText().toString(); 456 } 457 updateState(State state)458 private void updateState(State state) { 459 State previousState = uiState; 460 uiState = state; 461 if (previousState != state) { 462 previousState.onLeave(this); 463 pinEntry.setText(""); 464 uiState.onEnter(this); 465 } 466 uiState.onInputChanged(this); 467 } 468 469 /** 470 * Validates PIN and returns a message to display if PIN fails test. 471 * 472 * @param password the raw password the user typed in 473 * @return error message to show to user or null if password is OK 474 */ validatePassword(String password)475 private CharSequence validatePassword(String password) { 476 if (pinMinLength == 0 && pinMaxLength == 0) { 477 // Invalid length requirement is sent by the server, just accept anything and let the 478 // server decide. 479 return null; 480 } 481 482 if (password.length() < pinMinLength) { 483 return getString(R.string.vm_change_pin_error_too_short); 484 } 485 return null; 486 } 487 setHeader(int text)488 private void setHeader(int text) { 489 headerText.setText(text); 490 pinEntry.setContentDescription(headerText.getText()); 491 } 492 493 /** 494 * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not 495 * {@link PinChanger#CHANGE_PIN_SUCCESS} 496 */ getChangePinResultMessage(@hangePinResult int result)497 private CharSequence getChangePinResultMessage(@ChangePinResult int result) { 498 switch (result) { 499 case PinChanger.CHANGE_PIN_TOO_SHORT: 500 return getString(R.string.vm_change_pin_error_too_short); 501 case PinChanger.CHANGE_PIN_TOO_LONG: 502 return getString(R.string.vm_change_pin_error_too_long); 503 case PinChanger.CHANGE_PIN_TOO_WEAK: 504 return getString(R.string.vm_change_pin_error_too_weak); 505 case PinChanger.CHANGE_PIN_INVALID_CHARACTER: 506 return getString(R.string.vm_change_pin_error_invalid); 507 case PinChanger.CHANGE_PIN_MISMATCH: 508 return getString(R.string.vm_change_pin_error_mismatch); 509 case PinChanger.CHANGE_PIN_SYSTEM_ERROR: 510 return getString(R.string.vm_change_pin_error_system_error); 511 default: 512 LogUtil.e(TAG, "Unexpected ChangePinResult " + result); 513 return null; 514 } 515 } 516 verifyOldPin()517 private void verifyOldPin() { 518 processPinChange(oldPin, oldPin); 519 } 520 setNextEnabled(boolean enabled)521 private void setNextEnabled(boolean enabled) { 522 nextButton.setEnabled(enabled); 523 } 524 showError(CharSequence message)525 private void showError(CharSequence message) { 526 showError(message, null); 527 } 528 showError(CharSequence message, @Nullable OnDismissListener callback)529 private void showError(CharSequence message, @Nullable OnDismissListener callback) { 530 new AlertDialog.Builder(this) 531 .setMessage(message) 532 .setPositiveButton(android.R.string.ok, null) 533 .setOnDismissListener(callback) 534 .show(); 535 } 536 537 /** Asynchronous call to change the PIN on the server. */ processPinChange(String oldPin, String newPin)538 private void processPinChange(String oldPin, String newPin) { 539 progressDialog = new ProgressDialog(this); 540 progressDialog.setCancelable(false); 541 progressDialog.setMessage(getString(R.string.vm_change_pin_progress_message)); 542 progressDialog.show(); 543 544 ChangePinParams params = new ChangePinParams(); 545 params.pinChanger = pinChanger; 546 params.phoneAccountHandle = phoneAccountHandle; 547 params.oldPin = oldPin; 548 params.newPin = newPin; 549 550 changePinExecutor.executeSerial(params); 551 } 552 sendResult(@hangePinResult int result)553 private void sendResult(@ChangePinResult int result) { 554 LogUtil.i(TAG, "Change PIN result: " + result); 555 if (progressDialog.isShowing() 556 && !VoicemailChangePinActivity.this.isDestroyed() 557 && !VoicemailChangePinActivity.this.isFinishing()) { 558 progressDialog.dismiss(); 559 } else { 560 LogUtil.i(TAG, "Dialog not visible, not dismissing"); 561 } 562 handler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget(); 563 } 564 565 private static class ChangePinHandler extends Handler { 566 567 private final WeakReference<VoicemailChangePinActivity> activityWeakReference; 568 ChangePinHandler(WeakReference<VoicemailChangePinActivity> activityWeakReference)569 private ChangePinHandler(WeakReference<VoicemailChangePinActivity> activityWeakReference) { 570 this.activityWeakReference = activityWeakReference; 571 } 572 573 @Override handleMessage(Message message)574 public void handleMessage(Message message) { 575 VoicemailChangePinActivity activity = activityWeakReference.get(); 576 if (activity == null) { 577 return; 578 } 579 if (message.what == MESSAGE_HANDLE_RESULT) { 580 activity.uiState.handleResult(activity, message.arg1); 581 } 582 } 583 } 584 585 private static class ChangePinWorker implements Worker<ChangePinParams, Integer> { 586 587 @Nullable 588 @Override doInBackground(@ullable ChangePinParams input)589 public Integer doInBackground(@Nullable ChangePinParams input) throws Throwable { 590 return input.pinChanger.changePin(input.oldPin, input.newPin); 591 } 592 } 593 } 594