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